// ==UserScript== // @name Auto Dark Mode (Opt-in) // @namespace https://rksh.me/ // @version 1.2 // @description Adds a floating toggle to manually enable a smart dark mode theme per-website. // @author Rakesh Gautam // @homepageURL https://github.com/rkshrksh/dark-mode-userscript // @supportURL https://github.com/rkshrksh/dark-mode-userscript/issues // @updateURL https://raw.githubusercontent.com/rkshrksh/dark-mode-userscript/main/dark-mode-userscript.js // @downloadURL https://raw.githubusercontent.com/rkshrksh/dark-mode-userscript/main/dark-mode-userscript.js // @match *://*/* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; const domain = window.location.hostname; // Position options: bottom-right, bottom-left, top-right, top-left const POSITIONS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']; let currentPosition = getSetting(`dark_mode_position_${domain}`, 'bottom-right'); // Fallback for Userscript managers (like the macOS Safari one) that don't fully support GM API function getSetting(key, defaultValue) { try { if (typeof GM_getValue !== 'undefined') { return GM_getValue(key, defaultValue); } const val = localStorage.getItem(key); return val !== null ? JSON.parse(val) : defaultValue; } catch (e) { return defaultValue; } } function setSetting(key, value) { try { if (typeof GM_setValue !== 'undefined') { GM_setValue(key, value); } else { localStorage.setItem(key, JSON.stringify(value)); } } catch (e) { console.warn('Dark Mode: Cannot save setting.', e); } } // Check if user has explicitly enabled dark mode for this domain const isEnabledForDomain = getSetting(`dark_mode_${domain}`, false); // CSS styles to apply the dark mode const css = ` html.extension-dark-mode { filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(1.1) !important; background-color: white !important; color-scheme: dark !important; } /* Smooth transition for visually pleasing toggles (only applied to the HTML element) */ html { transition: filter 0.3s ease, background-color 0.3s ease; } /* Re-invert media elements to keep their original colors */ html.extension-dark-mode img, html.extension-dark-mode video, html.extension-dark-mode iframe, html.extension-dark-mode canvas, html.extension-dark-mode svg { filter: invert(1) hue-rotate(180deg) !important; } /* UI Toggle styles */ #dark-mode-toggle-btn { position: fixed; --toggle-bottom: 20px; --toggle-right: 20px; bottom: var(--toggle-bottom); right: var(--toggle-right); z-index: 2147483647; /* Max z-index */ background: #333; color: white; border: 2px solid #fff; outline: 2px solid #333; box-shadow: 0 4px 6px rgba(0,0,0,0.3), 0 0 0 2px rgba(255,255,255,0.3); border-radius: 50%; width: 50px; height: 50px; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; user-select: none; opacity: 0.8; } #dark-mode-toggle-btn.position-bottom-left { --toggle-right: auto; --toggle-left: 20px; left: var(--toggle-left); } #dark-mode-toggle-btn.position-top-right { --toggle-bottom: auto; --toggle-top: 20px; top: var(--toggle-top); } #dark-mode-toggle-btn.position-top-left { --toggle-bottom: auto; --toggle-top: 20px; --toggle-right: auto; --toggle-left: 20px; top: var(--toggle-top); left: var(--toggle-left); } #dark-mode-toggle-btn:hover { transform: scale(1.1); background: #555; opacity: 1; border-color: #fff; } #dark-mode-toggle-btn.active { background: #e2e8f0; color: #1a202c; border-color: #1a202c; outline-color: #e2e8f0; } `; // 1. Inject CSS and UI function injectStylesAndUI() { if (!document.documentElement) return; // Inject Core CSS if (!document.getElementById('dark-mode-core-css')) { const style = document.createElement('style'); style.id = 'dark-mode-core-css'; style.textContent = css; document.documentElement.appendChild(style); } // Apply dark mode immediately if enabled to prevent flicker if (isEnabledForDomain) { document.documentElement.classList.add('extension-dark-mode'); } } injectStylesAndUI(); // 2. Create the floating button UI when body is ready function createToggleButton() { if (window !== window.top) return; // Don't create button in iframes if (!document.body || document.getElementById('dark-mode-toggle-btn')) return; const currentlyEnabled = getSetting(`dark_mode_${domain}`, false); const btn = document.createElement('button'); btn.id = 'dark-mode-toggle-btn'; btn.innerHTML = currentlyEnabled ? '☀️' : '🌙'; btn.title = "Toggle Dark Mode for this site (right-click to change position)"; if (currentlyEnabled) btn.classList.add('active'); // Apply stored position class if (currentPosition && currentPosition !== 'bottom-right') { btn.classList.add(`position-${currentPosition}`); } btn.addEventListener('click', () => { const currentlyEnabled = document.documentElement.classList.contains('extension-dark-mode'); if (currentlyEnabled) { document.documentElement.classList.remove('extension-dark-mode'); setSetting(`dark_mode_${domain}`, false); btn.innerHTML = '🌙'; btn.classList.remove('active'); } else { document.documentElement.classList.add('extension-dark-mode'); setSetting(`dark_mode_${domain}`, true); btn.innerHTML = '☀️'; btn.classList.add('active'); } }); // Right-click to cycle through positions btn.addEventListener('contextmenu', (e) => { e.preventDefault(); const currentIdx = POSITIONS.indexOf(currentPosition); const nextIdx = (currentIdx + 1) % POSITIONS.length; const newPosition = POSITIONS[nextIdx]; // Update the variable currentPosition = newPosition; // Remove old position class POSITIONS.forEach(pos => btn.classList.remove(`position-${pos}`)); // Add new position class if (newPosition !== 'bottom-right') { btn.classList.add(`position-${newPosition}`); } // Save preference setSetting(`dark_mode_position_${domain}`, newPosition); // Show brief feedback btn.title = `Position: ${newPosition.replace('-', ' ')}`; setTimeout(() => { btn.title = "Toggle Dark Mode for this site (right-click to change position)"; }, 1500); }); document.body.appendChild(btn); } // 3. Fallback check for SPA routing // If navigation happens, ensure the dark mode class and button position are applied properly const observeNavigation = () => { const newDomain = window.location.hostname; const enabledState = getSetting(`dark_mode_${newDomain}`, false); const btnPosition = getSetting(`dark_mode_position_${newDomain}`, 'bottom-right'); let btn = document.getElementById('dark-mode-toggle-btn'); if (enabledState) { document.documentElement.classList.add('extension-dark-mode'); } else { document.documentElement.classList.remove('extension-dark-mode'); } // Recreate button if SPA destroyed it if (!btn) { createToggleButton(); btn = document.getElementById('dark-mode-toggle-btn'); } if (btn) { // Sync visual state if (enabledState) { btn.innerHTML = '☀️'; btn.classList.add('active'); } else { btn.innerHTML = '🌙'; btn.classList.remove('active'); } // Restore button position on navigation if (btnPosition) { POSITIONS.forEach(pos => btn.classList.remove(`position-${pos}`)); if (btnPosition !== 'bottom-right') { btn.classList.add(`position-${btnPosition}`); } currentPosition = btnPosition; } } }; window.addEventListener('popstate', observeNavigation); const pushState = history.pushState; if (pushState) { history.pushState = function () { const result = pushState.apply(this, arguments); observeNavigation(); return result; }; } const replaceState = history.replaceState; if (replaceState) { history.replaceState = function () { const result = replaceState.apply(this, arguments); observeNavigation(); return result; }; } // Wait for body to append UI robustly with polling function initUI() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createToggleButton); } else { createToggleButton(); } // Polling fallback - try every 500ms for up to 10 seconds let attempts = 0; const maxAttempts = 20; const pollInterval = setInterval(() => { attempts++; if (document.getElementById('dark-mode-toggle-btn')) { clearInterval(pollInterval); setupObserver(); return; } if (attempts >= maxAttempts) { clearInterval(pollInterval); setupObserver(); return; } createToggleButton(); }, 500); } initUI(); // 4. MutationObserver for aggressive SPA resilience // Ensures the button is recreated if a framework completely replaces the body function setupObserver() { if (!document.body) return; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { const btn = document.getElementById('dark-mode-toggle-btn'); if (!btn && window === window.top) { createToggleButton(); break; } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // Register command in Tampermonkey menu if (typeof GM_registerMenuCommand !== "undefined") { GM_registerMenuCommand("Toggle Dark Mode (This Site)", () => { document.getElementById('dark-mode-toggle-btn')?.click(); }); } })();