- // ==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);
- // };