Jupiter's Patreon Tools

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.1.01
// @author      -
// @description 3/23/2025, 6:50 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"] {
          max-width: ${this.value}px;
        }
      `;
				},
				on: false,
		},
		wideVideo: {
				id: 'wideVideo',
				type: 'button',
				buttonText: 'Wide Video',
				hiddenText: ' [active]',
				content: `
      .hides-video-overflow {
        overflow: visible;
        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" ID to the element
										child.classList.add('post-grid');

										// 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或关注我们的公众号极客氢云获取最新地址