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

})();