// ==UserScript==
// @name ☀️Dark Mode Toggle
// @author Cervantes Wu ([Link]
// @description Dark mode toggle button with SVG icons, customizable UI, and
advanced features, with per-site preferences.
// @namespace [Link]
// @version 2.2.0
// @match *://*/*
// @exclude devtools://*
// @grant [Link]
// @grant [Link]
// @grant [Link]
// @grant [Link]
// @require [Link]
// @homepageURL [Link]
// @supportURL [Link]
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Constants ---
const BUTTON_ID = 'darkModeToggle'; // ID of the dark mode toggle button
const UI_ID = 'darkModeToggleUI'; // ID of the settings UI
const TOGGLE_UI_BUTTON_ID = 'toggleDarkModeUIButton'; // ID of the button that
toggles the UI
const RESET_SETTINGS_BUTTON_ID = 'resetSettingsButton'; // ID of the reset
settings button
const SITE_EXCLUSION_INPUT_ID = 'siteExclusionInput'; // ID of the site
exclusion input field
const SITE_EXCLUSION_LIST_ID = 'siteExclusionList'; // ID of the site exclusion
list
const PER_SITE_SETTINGS_PREFIX = 'perSiteSettings_'; // Prefix for per-site
settings in storage
// --- SVG Icons ---
const moonIcon = `<svg xmlns="[Link] viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-
linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21
12.79z"></path></svg>`;
const sunIcon = `<svg xmlns="[Link] viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-
linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1"
x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22"
y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78"
y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12"
x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line
x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`;
// --- Default Settings ---
const defaultSettings = {
position: 'bottom-right', // Default position of the toggle button
offsetX: 30, // Default horizontal offset of the toggle button
offsetY: 30, // Default vertical offset of the toggle button
brightness: 100, // Default DarkReader brightness
contrast: 90, // Default DarkReader contrast
sepia: 10, // Default DarkReader sepia
themeColor: '#f7f7f7', // Default UI theme color
textColor: '#444', // Default UI text color
fontFamily: 'sans-serif', // Default font family for the UI
exclusionList: [], // Default list of excluded sites
iconMoon: moonIcon, // Default moon icon
iconSun: sunIcon, // Default sun icon
autoMode: false, // Track auto mode state (not implemented yet)
};
// --- DarkReader Constants (Example) ---
const DARKREADER_DEFAULT_BRIGHTNESS = 100; // Example constant for DarkReader
brightness
const DARKREADER_DEFAULT_CONTRAST = 90; // Example constant for DarkReader
contrast
const DARKREADER_DEFAULT_SEPIA = 10; // Example constant for DarkReader sepia
// --- Global Variables ---
let settings = { ...defaultSettings }; // Current settings, initialized with
default values
let uiVisible = false; // Whether the settings UI is visible
let darkModeEnabled = false; // Whether dark mode is currently enabled
// --- UI element references (Optimized) ---
const uiElements = {}; // Object to store references to UI elements for
efficient access
// --- Helper Functions ---
// Debounce function to limit the rate at which a function can fire.
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => [Link](context, args), delay);
};
}
// Check if the current site is excluded
function isSiteExcluded(url) {
return [Link](excluded => [Link](excluded));
}
// Function to create a button (DRY principle - Don't Repeat Yourself)
function createButton(id, text, onClick) {
const button = [Link]('button');
[Link] = id;
[Link] = text;
[Link]('click', onClick);
return button;
}
// Update the exclusion list display
function updateExclusionListDisplay() {
if (![Link]) return; // Check if the element exists
[Link] = ''; // Clear existing list
[Link](excludedSite => {
const listItem = [Link]('li');
[Link] = excludedSite;
const removeButton = createButton('removeButton-' + excludedSite,
'Remove', () => {
[Link] = [Link](site => site
!== excludedSite);
saveSettings();
updateExclusionListDisplay(); // Refresh display
});
[Link](removeButton);
[Link](listItem);
});
}
// Get per-site settings from storage
async function loadPerSiteSettings() {
const siteKey = PER_SITE_SETTINGS_PREFIX + [Link];
try {
const storedSettings = await [Link](siteKey, null);
if (storedSettings) {
// Apply per-site settings
settings = { ...settings, ...storedSettings };
darkModeEnabled = [Link] !== undefined ?
[Link] : false;
[Link](`Loaded per-site settings for $
{[Link]}:`, storedSettings);
} else {
[Link](`No per-site settings found for $
{[Link]}. Using global settings.`);
}
} catch (error) {
[Link](`Failed to load per-site settings for $
{[Link]}:`, error);
}
}
// Save per-site settings to storage
async function savePerSiteSettings() {
const siteKey = PER_SITE_SETTINGS_PREFIX + [Link];
const perSiteSettings = {
brightness: [Link],
contrast: [Link],
sepia: [Link],
position: [Link],
offsetX: [Link],
offsetY: [Link],
darkModeEnabled: darkModeEnabled,
fontFamily: [Link],
themeColor: [Link],
textColor: [Link]
};
try {
await [Link](siteKey, perSiteSettings);
[Link](`Saved per-site settings for ${[Link]}:`,
perSiteSettings);
} catch (error) {
[Link](`Failed to save per-site settings for $
{[Link]}:`, error);
}
}
// --- Setting Load/Save/Reset ---
// Load settings from GM storage
async function loadSettings() {
try {
const storedSettings = await [Link]('settings', defaultSettings);
settings = { ...defaultSettings, ...storedSettings }; // Merge stored
settings with default settings
updateButtonPosition();
// Ensure exclusionList is always an array
if () {
[Link] = [];
saveSettings(); // Save corrected data
}
} catch (error) {
[Link]('Failed to load settings:', error);
settings = { ...defaultSettings }; // Reset to default settings if
loading fails
alert('Failed to load settings. Using default settings.');
}
}
// Save settings to GM storage (Debounced to avoid excessive writes)
const saveSettingsDebounced = debounce(async () => {
try {
await [Link]('settings', settings);
updateButtonPosition();
updateDarkReaderConfig();
updateExclusionListDisplay(); // Ensure the exclusion list is up-to-
date
} catch (error) {
[Link]('Failed to save settings:', error);
alert('Failed to save settings.');
}
}, 500); // 500ms delay - Save settings only after 500ms of inactivity
function saveSettings() {
saveSettingsDebounced();
savePerSiteSettings(); // Also save per-site settings
}
// Reset settings to default
async function resetSettings() {
if (confirm('Are you sure you want to reset settings to default? This will
clear ALL settings.')) {
// Clear all stored settings
for (const key in defaultSettings) {
await [Link](key); // Use [Link] to clear each
setting
}
settings = { ...defaultSettings }; // Reset to default settings
await [Link]('settings', settings); // Store the default settings
darkModeEnabled = false;
updateButtonPosition();
updateDarkReaderConfig();
updateUIValues();
updateButtonState();
updateExclusionListDisplay();
toggleDarkMode(false); // Ensure dark mode is disabled.
await savePerSiteSettings();
}
}
// --- UI Update Functions ---
// Update UI element values based on current settings
function updateUIValues() {
if (![Link]) return; // Check if UI elements are created
yet
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
updateExclusionListDisplay(); // Update the exclusion list in the UI
}
// Function to update the button's class based on the dark mode state
function updateButtonState() {
const button = [Link](BUTTON_ID);
if (!button) return;
if (darkModeEnabled) {
[Link]('dark');
} else {
[Link]('dark');
}
}
// --- Dark Mode Logic ---
// Toggle dark mode function (with optional force parameter)
async function toggleDarkMode(force) {
// Use force to explicitly set the state, otherwise toggle it.
darkModeEnabled = force !== undefined ? force : !darkModeEnabled;
const button = [Link](BUTTON_ID);
if (darkModeEnabled) {
if (!isSiteExcluded([Link])) {
updateDarkReaderConfig();
await [Link]('darkMode', true);
[Link]('dark');
[Link]('Dark mode enabled.');
} else {
darkModeEnabled = false; // Revert the toggle
[Link]('dark');
[Link]('Site excluded. Dark mode disabled.');
[Link](); // Ensure DarkReader is disabled.
await [Link]('darkMode', false); // Update the stored value.
}
} else {
[Link]();
await [Link]('darkMode', false);
[Link]('dark');
[Link]('Dark mode disabled.');
}
await savePerSiteSettings();
}
// Update DarkReader configuration
function updateDarkReaderConfig() {
if (darkModeEnabled && !isSiteExcluded([Link])) {
[Link]({
brightness: [Link],
contrast: [Link],
sepia: [Link],
style: {
fontFamily: [Link]
}
});
} else {
[Link]();
}
}
// --- DOM Element Creation ---
// Create toggle button
function createToggleButton() {
const button = [Link]('button');
[Link] = BUTTON_ID;
[Link] = `<span class="icon">${moonIcon}</span>`; // Initial icon
[Link](button);
[Link]('click', () => {
toggleDarkMode();
});
updateButtonPosition();
}
// Update Button Position based on settings
function updateButtonPosition() {
const button = [Link](BUTTON_ID);
if (!button) return;
const { position, offsetX, offsetY } = settings;
[Link] = '';
[Link] = '';
[Link] = '';
[Link] = '';
switch (position) {
case 'top-left':
[Link] = `${offsetY}px`;
[Link] = `${offsetX}px`;
break;
case 'top-right':
[Link] = `${offsetY}px`;
[Link] = `${offsetX}px`;
break;
case 'bottom-left':
[Link] = `${offsetY}px`;
[Link] = `${offsetX}px`;
break;
case 'bottom-right':
default:
[Link] = `${offsetY}px`;
[Link] = `${offsetX}px`;
break;
}
}
// Create UI
function createUI() {
const ui = [Link]('div');
[Link] = UI_ID;
// --- Position Settings ---
const positionLabel = [Link]('label');
[Link] = 'Position:';
[Link] = [Link]('select');
[Link] = 'positionSelect';
const positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
[Link](pos => {
const option = [Link]('option');
[Link] = pos;
[Link] = pos;
[Link] = [Link] === pos;
[Link](option);
});
[Link]('change', (e) => {
[Link] = [Link];
saveSettings();
});
[Link](positionLabel);
[Link]([Link]);
const offsetXLabel = [Link]('label');
[Link] = 'Offset X:';
[Link] = [Link]('input');
[Link] = 'number';
[Link] = 'offsetXInput';
[Link] = [Link];
[Link]('change', (e) => {
[Link] = parseInt([Link]);
saveSettings();
});
[Link](offsetXLabel);
[Link]([Link]);
const offsetYLabel = [Link]('label');
[Link] = 'Offset Y:';
[Link] = [Link]('input');
[Link] = 'number';
[Link] = 'offsetYInput';
[Link] = [Link];
[Link]('change', (e) => {
[Link] = parseInt([Link]);
saveSettings();
});
[Link](offsetYLabel);
[Link]([Link]);
// --- DarkReader Settings ---
const brightnessLabel = [Link]('label');
[Link] = 'Brightness:';
[Link] = [Link]('input');
[Link] = 'number';
[Link] = 'brightnessInput';
[Link] = [Link];
[Link] = 0;
[Link] = 100;
[Link]('change', (e) => {
[Link] = parseInt([Link]);
saveSettings();
});
[Link](brightnessLabel);
[Link]([Link]);
const contrastLabel = [Link]('label');
[Link] = [Link]('input');
[Link] = 'Contrast:';
[Link] = 'number';
[Link] = 'contrastInput';
[Link] = [Link];
[Link] = 0;
[Link] = 100;
[Link]('change', (e) => {
[Link] = parseInt([Link]);
saveSettings();
});
[Link](contrastLabel);
[Link]([Link]);
const sepiaLabel = [Link]('label');
[Link] = 'Sepia:';
[Link] = [Link]('input');
[Link] = 'number';
[Link] = 'sepiaInput';
[Link] = [Link];
[Link] = 0;
[Link] = 100;
[Link]('change', (e) => {
[Link] = parseInt([Link]);
saveSettings();
});
[Link](sepiaLabel);
[Link]([Link]);
// --- Font Settings ---
const fontFamilyLabel = [Link]('label');
[Link] = 'Font Family:';
[Link] = [Link]('input');
[Link] = 'text';
[Link] = 'fontFamilyInput';
[Link] = [Link];
[Link]('change', (e) => {
[Link] = [Link];
saveSettings();
});
[Link](fontFamilyLabel);
[Link]([Link]);
// --- Theme Settings ---
const themeColorLabel = [Link]('label');
[Link] = 'UI Theme Color:';
[Link] = [Link]('input');
[Link] = 'color';
[Link] = 'themeColorInput';
[Link] = [Link];
[Link]('change', (e) => {
[Link] = [Link];
applyUIStyles(); // Apply the theme immediately
saveSettings();
});
[Link](themeColorLabel);
[Link]([Link]);
const textColorLabel = [Link]('label');
[Link] = 'UI Text Color:';
[Link] = [Link]('input');
[Link] = 'color';
[Link] = 'textColorInput';
[Link] = [Link];
[Link]('change', (e) => {
[Link] = [Link];
applyUIStyles(); // Apply the theme immediately
saveSettings();
});
[Link](textColorLabel);
[Link]([Link]);
// --- Site Exclusion ---
const siteExclusionLabel = [Link]('label');
[Link] = 'Exclude Site:';
[Link] = [Link]('input');
[Link] = 'text';
[Link] = SITE_EXCLUSION_INPUT_ID;
[Link] = 'Enter URL to exclude';
const addButton = createButton('addExclusionButton', 'Add Exclusion', () =>
{
const url = [Link]();
if (url && ) {
[Link](url);
saveSettings();
updateExclusionListDisplay();
[Link] = '';
}
});
[Link] = [Link]('ul');
[Link] = SITE_EXCLUSION_LIST_ID;
[Link](siteExclusionLabel);
[Link]([Link]);
[Link](addButton);
[Link]([Link]);
// --- Reset Settings Button ---
const resetSettingsButton = createButton(RESET_SETTINGS_BUTTON_ID, 'Reset
Settings', resetSettings);
[Link](resetSettingsButton);
[Link](ui);
}
// Create a button to toggle the UI
function createToggleUIButton() {
const toggleUIButton = createButton(TOGGLE_UI_BUTTON_ID, 'Settings',
toggleUI);
[Link](toggleUIButton);
}
// Toggle the visibility of the settings UI
function toggleUI() {
const ui = [Link](UI_ID);
uiVisible = !uiVisible;
if (uiVisible) {
[Link]('visible');
} else {
[Link]('visible');
}
}
// --- Dynamic Styles ---
// Apply UI styles dynamically based on settings
function applyUIStyles() {
const ui = [Link](UI_ID);
if (ui) {
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
}
// Re-apply the main styles to update the theme. This is a bit hacky, but
works.
[Link](generateStyles());
}
// --- Styling ---
function generateStyles() {
const { themeColor, textColor, iconMoon, iconSun } = settings;
return `
#${BUTTON_ID} {
width: 80px;
height: 40px;
background-color: #fff;
border-radius: 20px;
border: none;
cursor: pointer;
z-index: 1000;
opacity: 0.8;
transition-property: transform, opacity, box-shadow, background-
color;
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1), ease,
ease, ease;
display: flex;
align-items: center;
padding: 0 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
position: fixed;
}
#${BUTTON_ID}:hover {
opacity: 1;
transform: scale(1.1);
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275),
box-shadow 0.2s;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
}
#${BUTTON_ID} .icon {
width: 32px;
height: 32px;
border-radius: 50%;
transition-property: transform, background-color, -webkit-mask-
image, mask-image;
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55),
ease, ease, ease;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
color: #333;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background: none;
-webkit-mask-image: url('data:image/svg+xml;utf8,${iconMoon}');
mask-image: url('data:image/svg+xml;utf8,${iconMoon}');
-webkit-mask-size: cover;
mask-size: cover;
background-color: #333;
}
#${BUTTON_ID}.dark {
background-color: #000;
}
#${BUTTON_ID}.dark .icon {
transform: translateX(40px);
color: #ffeb3b;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background: none;
-webkit-mask-image: url('data:image/svg+xml;utf8,${iconSun}');
mask-image: url('data:image/svg+xml;utf8,${iconSun}');
-webkit-mask-size: cover;
mask-size: cover;
background-color: #fff;
}
/* UI Styles */
#${UI_ID} {
position: fixed;
top: 20px;
left: 20px;
background-color: ${themeColor};
border: 1px solid #ddd;
padding: 15px;
z-index: 1001;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: none;
color: ${textColor};
font-family: ${[Link]};
max-width: 90vw; /* 最大寬度為螢幕寬度的 90% */
max-height: 80vh; /* 最大高度為螢幕高度的 80% */
overflow: auto; /* 超出邊界時顯示滾動條 */
}
#${UI_ID}.visible {
display: block;
}
#${UI_ID} label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
#${UI_ID} select, #${UI_ID} input[type="number"], #${UI_ID}
input[type="color"], #${UI_ID} input[type="text"] {
margin-bottom: 12px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
color: #555;
width: 100%; /* 寬度為父元素的 100% */
max-width: 150px; /* 但不超過 150px */
box-sizing: border-box; /* 包含 padding 和 border */
}
#${UI_ID} ul#${SITE_EXCLUSION_LIST_ID} {
list-style-type: none;
padding: 0;
}
#${UI_ID} ul#${SITE_EXCLUSION_LIST_ID} li {
margin-bottom: 5px;
}
#${UI_ID} ul#${SITE_EXCLUSION_LIST_ID} li button {
margin-left: 10px;
background-color: #f44336;
color: white;
border: none;
padding: 5px 8px;
border-radius: 4px;
cursor: pointer;
}
/* Toggle UI Button Styles */
#${TOGGLE_UI_BUTTON_ID} {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%) rotate(-90deg);
background-color: #ddd;
border: 1px solid #ccc;
padding: 8px 12px;
z-index: 1002;
border-radius: 5px;
cursor: pointer;
color: #444;
font-size: 14px;
white-space: nowrap;
}
#${TOGGLE_UI_BUTTON_ID}:hover {
background-color: #eee;
}
/* Reset Settings Button Styles */
#${RESET_SETTINGS_BUTTON_ID} {
background-color: #f44336;
color: white;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
#${RESET_SETTINGS_BUTTON_ID}:hover {
background-color: #da190b;
}
`;
}
// Initial style injection
[Link](generateStyles());
// --- Initialization ---
async function init() {
await loadSettings(); // Load global settings from storage
await loadPerSiteSettings(); // Load per-site settings, overwriting global
settings if they exist
createToggleButton(); // Create the dark mode toggle button
createUI(); // Create the settings UI
createToggleUIButton(); // Create the button to toggle the UI
updateUIValues(); // Update the UI elements with the loaded settings
applyUIStyles(); // Apply UI styles based on the loaded settings
// Initial dark mode state based on stored preference
darkModeEnabled = await [Link]('darkMode', false); // Get stored state
if (darkModeEnabled && !isSiteExcluded([Link])) {
toggleDarkMode(true); // Force enable if stored as true
} else {
toggleDarkMode(false); // Force disable if stored as false or site is
excluded.
}
updateButtonState(); // Reflect initial state in the button's appearance.
}
// --- DOM Mutation Observer ---
// This observer monitors the document body for changes. If the toggle button
or UI is removed from the DOM, it recreates them.
const observer = new MutationObserver((mutations) => {
[Link]((mutation) => {
if ([Link] === 'childList') {
const buttonExists = [Link](BUTTON_ID);
if (!buttonExists) {
[Link]('Button lost, recreating...');
createToggleButton();
updateButtonPosition();
updateButtonState();
}
const uiExists = [Link](UI_ID);
if (!uiExists) {
[Link]('UI lost, recreating...');
createUI();
updateUIValues();
applyUIStyles();
}
const toggleUIButtonExists =
[Link](TOGGLE_UI_BUTTON_ID);
if (!toggleUIButtonExists) {
[Link]('Toggle UI button lost, recreating...');
createToggleUIButton();
}
}
});
});
[Link]([Link], {
childList: true,
subtree: true
});
init(); // Initialize the script
})();