Gamekyo Smart Notes

Addd notes to users and save their comments

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);

})();