您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easy manage hide.keywords on Komica
// ==UserScript== // @name Komica Keyman // @description Easy manage hide.keywords on Komica // @namespace https://greasyfork.org/en/users/112088 // @match https://gita.komica1.org/00b/* // @version 0.1a // @grant GM.addStyle // ==/UserScript== const Komica = {}; (function komicaDialog(exports) { 'use strict' const TAG = '[Komica_Dialog]'; function insertDialog(name, id, namespace, options = {}) { // WORKAROUND: GM4 double insert if (document.querySelector(`#${id}`)) { return; } const tabBox = createTabBox(namespace); const dialog = document.createElement('div'); dialog.id = id; dialog.className = `${namespace}-dialog`; tabBox.appendTo(dialog); const footer = document.createElement('div'); footer.className = `${namespace}-dialog-footer`; dialog.appendChild(footer); const closeBut = document.createElement('button'); closeBut.className = `${namespace}-dialog-close-button`; closeBut.innerHTML = '關閉'; closeBut.addEventListener('click', toggleDialog, false); dialog.appendChild(closeBut); document.body.insertBefore(dialog, document.body.firstChild); function toggleDialog() { dialog.classList.toggle(`${namespace}-dialog-show`); if (dialog.classList.contains(`${namespace}-dialog-show`)) { if (options.onopen) { options.onopen(); } tabBox.currentSelected = 0; } else if (options.onclose) { options.onclose(); } } return { tabBox, footer, toggleDialog }; } function createTabBox(namespace) { const eventListener = { onswitch: [] }; function addEventListener(name, cb) { if (!eventListener[name]) { // ignore unknown event return; } if (typeof cb === 'function') { eventListener[name].push(cb); } else { console.warn(TAG, 'event listener not a function'); } } function emitEvent(name, ...args) { try { eventListener[name].forEach(cb => cb(...args)); } catch (e) { console.error(TAG, e); } } const tabBox = document.createElement('div'); tabBox.className = `${namespace}-tabbox-header`; const pageBox = document.createElement('div'); pageBox.className = `${namespace}-tabbox-container`; const groups = new Map(); const pageInfos = []; let currentSelected = -1; function addPage(title = null, groupTitle = null) { const index = pageInfos.length; const page = document.createElement('div'); page.className = `${namespace}-tabbox-page`; pageBox.appendChild(page); function getOrAddGroup(groupTitle) { let group = groups.get(groupTitle); if (!group) { const header = document.createElement('div'); header.className = `${namespace}-tabbox-group-title`; header.innerHTML = groupTitle; tabBox.appendChild(header); group = document.createElement('div'); group.className = `${namespace}-tabbox-group`; tabBox.appendChild(group); groups.set(groupTitle, group); } return group; } function addTab(title, parent) { const tab = document.createElement('div'); tab.className = `${namespace}-tabbox-tab`; tab.innerHTML = title; tab.addEventListener('click', () => switchTab(index), false); parent.appendChild(tab); return tab; } const group = (groupTitle == null) ? null : getOrAddGroup(groupTitle); const tab = (title == null) ? null : addTab(title, group ?? tabBox); const newInfo = { index, page, tab, group }; pageInfos.push(newInfo); return newInfo; } function getPage(index) { if ((index < 0) || (index >= pageInfos.length)) { console.error(TAG, `invalid tab index: ${index}`); return null; } return pageInfos[index].page; } function switchTab(index) { if ((index < 0) || (index >= pageInfos.length)) { console.error(TAG, `invalid tab index: ${index}`); return; } else if (currentSelected == index) { return; } const prevIndex = currentSelected; const { page, tab } = pageInfos[index]; // emit before show to make time to render currentSelected = index; emitEvent('onswitch', index, page); // hide current tab if (prevIndex >= 0) { // hide current tab const { page, tab } = pageInfos[prevIndex]; if (tab) { tab.classList.remove(`${namespace}-tabbox-selected`); } page.classList.remove(`${namespace}-tabbox-selected`); } if (tab) { tab.classList.add(`${namespace}-tabbox-selected`); } page.classList.add(`${namespace}-tabbox-selected`); } function getCurrentPage() { if ((currentSelected < 0) || (currentSelected >= pageInfos.length)) { return null; } else { return pageInfos[currentSelected].page; } } return { get currentSelected() { return currentSelected; }, set currentSelected(index) { switchTab(index); }, getCurrentPage, addPage, getPage, appendTo(parent) { parent.appendChild(tabBox); parent.appendChild(pageBox); }, on(eventName, cb) { addEventListener(`on${eventName}`, cb); }, }; } exports.insertDialog = insertDialog; })(Komica); (function komicaPostQueryer(exports) { 'use strict' const POST_NO_FROM_POST_EL_ID_REGEXP = /^r(\d+)$/; const ID_FROM_NOWID_TEXT_REGEXP = /ID:([^\s]+)(?:\].*)?/; function idFromNowIdText(nowIdText) { const matches = ID_FROM_NOWID_TEXT_REGEXP.exec(nowIdText); return (matches) ? matches[1] : null; } function idWithTailFromNowIdText(nowIdText) { const matches = ID_FROM_NOWID_TEXT_REGEXP.exec(nowIdText); if (!matches) { return null; } else { // Maybe has a id tail code, but we just ignore that. return matches[1].substr(0, 8); } } const QUERYERS_KOMICA = { queryThreads: function queryThreadsKomica() { return document.getElementsByClassName('thread'); }, queryPosts: function queryPostsKomica() { return document.getElementsByClassName('post'); }, queryNo: function queryNoKomica(post) { if (post.dataset) { return post.dataset.no; } else { return null; } }, queryId: function queryIdKomica(post) { const idEl = post.querySelector('.post-head .id'); if (idEl) { return idEl.dataset.id; } else { const nowEl = post.querySelector('.post-head .now'); if (nowEl) { return idFromNowIdText(nowEl.innerHTML); } else { return null; } } }, queryThreadTitle: function queryThreadTitleKomica(post) { const titleEl = post.querySelector('span.title'); if (titleEl) { return titleEl.innerText; } else { return null; } }, queryName: function queryNameKomica(post) { const nameEl = post.querySelector('span.name'); if (nameEl) { return nameEl.innerText; } else { return null; } }, queryBody: function queryBodyKomica(post) { const bodyEl = post.querySelector('.quote'); if (bodyEl) { return bodyEl.innerText; } else { return null; } }, isThreadPost: function isThreadPostKomica(post) { return ((post.classList) && (post.classList.contains('threadpost'))); }, isReplyPost: function isReplyPostKomica(post) { return ((post.classList) && (post.classList.contains('reply'))); }, postNoEl: function postNoElKomica(post) { return post.querySelector('.post-head [data-no]'); }, }; const NULL_QUERYER = { queryThreads: function queryThreadsNull() { return []; }, queryPosts: function queryPostsNull() { return []; }, queryNo: function queryNoNull(post) { return null; }, queryId: function queryIdNull(post) { return null; }, queryThreadTitle: function queryThreadTitleNull(post) { return null; }, queryName: function queryNameNull(post) { return null; }, queryBody: function queryBodyNull(post) { return null; }, isThreadPost: function isThreadPostNull(post) { return false; }, isReplyPost: function isReplyPostNull(post) { return false; }, postNoEl: function postNoElNull(post) { return null; }, }; const MAPPER = { 'komica': QUERYERS_KOMICA, }; function postQueryer(host) { const ret = (MAPPER[host]) ? MAPPER[host] : NULL_QUERYER; return Object.assign({}, ret); } exports.postQueryer = postQueryer; })(Komica); // from https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js if (typeof GM == 'undefined') { this.GM = {}; } if (typeof GM_addStyle == 'undefined') { this.GM_addStyle = (aCss) => { 'use strict'; let head = document.getElementsByTagName('head')[0]; if (head) { let style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = aCss; head.appendChild(style); return style; } return null; }; } if (typeof GM['addStyle'] == 'undefined') { GM['addStyle'] = function(...args) { return new Promise((resolve, reject) => { try { resolve(GM_addStyle.apply(this, args)); } catch (e) { reject(e); } }); }; } (async function () { 'use strict'; const TAG = '[Komica_Keywords_Manager]'; const DEFAULT_STLYE_VARS = ` :root { --keyman-primary-background-color: #FFFFEE; --keyman-secondary-background-color: #F0E0D6; --keyman-highlight-background-color: #EEAA88; --keyman-primary-color: #800000; --keyman-highlight-color: #800000; --keyman-text-button-color: #00E; --keyman-text-button-hover-color: #D00; --keyman-separator-color: #000; --keyman-primary-shadow-color: #5f5059; } `; const DIALOG_STYLE = ` .keyman-textarea-page { display: flex; } .keyman-textarea-page textarea { width: 100%; } .keyman-keywords-page { display: grid; grid-template-columns: [i-start] auto max-content [i-end]; grid-auto-rows: min-content; } .keyman-keywords-page button { place-self: center; } .keyman-keywords-page span { margin: 0 6px; place-self: center start; } .keyman-keywords-page canvas { margin: 3px 0; } .keyman-separtor { grid-column: i; } .keyman-dialog { visibility: hidden; position: fixed; top: -10px; z-index: 1; opacity: 0; display: grid; grid-template: "h h" min-content "c c" auto "f b" min-content / max-content 1fr; width: 40%; height: 50%; margin: 0 30%; overflow: hidden; border-radius: 5px; box-shadow: 0 0 15px 5px var(--keyman-primary-shadow-color); background-color: var(--keyman-primary-background-color); transition: top 100ms, visibility 100ms, opacity 100ms; } .keyman-dialog-show { visibility: visible; opacity: 1; top: 30px; } .keyman-dialog-footer { grid-area: f; align-self: center; margin: 10px 20px; } .keyman-dialog-close-button { place-self: center end; margin: 10px 20px; } .keyman-tabbox-header { grid-area: h; display: flex; justify-content: start; background-color: var(--keyman-secondary-background-color); } .keyman-tabbox-tab { cursor: pointer; flex: 1; padding: 3px 12px; font-weight: bold; text-align: center; } .keyman-tabbox-tab:hover { background-color: var(--keyman-highlight-background-color); color: var(--keyman-highlight-color); } .keyman-tabbox-tab.keyman-tabbox-selected { background-color: var(--keyman-highlight-background-color); color: var(--keyman-highlight-color); } .keyman-tabbox-container { grid-area: c; display: flex; overflow-y: auto; } .keyman-tabbox-page { width: 0; opacity: 0; overflow-y: scroll; overflow-x: hidden; transition: opacity 200ms; } .keyman-tabbox-page.keyman-tabbox-selected { width: 100%; opacity: 1; padding: 10px; } @media screen and (max-device-width: 600px) { .keyman-dialog { width: calc(100vw - 20px); margin: 0 10px; } .keyman-tabbox-container { width: calc(100vw - 20px); } } `; const CONTEXT_STYLE = ` .keyman-text-button { cursor: pointer; color: var(--keyman-text-button-color); } .keyman-text-button:hover { color: var(--keyman-text-button-hover-color); } .keyman-context-menu { display: inline-flex; flex-direction: column; visibility: hidden; position: absolute; padding: 5px 10px; border-radius: 5px; margin-top: calc(-1.7em - 10px); transition: margin 100ms; width: max-content; color: var(--keyman-primary-color); background-color: var(--keyman-highlight-background-color); } .keyman-context:hover .keyman-context-menu { visibility: visible; margin-top: -1.7em;; } .popup_area .keyman-context { display: none; } .keyman-context { cursor: pointer; display: inline-block; } .keyman-context summary { list-style: none; } .keyman-context summary::-webkit-details-marker { display: none; } .keyman-context-menu-close-button { text-align: center; display: none; } @media screen and (max-device-width: 600px) { .keyman-context-menu { visibility: visible; margin: -1.7em 6px 0 6px; width: calc(100% - 32px); left: 0; } .keyman-context-menu-close-button { display: unset; align-self: center; } } `; const HOST_SETTINGS = { 'komica': { darkStyleVars: ` :root { --keyman-primary-background-color: #1D1F21; --keyman-secondary-background-color: rgb(40, 42, 46); --keyman-highlight-background-color: rgb(0, 0, 0); --keyman-primary-color: #C5C8C6; --keyman-highlight-color: rgb(178, 148, 187); --keyman-text-button-color: #81A2BE; --keyman-text-button-hover-color: #FFC685; --keyman-separator-color: gray; --keyman-primary-shadow-color: rgb(40, 42, 46); } `, getStyleVars: function () { const [themeCookie] = document.cookie.split(/;\s*/) .map(c => c.split(/=/,2)) .filter(([k, v]) => k == 'theme'); if ((themeCookie) && (themeCookie[1] == 'dark.css')) { return this.darkStyleVars; } else { return DEFAULT_STLYE_VARS; } }, }, }; const hostId = 'komica'; const queryer = Komica.postQueryer(hostId); const hostSettings = HOST_SETTINGS[hostId]; function readSettings() { function* commaSplited(input) { for (const word of input.split(/,/)) { if (word != '' ) { yield word; } } } return { keywords: '', keywordList: [], _changed: function () { this.keywords = this.keywordList.join(','); if (this.onchange) { this.onchange(); } }, addKeyword: function (input) { let changed = 0; for (const word of commaSplited(input)) { if (!this.keywordList.includes(word)) { this.keywordList.push(word); changed++; } } if (changed > 0) { this._changed(); } }, removeKeyword: function (input) { const set = new Set(commaSplited(input)); const newList = []; let changed = 0; for (const word of this.keywordList) { if (set.has(word)) { changed++; } else { newList.push(word); } } if (changed > 0) { this.keywordList = newList; this._changed(); } }, clearKeyword: function () { this.keywordList.length = 0; this.keywords = ''; if (this.onchange) { this.onchange(); } }, swapKeywords: function (input) { const list = []; const set = new Set(); for (const word of commaSplited(input)) { if (!set.has(word)) { list.push(word); set.add(word); } } this.keywordList = list; this._changed(); }, read: function () { const input = document.querySelector('#keywordInput'); this.keywordList = [...commaSplited(input.value)]; this.keywords = this.keywordList.join(','); }, update: function () { const input = document.querySelector('#keywordInput'); input.value = this.keywords; const button = document.querySelector('#keywordUpdateBtn'); button.click(); }, }; } function insertManagerButton({ toggleDialog }) { const toggleButton = document.createElement('button'); toggleButton.textContent = '編輯列表'; toggleButton.addEventListener('click', toggleDialog, false); const anchor = document.querySelector('#keywordInput'); anchor.after(toggleButton); } async function addStyle() { const styleVars = ((hostSettings) && (hostSettings.getStyleVars)) ? hostSettings.getStyleVars() : DEFAULT_STLYE_VARS; await GM.addStyle(styleVars); await GM.addStyle(DIALOG_STYLE); await GM.addStyle(CONTEXT_STYLE); } function onLoad(settings) { const dialog = insertSettingDialog(settings); insertManagerButton(dialog); for (const post of queryer.queryPosts()) { initPostMeta(post); } const threadObserver = new MutationObserver(function (records) { const postReplys = records.reduce((total, record) => { for (const node of record.addedNodes) { if (queryer.isReplyPost(node)) { total.push(node); } } return total; } , []); postReplys.forEach(initPostMeta); }); for (const thread of queryer.queryThreads()) { threadObserver.observe(thread, { childList: true }); } } function insertSettingDialog(settings) { const options = { onopen: () => { settings.read(); }, onclose: () => { settings.update(); }, }; const { tabBox, footer, toggleDialog } = Komica.insertDialog('編輯列表', 'keyman-settings-dialog', 'keyman', options); footer.textContent = '※更改需要關閉列表才套用'; const keywordsPageInfo = tabBox.addPage('關鍵字列表'); const textareaPageInfo = tabBox.addPage('編輯'); keywordsPageInfo.page.classList.add('keyman-keywords-page'); textareaPageInfo.page.classList.add('keyman-textarea-page'); function switchTab(pageIdx, root) { switch (pageIdx) { case keywordsPageInfo.index: renderBlacklist(root); break; default: renderTextarea(root); break; } } tabBox.on('switch', switchTab); settings.onchange = () => { const pageIdx = tabBox.currentSelected; const root = tabBox.getCurrentPage(); if (root) { switchTab(pageIdx, root); } }; function renderBlacklist(root) { root.innerHTML = ''; root.append(...createInputField('隱藏包含關鍵字貼文')); root.append(createGridSepartor()); for (const value of settings.keywordList) { root.append(...createListitem(value), createGridSepartor()); } const clearButton = document.createElement('button'); clearButton.textContent = '清空'; clearButton.addEventListener('click', async () => { await settings.clearKeyword(); }); } function createInputField(placeholder) { const inputView = document.createElement('input'); inputView.placeholder = placeholder; const addButton = document.createElement('button'); addButton.innerHTML = '加入'; addButton.addEventListener('click', async ev => { await settings.addKeyword(inputView.value); inputView.value = ''; inputView.focus(); }); return [inputView, addButton]; } function createListitem(value) { const valueView = document.createElement('span'); valueView.textContent = value; const delButton = document.createElement('span'); delButton.className = 'keyman-text-button'; delButton.textContent = '移除'; delButton.dataset.value = value; delButton.addEventListener('click', async ev => { const button = ev.target; await settings.removeKeyword(button.dataset.value); }) return [valueView, delButton]; } function renderTextarea(root) { root.innerHTML = ''; const textarea = document.createElement('textarea'); textarea.value = settings.keywords; textarea.addEventListener('change', async ev => { const textarea = ev.target; await settings.swapKeywords(textarea.value); }) root.append(textarea); const button = document.createElement('button'); button.textContent = '空行換成逗號'; button.addEventListener('click', async ev => { textarea.value = textarea.value.replaceAll(/\n/g, ','); await settings.swapKeywords(textarea.value); }) root.append(button); } function createGridSepartor() { const view = document.createElement('div'); view.className = 'keyman-separtor'; view.append(document.createElement('hr')); return view; } return { toggleDialog }; } const postMetas = {}; function initPostMeta(post) { const postNo = queryer.queryNo(post); if (!postNo) { return; } const postMeta = { no: postNo, id: queryer.queryId(post), name: queryer.queryName(post), isThreadPost: queryer.isThreadPost(post), contextMenuRoot: null, }; postMetas[postNo] = postMeta; const insertPoint = queryer.postNoEl(post); if (insertPoint) { const parent = insertPoint.parentElement; // WORKAROUND: GM4 double insert if (parent.querySelector('.keyman-context')) { return; } const contextMenuRoot = document.createElement('details'); contextMenuRoot.className = 'text-button keyman-context'; contextMenuRoot.addEventListener('mouseenter', autoToggleContextMenu); insertPoint.after(contextMenuRoot); postMeta.contextMenuRoot = contextMenuRoot; renderContextMenu(post, postMeta, ''); } } function autoToggleContextMenu() { this.open = true; } function renderContextMenu(post, postMeta) { const postId = postMeta.id; const postNo = postMeta.no; const postName = postMeta.name; const isThreadPost = postMeta.isThreadPost; const root = postMeta.contextMenuRoot; const menu = document.createElement('div'); menu.className = 'keyman-context-menu'; const summary = document.createElement('summary'); summary.innerHTML = ' HIDE'; const closeButton = document.createElement('button'); closeButton.type = 'button'; closeButton.classList.add('keyman-context-menu-close-button'); closeButton.textContent = '關閉'; closeButton.addEventListener('click', contextMenuCloseButtonCb); menu.appendChild(closeButton); const message = document.createElement('div'); message.textContent = '加入隱藏關鍵字列表'; menu.appendChild(message); for (const word of [postNo, postId, postName]) { const button = document.createElement('div'); button.className = 'keyman-text-button'; button.textContent = word; button.addEventListener('click', contextMenuCb); menu.appendChild(button); } root.replaceChildren(menu, summary); } function contextMenuCloseButtonCb() { this.parentElement.parentElement.open = false; } async function contextMenuCb() { const word = this.textContent; settings.read(); await settings.addKeyword(word); settings.update(); } const settings = readSettings(); await addStyle(); onLoad(settings); })();