您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Patreon has too much clutter. Simplify it with this script. Hide sidebars and pinned posts, make posts wider, and make videos bigger.
// ==UserScript== // @name Jupiter's Patreon Tools // @description Patreon has too much clutter. Simplify it with this script. Hide sidebars and pinned posts, make posts wider, and make videos bigger. // @namespace Violentmonkey Scripts // @license CC BY-SA // @match https://www.patreon.com/* // @grant none // @run-at document-start // @version 1.3 // @author - // @description 6/3/2025, 10:35 PM // ==/UserScript== // Save the original console methods const originalConsole = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug }; // After `es()` runs, restore the original console methods function restoreConsole() { console.log = originalConsole.log; console.warn = originalConsole.warn; console.error = originalConsole.error; console.info = originalConsole.info; console.debug = originalConsole.debug; } const debug = false; function log(...args) { throttle(() => { restoreConsole(); }, 250); // 250ms throttle interval restoreConsole(); if (!debug) { return; } console.log(...args); } // Debounce function to ensure the observer isn't triggered too often const debounce = (func, delay) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => func(...args), delay); }; }; let resizeObserver; let addenda = new Map(); function checkBody() { if (!document.body) { log('No document body.'); return false; } else { return true; } } // Declare global elements as constants const styleSheet = document.createElement('style'); const staticStyles = document.createElement('style'); staticStyles.innerHTML = ` #interface-toggle-open-menu { position: fixed; margin: 16px; right: 0; bottom: 0; display: flex; padding: 4px; border-radius: 8px; transition: opacity 500ms; border-width: 1px; } #interface-toggle-buttons { position: fixed; right: 0; bottom: 0; margin: 64px 16px; background: white; border: 1px solid black; padding: 12px; border-radius: 12px; } #interface-toggle-buttons button [hidden] { display: none; } #interface-toggle-buttons button .hidden-span { color: red; } #interface-toggle-open-menu, #interface-toggle-buttons { border-color: #AAA; } #interface-toggle-buttons-inner { display: flex; flex-direction: column; gap: 8px; } #interface-toggle-open-menu svg { width: 24px; height: 24px; } #interface-toggle-open-menu:not(.menu-open) { opacity: 0.25; } #interface-toggle-open-menu:not(.menu-open):hover { opacity: 1; } .input-and-checkbox-container { display: flex; align-items: center; gap: 8px; } .input-and-checkbox-container span { } .input-and-checkbox-container input[type="number"] { width: 4em; text-align: center; } .input-and-checkbox-container input[type="number"][disabled] { color: gray; } #linktree-button { all: unset; background: #41df5d; height: 24px; width: 24px; border-radius: 20%; margin: 0 -8px -8px auto; cursor: pointer; } #linktree-button svg { aspect-ratio: 1; padding: 15%; } `; document.head.appendChild(staticStyles); // Step 1: Define a map of items, each with an ID, content, and an on/off switch const itemMap = { leftSidebar: { id: 'leftSidebar', type: 'button', // Specify the type here buttonText: 'Left Sidebar', content: ` #main-app-navigation { display: none; } .main-body { margin-left: 0; } `, on: false, }, rightSidebar: { id: 'rightSidebar', type: 'button', // Specify the type here buttonText: 'Right Sidebar', content: ` .right-sidebar { display: none; } .post-grid { display: unset; } .post-grid-left { } figure[title="video thumbnail"] div[data-tag="media-container"] img { width: 100%; } `, extraFunction: moveSearch, on: false, }, pinnedPost: { id: 'pinnedPost', type: 'button', // Specify the type here buttonText: 'Pinned Post', content: ` .pinned-post { display: none; } `, on: false, }, maxWidth: { id: 'maxWidth', type: 'inputAndCheckbox', // Specify the type here label: 'Post Max Width', value: 1024, get content() { return ` .max-width-limited, [data-tag="collections-view"], [data-tag="about-patron-view"], [data-tag="membership-patron-view"], .post-section [class*="Areas_narrowContent"], .post-section [class*="Areas_wideContent"], .post-grid-parent { max-width: min(${this.value}px, 100%); } `; }, on: false, }, wideVideo: { id: 'wideVideo', type: 'button', buttonText: 'Wide Video', hiddenText: ' [active]', content: ` .hides-video-overflow { overflow: visible !important; box-shadow: none; } .subtle-has-video { border-top-left-radius: unset; border-top-right-radius: unset; } .video-holder { margin: 0 var(--video-negative-margin); max-height: 95vh; } `, on: false, }, // Add more items here as needed }; function loadSettingsFromLocalStorage() { // Get the saved settings from localStorage const settings = JSON.parse(localStorage.getItem('itemSettings')); // If settings exist, apply them if (settings) { for (const itemId in settings) { if (settings.hasOwnProperty(itemId)) { // Apply the saved settings by calling toggleItem with noSave set to true toggleItem(itemId, settings[itemId].on, true); // If the item has a value, apply it if (settings[itemId].hasOwnProperty('value')) { itemMap[itemId].value = settings[itemId].value; // Set the saved value // updateItemValue(itemId); // This is a function to update the input box if needed } } } } } // Step 2: Function to toggle the state of an item (on or off) function toggleItem(itemId, state, noSave) { if (itemMap[itemId]) { // If state is not specified, switch the item to its opposite state if (state === undefined) { itemMap[itemId].on = !itemMap[itemId].on; } else { itemMap[itemId].on = state; } // Check if the item has an extraFunction and call it if (itemMap[itemId].extraFunction) { itemMap[itemId].extraFunction(itemMap[itemId].on); // Pass the new state to the function if needed } if (!noSave) { // Save the updated item settings to localStorage by passing itemId and state saveSettingsToLocalStorage(itemId, itemMap[itemId].on); } updateStylesheet(); // If the item is 'wideVideo' and the state is true, set up the resize observer if (itemId === 'wideVideo') { if (state === true) { // Initialize the resize observer try { attachResizeObserver(); } catch (error) { log('Error occurred while attaching resize observer:', error); // Retry after 250ms if the operation fails setTimeout(attachResizeObserver, 250); } } else { // If state is false, disconnect the observer if (resizeObserver) { resizeObserver.disconnect(); log('Resize observer disconnected.'); } } } } } let resizeTimeout; function debounceResize(callback, wait) { return function () { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(callback, wait); }; } function attachResizeObserver() { const bodyPresent = checkBody(); if (!bodyPresent) { // throw new Error('The body isn\'t ready.'); setTimeout(attachResizeObserver, 250); // Retry after 250ms return; // Exit early and do nothing further } const debouncedResize = debounceResize(() => { log('Window size changed!'); videoRescale(); }, 250); // 250ms debounce delay // Create the ResizeObserver resizeObserver = new ResizeObserver(debouncedResize); // Start observing the window (or document body, or any other specific element) resizeObserver.observe(document.body); } function videoRescale() { // Get the body width const bodyWidth = document.body.offsetWidth; // Query all .video-holder elements const videoHolders = document.querySelectorAll('.video-holder'); let finalNegativeMargin = null; // Loop through each video-holder videoHolders.forEach((videoHolder) => { // Get the width of the video holder, including margins const style = window.getComputedStyle(videoHolder); const videoHolderWidth = videoHolder.offsetWidth; const marginLeft = parseInt(style.marginLeft, 10); const marginRight = parseInt(style.marginRight, 10); // Calculate the total width of the video holder including margins const totalWidth = videoHolderWidth + marginLeft + marginRight; // Calculate the difference between the body width and video holder width const difference = bodyWidth - totalWidth; // Divide the difference by 2 const negativeMargin = difference / 2; log('Calculated negative margin: ' + negativeMargin); if (finalNegativeMargin === null || finalNegativeMargin > negativeMargin) { finalNegativeMargin = negativeMargin; } }); const marginStyle = ` :root { --video-negative-margin: ${finalNegativeMargin * -1}px; } `; addToAddenda('negative-margin', marginStyle); } function addToAddenda(id, content) { if (addenda.hasOwnProperty(id)) { // If the entry exists, modify its content addenda[id].content = content; } else { // If the entry doesn't exist, create it and set its content addenda[id] = { content: content }; } updateStylesheet(); } function saveSettingsToLocalStorage(itemId, state) { const settings = JSON.parse(localStorage.getItem('itemSettings')) || {}; // Update the specific item's state settings[itemId] = { id: itemId, on: state, // Check if the item has a value property, and if it does, include it in the saved state ...(itemMap[itemId]?.value !== undefined ? { value: itemMap[itemId].value } : {}) }; // Save the updated settings to localStorage as a JSON string localStorage.setItem('itemSettings', JSON.stringify(settings)); } // Array to store hidden ancestors let hiddenAncestorsOfSearchBox = []; function moveSearch(state) { log('Move Search called with state:', state); // Select the search box inside #post-grid-left const searchBox = document.querySelector('#post-grid-left [data-tag="search-input-box"]'); if (searchBox) { // Step 1: Loop through ancestors and find hidden ones let currentElement = searchBox.parentElement; while (currentElement) { if (getComputedStyle(currentElement).display === 'none') { log('Hidden ancestor found:', currentElement); hiddenAncestorsOfSearchBox.push(currentElement); // Store the hidden ancestor currentElement.style.display = 'unset'; // Set its display to unset } currentElement = currentElement.parentElement; } if (state) { // Step 2: If state is true, display the hidden ancestors log('State is true, ancestors displayed'); // We already set the display to 'unset' in the loop above } else { // Step 3: If state is false, revert the display property of hidden ancestors log('State is false, reverting ancestors to hidden'); hiddenAncestorsOfSearchBox.forEach((ancestor) => { ancestor.style.display = ''; // Remove the display property altogether }); // Clear the hiddenAncestorsOfSearchBox array after reverting hiddenAncestorsOfSearchBox = []; } } else { log('Search box not found.'); } } // Step 3: Function to build the stylesheet based on the state of the items function buildStylesheet() { let styleSheetContent = ''; // Loop through each item and only include it if it's "on" for (const key in itemMap) { const item = itemMap[key]; // log(item.id, item.on); if (item.on) { styleSheetContent += item.content + '\n'; // Add the content of the item if it's "on" } } styleSheetContent += '\n'; // Add the addenda items for (const key in addenda) { const addendum = addenda[key]; styleSheetContent += addendum.content + '\n'; // Add each item from the addenda } return styleSheetContent; } // Step 4: Function to insert the stylesheet into the document function updateStylesheet() { styleSheet.id = 'dynamic-stylesheet'; styleSheet.type = 'text/css'; styleSheet.innerHTML = buildStylesheet(); document.head.appendChild(styleSheet); } // Initial stylesheet insertion updateStylesheet(); // Example of how to toggle items on/off // toggleItem('leftSidebar', true); // Turns the left sidebar rule on function toggleHiddenSpan(button, item, state) { const hiddenSpan = button.querySelector('.hidden-span'); if (!hiddenSpan) { log('Hidden span not found on button ' + item.id + '.'); return; } if (state === true) { log(item.id + 'is active.'); hiddenSpan.removeAttribute('hidden'); } else { log(item.id + 'is inactive.'); hiddenSpan.setAttribute('hidden', ''); } } // Step 3: Function to generate buttons for each item and return them as an array function generateButtons() { const buttons = []; // Loop through each item in the itemMap for (const key in itemMap) { const item = itemMap[key]; if (item.type === 'button') { // Create a new button element const button = document.createElement('button'); button.textContent = item.buttonText; const hiddenSpan = document.createElement('span'); hiddenSpan.classList.add('hidden-span'); if (!item.hiddenText) { hiddenSpan.textContent = ' [hidden]'; } else { hiddenSpan.textContent = item.hiddenText; } button.appendChild(hiddenSpan); toggleHiddenSpan(button, item, item.on); // Attach a click event to toggle the item's state when the button is clicked button.addEventListener('click', () => { event.stopPropagation(); // Stop the click event from bubbling up toggleItem(item.id); // Toggle the item by its ID initialDOMCheck(); toggleHiddenSpan(button, item, item.on); }); // Push the created button to the buttons array buttons.push(button); } if (item.type === 'inputAndCheckbox') { // Create a div container for input and checkbox const container = document.createElement('div'); container.classList.add('input-and-checkbox-container'); // Create the label span const labelSpan = document.createElement('span'); labelSpan.textContent = item.label; // Create the input box (grayed out initially) const inputBox = document.createElement('input'); inputBox.type = 'number'; inputBox.value = item.value; inputBox.disabled = !item.on; // Initially grayed out // Create the checkbox const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = item.on; // Add a listener to the checkbox to toggle the input box's editable state checkbox.addEventListener('change', () => { inputBox.disabled = !checkbox.checked; // Enable/Disable input box based on checkbox if (!checkbox.checked) { // When unchecked, reset the input value to the original one inputBox.value = item.value; } toggleItem(item.id, checkbox.checked); }); // Add a listener to the input box to update the item value and toggle it let debounceTimer; inputBox.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { item.value = parseInt(inputBox.value, 10); toggleItem(item.id, false); // Pass second argument as false toggleItem(item.id, true); // Pass second argument as true }, 250); // Debounced for 250ms }); // Append the label, input box, and checkbox to the container container.appendChild(labelSpan); container.appendChild(inputBox); container.appendChild(checkbox); // Push the container to the buttons array buttons.push(container); } } // Return the array of buttons and input-and-checkbox containers return buttons; } // Step 4: Function to append the buttons to a specific container function appendButtons() { const container = document.createElement('div'); container.id = 'interface-toggle-buttons'; const subContainer = document.createElement('div'); subContainer.id = 'interface-toggle-buttons-inner'; // Set the container's initial visibility to hidden container.style.display = 'none'; // Generate the buttons const buttons = generateButtons(); // Clear the container and append the buttons buttons.forEach(button => { subContainer.appendChild(button); }); const linktreeButton = document.createElement('button'); linktreeButton.id = 'linktree-button'; linktreeButton.innerHTML = ` <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 80 97.7" style="enable-background:new 0 0 80 97.7;" xml:space="preserve"> <path d="M0.2,33.1h24.2L7.1,16.7l9.5-9.6L33,23.8V0h14.2v23.8L63.6,7.1l9.5,9.6L55.8,33H80v13.5H55.7l17.3,16.7l-9.5,9.4L40,49.1 L16.5,72.7L7,63.2l17.3-16.7H0V33.1H0.2z M33.1,65.8h14.2v32H33.1V65.8z"> </path> </svg> `; linktreeButton.addEventListener('click', () => { window.open('https://linktr.ee/jupiterliar', '_blank'); }); subContainer.appendChild(linktreeButton); container.appendChild(subContainer); // Create the wrench icon button const wrenchButton = document.createElement('button'); wrenchButton.id = 'interface-toggle-open-menu'; wrenchButton.innerHTML = ` <svg fill="#000000" viewBox="0 0 512.00 512.00" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.00512" transform="rotate(0)matrix(-1, 0, 0, 1, 0, 0)"> <g id="SVGRepo_bgCarrier" stroke-width="0"></g> <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g> <g id="SVGRepo_iconCarrier"> <path d="M507.73 109.1c-2.24-9.03-13.54-12.09-20.12-5.51l-74.36 74.36-67.88-11.31-11.31-67.88 74.36-74.36c6.62-6.62 3.43-17.9-5.66-20.16-47.38-11.74-99.55.91-136.58 37.93-39.64 39.64-50.55 97.1-34.05 147.2L18.74 402.76c-24.99 24.99-24.99 65.51 0 90.5 24.99 24.99 65.51 24.99 90.5 0l213.21-213.21c50.12 16.71 107.47 5.68 147.37-34.22 37.07-37.07 49.7-89.32 37.91-136.73zM64 472c-13.25 0-24-10.75-24-24 0-13.26 10.75-24 24-24s24 10.74 24 24c0 13.25-10.75 24-24 24z"></path> </g> </svg> `; // Attach a click event to toggle the visibility of the button container wrenchButton.addEventListener('click', () => { event.stopPropagation(); // Stop the click event from bubbling up if (container.style.display === 'none') { container.style.display = 'block'; wrenchButton.classList.add('menu-open'); log('Displaying config buttons...'); } else { log('Hiding config buttons at point 1...'); container.style.display = 'none'; wrenchButton.classList.remove('menu-open'); } }); function appendButtonPair() { if (document.body) { // Add the wrench button before the container document.body.appendChild(wrenchButton); // Add the button container to the body document.body.appendChild(container); } else { log('Body not available yet, retrying...'); setTimeout(appendButtonPair, 250); // Retry after 250ms if the body is not available } } try { appendButtonPair(); } catch (error) { log('Error occurred while appending buttons:', error); // Retry after 250ms if the operation fails setTimeout(appendButtonPair, 250); } // Click outside the button container to hide it document.addEventListener('click', (event) => { if (!container.contains(event.target) && event.target !== wrenchButton) { log('Hiding config buttons at point 2...'); container.style.display = 'none'; wrenchButton.classList.remove('menu-open'); } }); } // Helper function to get all ancestors of an element function getAncestors(el) { let ancestors = []; let currentElement = el; while (currentElement.parentElement) { ancestors.push(currentElement.parentElement); currentElement = currentElement.parentElement; } return ancestors; } // List of criteria to find elements const criteriaList = [ { // Traversing the DOM and applying the existing criteria selector: 'body', // Starting point: body element handler: (el) => { const allElements = el.getElementsByTagName('*'); // Select all elements under body const viewportWidth = window.innerWidth; const existingMainBody = document.body.querySelector('.main-body'); if (!existingMainBody) { // Step 1: Find elements matching the criteria const matchingElements = []; for (let child of allElements) { // Skip if the element is a <script> tag if (child.tagName.toLowerCase() === 'script') { continue; } const style = window.getComputedStyle(child); // Skip if the element is not visible (display: none or visibility: hidden) if (style.display === 'none' || style.visibility === 'hidden') { continue; } const rect = child.getBoundingClientRect(); // Skip if the element is too small (width < 50% of viewport) if (rect.width < Math.floor(viewportWidth * 0.5)) { continue; } // If the element's left margin is greater than 20px const leftMargin = parseInt(style.marginLeft, 10); if (isNaN(leftMargin) || leftMargin <= 20) { continue; // Skip if the left margin is not greater than 20px } // Step 2: Collect matching elements matchingElements.push(child); } // Step 3: Evaluate the array of matching elements if (matchingElements.length === 1) { // Only one element matches, apply the class matchingElements[0].classList.add('main-body'); } else if (matchingElements.length > 1) { // Multiple matching elements, find the ancestor let ancestor = null; // Check if one element is an ancestor of the others for (let i = 0; i < matchingElements.length; i++) { const currentElement = matchingElements[i]; let isAncestor = true; // Check if this element is an ancestor of the others for (let j = 0; j < matchingElements.length; j++) { if (i === j) continue; // Skip itself const otherElement = matchingElements[j]; if (!currentElement.contains(otherElement)) { isAncestor = false; break; } } if (isAncestor) { ancestor = currentElement; break; } } // If an ancestor is found, apply the class to it if (ancestor) { ancestor.classList.add('main-body'); } } } }, }, { // Traversing the DOM and applying the criteria selector: '#main-app-navigation', // Look for the element with this ID handler: (el) => { //log('Found #main-app-navigation'); el.classList.add('left-sidebar'); // Add the "left-sidebar" class }, }, { selector: 'body', // Starting point: body element handler: (el) => { const children = el.querySelectorAll('*'); // Find all descendants const viewportWidth = window.innerWidth; const areaSpanTwo = el.querySelector('[class*="areaSpanTwo"]'); if (areaSpanTwo) { log('areaSpanTwo is present.'); } // Traverse all descendants of the body children.forEach(child => { const rect = child.getBoundingClientRect(); if (Array.from(child.classList).some(className => className.includes('areaSpanTwo'))) { log('Class includes "areaSpanTwo". Width:', parseFloat(rect.width)); } // If the element's width is less than 50% of the viewport, skip it if (rect.width < Math.floor(viewportWidth * 0.5)) { return; } // Check if it has a grid display style const style = window.getComputedStyle(child); const display = style.getPropertyValue('display'); const gridTemplateColumns = style.getPropertyValue('grid-template-columns').trim(); if (display === 'grid') { // log('Investigating element:', child); } // If it's a grid and has exactly three columns (just check for 3 values) if (display === 'grid' && gridTemplateColumns.split(' ').length === 3) { // Assign the "post-grid" class to the element child.classList.add('post-grid'); // Assign the class "post-grid-parent" to the parent child.parentElement.classList.add('post-grid-parent'); // If the element has children, assign the required IDs to the first two if (child.children.length > 1) { child.children[0].classList.add('post-grid-left'); // First child child.children[1].classList.add('right-sidebar'); // Second child } log('Found those pesky elements.'); log(child); // Stop once the element is found and processed return; } }); }, }, { selector: 'body', // Start from the body element handler: (el) => { // log('Looking for postcards...'); const postCards = Array.from(el.querySelectorAll('[data-tag="post-card"]')); if (postCards.length === 0) return; // No post cards, so no further action // Map to store ancestors for each post-card const ancestorsMap = new Map(); // Step 1: Collect ancestors for each post-card element postCards.forEach(postCard => { let currentAncestor = postCard; const ancestors = []; // Traverse upwards and store ancestors while (currentAncestor) { ancestors.push(currentAncestor); currentAncestor = currentAncestor.parentElement; } // Add the ancestors to the map ancestorsMap.set(postCard, ancestors); }); // Step 2: Find the common ancestor let commonAncestor = null; // Initialize commonAncestor as the root element (or body) let possibleAncestors = ancestorsMap.get(postCards[0]); // For each ancestor in the first post-card, check if it's common in all other post-cards for (let ancestor of possibleAncestors) { let isCommon = true; // Check if this ancestor is present in all post-cards' ancestors for (let [postCard, ancestors] of ancestorsMap) { if (!ancestors.includes(ancestor)) { isCommon = false; break; } } // If it's common, update commonAncestor if (isCommon) { commonAncestor = ancestor; break; } } // Step 3: Assign the ID to the common ancestor if (commonAncestor) { commonAncestor.classList.add('post-card-container'); } } }, { selector: '[data-tag="IconPushpin"]', // Starting point: find the element with this selector handler: (el) => { const postCard = document.querySelector('[data-tag="post-card"]'); if (postCard) { // Get the ancestors of both elements const iconPushpinAncestors = getAncestors(el); const postCardAncestors = getAncestors(postCard); // Find the common ancestor let commonAncestor = null; for (let ancestor of iconPushpinAncestors) { if (postCardAncestors.includes(ancestor)) { commonAncestor = ancestor; break; } } // If a common ancestor is found, assign the ID if (commonAncestor) { commonAncestor.classList.add('pinned-post'); return true; // Successfully found and assigned the ID } } return false; // If no common ancestor was found }, }, { selector: 'header', handler: (el) => { let nextSibling = el.nextElementSibling; const siblings = []; // Loop through the siblings until there are no more while (nextSibling) { siblings.push(nextSibling); nextSibling = nextSibling.nextElementSibling; } // If we have at least 3 siblings, assign them IDs if (siblings.length >= 3) { siblings[0].classList.add('top-with-avatar'); siblings[1].classList.add('categories-bar'); siblings[2].classList.add('post-section'); } else if (siblings.length >= 1) { // Apply 'post-section' class to the last sibling, regardless of the number of siblings siblings[siblings.length - 1].classList.add('post-section'); } }, }, { selector: '[data-tag="post-iframe-wrapper"]', handler: (el) => { // Step 1: Find all ancestors let ancestor = el.parentElement; let currentVideoHolder = null; let shvFlag = false; let hvoFlag = false; let vhFlag = false; if ((el.classList.contains('shv')) && (el.classList.contains('hvo')) && (el.classList.contains('vh'))) { // log('This video has been accounted for.'); return; } while (ancestor) { // Step 2: Check for the [elevation="subtle"] attribute and add "subtle-has-video" class if (!el.classList.contains('shv')) { if (ancestor.hasAttribute('elevation') && ancestor.getAttribute('elevation') === 'subtle') { ancestor.classList.add('subtle-has-video'); shvFlag = true; } } // Step 3: Check if the ancestor hides overflow (based on computed style) and add "hides-video-overflow" class if (!el.classList.contains('hvo')) { const computedStyle = getComputedStyle(ancestor); if (computedStyle.overflow === 'hidden' || computedStyle.overflowX === 'hidden' || computedStyle.overflowY === 'hidden') { ancestor.classList.add('hides-video-overflow'); hvoFlag = true; } } // Step 4: Check if the ancestor's height is close to the height of el (within 16px tolerance) if (!el.classList.contains('vh')) { const ancestorHeight = ancestor.getBoundingClientRect().height; if ((Math.abs(ancestorHeight - el.getBoundingClientRect().height) <= 16) && (!ancestor.classList.contains('vh-checked'))) { ancestor.classList.add('video-holder'); ancestor.classList.add('vh-checked'); vhFlag = true; // If a previous ancestor was marked as 'video-holder', remove it if (currentVideoHolder && currentVideoHolder !== ancestor) { currentVideoHolder.classList.remove('video-holder'); } // Update the current video holder to this ancestor currentVideoHolder = ancestor; } } // Step 5: If the ancestor matches [data-tag="post-card"], stop (this is the final ancestor) if (ancestor.matches('[data-tag="post-card"]')) { if (shvFlag = true) { el.classList.add('shv'); } if (hvoFlag = true) { el.classList.add('hvo'); } if (vhFlag = true) { el.classList.add('vh'); } break; } // Move to the next ancestor ancestor = ancestor.parentElement; } }, }, { selector: '.post-section', // Target the post-section handler: (el) => { const postSectionHeight = parseFloat(window.getComputedStyle(el).height); // Get the computed height of .post-section if (postSectionHeight >= 400) { log('Finding width-limited descendents based on a post-section height of: ' + postSectionHeight); const validDescendants = []; // Array to store valid descendants // Traverse through all descendants const allDescendants = el.getElementsByTagName('*'); for (let child of allDescendants) { const childHeight = parseFloat(window.getComputedStyle(child).height); // Skip checking the children if the current child is too short if (childHeight < postSectionHeight * 0.5) { continue; } // If the child's height is at least 50% of the post-section's height, add it to the array if (childHeight >= postSectionHeight * 0.5) { validDescendants.push(child); } } // From the valid descendants, find those with a computed max-width validDescendants.forEach((descendant) => { const computedStyle = window.getComputedStyle(descendant); const maxWidth = computedStyle.maxWidth; // If max-width is set (not "none"), add the class if (maxWidth !== 'none') { descendant.classList.add('max-width-limited'); } }); } }, }, // Add more criteria as needed ]; // Handle elements that fit our criteria const handleElement = (element) => { // log('Handling element: ' + element); // log('Handling element...'); criteriaList.forEach((criterion) => { if (element.matches(criterion.selector)) { criterion.handler(element); } }); }; // Global variable to track idle state let isIdle = false; let idleInterval = null; // Interval that checks if idle time has passed let mutationTimeout; // Throttled function to handle mutations const throttledHandleMutations = throttle((mutationsList) => { mutationsList.forEach((mutation) => { // log('Mutation detected:', mutation); log('Mutation detected.'); // Reset the idleInterval whenever mutations occur isIdle = false; resetIdleInterval(); mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Only process element nodes setTimeout(() => { handleElement(node); // Optionally, handle child nodes or deep inspection here node.querySelectorAll('*').forEach(handleElement); // Check children as well }, 0); } }); }); }, 250); // 250ms throttle interval // Function to reset idleInterval and prevent going idle prematurely function resetIdleInterval() { // Clear any existing idleInterval clearTimeout(idleInterval); // Set a new idleInterval to mark system as idle after 500ms of inactivity idleInterval = setTimeout(() => { isIdle = true; // Mark system as idle after 500ms of inactivity log('System is now idle'); }, 2500); // 500ms idle period } function throttle(func, wait) { let timeout = null; let lastExec = 0; return function (...args) { const now = Date.now(); if (now - lastExec >= wait) { func.apply(this, args); lastExec = now; } else { clearTimeout(timeout); timeout = setTimeout(() => { func.apply(this, args); lastExec = now; }, wait - (now - lastExec)); } }; } let idcInProgress = false; let queuedIDC = false; // Function to perform an initial check of the DOM function initialDOMCheck(timeDelay) { const defaultDelay = 500; // if (isIdle) { // return; // } if (!timeDelay) { timeDelay = defaultDelay; // log('No time delay specified. Defaulting to ' + timeDelay); } if (idcInProgress) { queuedIDC = true; return; } log('Initial DOM check...'); idcInProgress = true; setTimeout(() =>{ idcInProgress = false; tryQueuedDelay(); }, timeDelay); try { // return; // log('Performing initial dom check...'); // Start from the body element const body = document.body; // Traverse and process all children of the body element as well body.querySelectorAll('*').forEach(handleElement); // Process all descendants of the body handleElement(body); // Process the body itself } catch (error) { log('Error occurred while appending buttons:', error); // Retry after 250ms if the operation fails setTimeout(initialDOMCheck, timeDelay); } function tryQueuedDelay() { if (queuedIDC) { queuedIDC = false; initialDOMCheck(timeDelay); } } } // Run the initial check to process existing elements initialDOMCheck(); // Initialize MutationObserver let observer = new MutationObserver(throttledHandleMutations); loadSettingsFromLocalStorage(); try { appendButtons(); } catch (error) { log('Error occurred while appending buttons:', error); // Retry after 250ms if the operation fails setTimeout(appendButtons, 250); } let preload = true; // Define the function function repeatPreloadCheck(recheckTime) { const defaultDelay = 500; if (!recheckTime) { recheckTime = defaultDelay; // log('No time delay specified. Defaulting to ' + timeDelay); } initialDOMCheck(recheckTime); // Repeat the check again after 1 second if preload is true if (preload) { setTimeout(() => repeatPreloadCheck(500), 500); } } // Set the initial timeout to start the check setTimeout(() => repeatPreloadCheck(500), 500); // Call repeatPreloadCheck after 1 second and pass 1000 window.addEventListener("load", function () { preload = false; log('DOM has loaded.'); // Initial DOM check initialDOMCheck(); loadSettingsFromLocalStorage(); setTimeout(function () { loadSettingsFromLocalStorage(); }, 1000); // loopAttachObservers(); let loadCount = 0; // Set a timeout to do the check again after a specified delay (e.g., 1000ms = 1 second) setTimeout(function repeatCheck() { initialDOMCheck(); loadCount++; // Repeat the check again after 1 second if count is less than 10 if (loadCount < 10) { setTimeout(repeatCheck, 1000); } }, 1000); // 1000ms = 1 second delay (you can adjust this delay as needed) }); // Configuration for the observer const config = { childList: true, // Watch for added/removed nodes subtree: true, // Watch the entire body and subtrees // attributes: true, // Watch for attribute changes }; // Start observing the document body function attachBodyObserver() { const bodyPresent = checkBody(); if (!bodyPresent) { // throw new Error('The body isn\'t ready.'); setTimeout(attachBodyObserver, 250); // Retry after 250ms return; // Exit early and do nothing further } observer.observe(document.body, config); log('Mutation observer is set up'); } attachBodyObserver(); // Save original functions const originalPushState = history.pushState; const originalReplaceState = history.replaceState; // Override pushState history.pushState = function (state, title, url) { log('pushState called:', { state, title, url }); preload = true; isIdle = false; repeatPreloadCheck(500); setTimeout(() => { preload = false; isIdle = true; }, 8000); // return originalPushState.apply(history, arguments); }; // // Override replaceState // history.replaceState = function (state, title, url) { // log('replaceState called:', { state, title, url }); // // return originalReplaceState.apply(history, arguments); // };
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址