您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Thread visibility management
当前为
// ==UserScript== // @name Empornium ThreadMan // @description Thread visibility management // @namespace Empornium Scripts // @version 1.3.1 // @author vandenium // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @include /^https:\/\/(www\.)?empornium\.(me|sx|is)\/$/ // @include /^https:\/\/(www\.)?empornium\.(me|sx|is)\/(forum|torrents|articles|userhistory|index)+/ // ==/UserScript== // Changelog: // Version 1.3.1 // - Instead of changing link color, add green heart next to thread. // Version 1.3.0 // - Add Greenlist hotkey, "g". // - Sets color of links to green. // Version 1.2.1 // - Move settings code to end of code // - Move processThreads code higher. // Version 1.2.0 // - Adds Greenlist // - Threads in the Greenlist are always shown regardless of viewed status. // - Threads created by users (after updating to 1.2.0) are always added to the Greenlist. // - The Greenlist is not retroactive. Threads created by users prior to using version // 1.2.0 need to be manually added to the Greenlist. // - Fix eventing issue causing repeated event listener executions. // Version 1.1.1 // - Ignore keyup event when cursor in new post area. // Version 1.1.0 // - Added post forward/back navigation hotkeys in threads. // - Forward: d, Backward: e // Version 1.0.10 // - Fix textarea background color for all themes // Version 1.0.9 // - Fix h1,h3 style // Version 1.0.8 // - Fix Settings dimensions // Version 1.0.7 // - Update Settings colors to be consistent in all themes. // Version 1.0.6 // - Fix break in 1.0.5 to include the homepage (with/without index.php) // Version 1.0.5 // - Fix @include, was trying to run on pages with no Latest Forum Threads section. // Version 1.0.4 // - Update @include to one-liner. // Version 1.0.3 // - Adding index to includes. // Version 1.0.2 // - Move settings link to the furthest left of user dropdown area. // Version 1.0.1 // - Update hotkey thread highlighting // Version 1.0.0 // - The initial version: // - Features: // - Whitelist/blacklist/Mark Read // - Works on Latest Forum Threads and Forum pages. // - Hide thread based on whether you've clicked since most recent. // - Hotkeys: blacklist (b), whitelist (w), mark read (r) // - Settings dialog to set/clear above. // Todo: // - Status area (how many threads currently hidden, etc.) GM_addStyle(` @keyframes greenlist { 0% { background-color: #90b180; } 100% { background-color: initial; } } .greenlist-animation { animation: greenlist 1s; } `); const initialOptions = { options: { whitelist: { threads: new Set(), }, blacklist: { threads: new Set(), }, greenlist: { threads: new Set(), } }, userSelected: { threads: new Set(), }, }; const optionsKey = 'empornium-threadman-options'; const getOptions = () => { const options = GM_getValue(optionsKey); if (options) { const rawOptions = JSON.parse(options); if (!rawOptions.options.greenlist) { rawOptions.options.greenlist = { threads: '[]' }; } // convert whitelist/blacklist back to sets rawOptions.options.whitelist.threads = new Set(JSON.parse(rawOptions.options.whitelist.threads)); rawOptions.options.blacklist.threads = new Set(JSON.parse(rawOptions.options.blacklist.threads)); rawOptions.options.greenlist.threads = new Set(JSON.parse(rawOptions.options.greenlist.threads)); rawOptions.userSelected.threads = new Set(JSON.parse(rawOptions.userSelected.threads)); // console.log('Options from GM: ', JSON.stringify(rawOptions, null, 4)) return rawOptions; } return initialOptions; }; const setOptions = (options = initialOptions) => { // console.log(`Setting options to ${optionsKey}:`, options); // convert sets to arrays options.options.whitelist.threads = JSON.stringify([...options.options.whitelist.threads]); options.options.blacklist.threads = JSON.stringify([...options.options.blacklist.threads]); options.options.greenlist.threads = JSON.stringify([...options.options.greenlist.threads]); options.userSelected.threads = JSON.stringify([...options.userSelected.threads]); GM_setValue(optionsKey, JSON.stringify(options ? options : initialOptions)); }; const getLatestForumThreads = () => Array.from(document.querySelectorAll('.latest_threads > span')); const getForumThreads = () => Array.from(document.querySelectorAll('table.forum_list tr.rowa, table.forum_list tr.rowb')); const getThreadMetaData = (thread) => { if (thread.children[0].nodeName.toLowerCase() === 'span') { // Latest forum thread const link = thread.children[1].href; return { id: link.split('/thread/')[1].split('?')[0], name: thread.children[1].textContent.trim(), timestamp: new Date(thread.children[3].title), }; } else { // forum const firstAnchor = thread.querySelector('a'); return { name: firstAnchor.textContent, id: firstAnchor.href.split('/thread/')[1], timestamp: thread.querySelector('span.time').title, }; } }; // Shows/hides threads based on options and displayed threads. // - Latest Forums section (top of multiple pages) // - Forum Page const processThreads = (threads) => { const options = getOptions(); threads.forEach(thread => { const threadMetaData = getThreadMetaData(thread); // Handle whitelist threads if (options.options.whitelist.threads.size > 0) { if (options.options.whitelist.threads.has(threadMetaData.id)) { thread.hidden = false; } else { thread.hidden = true; } } else { // Handle blacklisted threads if (options.options.blacklist.threads.has(threadMetaData.id)) { thread.hidden = true; } } // Handle greenlist threads (these are always shown - they trump previous visibility) if (options.options.greenlist.threads.size > 0) { if (options.options.greenlist.threads.has(threadMetaData.id)) { const link = thread.querySelector('a'); link.textContent = `💚${link.textContent}`; thread.hidden = false; } } // Handle clicked threads. Iterate over userselected threads. if any match current thread, check date and optionally hide. options.userSelected.threads.forEach(selectedThread => { if (selectedThread.id === threadMetaData.id) { const threadDate = new Date(threadMetaData.timestamp); const clickedDate = new Date(selectedThread.lastClicked); // check date and if greenlisted. always show greenlisted thread. if (clickedDate > threadDate && !options.options.greenlist.threads.has(threadMetaData.id)) { console.log(`Hiding already viewed thread, ${threadMetaData.name}`); thread.hidden = true; } } }); }); }; //-----------Settings Dialog---------------- const template = ` <style> #threadman-options-outer-container { position: absolute; left: 50%; transform: translate(-50%, -50%); z-index: 100; top: 50%; width: 1000px; height: 430px; border: solid #333 1px; background-color: rgb(0,0,0,0.9); border-radius: 15px; margin: 5px; } #threadman-options-outer-container h1 { color: #ccc; } #threadman-options-outer-container h3 { color: #ccc; } .threadman-options-container { max-width: 1000px; width: 100%; position: relative; margin: 15px; } .threadman-options-container textarea { background: #333; color: #ccc; } .options-inner { width: 24%; margin-right: 5px; display: inline-block; } #threadman-save-settings { margin-top: 10px; } #close-threadman-settings a { float: right; margin: 0px 30px; text-decoration:none; width: 20px; height: 20px; border-radius: 10px; font-size: 1.3em; } #close-threadman-settings a:hover { background-color: rgba(100,100,10,0.9); } </style> <div class="threadman-options-container" id="threadman-option-container"> <div id='close-threadman-settings'><a href='#'>✖️</a></div> <h1>Empornium ThreadMan Settings</h1> <div> <div class='options-inner'> <h3>Blacklist</h3> <textarea id= "blacklist" rows="15" cols="27" placeholder="Hide all threads in this list (comma-separated thread IDs)"></textarea> </div> <div class='options-inner'> <h3>Whitelist</h3> <textarea id="whitelist" rows="15" cols="27" placeholder="Only show threads in this list (comma-separated thread IDs, Blacklist ignored)"></textarea> </div> <div class='options-inner'> <h3>Greenlist</h3> <textarea id="greenlist" rows="15" cols="27" placeholder="Always show threads in this list. Forum threads you create will be automatically added to this list. (comma-separated thread IDs)"></textarea> </div> <div class='options-inner'> <h3>Thread Click Log</h3> <textarea id="userclicks" rows="15" cols="27"></textarea> </div> </div> <div> <button id='threadman-save-settings'>Save Settings</button> </div> </div> `; //----------------------------------------- const hideSettings = () => document.querySelector('#threadman-options-outer-container').remove(); const isNumeric = (num) => !isNaN(num); const cleanUserSettings = (list) => list.filter(val => val !== '' && isNumeric(val)); const showSettings = () => { const createTemplateDOM = (str) => { const template = document.createElement('div'); template.id = 'threadman-options-outer-container'; template.innerHTML = str; return template; }; const dom = createTemplateDOM(template); // Get settings const options = getOptions(); console.log('options in settings', options); // Set blacklist settings dom.querySelector('#blacklist').textContent = [...options.options.blacklist.threads]; // Set whitelist settings dom.querySelector('#whitelist').textContent = [...options.options.whitelist.threads]; // Set greenlist settings dom.querySelector('#greenlist').textContent = [...options.options.greenlist.threads]; // Set userclicks dom.querySelector('#userclicks').textContent = JSON.stringify([...options.userSelected.threads]); // Save settings dom.querySelector('#threadman-save-settings').addEventListener('click', () => { const blacklistSettingsRaw = dom.querySelector('#blacklist').value.trim(); const whitelistSettingsRaw = dom.querySelector('#whitelist').value.trim(); const greenlistSettingsRaw = dom.querySelector('#greenlist').value.trim(); const userSelectedRaw = dom.querySelector('#userclicks').value.trim(); // clean blacklist const blacklistSettingsListRaw = blacklistSettingsRaw.split(','); const blacklistSettings = cleanUserSettings(blacklistSettingsListRaw); // clean whitelist const whitelistSettingsListRaw = whitelistSettingsRaw.split(','); const whitelistSettings = cleanUserSettings(whitelistSettingsListRaw); // clean greenlist const greenlistSettingsListRaw = greenlistSettingsRaw.split(','); const greenlistSettings = cleanUserSettings(greenlistSettingsListRaw); // set options, save, close, refresh. options.options.blacklist.threads = new Set(blacklistSettings); options.options.whitelist.threads = new Set(whitelistSettings); options.options.greenlist.threads = new Set(greenlistSettings); options.userSelected.threads = userSelectedRaw === '' ? new Set() : new Set(JSON.parse(userSelectedRaw)); setOptions(options); hideSettings(); window.location.reload(); }); // Close settings dom.querySelector('#close-threadman-settings a').addEventListener('click', hideSettings); // Add to document. const body = document.querySelector('body'); body.appendChild(dom); }; // On click, need to set the lastClicked property on options. // Search through all threads, if exists, update, else create new. const addClickToOptions = (threadMetaData) => { const options = getOptions(); const userSelectedThreads = [...options.userSelected.threads]; const found = userSelectedThreads.find(thread => thread.id === threadMetaData.id); if (found) { // delete old value, add new with updated date options.userSelected.threads.delete(found); options.userSelected.threads.add({ id: found.id, lastClicked: new Date(), }); } else { options.userSelected.threads.add({ id: threadMetaData.id, lastClicked: new Date(), }); } setOptions(options); }; /** * Adds click handlers to threads * @param {*} threads List of threads * @param {*} selectorToThread Selector to the top-level element of thread starting from the event target. */ const addClickHandlerToThreads = (threads, selectorToThread) => { threads.forEach(thread => { thread.addEventListener('click', (el) => { const threadMetaData = getThreadMetaData(el.target.closest(selectorToThread)); addClickToOptions(threadMetaData); }); thread.addEventListener('mouseenter', (e) => { e.target.classList.add('threadman-thread-target'); }); thread.addEventListener('mouseleave', (e) => { e.target.classList.remove('threadman-thread-target'); }); }); }; // Add settings link to page. const addSettingsLink = () => { const ul = document.createElement('ul'); const li = document.createElement('li'); ul.append(li); ul.style.display = 'inline-block'; const a = document.createElement('a'); a.href = '#'; a.textContent = 'ThreadMan🦸♂️Settings'; a.addEventListener('click', () => { showSettings(); }); li.appendChild(a); const container = document.querySelector('#major_stats'); container.prepend(ul); }; /** * Main execution */ //setOptions(); // For clearing out all settings. // Get threads const latestForumThreads = getLatestForumThreads(); const forumThreads = getForumThreads(); // Process Latest Forum threads processThreads(latestForumThreads); // Process Forum Pages processThreads(forumThreads); // Click handlers addClickHandlerToThreads(latestForumThreads, '.latest_threads > span'); addClickHandlerToThreads(forumThreads, 'tr'); const markThread = (type, thread) => { let bgColor; if (type === 'blacklist') { bgColor = '#333'; thread.classList.remove('rowa'); thread.classList.remove('rowb'); thread.style.transition = 'opacity 0.75s'; thread.style.opacity = 0; thread.style.border = 'solid gainsboro 1px'; } if (type === 'whitelist') { bgColor = 'whitesmoke'; thread.classList.remove('rowa'); thread.classList.remove('rowb'); thread.style.border = 'solid gainsboro 1px'; thread.style.transition = 'opacity 0.75s'; } if (type === 'greenlist') { thread.classList.add('greenlist-animation'); } if (type === 'read') { bgColor = '#ACE1AF'; thread.classList.remove('rowa'); thread.classList.remove('rowb'); thread.style.transition = 'opacity 1.2s'; thread.style.opacity = 0; thread.style.border = 'solid gainsboro 1px'; } thread.style.backgroundColor = bgColor; thread.style.borderRadius = '2px'; if (type === 'blacklist' || type === 'read') { setTimeout(() => { thread.remove(); }, 700); } }; // Handles hotkey presses for latest forum and forum threads const hotkeyHandler = (thread, e) => { const threadMetaData = getThreadMetaData(thread); if (e.keyCode === 66) { // b blacklist if (thread.classList.contains('threadman-thread-target')) { // add thread id to blacklist options const options = getOptions(); if (!options.options.greenlist.threads.has(threadMetaData.id)) { options.options.blacklist.threads.add(threadMetaData.id); setOptions(options); markThread('blacklist', thread); } } } if (e.keyCode === 71) { // g greenlist if (thread.classList.contains('threadman-thread-target')) { // add thread id to greenlist options const options = getOptions(); if (!options.options.greenlist.threads.has(threadMetaData.id)) { options.options.greenlist.threads.add(threadMetaData.id); const link = thread.querySelector('a'); link.textContent = `💚${link.textContent}`; setOptions(options); markThread('greenlist', thread); } } } if (e.keyCode === 87) { // w whitelist if (thread.classList.contains('threadman-thread-target')) { const options = getOptions(); if (!options.options.whitelist.threads.has(threadMetaData.id)) { options.options.whitelist.threads.add(threadMetaData.id); } setOptions(options); markThread('whitelist', thread); } } if (e.keyCode === 82) { // r read if (thread.classList.contains('threadman-thread-target')) { addClickToOptions(threadMetaData); markThread('read', thread); } } }; // For post stepping hotkeys. if (window.location.pathname.includes('forum/thread')) { const posts = document.querySelectorAll('table.forum_post[id*=post]'); let n = window.scrollY > 0 ? posts.length - 1 : 0; const highlightStyleSet = 'box-shadow: 0px 0px 5px 4px; filter: hue-rotate(25deg)'; const highlightStyleSetUnset = 'box-shadow: none; filter:none'; posts[n].children[0].style.cssText = highlightStyleSet; document.addEventListener('keyup', (e) => { if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') return; if (e.key === 'd') { if (n >= (posts.length - 1)) { n = posts.length - 1; } else { posts[n].children[0].style = highlightStyleSetUnset; const nextEl = posts[n].nextElementSibling; nextEl.children[0].style.cssText = highlightStyleSet; nextEl.scrollIntoView({ behavior: 'smooth' }); n += 1; } } if (e.key === 'e') { if (n <= 0) { window.scrollTo(0, 0); n = 0; } else { posts[n].children[0].style = highlightStyleSetUnset; const prevEl = posts[n].previousElementSibling; prevEl.children[0].style = highlightStyleSet; prevEl.scrollIntoView({ behavior: 'smooth' }); n -= 1; } } }); } // Greenlist-related code. const isThreadPage1 = () => { return /forum\/thread\/[0-9]+(\?page=1)?$/.test(document.location.pathname) && (document.location.search === '' || document.location.search.includes('page=1')); }; const getPostAt = (n) => document.querySelectorAll('table.forum_post')[n]; const getPostOwnerId = (postDom) => postDom.querySelector('.user_name a').href.split('=')[1]; const getCurrentUserId = () => document.querySelector('#nav_userinfo a').href.split('=')[1]; const isModeratorMovedPost = () => { const firstPost = getPostAt(0); const firstPostUserText = firstPost.querySelector('#user_dropdown'); // not authenticated user's if (firstPostUserText) { const postUserText = firstPostUserText.innerText.toLowerCase(); return postUserText.includes('moderator') || postUserText.includes('admin'); } return false; }; const isThreadStartedByMe = () => { const userId = getCurrentUserId(); if (isModeratorMovedPost()) { return userId === getPostOwnerId(getPostAt(1)); } return (userId === getPostOwnerId(getPostAt(0))); }; // After creating a new thread, the user will hit this conditional. // This id should be greenlisted. if (isThreadPage1() && isThreadStartedByMe()) { const threadId = document.location.pathname.split('/')[3]; const options = getOptions(); options.options.greenlist.threads.add(threadId); setOptions(options); } // Hotkeys document.querySelector('body').addEventListener('keydown', (e) => { // Escape closes options dialog if (e.key === 'Escape') { hideSettings(); } // Setup hotkey functionality for latest forum and forum threads. getLatestForumThreads().forEach((thread) => hotkeyHandler(thread, e)); getForumThreads().forEach((thread) => hotkeyHandler(thread, e)); }); // Settings link addSettingsLink();