// ==UserScript== // @name 豆包深色/浅色切换(可拖动+灯泡图标) // @namespace local.doubao.theme.toggle // @version 0.0.1 // @description 页面可拖动灯泡图标按钮,切换浅色/深色模式 // @match https://www.doubao.com/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'tm_theme_override_v1'; let isDragging = false; let dragStartX = 0; let dragStartY = 0; let btnStartX = 0; let btnStartY = 0; function detectColorScheme() { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } return 'light'; } function setDataTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } function getOverrideTheme() { const v = localStorage.getItem(STORAGE_KEY); return v === 'light' || v === 'dark' ? v : null; } function setOverrideTheme(theme) { localStorage.setItem(STORAGE_KEY, theme); } function applyThemeFromState() { const override = getOverrideTheme(); setDataTheme(override ?? detectColorScheme()); return override; } // 创建可拖动图标按钮 function ensureButton() { const existing = document.getElementById('tm-theme-toggle-btn'); if (existing) return existing; const button = document.createElement('button'); button.id = 'tm-theme-toggle-btn'; button.type = 'button'; button.setAttribute('aria-label', '切换浅色/深色模式'); // 灯泡图标容器 const icon = document.createElement('span'); icon.className = 'tm-theme-icon'; button.appendChild(icon); // 样式:灯泡图标 + 按钮样式 const style = document.createElement('style'); style.textContent = ` #tm-theme-toggle-btn{ position: fixed; top: 16px; right: 16px; width: 40px; height: 40px; box-sizing: border-box; border-radius: 50%; border: 1px solid rgba(0,0,0,.10); background: rgba(255,255,255,.78); box-shadow: 0 4px 12px rgba(0,0,0,.12); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 2147483647; cursor: grab; user-select: none; -webkit-tap-highlight-color: transparent; display: flex; align-items: center; justify-content: center; transition: transform 0.2s ease, box-shadow 0.2s ease; } #tm-theme-toggle-btn:active { cursor: grabbing; } #tm-theme-toggle-btn:hover{ transform: translateY(-2px); } #tm-theme-toggle-btn:active{ transform: translateY(0); box-shadow: 0 2px 8px rgba(0,0,0,.14); } #tm-theme-toggle-btn:focus-visible{ outline: none; box-shadow: 0 0 0 3px rgba(59,130,246,.35), 0 4px 12px rgba(0,0,0,.12); } /* 灯泡基础样式 */ .tm-theme-icon { width: 22px; height: 26px; position: relative; display: block; } .tm-theme-icon::before, .tm-theme-icon::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); } /* 灯泡灯罩 */ .tm-theme-icon::before { top: 0; width: 16px; height: 16px; border-radius: 50% 50% 40% 40%; background: #ffd000; transition: background 0.25s ease, box-shadow 0.25s ease; box-shadow: 0 0 6px #ffd000; } /* 灯泡底座 */ .tm-theme-icon::after { bottom: 0; width: 8px; height: 6px; background: #999; border-radius: 2px; } /* 深色模式:灯泡熄灭 */ html[data-theme="dark"] .tm-theme-icon::before { background: #666; box-shadow: none; } html[data-theme="dark"] #tm-theme-toggle-btn{ border: 1px solid rgba(255,255,255,.14); background: rgba(20,20,20,.72); box-shadow: 0 6px 16px rgba(0,0,0,.35); } @media (prefers-reduced-motion: reduce) { #tm-theme-toggle-btn{ transition: none !important; } #tm-theme-toggle-btn:hover{ transform: none; } #tm-theme-toggle-btn:active{ transform: none; } } `; document.head.appendChild(style); document.body.appendChild(button); // 拖拽逻辑 function dragStart(e) { isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; btnStartX = button.offsetLeft; btnStartY = button.offsetTop; button.style.transition = 'none'; e.preventDefault(); } function dragMove(e) { if (!isDragging) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; let newLeft = btnStartX + dx; let newTop = btnStartY + dy; // 边界限制 const winW = window.innerWidth; const winH = window.innerHeight; const btnW = button.offsetWidth; const btnH = button.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, winW - btnW)); newTop = Math.max(0, Math.min(newTop, winH - btnH)); button.style.left = newLeft + 'px'; button.style.top = newTop + 'px'; button.style.right = 'auto'; } function dragEnd() { if (!isDragging) return; isDragging = false; button.style.transition = ''; } button.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', dragMove); document.addEventListener('mouseup', dragEnd); document.addEventListener('mouseleave', dragEnd); return button; } function updateButtonIcon() {} function toggleTheme() { const current = document.documentElement.getAttribute('data-theme') || detectColorScheme(); const next = current === 'dark' ? 'light' : 'dark'; setOverrideTheme(next); setDataTheme(next); } const button = ensureButton(); applyThemeFromState(); updateButtonIcon(button); button.addEventListener('click', () => { if (!isDragging) toggleTheme(); }); // 监听系统配色变化 const mql = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; if (mql) { const onChange = () => { if (getOverrideTheme() !== null) return; setDataTheme(detectColorScheme()); }; if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onChange); else if (typeof mql.addListener === 'function') mql.addListener(onChange); } })();