您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
For Script User Blocking and Memo
// ==UserScript== // @name kone user note // @namespace http://tampermonkey.net/ // @version 0.5 // @license MIT // @description For Script User Blocking and Memo // @author onanymous // @match https://kone.gg/s/* // @exclude https://kone.gg/s/*/write // @icon https://www.google.com/s2/favicons?sz=64&domain=kone.gg // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-end // ==/UserScript== (function () { 'use strict'; const ARTICLE_SELECTOR = 'div.overflow-hidden .flex.items-end a.hover\\:underline span'; const DB_NAME = 'koneUserNoteDB'; const STORE_NAME = 'users'; let isHidden = true; const USER_BLOCK_MENU_STYLE = { position: 'absolute', background: '#222', color: '#fff', padding: '10px 18px', borderRadius: '8px', fontSize: '14px', zIndex: 999999, boxShadow: '0 2px 8px rgba(0,0,0,0.16)' }; const USER_BLOCK_USER_LABEL_STYLE = { fontWeight: "bold", marginBottom: "2px", pointerEvents: "none" }; const USER_BLOCK_NOTE_LABEL_STYLE = { color: "#ffeb3b", marginBottom: "6px", fontSize: "13px", pointerEvents: "none" }; const USER_BLOCK_BTN_WRAP_STYLE = { display: "flex", flexDirection: "column", gap: "4px" }; function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'handle' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getBlockUsers() { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const request = store.getAll(); request.onsuccess = () => resolve( request.result.filter(u => u.block === true).flatMap(u => u.handle) ); request.onerror = () => reject(request.error); }); } async function getAllUser() { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const request = store.getAll(); request.onsuccess = () => resolve( request.result.map(u => ({ handle: u.handle, block: u.block, username: u.username, note: u.note })) ); request.onerror = () => reject(request.error); }); } async function getUser(handle) { const db = await openDB(); handle = handle.toLowerCase(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const req = tx.objectStore(STORE_NAME).get(handle); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function getNote(handle) { const db = await openDB(); handle = handle.toLowerCase(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const req = tx.objectStore(STORE_NAME).get(handle); req.onsuccess = () => resolve(req.result && req.result.note ? req.result.note : ''); req.onerror = () => reject(req.error); }); } async function addBlockUser(handle, username) { const db = await openDB(); handle = handle.toLowerCase(); username = username.toLowerCase(); let user = await getUser(handle); let usernameArr = []; if (user) { const set = new Set(user.username.slice()); set.add(username); usernameArr = Array.from(set); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).put({ handle, block: true, username: usernameArr, note: user.note || "" }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } else { usernameArr = [username]; return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).put({ handle, block: true, username: usernameArr }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } } async function addNote(handle, username, note, overwrite = false) { const db = await openDB(); handle = handle.toLowerCase(); username = username.toLowerCase(); let user = await getUser(handle); let usernameArr = []; if (user) { const set = new Set(user.username.slice()); set.add(username); usernameArr = Array.from(set); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).put({ handle, block: user.block, username: usernameArr, note: overwrite ? note : (user.note ? (user.note + "\n" + note) : note) }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } else { usernameArr = [username]; return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).put({ handle, block: false, username: usernameArr, note: note }); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } } async function removeUser(handle) { const db = await openDB(); handle = handle.toLowerCase(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).delete(handle); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function removeAllBlockUser() { const db = await openDB(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); const request = store.getAll(); request.onsuccess = () => { const blocked = request.result.filter(u => u.block === true); blocked.forEach(u => store.delete(u.handle)); }; tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function getHandleFromRow(el) { const writerDiv = el.querySelector('a div.text-xs div.text-ellipsis'); return writerDiv ? writerDiv.dataset.handle : ''; } function targetList(articles) { const targets = [ ...document.querySelectorAll('div.group\\/post-wrapper'), ...document.querySelectorAll('div.grow.flex div.flex-col div.grid > div.contents') ]; targets.forEach(row => { const titleSpan = row.querySelector('span.overflow-hidden.text-nowrap.text-ellipsis'); if (!titleSpan) return; const title = titleSpan.textContent.trim(); const article = articles.find(a => a.title.trim() === title); if (!article) return; const writerDiv = row.querySelector('.overflow-hidden.text-center.whitespace-nowrap.text-ellipsis'); writerDiv.setAttribute('data-handle', article.writer.handle); }); } function targetArticle(article, comments) { const nodeList = document.querySelectorAll(ARTICLE_SELECTOR); const [articleAuthor, ...rest] = Array.from(nodeList); articleAuthor.setAttribute('data-handle', article.writer.handle) comments.forEach(c => { const commentSpan = document.querySelector(`#c_${c.id}`).parentElement.parentElement.querySelector(ARTICLE_SELECTOR); commentSpan.setAttribute('data-handle', c.handle) }); } function flattenComments(arr, result = []) { arr.forEach(item => { result.push({ id: item.id, content: item.content, handle: item.writer.handle, display_name: item.writer.display_name }); if (Array.isArray(item.children) && item.children.length > 0) { flattenComments(item.children, result); } }); return result; } function extractData() { const raw = unsafeWindow.__next_f.map(x => x[1]).join(''); const start = raw.indexOf('e:['); if (start === -1) return null; let bracket = 0, inStr = false, esc = false, end = -1; for (let i = start + 2; i < raw.length; i++) { const ch = raw[i]; if (!inStr) { if (ch === '"') inStr = true; else if (ch === '[') bracket++; else if (ch === ']') { bracket--; if (bracket === 0) { end = i + 1; break; } } } else { if (esc) esc = false; else if (ch === '\\') esc = true; else if (ch === '"') inStr = false; } } if (end === -1) return null; const arr = JSON.parse(raw.slice(start + 2, end)); return arr[3]; } function loadData() { const raw = extractData(); if (!raw) return; targetList(raw.Articles); if (raw.Article) { targetArticle(raw.Article, flattenComments(raw.Comments)); } } async function hide() { const hiddenUsers = (await getBlockUsers()).map(u => u.toLowerCase()); if (hiddenUsers.length === 0) return; const targets = [ ...document.querySelectorAll('div.group\\/post-wrapper'), ...document.querySelectorAll('div.grow.flex div.flex-col div.grid > div.contents') ]; targets.forEach(el => { const username = getHandleFromRow(el); if (username && hiddenUsers.includes(username.toLowerCase())) { el.style.display = 'none'; el.dataset.toggled = true; } }); document.querySelectorAll(`div.group\\/comment a.hover\\:underline span`).forEach(c => { if (c.dataset.toggled === "true") return; if (c.dataset.handle && hiddenUsers.includes(c.dataset.handle)) { c.dataset.hiddenUserName = c.textContent; c.textContent = '[숨김처리 된 유저]'; c.dataset.toggled = true; const profile = c.closest('div.group\\/comment').querySelector('img'); profile.dataset.hiddenSrc = profile.src; profile.src = '/images/profile.png'; profile.dataset.toggled = true; const comment = c.closest('div.group\\/comment').querySelector('p'); comment.dataset.hiddenContent = comment.textContent; comment.textContent = '숨김처리 된 코멘트입니다.'; comment.classList.add('opacity-50'); comment.dataset.toggled = true; } }); } async function show() { const hiddenUsers = (await getBlockUsers()).map(u => u.toLowerCase()); if (hiddenUsers.length === 0) return; const targets = [ ...document.querySelectorAll('div.group\\/post-wrapper'), ...document.querySelectorAll('div.grow.flex div.flex-col div.grid > div.contents') ]; targets.forEach(el => { if (el.dataset.toggled === "true") { el.style.display = ''; delete el.dataset.toggled; } }); document.querySelectorAll('div.group\\/comment a.hover\\:underline span').forEach(c => { if (c.dataset.toggled === "true") { if (c.dataset.hiddenUserName) { c.textContent = c.dataset.hiddenUserName; delete c.dataset.hiddenUserName; } delete c.dataset.toggled; const commentDiv = c.closest('div.group\\/comment'); const profile = commentDiv.querySelector('img'); if (profile && profile.dataset.toggled === "true") { if (profile.dataset.hiddenSrc) { profile.src = profile.dataset.hiddenSrc; delete profile.dataset.hiddenSrc; } delete profile.dataset.toggled; } const comment = commentDiv.querySelector('p'); if (comment && comment.dataset.toggled === "true") { if (comment.dataset.hiddenContent) { comment.textContent = comment.dataset.hiddenContent; delete comment.dataset.hiddenContent; } comment.classList.remove('opacity-50'); delete comment.dataset.toggled; } } }); } async function displayNote() { const userDict = Object.fromEntries((await getAllUser()).map(n => [n.handle, n])); document.querySelectorAll(ARTICLE_SELECTOR).forEach(s => { const user = userDict[s.dataset.handle]; if (!user) return; const anchor = s.closest('a'); if (!anchor) return; let wrapper = anchor.nextSibling; if (wrapper && wrapper.classList && wrapper.classList.contains('note-span-wrapper')) { wrapper.querySelector('span').textContent = user.note; return; } wrapper = document.createElement('div'); wrapper.className = 'note-span-wrapper flex'; const span = document.createElement('span'); span.className = 'text-xs'; span.textContent = user.note; wrapper.appendChild(span); anchor.parentNode.insertBefore(wrapper, anchor.nextSibling); }); } function createDiv(text, styleObj) { const div = document.createElement('div'); div.textContent = text; if (styleObj) Object.assign(div.style, styleObj); return div; } function createBlockBtn(el, menu) { const btn = createDiv('사용자 차단', { padding: '4px 0', cursor: 'pointer' }); btn.addEventListener('click', async () => { const handle = el.dataset.handle || el.textContent.trim(); const username = el.textContent.trim(); await addBlockUser(handle, username); alert(`[${el.textContent.trim()}(${el.dataset.handle})] 차단 리스트에 추가됨`); menu.remove(); hide(); }); return btn; } function createNoteBtn(el, menu) { const btn = createDiv('메모 수정/추가', { padding: '4px 0', cursor: 'pointer' }); btn.addEventListener('click', async () => { const handle = el.dataset.handle || el.textContent.trim(); const username = el.textContent.trim(); const oldNote = await getNote(handle); const note = prompt( `[${username}(${handle})]에게 남길 메모를 입력:`, oldNote || "" ); if (note !== null) { await addNote(handle, username, note.trim(), true); alert('메모 저장됨'); await updateAll(); } menu.remove(); }); return btn; } async function showUserBlockMenu(el) { if (!el.dataset.handle) return; if (window.currentUserBlockMenu) window.currentUserBlockMenu.remove(); const menu = document.createElement('div'); const rect = el.getBoundingClientRect(); Object.assign(menu.style, USER_BLOCK_MENU_STYLE, { left: (rect.right + window.scrollX + 8) + 'px', top: (rect.top + window.scrollY - 2) + 'px' }); menu.className = 'username-block-menu'; const userLabel = createDiv( `${el.textContent.trim()}(${el.dataset.handle})`, USER_BLOCK_USER_LABEL_STYLE ); const note = await getNote(el.dataset.handle); let noteLabel = null; if (note) noteLabel = createDiv(note, USER_BLOCK_NOTE_LABEL_STYLE); const btnWrap = document.createElement('div'); Object.assign(btnWrap.style, USER_BLOCK_BTN_WRAP_STYLE); btnWrap.appendChild(createBlockBtn(el, menu)); btnWrap.appendChild(createNoteBtn(el, menu)); menu.appendChild(userLabel); if (noteLabel) menu.appendChild(noteLabel); menu.appendChild(btnWrap); menu.addEventListener('mouseleave', () => menu.remove()); document.body.appendChild(menu); window.currentUserBlockMenu = menu; } function setupUsernameHoverMenu() { let hoverTimer = null; function bindMenuToUsernames() { const targets = [ ...document.querySelectorAll('div.group\\/post-wrapper a div.text-xs div.text-ellipsis'), ...document.querySelectorAll('div.grow.flex div.flex-col div.grid > div.contents a div.text-xs div.text-ellipsis'), ...document.querySelectorAll(ARTICLE_SELECTOR) ]; targets.forEach(el => { if (el.dataset.menuBound) return; el.dataset.menuBound = "1"; el.addEventListener('mouseenter', function handler(e) { hoverTimer = setTimeout(() => { showUserBlockMenu(el); }, 100); }); el.addEventListener('mouseleave', () => { clearTimeout(hoverTimer); setTimeout(() => { if (window.currentUserBlockMenu) window.currentUserBlockMenu.remove(); }, 3000); }); }); } const mo = new MutationObserver(bindMenuToUsernames); mo.observe(document.body, { childList: true, subtree: true }); bindMenuToUsernames(); } function showButton() { const targets = [ ...document.querySelectorAll('div.group\\/post-wrapper[style*="display: none"]'), ...document.querySelectorAll('div.grow.flex div.flex-col div.grid > div.contents[style*="display: none"]') ]; const hidden = targets.length; const table = document.querySelector('div.h-full.flex'); if (!table) return; let oldRow = table.querySelector('.hidden-post-counter'); if (oldRow) oldRow.remove(); const newRow = document.createElement('div'); newRow.classList.add('hidden-post-counter', 'ml-auto'); const innerSpan1 = document.createElement('span'); innerSpan1.textContent = '숨김처리 된 게시물 : ' + hidden; innerSpan1.classList.add('text-[13px]', 'text-center', 'pt-0.5', 'text-nowrap'); newRow.appendChild(innerSpan1); newRow.style.cursor = 'pointer'; newRow.addEventListener('click', async () => { isHidden = !isHidden; await updateAll(); }); table.insertBefore(newRow, table.firstChild); } // ====== Tampermonkey 메뉴 등록 ====== if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('차단 유저 추가', async () => { const handle = prompt('차단할 유저 handle 입력'); if (!handle) return; const users = (await getBlockUsers()).map(u => u.toLowerCase()); if (!users.includes(handle.toLowerCase())) { await addBlockUser(handle, ''); alert(`[${handle}] 차단 리스트에 추가됨`); hide(); } else { alert('이미 추가된 유저임'); } }); GM_registerMenuCommand('차단 유저 삭제', async () => { const users = (await getBlockUsers()).map(u => u.toLowerCase()); if (users.length === 0) { alert('차단 유저 없음'); return; } const toRemove = prompt('삭제할 유저 handle 입력 (현재 차단 유저: ' + users.join(', ') + ')'); if (!toRemove) return; if (users.includes(toRemove.toLowerCase())) { await removeUser(toRemove); alert(`[${toRemove}] 차단 해제됨`); hide(); } else { alert(`[${toRemove}]은 차단 리스트에 없음`); } }); GM_registerMenuCommand('차단 유저 모두 삭제', async () => { await removeAllBlockUser(); alert('차단 유저 모두 삭제됨'); }); GM_registerMenuCommand('차단 유저 목록 보기', async () => { const users = await getAllUser(); const list = users.filter(u => u.block === true).map(u => `${u.username.join(', ')} (${u.handle})`).join('\n'); alert(users.length > 0 ? list : '차단 유저 없음'); }); GM_registerMenuCommand('메모 전체 목록 보기', async () => { const notes = (await getAllUser()).filter(u => u.note && u.note.trim() !== ''); if (notes.length === 0) { alert('작성된 메모 없음'); return; } alert(notes.map(n => `${n.note} - ${n.username}(${n.handle})`).join('\n')); }); } setupUsernameHoverMenu(); (function removeRouter() { history.pushState = function () { }; history.replaceState = function () { }; document.addEventListener('click', function (e) { const a = e.target.closest('a'); if (a && a.href && a.target !== '_blank') { e.preventDefault(); window.location.href = a.href; } }, true); })(); (async () => { async function updateAll() { loadData(); if (isHidden) await hide(); else await show(); await displayNote(); showButton(); } window.updateAll = updateAll; await updateAll(); const observer = new MutationObserver(async (mutations) => { const onlyMenuChange = mutations.every(mutation => Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)) .every(node => node.nodeType === 1 && node.classList && node.classList.contains('username-block-menu') ) ); if (onlyMenuChange) return; observer.disconnect(); await updateAll(); observer.observe(document.body, { childList: true, subtree: true }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); })();