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.

  1. // ==UserScript==
  2. // @name Jupiter's Patreon Tools
  3. // @description Patreon has too much clutter. Simplify it with this script. Hide sidebars and pinned posts, make posts wider, and make videos bigger.
  4. // @namespace Violentmonkey Scripts
  5. // @license CC BY-SA
  6. // @match https://www.patreon.com/*
  7. // @grant none
  8. // @run-at document-start
  9. // @version 1.1.01
  10. // @author -
  11. // @description 3/23/2025, 6:50 PM
  12. // ==/UserScript==
  13.  
  14. // Save the original console methods
  15. const originalConsole = {
  16. log: console.log,
  17. warn: console.warn,
  18. error: console.error,
  19. info: console.info,
  20. debug: console.debug
  21. };
  22.  
  23. // After `es()` runs, restore the original console methods
  24. function restoreConsole() {
  25. console.log = originalConsole.log;
  26. console.warn = originalConsole.warn;
  27. console.error = originalConsole.error;
  28. console.info = originalConsole.info;
  29. console.debug = originalConsole.debug;
  30. }
  31.  
  32. const debug = false;
  33.  
  34. function log(...args) {
  35. throttle(() => {
  36. restoreConsole();
  37. }, 250); // 250ms throttle interval
  38. restoreConsole();
  39. if (!debug) {
  40. return;
  41. }
  42. console.log(...args);
  43. }
  44.  
  45. // Debounce function to ensure the observer isn't triggered too often
  46. const debounce = (func, delay) => {
  47. let timer;
  48. return (...args) => {
  49. clearTimeout(timer);
  50. timer = setTimeout(() => func(...args), delay);
  51. };
  52. };
  53.  
  54. let resizeObserver;
  55. let addenda = new Map();
  56.  
  57. function checkBody() {
  58. if (!document.body) {
  59. log('No document body.');
  60. return false;
  61. } else {
  62. return true;
  63. }
  64. }
  65.  
  66. // Declare global elements as constants
  67. const styleSheet = document.createElement('style');
  68. const staticStyles = document.createElement('style');
  69. staticStyles.innerHTML = `
  70. #interface-toggle-open-menu {
  71. position: fixed;
  72. margin: 16px;
  73. right: 0;
  74. bottom: 0;
  75. display: flex;
  76. padding: 4px;
  77. border-radius: 8px;
  78. transition: opacity 500ms;
  79. border-width: 1px;
  80. }
  81.  
  82. #interface-toggle-buttons {
  83. position: fixed;
  84. right: 0;
  85. bottom: 0;
  86. margin: 64px 16px;
  87. background: white;
  88. border: 1px solid black;
  89. padding: 12px;
  90. border-radius: 12px;
  91. }
  92.  
  93. #interface-toggle-buttons button [hidden] {
  94. display: none;
  95. }
  96.  
  97. #interface-toggle-buttons button .hidden-span {
  98. color: red;
  99. }
  100.  
  101. #interface-toggle-open-menu,
  102. #interface-toggle-buttons {
  103. border-color: #AAA;
  104. }
  105.  
  106. #interface-toggle-buttons-inner {
  107. display: flex;
  108. flex-direction: column;
  109. gap: 8px;
  110. }
  111.  
  112. #interface-toggle-open-menu svg {
  113. width: 24px;
  114. height: 24px;
  115. }
  116.  
  117. #interface-toggle-open-menu:not(.menu-open) {
  118. opacity: 0.25;
  119. }
  120.  
  121. #interface-toggle-open-menu:not(.menu-open):hover {
  122. opacity: 1;
  123. }
  124.  
  125. .input-and-checkbox-container {
  126. display: flex;
  127. align-items: center;
  128. gap: 8px;
  129. }
  130.  
  131. .input-and-checkbox-container span {
  132.  
  133. }
  134.  
  135. .input-and-checkbox-container input[type="number"] {
  136. width: 4em;
  137. text-align: center;
  138. }
  139.  
  140. .input-and-checkbox-container input[type="number"][disabled] {
  141. color: gray;
  142. }
  143.  
  144. #linktree-button {
  145. all: unset;
  146. background: #41df5d;
  147. height: 24px;
  148. width: 24px;
  149. border-radius: 20%;
  150. margin: 0 -8px -8px auto;
  151. cursor: pointer;
  152. }
  153.  
  154. #linktree-button svg {
  155. aspect-ratio: 1;
  156. padding: 15%;
  157. }
  158. `;
  159. document.head.appendChild(staticStyles);
  160.  
  161.  
  162. // Step 1: Define a map of items, each with an ID, content, and an on/off switch
  163. const itemMap = {
  164. leftSidebar: {
  165. id: 'leftSidebar',
  166. type: 'button', // Specify the type here
  167. buttonText: 'Left Sidebar',
  168. content: `
  169. #main-app-navigation {
  170. display: none;
  171. }
  172.  
  173. .main-body {
  174. margin-left: 0;
  175. }
  176. `,
  177. on: false,
  178. },
  179. rightSidebar: {
  180. id: 'rightSidebar',
  181. type: 'button', // Specify the type here
  182. buttonText: 'Right Sidebar',
  183. content: `
  184. .right-sidebar {
  185. display: none;
  186. }
  187.  
  188. .post-grid {
  189. display: unset;
  190. }
  191.  
  192. .post-grid-left {
  193.  
  194. }
  195.  
  196. figure[title="video thumbnail"] div[data-tag="media-container"] img {
  197. width: 100%;
  198. }
  199. `,
  200. extraFunction: moveSearch,
  201. on: false,
  202. },
  203. pinnedPost: {
  204. id: 'pinnedPost',
  205. type: 'button', // Specify the type here
  206. buttonText: 'Pinned Post',
  207. content: `
  208. .pinned-post {
  209. display: none;
  210. }
  211. `,
  212. on: false,
  213. },
  214. maxWidth: {
  215. id: 'maxWidth',
  216. type: 'inputAndCheckbox', // Specify the type here
  217. label: 'Post Max Width',
  218. value: 1024,
  219. get content() {
  220. return `
  221. .max-width-limited,
  222. [data-tag="collections-view"],
  223. [data-tag="about-patron-view"],
  224. [data-tag="membership-patron-view"],
  225. .post-section [class*="Areas_narrowContent"],
  226. .post-section [class*="Areas_wideContent"] {
  227. max-width: ${this.value}px;
  228. }
  229. `;
  230. },
  231. on: false,
  232. },
  233. wideVideo: {
  234. id: 'wideVideo',
  235. type: 'button',
  236. buttonText: 'Wide Video',
  237. hiddenText: ' [active]',
  238. content: `
  239. .hides-video-overflow {
  240. overflow: visible;
  241. box-shadow: none;
  242. }
  243.  
  244. .subtle-has-video {
  245. border-top-left-radius: unset;
  246. border-top-right-radius: unset;
  247. }
  248.  
  249. .video-holder {
  250. margin: 0 var(--video-negative-margin);
  251. max-height: 95vh;
  252. }
  253. `,
  254. on: false,
  255. },
  256. // Add more items here as needed
  257. };
  258.  
  259. function loadSettingsFromLocalStorage() {
  260. // Get the saved settings from localStorage
  261. const settings = JSON.parse(localStorage.getItem('itemSettings'));
  262.  
  263. // If settings exist, apply them
  264. if (settings) {
  265. for (const itemId in settings) {
  266. if (settings.hasOwnProperty(itemId)) {
  267. // Apply the saved settings by calling toggleItem with noSave set to true
  268. toggleItem(itemId, settings[itemId].on, true);
  269.  
  270. // If the item has a value, apply it
  271. if (settings[itemId].hasOwnProperty('value')) {
  272. itemMap[itemId].value = settings[itemId].value; // Set the saved value
  273. // updateItemValue(itemId); // This is a function to update the input box if needed
  274. }
  275. }
  276. }
  277. }
  278. }
  279.  
  280. // Step 2: Function to toggle the state of an item (on or off)
  281. function toggleItem(itemId, state, noSave) {
  282. if (itemMap[itemId]) {
  283. // If state is not specified, switch the item to its opposite state
  284. if (state === undefined) {
  285. itemMap[itemId].on = !itemMap[itemId].on;
  286. } else {
  287. itemMap[itemId].on = state;
  288. }
  289.  
  290. // Check if the item has an extraFunction and call it
  291. if (itemMap[itemId].extraFunction) {
  292. itemMap[itemId].extraFunction(itemMap[itemId].on); // Pass the new state to the function if needed
  293. }
  294.  
  295. if (!noSave) {
  296. // Save the updated item settings to localStorage by passing itemId and state
  297. saveSettingsToLocalStorage(itemId, itemMap[itemId].on);
  298. }
  299.  
  300. updateStylesheet();
  301.  
  302. // If the item is 'wideVideo' and the state is true, set up the resize observer
  303. if (itemId === 'wideVideo') {
  304. if (state === true) {
  305. // Initialize the resize observer
  306. try {
  307. attachResizeObserver();
  308. } catch (error) {
  309. log('Error occurred while attaching resize observer:', error);
  310. // Retry after 250ms if the operation fails
  311. setTimeout(attachResizeObserver, 250);
  312. }
  313. } else {
  314. // If state is false, disconnect the observer
  315. if (resizeObserver) {
  316. resizeObserver.disconnect();
  317. log('Resize observer disconnected.');
  318. }
  319. }
  320. }
  321. }
  322. }
  323.  
  324. let resizeTimeout;
  325.  
  326. function debounceResize(callback, wait) {
  327. return function () {
  328. clearTimeout(resizeTimeout);
  329. resizeTimeout = setTimeout(callback, wait);
  330. };
  331. }
  332.  
  333. function attachResizeObserver() {
  334. const bodyPresent = checkBody();
  335. if (!bodyPresent) {
  336. // throw new Error('The body isn\'t ready.');
  337. setTimeout(attachResizeObserver, 250); // Retry after 250ms
  338. return; // Exit early and do nothing further
  339. }
  340. const debouncedResize = debounceResize(() => {
  341. log('Window size changed!');
  342. videoRescale();
  343. }, 250); // 250ms debounce delay
  344.  
  345. // Create the ResizeObserver
  346. resizeObserver = new ResizeObserver(debouncedResize);
  347.  
  348. // Start observing the window (or document body, or any other specific element)
  349. resizeObserver.observe(document.body);
  350. }
  351.  
  352. function videoRescale() {
  353. // Get the body width
  354. const bodyWidth = document.body.offsetWidth;
  355.  
  356. // Query all .video-holder elements
  357. const videoHolders = document.querySelectorAll('.video-holder');
  358.  
  359. let finalNegativeMargin = null;
  360.  
  361. // Loop through each video-holder
  362. videoHolders.forEach((videoHolder) => {
  363. // Get the width of the video holder, including margins
  364. const style = window.getComputedStyle(videoHolder);
  365. const videoHolderWidth = videoHolder.offsetWidth;
  366. const marginLeft = parseInt(style.marginLeft, 10);
  367. const marginRight = parseInt(style.marginRight, 10);
  368.  
  369. // Calculate the total width of the video holder including margins
  370. const totalWidth = videoHolderWidth + marginLeft + marginRight;
  371.  
  372. // Calculate the difference between the body width and video holder width
  373. const difference = bodyWidth - totalWidth;
  374.  
  375. // Divide the difference by 2
  376. const negativeMargin = difference / 2;
  377.  
  378. log('Calculated negative margin: ' + negativeMargin);
  379.  
  380. if (finalNegativeMargin === null || finalNegativeMargin > negativeMargin) {
  381. finalNegativeMargin = negativeMargin;
  382. }
  383. });
  384.  
  385. const marginStyle = `
  386. :root {
  387. --video-negative-margin: ${finalNegativeMargin * -1}px;
  388. }
  389. `;
  390.  
  391. addToAddenda('negative-margin', marginStyle);
  392. }
  393.  
  394. function addToAddenda(id, content) {
  395. if (addenda.hasOwnProperty(id)) {
  396. // If the entry exists, modify its content
  397. addenda[id].content = content;
  398. } else {
  399. // If the entry doesn't exist, create it and set its content
  400. addenda[id] = {
  401. content: content
  402. };
  403. }
  404.  
  405. updateStylesheet();
  406. }
  407.  
  408. function saveSettingsToLocalStorage(itemId, state) {
  409. const settings = JSON.parse(localStorage.getItem('itemSettings')) || {};
  410.  
  411. // Update the specific item's state
  412. settings[itemId] = {
  413. id: itemId,
  414. on: state,
  415. // Check if the item has a value property, and if it does, include it in the saved state
  416. ...(itemMap[itemId]?.value !== undefined ? {
  417. value: itemMap[itemId].value
  418. } : {})
  419. };
  420.  
  421. // Save the updated settings to localStorage as a JSON string
  422. localStorage.setItem('itemSettings', JSON.stringify(settings));
  423. }
  424.  
  425. // Array to store hidden ancestors
  426. let hiddenAncestorsOfSearchBox = [];
  427.  
  428. function moveSearch(state) {
  429. log('Move Search called with state:', state);
  430.  
  431. // Select the search box inside #post-grid-left
  432. const searchBox = document.querySelector('#post-grid-left [data-tag="search-input-box"]');
  433.  
  434. if (searchBox) {
  435. // Step 1: Loop through ancestors and find hidden ones
  436. let currentElement = searchBox.parentElement;
  437. while (currentElement) {
  438. if (getComputedStyle(currentElement).display === 'none') {
  439. log('Hidden ancestor found:', currentElement);
  440. hiddenAncestorsOfSearchBox.push(currentElement); // Store the hidden ancestor
  441. currentElement.style.display = 'unset'; // Set its display to unset
  442. }
  443. currentElement = currentElement.parentElement;
  444. }
  445.  
  446. if (state) {
  447. // Step 2: If state is true, display the hidden ancestors
  448. log('State is true, ancestors displayed');
  449. // We already set the display to 'unset' in the loop above
  450. } else {
  451. // Step 3: If state is false, revert the display property of hidden ancestors
  452. log('State is false, reverting ancestors to hidden');
  453. hiddenAncestorsOfSearchBox.forEach((ancestor) => {
  454. ancestor.style.display = ''; // Remove the display property altogether
  455. });
  456. // Clear the hiddenAncestorsOfSearchBox array after reverting
  457. hiddenAncestorsOfSearchBox = [];
  458. }
  459. } else {
  460. log('Search box not found.');
  461. }
  462. }
  463.  
  464.  
  465. // Step 3: Function to build the stylesheet based on the state of the items
  466. function buildStylesheet() {
  467. let styleSheetContent = '';
  468.  
  469. // Loop through each item and only include it if it's "on"
  470. for (const key in itemMap) {
  471. const item = itemMap[key];
  472. // log(item.id, item.on);
  473. if (item.on) {
  474. styleSheetContent += item.content + '\n'; // Add the content of the item if it's "on"
  475. }
  476. }
  477.  
  478. styleSheetContent += '\n';
  479.  
  480. // Add the addenda items
  481.  
  482. for (const key in addenda) {
  483. const addendum = addenda[key];
  484. styleSheetContent += addendum.content + '\n'; // Add each item from the addenda
  485. }
  486.  
  487.  
  488. return styleSheetContent;
  489. }
  490.  
  491. // Step 4: Function to insert the stylesheet into the document
  492. function updateStylesheet() {
  493. styleSheet.id = 'dynamic-stylesheet';
  494. styleSheet.type = 'text/css';
  495. styleSheet.innerHTML = buildStylesheet();
  496. document.head.appendChild(styleSheet);
  497. }
  498.  
  499. // Initial stylesheet insertion
  500. updateStylesheet();
  501.  
  502. // Example of how to toggle items on/off
  503. // toggleItem('leftSidebar', true); // Turns the left sidebar rule on
  504.  
  505. function toggleHiddenSpan(button, item, state) {
  506. const hiddenSpan = button.querySelector('.hidden-span');
  507.  
  508. if (!hiddenSpan) {
  509. log('Hidden span not found on button ' + item.id + '.');
  510. return;
  511. }
  512.  
  513. if (state === true) {
  514. log(item.id + 'is active.');
  515. hiddenSpan.removeAttribute('hidden');
  516. } else {
  517. log(item.id + 'is inactive.');
  518. hiddenSpan.setAttribute('hidden', '');
  519. }
  520. }
  521.  
  522.  
  523. // Step 3: Function to generate buttons for each item and return them as an array
  524. function generateButtons() {
  525. const buttons = [];
  526.  
  527. // Loop through each item in the itemMap
  528. for (const key in itemMap) {
  529. const item = itemMap[key];
  530.  
  531. if (item.type === 'button') {
  532. // Create a new button element
  533. const button = document.createElement('button');
  534. button.textContent = item.buttonText;
  535. const hiddenSpan = document.createElement('span');
  536. hiddenSpan.classList.add('hidden-span');
  537. if (!item.hiddenText) {
  538. hiddenSpan.textContent = ' [hidden]';
  539. } else {
  540. hiddenSpan.textContent = item.hiddenText;
  541. }
  542.  
  543. button.appendChild(hiddenSpan);
  544.  
  545. toggleHiddenSpan(button, item, item.on);
  546.  
  547. // Attach a click event to toggle the item's state when the button is clicked
  548. button.addEventListener('click', () => {
  549. event.stopPropagation(); // Stop the click event from bubbling up
  550. toggleItem(item.id); // Toggle the item by its ID
  551. initialDOMCheck();
  552. toggleHiddenSpan(button, item, item.on);
  553. });
  554.  
  555. // Push the created button to the buttons array
  556. buttons.push(button);
  557. }
  558.  
  559. if (item.type === 'inputAndCheckbox') {
  560. // Create a div container for input and checkbox
  561. const container = document.createElement('div');
  562. container.classList.add('input-and-checkbox-container');
  563.  
  564. // Create the label span
  565. const labelSpan = document.createElement('span');
  566. labelSpan.textContent = item.label;
  567.  
  568. // Create the input box (grayed out initially)
  569. const inputBox = document.createElement('input');
  570. inputBox.type = 'number';
  571. inputBox.value = item.value;
  572. inputBox.disabled = !item.on; // Initially grayed out
  573.  
  574. // Create the checkbox
  575. const checkbox = document.createElement('input');
  576. checkbox.type = 'checkbox';
  577. checkbox.checked = item.on;
  578.  
  579. // Add a listener to the checkbox to toggle the input box's editable state
  580. checkbox.addEventListener('change', () => {
  581. inputBox.disabled = !checkbox.checked; // Enable/Disable input box based on checkbox
  582. if (!checkbox.checked) {
  583. // When unchecked, reset the input value to the original one
  584. inputBox.value = item.value;
  585. }
  586. toggleItem(item.id, checkbox.checked);
  587. });
  588.  
  589. // Add a listener to the input box to update the item value and toggle it
  590. let debounceTimer;
  591. inputBox.addEventListener('input', () => {
  592. clearTimeout(debounceTimer);
  593. debounceTimer = setTimeout(() => {
  594. item.value = parseInt(inputBox.value, 10);
  595. toggleItem(item.id, false); // Pass second argument as false
  596. toggleItem(item.id, true); // Pass second argument as true
  597. }, 250); // Debounced for 250ms
  598. });
  599.  
  600. // Append the label, input box, and checkbox to the container
  601. container.appendChild(labelSpan);
  602. container.appendChild(inputBox);
  603. container.appendChild(checkbox);
  604.  
  605. // Push the container to the buttons array
  606. buttons.push(container);
  607. }
  608. }
  609.  
  610. // Return the array of buttons and input-and-checkbox containers
  611. return buttons;
  612. }
  613.  
  614. // Step 4: Function to append the buttons to a specific container
  615. function appendButtons() {
  616. const container = document.createElement('div');
  617. container.id = 'interface-toggle-buttons';
  618. const subContainer = document.createElement('div');
  619. subContainer.id = 'interface-toggle-buttons-inner';
  620.  
  621. // Set the container's initial visibility to hidden
  622. container.style.display = 'none';
  623.  
  624. // Generate the buttons
  625. const buttons = generateButtons();
  626.  
  627. // Clear the container and append the buttons
  628. buttons.forEach(button => {
  629. subContainer.appendChild(button);
  630. });
  631.  
  632. const linktreeButton = document.createElement('button');
  633. linktreeButton.id = 'linktree-button';
  634. linktreeButton.innerHTML = `
  635. <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">
  636. <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
  637. L16.5,72.7L7,63.2l17.3-16.7H0V33.1H0.2z M33.1,65.8h14.2v32H33.1V65.8z">
  638. </path>
  639. </svg>
  640. `;
  641.  
  642. linktreeButton.addEventListener('click', () => {
  643. window.open('https://linktr.ee/jupiterliar', '_blank');
  644. });
  645.  
  646. subContainer.appendChild(linktreeButton);
  647.  
  648. container.appendChild(subContainer);
  649.  
  650. // Create the wrench icon button
  651. const wrenchButton = document.createElement('button');
  652. wrenchButton.id = 'interface-toggle-open-menu';
  653. wrenchButton.innerHTML = `
  654. <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)">
  655. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  656. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  657. <g id="SVGRepo_iconCarrier">
  658. <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>
  659. </g>
  660. </svg>
  661. `;
  662.  
  663. // Attach a click event to toggle the visibility of the button container
  664. wrenchButton.addEventListener('click', () => {
  665. event.stopPropagation(); // Stop the click event from bubbling up
  666. if (container.style.display === 'none') {
  667. container.style.display = 'block';
  668. wrenchButton.classList.add('menu-open');
  669. log('Displaying config buttons...');
  670. } else {
  671. log('Hiding config buttons at point 1...');
  672. container.style.display = 'none';
  673. wrenchButton.classList.remove('menu-open');
  674. }
  675. });
  676.  
  677. function appendButtonPair() {
  678. if (document.body) {
  679. // Add the wrench button before the container
  680. document.body.appendChild(wrenchButton);
  681.  
  682. // Add the button container to the body
  683. document.body.appendChild(container);
  684. } else {
  685. log('Body not available yet, retrying...');
  686. setTimeout(appendButtonPair, 250); // Retry after 250ms if the body is not available
  687. }
  688. }
  689.  
  690. try {
  691. appendButtonPair();
  692. } catch (error) {
  693. log('Error occurred while appending buttons:', error);
  694. // Retry after 250ms if the operation fails
  695. setTimeout(appendButtonPair, 250);
  696. }
  697.  
  698.  
  699.  
  700. // Click outside the button container to hide it
  701. document.addEventListener('click', (event) => {
  702. if (!container.contains(event.target) && event.target !== wrenchButton) {
  703. log('Hiding config buttons at point 2...');
  704. container.style.display = 'none';
  705. wrenchButton.classList.remove('menu-open');
  706. }
  707. });
  708. }
  709.  
  710. // Helper function to get all ancestors of an element
  711. function getAncestors(el) {
  712. let ancestors = [];
  713. let currentElement = el;
  714.  
  715. while (currentElement.parentElement) {
  716. ancestors.push(currentElement.parentElement);
  717. currentElement = currentElement.parentElement;
  718. }
  719.  
  720. return ancestors;
  721. }
  722.  
  723. // List of criteria to find elements
  724. const criteriaList = [
  725. {
  726. // Traversing the DOM and applying the existing criteria
  727. selector: 'body', // Starting point: body element
  728. handler: (el) => {
  729. const allElements = el.getElementsByTagName('*'); // Select all elements under body
  730. const viewportWidth = window.innerWidth;
  731. const existingMainBody = document.body.querySelector('.main-body');
  732.  
  733. if (!existingMainBody) {
  734.  
  735.  
  736. // Step 1: Find elements matching the criteria
  737. const matchingElements = [];
  738.  
  739. for (let child of allElements) {
  740. // Skip if the element is a <script> tag
  741. if (child.tagName.toLowerCase() === 'script') {
  742. continue;
  743. }
  744.  
  745. const style = window.getComputedStyle(child);
  746.  
  747. // Skip if the element is not visible (display: none or visibility: hidden)
  748. if (style.display === 'none' || style.visibility === 'hidden') {
  749. continue;
  750. }
  751.  
  752. const rect = child.getBoundingClientRect();
  753.  
  754. // Skip if the element is too small (width < 50% of viewport)
  755. if (rect.width < Math.floor(viewportWidth * 0.5)) {
  756. continue;
  757. }
  758.  
  759. // If the element's left margin is greater than 20px
  760. const leftMargin = parseInt(style.marginLeft, 10);
  761. if (isNaN(leftMargin) || leftMargin <= 20) {
  762. continue; // Skip if the left margin is not greater than 20px
  763. }
  764.  
  765. // Step 2: Collect matching elements
  766. matchingElements.push(child);
  767. }
  768.  
  769. // Step 3: Evaluate the array of matching elements
  770. if (matchingElements.length === 1) {
  771. // Only one element matches, apply the class
  772. matchingElements[0].classList.add('main-body');
  773. } else if (matchingElements.length > 1) {
  774. // Multiple matching elements, find the ancestor
  775. let ancestor = null;
  776.  
  777. // Check if one element is an ancestor of the others
  778. for (let i = 0; i < matchingElements.length; i++) {
  779. const currentElement = matchingElements[i];
  780. let isAncestor = true;
  781.  
  782. // Check if this element is an ancestor of the others
  783. for (let j = 0; j < matchingElements.length; j++) {
  784. if (i === j) continue; // Skip itself
  785.  
  786. const otherElement = matchingElements[j];
  787. if (!currentElement.contains(otherElement)) {
  788. isAncestor = false;
  789. break;
  790. }
  791. }
  792.  
  793. if (isAncestor) {
  794. ancestor = currentElement;
  795. break;
  796. }
  797. }
  798.  
  799. // If an ancestor is found, apply the class to it
  800. if (ancestor) {
  801. ancestor.classList.add('main-body');
  802. }
  803. }
  804. }
  805. },
  806. },
  807.  
  808. {
  809. // Traversing the DOM and applying the criteria
  810. selector: '#main-app-navigation', // Look for the element with this ID
  811. handler: (el) => {
  812. //log('Found #main-app-navigation');
  813. el.classList.add('left-sidebar'); // Add the "left-sidebar" class
  814. },
  815. },
  816.  
  817. {
  818. selector: 'body', // Starting point: body element
  819. handler: (el) => {
  820. const children = el.querySelectorAll('*'); // Find all descendants
  821. const viewportWidth = window.innerWidth;
  822.  
  823. const areaSpanTwo = el.querySelector('[class*="areaSpanTwo"]');
  824.  
  825. if (areaSpanTwo) {
  826. log('areaSpanTwo is present.');
  827. }
  828.  
  829. // Traverse all descendants of the body
  830. children.forEach(child => {
  831. const rect = child.getBoundingClientRect();
  832.  
  833. if (Array.from(child.classList).some(className => className.includes('areaSpanTwo'))) {
  834. log('Class includes "areaSpanTwo". Width:', parseFloat(rect.width));
  835. }
  836.  
  837. // If the element's width is less than 50% of the viewport, skip it
  838. if (rect.width < Math.floor(viewportWidth * 0.5)) {
  839. return;
  840. }
  841.  
  842. // Check if it has a grid display style
  843. const style = window.getComputedStyle(child);
  844. const display = style.getPropertyValue('display');
  845. const gridTemplateColumns = style.getPropertyValue('grid-template-columns').trim();
  846.  
  847. if (display === 'grid') {
  848. // log('Investigating element:', child);
  849. }
  850.  
  851. // If it's a grid and has exactly three columns (just check for 3 values)
  852. if (display === 'grid' && gridTemplateColumns.split(' ').length === 3) {
  853. // Assign the "post-grid" ID to the element
  854. child.classList.add('post-grid');
  855.  
  856. // If the element has children, assign the required IDs to the first two
  857. if (child.children.length > 1) {
  858. child.children[0].classList.add('post-grid-left'); // First child
  859. child.children[1].classList.add('right-sidebar'); // Second child
  860. }
  861.  
  862. log('Found those pesky elements.');
  863. log(child);
  864.  
  865. // Stop once the element is found and processed
  866. return;
  867. }
  868. });
  869. },
  870. },
  871.  
  872. {
  873. selector: 'body', // Start from the body element
  874. handler: (el) => {
  875. // log('Looking for postcards...');
  876. const postCards = Array.from(el.querySelectorAll('[data-tag="post-card"]'));
  877.  
  878. if (postCards.length === 0) return; // No post cards, so no further action
  879.  
  880. // Map to store ancestors for each post-card
  881. const ancestorsMap = new Map();
  882.  
  883. // Step 1: Collect ancestors for each post-card element
  884. postCards.forEach(postCard => {
  885. let currentAncestor = postCard;
  886. const ancestors = [];
  887.  
  888. // Traverse upwards and store ancestors
  889. while (currentAncestor) {
  890. ancestors.push(currentAncestor);
  891. currentAncestor = currentAncestor.parentElement;
  892. }
  893.  
  894. // Add the ancestors to the map
  895. ancestorsMap.set(postCard, ancestors);
  896. });
  897.  
  898. // Step 2: Find the common ancestor
  899. let commonAncestor = null;
  900.  
  901. // Initialize commonAncestor as the root element (or body)
  902. let possibleAncestors = ancestorsMap.get(postCards[0]);
  903.  
  904. // For each ancestor in the first post-card, check if it's common in all other post-cards
  905. for (let ancestor of possibleAncestors) {
  906. let isCommon = true;
  907.  
  908. // Check if this ancestor is present in all post-cards' ancestors
  909. for (let [postCard, ancestors] of ancestorsMap) {
  910. if (!ancestors.includes(ancestor)) {
  911. isCommon = false;
  912. break;
  913. }
  914. }
  915.  
  916. // If it's common, update commonAncestor
  917. if (isCommon) {
  918. commonAncestor = ancestor;
  919. break;
  920. }
  921. }
  922.  
  923. // Step 3: Assign the ID to the common ancestor
  924. if (commonAncestor) {
  925. commonAncestor.classList.add('post-card-container');
  926. }
  927. }
  928. },
  929.  
  930. {
  931. selector: '[data-tag="IconPushpin"]', // Starting point: find the element with this selector
  932. handler: (el) => {
  933. const postCard = document.querySelector('[data-tag="post-card"]');
  934. if (postCard) {
  935. // Get the ancestors of both elements
  936. const iconPushpinAncestors = getAncestors(el);
  937. const postCardAncestors = getAncestors(postCard);
  938.  
  939. // Find the common ancestor
  940. let commonAncestor = null;
  941. for (let ancestor of iconPushpinAncestors) {
  942. if (postCardAncestors.includes(ancestor)) {
  943. commonAncestor = ancestor;
  944. break;
  945. }
  946. }
  947.  
  948. // If a common ancestor is found, assign the ID
  949. if (commonAncestor) {
  950. commonAncestor.classList.add('pinned-post');
  951. return true; // Successfully found and assigned the ID
  952. }
  953. }
  954.  
  955. return false; // If no common ancestor was found
  956. },
  957. },
  958.  
  959. {
  960. selector: 'header',
  961. handler: (el) => {
  962. let nextSibling = el.nextElementSibling;
  963. const siblings = [];
  964.  
  965. // Loop through the siblings until there are no more
  966. while (nextSibling) {
  967. siblings.push(nextSibling);
  968. nextSibling = nextSibling.nextElementSibling;
  969. }
  970.  
  971. // If we have at least 3 siblings, assign them IDs
  972. if (siblings.length >= 3) {
  973. siblings[0].classList.add('top-with-avatar');
  974. siblings[1].classList.add('categories-bar');
  975. siblings[2].classList.add('post-section');
  976. } else if (siblings.length >= 1) {
  977. // Apply 'post-section' class to the last sibling, regardless of the number of siblings
  978. siblings[siblings.length - 1].classList.add('post-section');
  979. }
  980. },
  981. },
  982.  
  983. {
  984. selector: '[data-tag="post-iframe-wrapper"]',
  985. handler: (el) => {
  986. // Step 1: Find all ancestors
  987. let ancestor = el.parentElement;
  988. let currentVideoHolder = null;
  989. let shvFlag = false;
  990. let hvoFlag = false;
  991. let vhFlag = false;
  992.  
  993. if ((el.classList.contains('shv')) && (el.classList.contains('hvo')) && (el.classList.contains('vh'))) {
  994. // log('This video has been accounted for.');
  995. return;
  996. }
  997.  
  998. while (ancestor) {
  999. // Step 2: Check for the [elevation="subtle"] attribute and add "subtle-has-video" class
  1000. if (!el.classList.contains('shv')) {
  1001. if (ancestor.hasAttribute('elevation') && ancestor.getAttribute('elevation') === 'subtle') {
  1002. ancestor.classList.add('subtle-has-video');
  1003. shvFlag = true;
  1004. }
  1005. }
  1006.  
  1007. // Step 3: Check if the ancestor hides overflow (based on computed style) and add "hides-video-overflow" class
  1008. if (!el.classList.contains('hvo')) {
  1009. const computedStyle = getComputedStyle(ancestor);
  1010. if (computedStyle.overflow === 'hidden' || computedStyle.overflowX === 'hidden' || computedStyle.overflowY === 'hidden') {
  1011. ancestor.classList.add('hides-video-overflow');
  1012. hvoFlag = true;
  1013. }
  1014. }
  1015.  
  1016. // Step 4: Check if the ancestor's height is close to the height of el (within 16px tolerance)
  1017. if (!el.classList.contains('vh')) {
  1018. const ancestorHeight = ancestor.getBoundingClientRect().height;
  1019. if ((Math.abs(ancestorHeight - el.getBoundingClientRect().height) <= 16) && (!ancestor.classList.contains('vh-checked'))) {
  1020. ancestor.classList.add('video-holder');
  1021. ancestor.classList.add('vh-checked');
  1022. vhFlag = true;
  1023.  
  1024. // If a previous ancestor was marked as 'video-holder', remove it
  1025. if (currentVideoHolder && currentVideoHolder !== ancestor) {
  1026. currentVideoHolder.classList.remove('video-holder');
  1027.  
  1028. }
  1029.  
  1030. // Update the current video holder to this ancestor
  1031. currentVideoHolder = ancestor;
  1032. }
  1033. }
  1034.  
  1035. // Step 5: If the ancestor matches [data-tag="post-card"], stop (this is the final ancestor)
  1036. if (ancestor.matches('[data-tag="post-card"]')) {
  1037. if (shvFlag = true) {
  1038. el.classList.add('shv');
  1039. }
  1040. if (hvoFlag = true) {
  1041. el.classList.add('hvo');
  1042. }
  1043. if (vhFlag = true) {
  1044. el.classList.add('vh');
  1045. }
  1046. break;
  1047. }
  1048.  
  1049. // Move to the next ancestor
  1050. ancestor = ancestor.parentElement;
  1051. }
  1052. },
  1053. },
  1054.  
  1055. {
  1056. selector: '.post-section', // Target the post-section
  1057. handler: (el) => {
  1058. const postSectionHeight = parseFloat(window.getComputedStyle(el).height); // Get the computed height of .post-section
  1059.  
  1060. if (postSectionHeight >= 400) {
  1061. log('Finding width-limited descendents based on a post-section height of: ' + postSectionHeight);
  1062.  
  1063. const validDescendants = []; // Array to store valid descendants
  1064.  
  1065. // Traverse through all descendants
  1066. const allDescendants = el.getElementsByTagName('*');
  1067. for (let child of allDescendants) {
  1068. const childHeight = parseFloat(window.getComputedStyle(child).height);
  1069.  
  1070. // Skip checking the children if the current child is too short
  1071. if (childHeight < postSectionHeight * 0.5) {
  1072. continue;
  1073. }
  1074.  
  1075. // If the child's height is at least 50% of the post-section's height, add it to the array
  1076. if (childHeight >= postSectionHeight * 0.5) {
  1077. validDescendants.push(child);
  1078. }
  1079. }
  1080.  
  1081. // From the valid descendants, find those with a computed max-width
  1082. validDescendants.forEach((descendant) => {
  1083. const computedStyle = window.getComputedStyle(descendant);
  1084. const maxWidth = computedStyle.maxWidth;
  1085.  
  1086. // If max-width is set (not "none"), add the class
  1087. if (maxWidth !== 'none') {
  1088. descendant.classList.add('max-width-limited');
  1089. }
  1090. });
  1091. }
  1092. },
  1093. },
  1094.  
  1095.  
  1096.  
  1097. // Add more criteria as needed
  1098. ];
  1099.  
  1100. // Handle elements that fit our criteria
  1101. const handleElement = (element) => {
  1102. // log('Handling element: ' + element);
  1103. // log('Handling element...');
  1104. criteriaList.forEach((criterion) => {
  1105. if (element.matches(criterion.selector)) {
  1106. criterion.handler(element);
  1107. }
  1108. });
  1109. };
  1110.  
  1111. // Global variable to track idle state
  1112. let isIdle = false;
  1113. let idleInterval = null; // Interval that checks if idle time has passed
  1114.  
  1115. let mutationTimeout;
  1116.  
  1117. // Throttled function to handle mutations
  1118. const throttledHandleMutations = throttle((mutationsList) => {
  1119. mutationsList.forEach((mutation) => {
  1120. // log('Mutation detected:', mutation);
  1121. log('Mutation detected.');
  1122. // Reset the idleInterval whenever mutations occur
  1123. isIdle = false;
  1124. resetIdleInterval();
  1125. mutation.addedNodes.forEach((node) => {
  1126. if (node.nodeType === 1) { // Only process element nodes
  1127. setTimeout(() => {
  1128. handleElement(node);
  1129. // Optionally, handle child nodes or deep inspection here
  1130. node.querySelectorAll('*').forEach(handleElement); // Check children as well
  1131. }, 0);
  1132. }
  1133. });
  1134. });
  1135. }, 250); // 250ms throttle interval
  1136.  
  1137. // Function to reset idleInterval and prevent going idle prematurely
  1138. function resetIdleInterval() {
  1139.  
  1140. // Clear any existing idleInterval
  1141. clearTimeout(idleInterval);
  1142.  
  1143. // Set a new idleInterval to mark system as idle after 500ms of inactivity
  1144. idleInterval = setTimeout(() => {
  1145. isIdle = true; // Mark system as idle after 500ms of inactivity
  1146. log('System is now idle');
  1147. }, 2500); // 500ms idle period
  1148. }
  1149.  
  1150. function throttle(func, wait) {
  1151. let timeout = null;
  1152. let lastExec = 0;
  1153.  
  1154. return function (...args) {
  1155. const now = Date.now();
  1156. if (now - lastExec >= wait) {
  1157. func.apply(this, args);
  1158. lastExec = now;
  1159. } else {
  1160. clearTimeout(timeout);
  1161. timeout = setTimeout(() => {
  1162. func.apply(this, args);
  1163. lastExec = now;
  1164. }, wait - (now - lastExec));
  1165. }
  1166. };
  1167. }
  1168.  
  1169. let idcInProgress = false;
  1170. let queuedIDC = false;
  1171.  
  1172. // Function to perform an initial check of the DOM
  1173. function initialDOMCheck(timeDelay) {
  1174. const defaultDelay = 500;
  1175.  
  1176. // if (isIdle) {
  1177. // return;
  1178. // }
  1179.  
  1180. if (!timeDelay) {
  1181. timeDelay = defaultDelay;
  1182. // log('No time delay specified. Defaulting to ' + timeDelay);
  1183. }
  1184.  
  1185. if (idcInProgress) {
  1186. queuedIDC = true;
  1187. return;
  1188. }
  1189.  
  1190. log('Initial DOM check...');
  1191.  
  1192. idcInProgress = true;
  1193.  
  1194. setTimeout(() =>{
  1195. idcInProgress = false;
  1196. tryQueuedDelay;
  1197. }, timeDelay);
  1198.  
  1199. try {
  1200. // return;
  1201. // log('Performing initial dom check...');
  1202. // Start from the body element
  1203. const body = document.body;
  1204. // Traverse and process all children of the body element as well
  1205. body.querySelectorAll('*').forEach(handleElement); // Process all descendants of the body
  1206. handleElement(body); // Process the body itself
  1207. } catch (error) {
  1208. log('Error occurred while appending buttons:', error);
  1209. // Retry after 250ms if the operation fails
  1210. setTimeout(initialDOMCheck, timeDelay);
  1211. }
  1212.  
  1213. function tryQueuedDelay() {
  1214. if (queuedIDC) {
  1215. queuedIDC = false;
  1216. initialDOMCheck(timeDelay);
  1217. }
  1218. }
  1219. }
  1220.  
  1221. // Run the initial check to process existing elements
  1222. initialDOMCheck();
  1223.  
  1224. // Initialize MutationObserver
  1225. let observer = new MutationObserver(throttledHandleMutations);
  1226.  
  1227. loadSettingsFromLocalStorage();
  1228.  
  1229. try {
  1230. appendButtons();
  1231. } catch (error) {
  1232. log('Error occurred while appending buttons:', error);
  1233. // Retry after 250ms if the operation fails
  1234. setTimeout(appendButtons, 250);
  1235. }
  1236.  
  1237. let preload = true;
  1238.  
  1239. // Define the function
  1240. function repeatPreloadCheck(recheckTime) {
  1241. const defaultDelay = 500;
  1242.  
  1243. if (!recheckTime) {
  1244. recheckTime = defaultDelay;
  1245. // log('No time delay specified. Defaulting to ' + timeDelay);
  1246. }
  1247.  
  1248. initialDOMCheck(recheckTime);
  1249.  
  1250. // Repeat the check again after 1 second if preload is true
  1251. if (preload) {
  1252. setTimeout(() => repeatPreloadCheck(500), 500);
  1253. }
  1254. }
  1255.  
  1256. // Set the initial timeout to start the check
  1257. setTimeout(() => repeatPreloadCheck(500), 500); // Call repeatPreloadCheck after 1 second and pass 1000
  1258.  
  1259. window.addEventListener("load", function () {
  1260. preload = false;
  1261. log('DOM has loaded.');
  1262.  
  1263. // Initial DOM check
  1264. initialDOMCheck();
  1265. loadSettingsFromLocalStorage();
  1266.  
  1267. setTimeout(function () {
  1268. loadSettingsFromLocalStorage();
  1269. }, 1000);
  1270.  
  1271. // loopAttachObservers();
  1272.  
  1273. let loadCount = 0;
  1274. // Set a timeout to do the check again after a specified delay (e.g., 1000ms = 1 second)
  1275. setTimeout(function repeatCheck() {
  1276. initialDOMCheck();
  1277. loadCount++;
  1278.  
  1279. // Repeat the check again after 1 second if count is less than 10
  1280. if (loadCount < 10) {
  1281. setTimeout(repeatCheck, 1000);
  1282. }
  1283. }, 1000); // 1000ms = 1 second delay (you can adjust this delay as needed)
  1284. });
  1285.  
  1286. // Configuration for the observer
  1287. const config = {
  1288. childList: true, // Watch for added/removed nodes
  1289. subtree: true, // Watch the entire body and subtrees
  1290. // attributes: true, // Watch for attribute changes
  1291. };
  1292.  
  1293. // Start observing the document body
  1294. function attachBodyObserver() {
  1295. const bodyPresent = checkBody();
  1296. if (!bodyPresent) {
  1297. // throw new Error('The body isn\'t ready.');
  1298. setTimeout(attachBodyObserver, 250); // Retry after 250ms
  1299. return; // Exit early and do nothing further
  1300. }
  1301.  
  1302. observer.observe(document.body, config);
  1303. log('Mutation observer is set up');
  1304. }
  1305.  
  1306. attachBodyObserver();
  1307.  
  1308. // Save original functions
  1309. const originalPushState = history.pushState;
  1310. const originalReplaceState = history.replaceState;
  1311.  
  1312. // Override pushState
  1313. history.pushState = function (state, title, url) {
  1314. log('pushState called:', { state, title, url });
  1315. preload = true;
  1316. isIdle = false;
  1317. repeatPreloadCheck(500);
  1318. setTimeout(() => {
  1319. preload = false;
  1320. isIdle = true;
  1321. }, 8000);
  1322. // return originalPushState.apply(history, arguments);
  1323. };
  1324.  
  1325. // // Override replaceState
  1326. // history.replaceState = function (state, title, url) {
  1327. // log('replaceState called:', { state, title, url });
  1328. // // return originalReplaceState.apply(history, arguments);
  1329. // };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址