F-list inverted color theme with auto-detection.

Depends on "light" theme being the default one in f-list. Adds two buttons to TamperMonkey menu itself to switch and auto-detect light/dark.

当前为 2025-02-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name F-list inverted color theme with auto-detection.
  3. // @license MIT
  4. // @namespace https://www.f-list.net
  5. // @version 2025-02-24
  6. // @description Depends on "light" theme being the default one in f-list. Adds two buttons to TamperMonkey menu itself to switch and auto-detect light/dark.
  7. // @author Me
  8. // @match https://www.f-list.net/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=f-list.net
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_addStyle
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16.  
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. // CSS overrides. Note: the colors here are the inverse of what you want.
  23. const cssCustomDarkOverrides = `
  24. /* core changes - hue is for a more pleasant shade of background-image elements. */
  25. html {
  26. filter: invert(1) hue-rotate(180deg);
  27. }
  28. /* invert images again to return them to the original color and hue */
  29. .inverted-img {
  30. filter: invert(1) hue-rotate(180deg);
  31. }
  32. /* special cases where the core changes above don't work well */
  33. .messages-both .message-ad {
  34. background-color: #ccd !important;
  35. }
  36. .ads-text-box, .ads-text-box:focus {
  37. background-color: #ccd !important;
  38. }
  39. `;
  40.  
  41. // For auto-detection.
  42. const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  43.  
  44. // Current dark mode state and mutation observer reference.
  45. let isDarkModeEnabled = false;
  46. let observer = null;
  47.  
  48. // Saved dark mode states.
  49. const DarkModeStates = Object.freeze({
  50. AUTO: "auto",
  51. MANUAL_ON: "manual_on",
  52. MANUAL_OFF: "manual_off"
  53. });
  54. let darkModeState = GM_getValue('darkModeState', DarkModeStates.MANUAL_OFF);
  55.  
  56. // Hold the menu command IDs so we can update their labels.
  57. let darkModeCommandId;
  58. let autoDetectCommandId;
  59.  
  60. // Insert custom CSS overrides using jQuery.
  61. function enableCustomOverrides() {
  62. if ($('#customOverridesStyle').length === 0) {
  63. const $styleEl = $('<style>', { id: 'customOverridesStyle', type: 'text/css' }).text(cssCustomDarkOverrides);
  64. $('head').append($styleEl);
  65. }
  66. }
  67.  
  68. // Remove custom CSS overrides.
  69. function disableCustomOverrides() {
  70. $('#customOverridesStyle').remove();
  71. }
  72.  
  73. // Update GM menu commands with dynamic text based on current state.
  74. function updateMenuCommands() {
  75. if (typeof GM_unregisterMenuCommand === 'function') {
  76. if (darkModeCommandId) { GM_unregisterMenuCommand(darkModeCommandId); }
  77. if (autoDetectCommandId) { GM_unregisterMenuCommand(autoDetectCommandId); }
  78. }
  79. darkModeCommandId = GM_registerMenuCommand(
  80. darkModeState === DarkModeStates.MANUAL_ON ? "Disable Dark Mode" : "Enable Dark Mode",
  81. toggleDarkMode
  82. );
  83. autoDetectCommandId = GM_registerMenuCommand(
  84. darkModeState === DarkModeStates.AUTO ? "Disable Auto-Detection" : "Enable Auto-Detection",
  85. toggleAutoDetection
  86. );
  87. }
  88.  
  89. // Use jQuery to add the inverted class to images, picture, and video elements.
  90. function addInvertedClassToNode(node) {
  91. const $node = $(node);
  92. if ($node.is('img, picture, video') && !$node.hasClass('inverted-img')) {
  93. $node.addClass('inverted-img');
  94. }
  95. $node.find('img, picture, video').not('.inverted-img').addClass('inverted-img');
  96. }
  97.  
  98. // Enable dark mode: add custom CSS and observe the DOM for new elements.
  99. function enableDarkMode() {
  100. enableCustomOverrides();
  101.  
  102. observer = new MutationObserver(mutations => {
  103. mutations.forEach(mutation => {
  104. mutation.addedNodes.forEach(node => {
  105. if (node.nodeType === Node.ELEMENT_NODE) {
  106. addInvertedClassToNode(node);
  107. }
  108. });
  109. });
  110. });
  111. observer.observe(document.body, { childList: true, subtree: true });
  112. $('img, picture, video').not('.inverted-img').addClass('inverted-img');
  113. isDarkModeEnabled = true;
  114. }
  115.  
  116. // Disable dark mode: remove custom CSS and stop DOM observation.
  117. function disableDarkMode() {
  118. disableCustomOverrides();
  119. if (observer) {
  120. observer.disconnect();
  121. observer = null;
  122. }
  123. $('img, picture, video').removeClass('inverted-img');
  124. isDarkModeEnabled = false;
  125. }
  126.  
  127. // Toggle dark mode manually.
  128. function toggleDarkMode() {
  129. if (isDarkModeEnabled) {
  130. darkModeState = DarkModeStates.MANUAL_OFF;
  131. disableDarkMode();
  132. } else {
  133. darkModeState = DarkModeStates.MANUAL_ON;
  134. enableDarkMode();
  135. }
  136. GM_setValue('darkModeState', darkModeState);
  137. updateMenuCommands();
  138. }
  139.  
  140. // When system dark mode preference changes.
  141. function handleColorSchemeChange(event) {
  142. console.log(`Browser changed its preference. Should we use dark mode now? ${event.matches}`);
  143. event.matches ? enableDarkMode() : disableDarkMode();
  144. }
  145.  
  146. // Update media query listener safely.
  147. function updateMediaQueryListener() {
  148. darkModeMediaQuery.removeEventListener('change', handleColorSchemeChange);
  149. if (darkModeState === DarkModeStates.AUTO) {
  150. darkModeMediaQuery.addEventListener('change', handleColorSchemeChange);
  151. }
  152. }
  153.  
  154. // Apply auto-detection or manual override.
  155. function autoDetectNow() {
  156. updateMediaQueryListener();
  157. if (darkModeState === DarkModeStates.AUTO) {
  158. console.log(`Autodetection active; system preference is dark? ${darkModeMediaQuery.matches}`);
  159. darkModeMediaQuery.matches ? enableDarkMode() : disableDarkMode();
  160. } else {
  161. const manualOn = (darkModeState === DarkModeStates.MANUAL_ON);
  162. console.log(`Autodetection disabled. Manual override? ${manualOn}`);
  163. manualOn ? enableDarkMode() : disableDarkMode();
  164. }
  165. }
  166.  
  167. // Toggle auto-detection.
  168. function toggleAutoDetection() {
  169. darkModeState = darkModeState === DarkModeStates.AUTO ? DarkModeStates.MANUAL_OFF : DarkModeStates.AUTO;
  170. GM_setValue('darkModeState', darkModeState);
  171. autoDetectNow();
  172. updateMenuCommands();
  173. }
  174.  
  175. // Initialize.
  176. autoDetectNow();
  177. updateMenuCommands();
  178.  
  179. })();