Gamekyo Smart Notes

Addd notes to users and save their comments

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Gamekyo Smart Notes
// @namespace    gk-smart-notes
// @version      1.0
// @match        https://www.gamekyo.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// @description  Addd notes to users and save their comments
// @license MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gamekyo.com
// ==/UserScript==

(function () {
    "use strict";

    let searchQuery = "";

    /* ---------------- CONFIG ---------------- */

    const STORAGE_KEY = "gkSmartNotesV4";

    /* ---------------- STORAGE ---------------- */

    const loadDB = () => {
        console.log("loadDB ");
        try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"); }
        catch { return {}; }
    };

    const saveDB = db => {
        console.log("saveDB ");
        localStorage.setItem(STORAGE_KEY, JSON.stringify(db));
    };

    const uuid = () =>
    crypto.randomUUID ? crypto.randomUUID() :
    Date.now()+"-"+Math.random();

    /* ---------------- CSS ---------------- */

    GM_addStyle(`
#gk-overlay{
 position:fixed; inset:0;
 background:rgba(0,0,0,.65);
 display:none; align-items:center; justify-content:center;
 z-index:999999;
}

#gk-app{
 width:820px; height:560px;
 background:#1e1e1e;
 color:white;
 border-radius:12px;
 display:flex; flex-direction:column;
 font-family:Arial;
}

.gk-header{
 padding:12px 16px;
 border-bottom:1px solid #333;
 display:flex; justify-content:space-between;
}

.gk-tabs{display:flex; gap:6px;}

.gk-tab{
 padding:6px 12px; background:#2b2b2b;
 border-radius:6px; cursor:pointer;
}

.gk-tab.active{background:#4fc3f7; color:black;}

.gk-body{flex:1; display:flex;}

.gk-sidebar{
 width:240px; border-right:1px solid #333;
 display:flex; flex-direction:column;
}

.gk-list{flex:1; overflow:auto;}

.gk-note-item{
 padding:6px 10px; cursor:pointer;
 border-left:3px solid transparent;
}

.gk-note-item.active{
 background:#2d2d2d;
 border-left:3px solid #4fc3f7;
}

.gk-editor{
 flex:1; display:flex; flex-direction:column;
 padding:10px; gap:6px;
}

.gk-input,.gk-editor textarea{
 background:#252526; border:none;
 color:white; padding:6px; border-radius:6px;
}

.gk-editor textarea{flex:1; resize:none;}

.gk-tags{display:flex; flex-wrap:wrap; gap:6px;}

.gk-tag{
 background:#3a3a3a; padding:2px 8px;
 border-radius:10px; font-size:11px; cursor:pointer;
}

.gk-btn{
 background:#3a3a3a; border:none;
 color:white; padding:6px;
 border-radius:6px; cursor:pointer;
}

.gk-stars span{cursor:pointer; font-size:18px;}

.gk-note-icon{margin-left:4px; cursor:pointer; opacity:.6;}
.gk-note-icon:hover{opacity:1;}
`);

    /* ---------------- TOOLTIP ---------------- */

    const tooltip = document.createElement("div");
    tooltip.style.position="absolute";
    tooltip.style.background="#1f1f1f";
    tooltip.style.color="white";
    tooltip.style.padding="6px 8px";
    tooltip.style.borderRadius="6px";
    tooltip.style.fontSize="12px";
    tooltip.style.display="none";
    tooltip.style.zIndex="999999";
    tooltip.style.maxWidth="260px";
    document.body.appendChild(tooltip);

    function showTooltip(e,id){
        console.log("showTooltip ");
        const db=loadDB();
        const m=db[id]?.member;
        if(!m) return;

        tooltip.innerHTML=`
 ${m.description?m.description.replace(/\n/g, '<br>')+"<br><hr>":""}
 <b>Rating</b>: ${"★".repeat(m.rating||0)}${"☆".repeat(5-(m.rating||0))}
 `;

        tooltip.style.left=e.pageX+10+"px";
        tooltip.style.top=e.pageY+10+"px";
        tooltip.style.display="block";
    }
    const hideTooltip=()=>tooltip.style.display="none";

    /* ---------------- OVERLAY ---------------- */

    const overlay=document.createElement("div");
    overlay.id="gk-overlay";
    overlay.innerHTML=`<div id="gk-app"></div>`;
    document.body.appendChild(overlay);

    const app=overlay.querySelector("#gk-app");

    overlay.addEventListener("click",e=>{
        if(e.target===overlay) overlay.style.display="none";
    });

    document.addEventListener("keydown",e=>{
        if(e.key==="Escape") overlay.style.display="none";
    });

    /* ---------------- UI ---------------- */

    function openUI(memberId,username,commentTitle="New title",commentText=""){
        console.log("openUI ");
        const db=loadDB();

        if(!db[memberId]){
            db[memberId]={
                member:{description:"",tags:[],rating:0},
                notes:[],
                activeNoteId:null
            };
        }

        const data=db[memberId];

        /* ---- MIGRATION V2 ---- */

        if(data.mainNote!==undefined){
            data.notes=[{
                id:uuid(),
                title:"Note principale",
                content:data.mainNote,
                tags:[],
                created:Date.now(),
                updated:Date.now()
            }];
            data.activeNoteId=data.notes[0].id;
            delete data.mainNote;
        }

        let tab="notes";

        render();
        overlay.style.display="flex";

        /* ---------------- RENDER ---------------- */

        function render(){
            console.log("render ");
            app.innerHTML=`
 <div class="gk-header">
   <div>${username}</div>
   <div class="gk-tabs">
<div class="gk-tab ${tab==="member"?"active":""}" data-tab="member">Member</div>
<div class="gk-tab ${tab==="notes"?"active":""}" data-tab="notes">Notes</div>
   </div>
 </div>

 <div class="gk-body">
   ${tab==="member"?renderMember():renderNotes()}
 </div>
 `;

            app.querySelectorAll(".gk-tab").forEach(t=>{
                t.onclick=()=>{tab=t.dataset.tab;render();}
            });
        }

        /* ---------------- MEMBER TAB ---------------- */

        function renderMember(){
            console.log("renderMember ");
            setTimeout(()=>{

                const desc=document.getElementById("m-desc");
                const tagsDiv=document.getElementById("m-tags");
                const tagInput=document.getElementById("m-tag-input");

                function renderTags(){
                    tagsDiv.innerHTML="";
                    data.member.tags.forEach((t,i)=>{
                        const el=document.createElement("div");
                        el.className="gk-tag";
                        el.textContent=t;
                        el.onclick=()=>{data.member.tags.splice(i,1);renderTags();}
                        tagsDiv.appendChild(el);
                    });
                }
                renderTags();

                tagInput.onkeydown=e=>{
                    if(e.key==="Enter"){
                        const v=tagInput.value.trim();
                        if(v) data.member.tags.push(v);
                        tagInput.value="";
                        saveDB(db);
                        renderTags();
                    }
                };

                document.querySelectorAll("#m-rating span").forEach(s=>{
                    s.onclick=()=>{
                        data.member.rating=Number(s.dataset.r);
                        saveDB(db);
                        refreshIcons();
                        render();
                    };
                });

                desc.onkeyup=()=>{
                    data.member.description=desc.value;
                    saveDB(db);
                };

            },0);

            return`
<div class="gk-editor">

<label>Description</label>
<textarea id="m-desc">${data.member.description||""}</textarea>

<label>Rating</label>
<div class="gk-stars" id="m-rating">
${[1,2,3,4,5].map(i=>`<span data-r="${i}">${i<=data.member.rating?"★":"☆"}</span>`).join("")}
</div>

<label>Tags</label>
<div class="gk-tags" id="m-tags"></div>
<input class="gk-input" id="m-tag-input" placeholder="Add tag"/>
</div>`;
        }

        /* ---------------- NOTES TAB ---------------- */

        function renderNotes(){
            console.log("renderNotes ");
            setTimeout(()=>bindNotes(),0);

            return`
<div class="gk-sidebar">
<button class="gk-btn" id="n-add">+ Nouvelle Note</button>
<button class="gk-btn" id="n-add-comment">+ Depuis commentaire</button>
<input class="gk-input" id="n-search" placeholder="Recherche"/>
<div class="gk-list" id="n-list"></div>
</div>

<div class="gk-editor">
<input class="gk-input" id="n-title" placeholder="Title"/>
<textarea id="n-content"></textarea>
<label>Tags</label>
<div class="gk-tags" id="n-tags"></div>
<input class="gk-input" id="n-tag-input" placeholder="Add tag"/>
</div>`;
        }

        function bindNotes(){
            console.log("bindNotes ");
            const list=document.getElementById("n-list");
            const title=document.getElementById("n-title");
            const content=document.getElementById("n-content");
            const tagsDiv=document.getElementById("n-tags");
            const tagInput=document.getElementById("n-tag-input");


            function renderList(){
                console.log("renderList ");
                list.innerHTML = "";

                const filteredNotes = data.notes.filter(n => {

                    if(!searchQuery) return true;

                    const title = (n.title || "").toLowerCase();
                    const content = (n.content || "").toLowerCase();
                    const tags = (n.tags || []).join(" ").toLowerCase();

                    return (
                        title.includes(searchQuery) ||
                        content.includes(searchQuery) ||
                        tags.includes(searchQuery)
                    );
                });

                filteredNotes.forEach(n=>{
                    const el=document.createElement("div");
                    el.className="gk-note-item "+(n.id===data.activeNoteId?"active":"");
                    el.textContent=n.title||"Untitled";

                    el.onclick=()=>{
                        data.activeNoteId=n.id;
                        loadActive();
                        renderList();
                    };

                    // --- Bouton suppression ---
                    const del = document.createElement("span");
                    del.textContent = "❌";
                    del.style.marginLeft = "8px";
                    del.style.cursor = "pointer";

                    del.onclick = (e)=>{
                        e.stopPropagation(); // évite de sélectionner la note

                        // suppression
                        data.notes = data.notes.filter(x => x.id !== n.id);

                        // si la note supprimée était active
                        if(data.activeNoteId === n.id){
                            data.activeNoteId = data.notes[0]?.id || null;
                            loadActive();
                        }
                        renderList();
                        saveDB(db);
                    };

                    list.appendChild(el);
                    el.appendChild(del);
                });
            }



            function loadActive(){
                console.log("loadActive : ");
                const n=data.notes.find(x=>x.id===data.activeNoteId);
                if(!n){title.value="";content.value="";return;}
                title.value=n.title;
                content.value=n.content;
                renderTags();
            }

            function renderTags(){
                console.log("renderTags ");
                const n=data.notes.find(x=>x.id===data.activeNoteId);
                tagsDiv.innerHTML="";
                if(!n) return;
                n.tags.forEach((t, i) => {
                    console.log("- renderTag "+ t);
                    const el = document.createElement("div");
                    el.className = "gk-xtag";
                    // On utilise flex pour aligner le texte et la croix proprement
                    el.style.display = "inline-flex";
                    el.style.alignItems = "center";
                    el.style.gap = "5px";
                    el.textContent = t;

                    // Création de l'icône croix
                    const closeBtn = document.createElement("span");
                    closeBtn.textContent = "✕"; // Caractère multiplication ou "x"
                    console.log("closeBtn : " + closeBtn);
                    // Action au clic sur la croix
                    closeBtn.onclick = (e) => {
                        e.stopPropagation(); // Empêche le clic de déclencher d'autres événements sur le parent

                        // --- VOTRE ACTION ICI ---
                        console.log("Suppression du tag : " + t);

                        n.tags.splice(i, 1);
                        renderTags();
                    };

                    tagsDiv.appendChild(el);
                    tagsDiv.appendChild(el);
                });
            }

            tagInput.onkeydown=e=>{
                if(e.key==="Enter"){
                    console.log("tagInput.Enter");
                    const n=data.notes.find(x=>x.id===data.activeNoteId);
                    if(!n) return;
                    const v=tagInput.value.trim();
                    if(v) n.tags.push(v);
                    tagInput.value="";
                    saveDB(db);
                    renderTags();
                }
            };

            title.oninput=()=>{
                console.log("title.oninput");
                const n=data.notes.find(x=>x.id===data.activeNoteId);
                if(n) n.title=title.value;
                renderList();
                saveDB(db);
            };

            content.oninput=()=>{
                console.log("content.oninput");
                const n=data.notes.find(x=>x.id===data.activeNoteId);
                if(n) n.content=content.value;
                saveDB(db);
            };

            document.getElementById("n-add").onclick=()=>{
                console.log("n-add.onclick");
                const n={id:uuid(),title:"New note",content:"",tags:[],created:Date.now()};
                data.notes.push(n);
                data.activeNoteId=n.id;
                renderList();
                loadActive();
                saveDB(db);
            };

            document.getElementById("n-search").addEventListener("keyup", (e)=>{
                searchQuery = e.target.value.toLowerCase().trim();
                renderList();
            });

            document.getElementById("n-add-comment").onclick=()=>{
                console.log("n-add-comment.onclick");
                if(!commentText) return;
                const n={id:uuid(),title:commentTitle,content:commentText,tags:[],created:Date.now()};
                data.notes.push(n);
                data.activeNoteId=n.id;
                renderList();
                loadActive();
                saveDB(db);
            };

            renderList();
            loadActive();
        }

    }

    /* ---------------- ICONS ---------------- */

    function refreshIcons(){
        console.log("refreshIcons");
        document.querySelectorAll(".gk-note-icon").forEach(e=>e.remove());
        scan();
    }

    function getMemberId(link){
        console.log("getMemberId " + link);
        const m=link.href.match(/member(\d+)/);
        return m?m[1]:null;
    }

    function createIcon(id,user,commentTitle, comment){
        console.log("createIcon " + user);
        const ic=document.createElement("span");
        ic.textContent="📝";
        ic.className="gk-note-icon";

        ic.onmouseenter=e=>showTooltip(e,id);
        ic.onmouseleave=hideTooltip;

        ic.onclick=e=>{
            e.stopPropagation();
            openUI(id,user,commentTitle || "",comment || "");
        };

        return ic;
    }

    /* ---------------- SCAN ---------------- */

    function scan(){
        console.log("scan ");
        document.querySelectorAll(".comment-information").forEach(commentInfo => {
            const link = commentInfo.select("a.member")[0];
            if(link.dataset.gkBound) return;
            const id = getMemberId(link);
            if(!id) return;

            const username = link.textContent.trim();

            const commentDateTime =commentInfo.select("span.date")[0].innerText.trim();
            const commentText =
                  "Article : " + window.location.href + "\n"+
                  "Date    : " + commentDateTime + "\n\n"+
                  commentInfo.select(".comment-text")[0].innerText.trim();

            link.after(createIcon(id, username,commentDateTime, commentText));

            link.dataset.gkBound = "1";
        });
    }
    unsafeWindow.scan = scan;


    /* ---------------- OBSERVER ---------------- */

    new MutationObserver(scan).observe(document.body,{childList:true,subtree:true});
    window.addEventListener("load",scan);

})();