您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This script will automatically fold some of the forum posts based on their content
// ==UserScript== // @namespace Planetsuzy // @name Planetsuzy Threads - Auto fold posts // @description This script will automatically fold some of the forum posts based on their content // @version 1.3.1 // @license MIT // @icon http://ps.fscache.com/styles/style1/images/statusicon/forum_old.gif // @include /^https?://(www\.)?planetsuzy\.org/showthread\.php/ // @include /^https?:\/\/(www\.)?planetsuzy\.org\/t\d+[\w-]+\.html$/ // @require https://cdnjs.cloudflare.com/ajax/libs/autolinker/1.8.3/Autolinker.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== const decoratePosts = function() { /** * Posts containing only links from these domains will be less visible (values stored in settings). */ const blacklistedDomains = GM_getValue('blacklistedDomains', []); /** * Returns two top levels of the URL's hostname. */ const parseDomain = function (href) { return new URL(href, document.location).hostname.split('.').slice(-2).join('.'); }; /** * Returns 'inner' URL if it wrapped by a link anonymizer. */ const unwrapUrl = function (href) { const baseUrl = new URL(href, document.location); if (baseUrl.pathname === '/' && baseUrl.search.length > 1) { const innerParam = baseUrl.search.slice(1); try { new URL(innerParam); return innerParam; } catch (e) {} } return href; }; /** * Store domain of current document for later. */ const documentDomain = parseDomain(document.location.href); /** * Class representing list of links in each post message sorted by link domains. */ class LinksList { constructor() { this.links = {}; } add(href) { let domain; try { domain = parseDomain(unwrapUrl(href)); } catch (e) { // Invalid link, ignore. return; } // Local links ignored. if (domain !== documentDomain) { if (!(domain in this.links)) { this.links[domain] = []; } this.links[domain].push(href); } } get domains() { return Object.keys(this.links); } }; /** * Class representing lists of elements linked with a tag such as a domain name * with a function to change multiple elements appearance at once. */ class TaggedElementsList { constructor(decoratorFn) { this.elements = {}; this.decoratorFn = decoratorFn; } addElement(tag, element) { if (this.elements[tag] === undefined) { this.elements[tag] = []; } this.elements[tag].push(element); } filter(tag, fn) { if (this.elements[tag] === undefined) { return []; } return this.elements[tag].filter(fn); } decorateElement(element) { const args = Array.from(arguments).slice(1); this.decoratorFn.apply(element, args); } decorateElements(tag) { const args = Array.from(arguments).slice(1); const that = this; this.elements[tag].forEach(function (element) { that.decoratorFn.apply(element, args); }); } }; const domainSwitches = new TaggedElementsList(function (isBlacklisted) { this.style.fontWeight = isBlacklisted ? 'normal' : 'bold'; this.style.textDecoration = isBlacklisted ? 'line-through' : 'none'; }); const expandToggles = new TaggedElementsList(function (makeVisible) { const needTriggerClick = (makeVisible === true && this.getAttribute('data-initial-state') === '0' && this.getAttribute('data-state') === '0') || (makeVisible === false && this.getAttribute('data-initial-state') === '0' && this.getAttribute('data-state') === '1'); if (needTriggerClick) { this.click(); } }); const refreshLabels = new TaggedElementsList(function (makeVisible) { this.style.display = makeVisible ? 'inline' : 'none'; }); document.querySelectorAll('table[id^=post]') .forEach(function (post) { // Find messages in thread and examine them for any links. const postId = post.getAttribute('id').substring(4); const message = post.querySelector('[id^=post_message_]'); if (message) { const links = new LinksList(); // Find anchors in post message. Array.from(message.querySelectorAll('a[href]')) .filter(function(link) { // Function to determine whether the image element inside anchor can be excluded due to not being a file hoster link. const canExcludeImage = function (image) { const parseUrlSegments = function (href) { // Returns URL pathname except the file name. return new URL(href, document.location).pathname.substring(1).split('/').slice(0, -1); }; const isWorthlessLink = function (href) { const url = new URL(href, document.location); return (url.pathname.length === 1 && url.search.length <= 1) // href is to website root URL || url.hostname.indexOf('.') === -1; // hostname is TLD }; const imageSrc = image.getAttribute('src'); try { const linkHref = unwrapUrl(link.getAttribute('href')); const linkDomain = parseDomain(linkHref); return isWorthlessLink(linkHref) // exclude if linked to the root of a website ("worthless link") || [linkDomain, documentDomain].includes(parseDomain(imageSrc)) // exclude if same domain or local image || parseUrlSegments(imageSrc).includes(linkDomain); // exclude if link domain is contained in URL (eg. 3rd part CDN used for previews) } catch (e) { return false; // invalid link, exclude for sure } }; // Exclude images inside anchors if images hostnames match anchors hostnames (previews) or local images. // May cause false positives when download link contains an image (eg icon) of a file hoster directly from its domain name. const images = link.querySelectorAll('img'); if (images.length) { const hostnamesExcluded = Array.from(images) .reduce(function (acc, cur) { return acc && canExcludeImage(cur) }, true); return !hostnamesExcluded; } return true; }) .forEach(function(link) { const deemphasise = function (element) { element.style.opacity = '0.2'; }; const href = link.getAttribute('href').toString(); links.add(href); try { if (blacklistedDomains.includes(parseDomain(unwrapUrl(href)))) { deemphasise(link); } } catch (e) { if (link.querySelectorAll('*').length === 0) { deemphasise(link); // invalid link containing no other elements such as image previews, deemphasise for sure } } }); // Additionally, parse links in 'pre' elements. message.querySelectorAll('pre') .forEach(function (pre) { Autolinker.parse(message.innerHTML, {urls: true}) .forEach(function (match) { links.add(match.getUrl()); }); }); // Set up additional elements. const postCountElement = post.querySelector('#postcount' + postId); const postAnchorElement = post.querySelector('a[name=' + post.getAttribute('id') + ']'); const hasBlacklistedLinks = links.domains.length && !links.domains.filter(function (domain) { return !blacklistedDomains.includes(domain) }).length; const contentRemoved = message.innerText.replace(/Removed at request of claimed rights holder/, '').length < 10; const hiddenPost = hasBlacklistedLinks || contentRemoved; // Collapse/reveal post toggle switches. postCountElement.parentElement.prepend((function () { const labels = ['►', '▼']; const initialState = hiddenPost ? 0 : 1; const el = document.createElement('a'); el.innerHTML = labels[1]; el.setAttribute('data-state', 1); el.setAttribute('data-initial-state', initialState); el.style.cursor = 'pointer'; el.addEventListener('click', function (e) { const newState = (parseInt(el.getAttribute('data-state')) + 1) % labels.length; Array.from(postCountElement.closest('tbody').querySelectorAll(':scope > tr')) .filter(function (row) { return row !== postCountElement.closest('tr') }) .forEach(function (row) { row.style.display = newState ? 'table-row' : 'none' }); post.parentElement.querySelectorAll('#post_thanks_box_' + postId) .forEach(function (row) { row.style.display = newState ? 'block' : 'none' }); el.innerHTML = labels[newState]; el.setAttribute('data-state', newState); }); if (!initialState) { el.click(); } expandToggles.addElement('a', el); return el; })()); // Refresh label that shows up to indicate that a refresh is needed to apply changes. postCountElement.parentElement.prepend((function () { const el = document.createElement('a') el.innerHTML = 'F5 ⟳'; el.classList.add('post-thead-label'); el.classList.add('button'); el.style.cursor = 'pointer'; el.addEventListener('click', function () { window.location.reload(); }); refreshLabels.addElement('F5', el); refreshLabels.decorateElement(el, false); return el; })()); // Domain name toggle switches. postCountElement.parentElement.prepend((function () { const el = document.createElement('span'); if (hiddenPost) { el.appendChild(document.createTextNode('⚠️ ')); } links.domains.forEach(function (domain, i) { const ch = document.createElement('span'); ch.innerHTML = domain; ch.style.cursor = 'pointer'; ch.addEventListener('click', function () { // Update domains blacklist property and redecorate elements. const oldBlacklist = GM_getValue('blacklistedDomains', []); const wasBlacklisted = oldBlacklist.includes(domain); const newBlacklist = wasBlacklisted ? oldBlacklist.filter(function (value) { return value !== domain}) : oldBlacklist.concat([domain]); domainSwitches.decorateElements(domain, !wasBlacklisted); GM_setValue('blacklistedDomains', newBlacklist); const needRefresh = blacklistedDomains.length !== newBlacklist.length || blacklistedDomains.filter(function(a) { return newBlacklist.indexOf(a) === -1; }).length; refreshLabels.decorateElements('F5', needRefresh); }); el.appendChild(ch); el.appendChild(document.createTextNode(i < links.domains.length - 1 ? ', ' : ' ')); domainSwitches.addElement(domain, ch); domainSwitches.decorateElement(ch, blacklistedDomains.includes(domain)); }); return el; })()); // Mark linked post. if (document.location.hash.slice(1) === post.getAttribute('id')) { post.classList.add('thread-tools-linked-post'); postAnchorElement.parentElement.appendChild((function () { const label = document.createElement('div'); const container = document.createElement('div'); label.classList.add('thread-tools-linked-post-id-label'); label.innerHTML = '#' + post.getAttribute('id'); container.classList.add('thread-tools-linked-post-id-label-container'); container.appendChild(label); return container; })()); } if (contentRemoved) { // Refresh label that shows up to indicate that a refresh is needed to apply changes. postCountElement.parentElement.prepend((function () { const el = document.createElement('span') el.innerHTML = 'Removed'; el.classList.add('post-thead-label'); el.classList.add('removed'); return el; })()); } } }); // Toggle buttons for multiple posts at once. const threadToolsMenu = document.querySelector('#threadtools_menu tbody'); if (threadToolsMenu !== null) { const createThreadToolsMenuElement = function (autoFoldedElements, caption, nothingCaption) { const labels = ['►', '▼']; const tr = document.createElement('tr'); tr.setAttribute('data-feature-multi-toggle', true); const td = document.createElement('td'); const toggle = document.createElement('a'); toggle.innerHTML = caption; const icon = document.createElement('span'); icon.innerHTML = labels[0]; icon.style.padding = '0 .7em'; const onMouseOver = function () { td.setAttribute('class', 'vbmenu_hilite vbmenu_hilite_alink'); td.style.cursor = 'pointer'; }; const onMouseOut = function () { td.setAttribute('class', 'vbmenu_option vbmenu_option_alink'); td.style.cursor = 'default'; }; onMouseOut(); if (autoFoldedElements.length) { td.addEventListener('mouseover', onMouseOver); td.addEventListener('mouseout', onMouseOut); toggle.setAttribute('data-state', 0); toggle.addEventListener('click', function () { const newState = (parseInt(toggle.getAttribute('data-state')) + 1) % labels.length; autoFoldedElements.forEach(function (el) { expandToggles.decorateElement(el, newState === 1); }); toggle.setAttribute('data-state', newState); icon.innerHTML = labels[newState]; }); const threadToolsButton = document.querySelector('#threadtools'); if (threadToolsButton !== null) { threadToolsButton.classList.add('thread-tools-have-additions') } } else { toggle.innerHTML += ' (' + nothingCaption + ')'; toggle.style.opacity = '0.7'; } toggle.prepend(icon); td.appendChild(toggle); tr.appendChild(td); return tr; }; threadToolsMenu.appendChild(createThreadToolsMenuElement( expandToggles.filter('a', function (element) { return element.getAttribute('data-initial-state') === '0'; }), 'Toggle all auto-hidden posts', 'nothing hidden' )); threadToolsMenu.appendChild(createThreadToolsMenuElement( expandToggles.filter('a', function (element) { if (element.getAttribute('data-initial-state') === '0') { const statusImage = element.closest('tr').querySelector('td a[name^=post] img.inlineimg'); if (statusImage !== null) { return statusImage.getAttribute('alt') === 'Unread'; } } return false; }), 'Toggle unread auto-hidden posts', document.querySelector('a[name=newpost]') !== null ? 'nothing hidden' : 'no unread posts', )); // New feature notifier. const isMultiToggleNewFeature = GM_getValue('featureMultiToggleSeen', false); if (isMultiToggleNewFeature === false) { const threadToolsButton = document.querySelector('#threadtools'); if (threadToolsButton !== null) { threadToolsMenu.querySelectorAll('tr[data-feature-multi-toggle]').forEach(function (tr) { const el = document.createElement('span') el.innerHTML = 'new'; el.classList.add('post-thead-label'); tr.querySelector(':scope > td').appendChild(el); }); threadToolsButton.addEventListener('click', function () { GM_setValue('featureMultiToggleSeen', true); if (threadToolsButton.classList.contains('thread-tools-have-additions-feature')) { threadToolsButton.classList.remove('thread-tools-have-additions-feature'); } }); if (threadToolsButton.classList.contains('thread-tools-have-additions')) { threadToolsButton.classList.add('thread-tools-have-additions-feature') } } } } }; GM_addStyle(` @-webkit-keyframes BackgroundPositionAnimation { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} } @-moz-keyframes BackgroundPositionAnimation { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} } @keyframes BackgroundPositionAnimation { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} } .thread-tools-have-additions { background: #2c539e; transition: background 300ms linear; } .thread-tools-have-additions-feature { background: linear-gradient(270deg, #f2a304, #314ff5); background-size: 400% 400%; -webkit-animation: BackgroundPositionAnimation 30s ease infinite; -moz-animation: BackgroundPositionAnimation 30s ease infinite; animation: BackgroundPositionAnimation 30s ease infinite; } .post-thead-label { background: white; border-radius: 1px; color: #0b198c; font-weight: bold; margin: 0 3px; padding: 1px 3px; } .post-thead-label.removed { color: white; background: red; } .thead .post-thead-label.button:hover { color: #0b198c; } .thread-tools-linked-post { border-width: 2px; margin: -2px; width: calc(100% + 4px); } .thread-tools-linked-post-id-label { background: #0B198C; margin-top: -7px; cursor: default; padding: 3px; } .thread-tools-linked-post-id-label-container { display: none; margin-left: 15px; vertical-align: top; } .thread-tools-linked-post-id-label-container + .thread-tools-linked-post-id-label-container { margin-left: 2px; } .thread-tools-linked-post:hover .thread-tools-linked-post-id-label-container { display: inline-block; } `); decoratePosts();