// ==UserScript== // @name 🌙 高级定制网页护眼模式🌙 // @namespace mt-fenda // @version 1.0.25 // @author fenda // @description 高度自定义和方便的预设功能,更好的适配您的浏览体验。 // @match *://*/* // @license MPL-2.0 // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect scriptcat.org // ==/UserScript== (function () { "use strict"; const SCRIPT_UPDATE_URL = "https://scriptcat.org/scripts/code/3257/%E9%AB%98%E7%BA%A7%E5%AE%9A%E5%88%B6%E7%BD%91%E9%A1%B5%E6%8A%A4%E7%9C%BC%E6%A8%A1%E5%BC%8F.meta.json.meta.js"; const DEFAULT_CANCEL_FILTER = [ /* ----------------------------------- 通用 ----------------------------------- */ "img", "picture", "picture *", "video", "video *", "canvas", "iframe", "embed", "object", "svg", "svg *", "[role='img']", "[class*='icon']", "[class*='Icon']", "[class*='avatar']", "[class*='Avatar']", "[class*='logo']", "[class*='Logo']", "[style*='background-image']", /* ---------------------------------- 哔哩哔哩 ---------------------------------- */ ".h .h-inner", ".left-box", ".bpx-player-ending-content .bpx-player-ending-functions-avatar", ".avatar-container .bili-avatar", "#playerWrap .bpx-docker", ".bpx-player-primary-area .bpx-player-control-wrap", ".bpx-player-primary-area .bpx-player-sending-bar", "#playerWrap #bilibili-player-placeholder-top", ".bpx-player-primary-area .bpx-player-video-perch", /* ----------------------------------- 虎牙 ----------------------------------- */ "#video_embed_flash #video-container", ".main-vplayer #video_embed", ".duya-header-bd .link-instanted", ".ssr-wrapper #banner", ".helperbar-root--12hgWk_4zOxrdJ73vtf1YI .helper-mm--1oyaFz1h_kqr-COcKdzhCQ", ".star-bd mfe-app", /* ----------------------------------- 斗鱼 ----------------------------------- */ ".layout-Section .layout-Slide-bannerInner", ]; let nightModeStyleElement = null; function normalizeSelectorList(selectors) { if (typeof selectors === "string") { selectors = selectors.split(","); } if (!Array.isArray(selectors)) { selectors = []; } return [...new Set(selectors.map((selector) => selector.trim()).filter(Boolean))]; } function getCancelFilterSelectors() { return normalizeSelectorList([ ...DEFAULT_CANCEL_FILTER, ...normalizeSelectorList(GM_getValue("cancelFilter", [])), ]); } function getValidSelectors(selectors) { return normalizeSelectorList(selectors).filter((selector) => { try { document.querySelector(selector); return true; } catch (e) { console.warn("Invalid night mode exclude selector:", selector); return false; } }); } function markCancelFilterElements(selectors) { document .querySelectorAll( ".advanced-night-mode-cancel-filter, .advanced-night-mode-filter-target", ) .forEach((element) => { element.classList.remove( "advanced-night-mode-cancel-filter", "advanced-night-mode-filter-target", ); }); getValidSelectors(selectors).forEach((selector) => { document.querySelectorAll(selector).forEach((element) => { element.classList.add("advanced-night-mode-cancel-filter"); }); }); document.querySelectorAll("body *").forEach((element) => { const backgroundImage = getComputedStyle(element).backgroundImage; if (backgroundImage && backgroundImage !== "none") { element.classList.add("advanced-night-mode-cancel-filter"); } }); } function setNightModeStyle(cssText) { if (!nightModeStyleElement) { nightModeStyleElement = document.createElement("style"); nightModeStyleElement.id = "advanced-night-mode-style"; document.head.appendChild(nightModeStyleElement); } nightModeStyleElement.textContent = cssText; } function getFullscreenElement() { return ( document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ); } function isMediaFullscreenActive() { const fullscreenElement = getFullscreenElement(); if (!fullscreenElement || !(fullscreenElement instanceof Element)) { return false; } const mediaSelector = "video, canvas, iframe, embed, object, img, picture, svg, [role='img']"; return ( fullscreenElement.matches(mediaSelector) || fullscreenElement.querySelector(mediaSelector) !== null ); } function requestText(url) { const xmlHttpRequest = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : null; if (xmlHttpRequest) { return new Promise((resolve, reject) => { xmlHttpRequest({ method: "GET", url, timeout: 15000, onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve(response.responseText); } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: () => reject(new Error("GM_xmlhttpRequest failed")), ontimeout: () => reject(new Error("GM_xmlhttpRequest timeout")), }); }); } return fetch(url).then((response) => response.text()); } function checkForUpdates() { var lastCheckedTime = GM_getValue("lastCheckedTime", 0); var currentTime = Date.now(); if (currentTime - lastCheckedTime >= 3600000) { GM_setValue("lastCheckedTime", currentTime); requestText(SCRIPT_UPDATE_URL) .then(function (text) { var versionMatch = text.match(/@version\s+([^\n]+)/); var latestVersion = versionMatch ? versionMatch[1].trim() : ""; if (latestVersion) { GM_setValue("latestVersion", latestVersion); } }) .catch(function (error) { console.warn("An error occurred while checking for updates:", error); }); } } checkForUpdates(); console.log; function createVersionInfoElement() { const versionInfo = createElementWithStylesAndAttributes("div", { color: "#4B5563", textAlign: "right", position: "absolute", bottom: "-3px", fontSize: "9px", fontWeight: "bold", right: "0%", whiteSpace: "nowrap", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", }); const currentVersion = typeof GM_info !== "undefined" && GM_info.script && GM_info.script.version ? GM_info.script.version : "1.0.24"; const latestVersion = GM_getValue("latestVersion", ""); versionInfo.innerHTML = `版本:${currentVersion}(最新:${latestVersion})`; return versionInfo; } const versionInfoElement = createVersionInfoElement(); const initialCustomModes = ["护眼模式", "暗夜模式", "白昼模式"]; //预设的三个模式的名字 let advancedNightModeSettingsPanel = createElementWithStylesAndAttributes( "div", { //主显示面板 position: "fixed", width: "305px", height: "415px", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "9999", backgroundColor: "rgba(236, 241, 247, 0.96)", border: "1px solid rgba(255, 255, 255, 0.72)", borderRadius: "18px", boxShadow: "0 18px 42px rgba(31, 41, 55, 0.18), inset 1px 1px 0 rgba(255, 255, 255, 0.72)", backdropFilter: "blur(12px)", display: "none", }, { className: "advancedNightModeSettingsPanel", }, ); const settings = [ //默认设置值,通过修改这个可以减少和增加功能 { name: "亮度", defaultValue: "75" }, { name: "灰度", defaultValue: "0" }, { name: "色相", defaultValue: "0" }, { name: "对比度", defaultValue: "30" }, { name: "反转度", defaultValue: "10" }, { name: "饱和度", defaultValue: "250" }, { name: "棕褐色", defaultValue: "30" }, { name: "自动开启与关闭", defaultValue: "18:00,06:00" }, ]; const customModes = { //预设三个模式默认模式的值 护眼模式: { 亮度: "75", 灰度: "0", 色相: "0", 对比度: "30", 反转度: "10", 饱和度: "250", 棕褐色: "30", }, 暗夜模式: { 亮度: "80", 灰度: "0", 色相: "0", 对比度: "35", 反转度: "90", 饱和度: "180", 棕褐色: "5", }, 白昼模式: { 亮度: "90", 灰度: "0", 色相: "0", 对比度: "30", 反转度: "0", 饱和度: "250", 棕褐色: "0", }, }; function createElementWithStylesAndAttributes(tag, styles, attributes) { //为一系列创建元素的函数添加样式和属性 let element = document.createElement(tag); if (styles) { Object.assign(element.style, styles); } if (attributes) { for (const key in attributes) { if (attributes.hasOwnProperty(key)) { if (key in element) { element[key] = attributes[key]; } else { element.setAttribute(key, attributes[key]); } } } } return element; } function updateStylesWithAttributes(element, styleAttributes) { //更新元素的样式和属性 for (const [key, value] of Object.entries(styleAttributes)) { element.style[key] = value; } } function calculatePercentageForSetting(settingName, defaultValue) { let min, max; switch (settingName) { case "亮度": min = 20; max = 100; break; case "色相": min = 0; max = 360; break; case "对比度": min = 20; max = 100; break; case "饱和度": min = 0; max = 250; break; default: min = 0; max = 100; } return ((defaultValue - min) / (max - min)) * 100; } function createSwitch(checked) { /*创建一个开关*/ let switchContainer = createElementWithStylesAndAttributes( "label", { verticalAlign: "middle", marginLeft: "5px", }, { className: "optimizedswitchcontainer" }, ); let switchInput = document.createElement("input"); switchInput.type = "checkbox"; switchInput.checked = checked; let switchSlider = document.createElement("span"); switchSlider.className = "optimizedswitchslider"; switchContainer.appendChild(switchInput); switchContainer.appendChild(switchSlider); return switchContainer; } function addStyles(element, styles) { Object.assign(element.style, styles); } const styleSheet = document.createElement("style"); /*为开关添加样式*/ styleSheet.type = "text/css"; styleSheet.innerText = '.optimizedswitchcontainer input[type="checkbox"] { width: 10px; height: 10px; } .optimizedswitchcontainer { position: relative; display: inline-block; width: 34px; height: 14px; } .optimizedswitchcontainer input { opacity: 0; width: 0; height: 0; } .optimizedswitchslider { position: absolute; cursor: pointer; top: 0; bottom: 0; left: 0; right: 0; background-color: #ccc; transition: .4s; } .optimizedswitchslider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 0; bottom: -3px; background-color: white; transition: .4s; border-radius: 0%; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } .optimizedswitchcontainer input:checked + .optimizedswitchslider { background-color: #00FF00; } .optimizedswitchcontainer input:focus + .optimizedswitchslider { box-shadow: 0 0 1px #2196F3; } .optimizedswitchcontainer input:checked + .optimizedswitchslider:before { transform: translateX(18px); } '; document.head.appendChild(styleSheet); // 检查当前页面或者上层任何页面是否具有特定属性标志着夜间模式已应用 function isNightModeApplied() { var win = window; while (win) { try { // 如果当前窗口具有data-night-mode属性,则返回true if (win.document.documentElement.hasAttribute("data-night-mode")) { return true; } if (win.parent === win) { break; // 如果没有父窗口,则跳出循环 } win = win.parent; // 查看父窗口 } catch (e) { break; // 跨域访问时会抛出异常,此时无法查看父窗口的属性 } } return false; // 如果所有相关的窗口都没有data-night-mode属性,则返回false } function isDarkMode() { const bodyElement = document.body; const linkElement = document.querySelector("a"); if (!bodyElement || !linkElement) { return false; // Return false or any default value } const bgColor = window.getComputedStyle(bodyElement).backgroundColor; const textColor = window.getComputedStyle(bodyElement).color; const linkColor = window.getComputedStyle(linkElement).color; function calculateBrightness(color) { let colors = color.match(/\d+/g); colors = colors.map((c) => parseInt(c, 10)); const brightness = Math.round( (colors[0] * 299 + colors[1] * 587 + colors[2] * 114) / 1000, ); return brightness; } const bgBrightness = calculateBrightness(bgColor); const textBrightness = calculateBrightness(textColor); const linkBrightness = calculateBrightness(linkColor); return bgBrightness < 128 && textBrightness > 128 && linkBrightness > 128; } function parameterSetting() { /*设置参数,这是主要的功能参数应用器*/ let shouldApplySettings = true; const autoSwitchState = GM_getValue("自动开启与关闭_switchState", false); //根据开关来判断是否启用设置 if (autoSwitchState) { const currentTime = new Date(); const startTime = GM_getValue("自动开启与关闭_start", "18:00").split(":"); const endTime = GM_getValue("自动开启与关闭_end", "06:00").split(":"); const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes(); const startMinutes = parseInt(startTime[0]) * 60 + parseInt(startTime[1]); const endMinutes = parseInt(endTime[0]) * 60 + parseInt(endTime[1]); if (endMinutes < startMinutes) { //跨天 shouldApplySettings = currentMinutes >= startMinutes || currentMinutes < endMinutes; } else { shouldApplySettings = currentMinutes >= startMinutes && currentMinutes < endMinutes; } } if (!shouldApplySettings) { setNightModeStyle(""); return; } if ( document.documentElement.classList.contains("theme-dark") || document.documentElement.hasAttribute("darker-dark-theme") || document.documentElement.hasAttribute("dark") ) { setNightModeStyle(""); return; } //根据网页主题判断是否启用设置 let parentFilterApplied = isNightModeApplied(); let currentScriptFilterApplied = document.documentElement.hasAttribute( "data-night-mode-by-this-instance", ); if ( (parentFilterApplied || isNightModeApplied()) && !currentScriptFilterApplied ) { return; } else { document.documentElement.setAttribute("data-night-mode", "true"); document.documentElement.setAttribute( "data-night-mode-by-this-instance", "true", ); // 标记由此实例应用滤镜 } const parameters = [ "亮度", "灰度", "色相", "对比度", "反转度", "饱和度", "棕褐色", ]; const defaultValues = settings.map((setting) => setting.defaultValue); const parameterValues = parameters.map((parameter, index) => GM_getValue(parameter, defaultValues[index]), ); const [ brightnessVal, grayScaleVal, hueRotateVal, contrastVal, inverseValue, saturateVal, sepiaVal, ] = parameterValues; const baseFilters = [ `brightness(${brightnessVal / 100})`, `grayscale(${grayScaleVal / 100})`, `hue-rotate(${hueRotateVal}deg)`, `contrast(${contrastVal / 25})`, `saturate(${saturateVal / 100})`, `sepia(${sepiaVal / 100})`, ]; const cancelFilter = getCancelFilterSelectors(); const validCancelFilter = getValidSelectors(cancelFilter); const cancelFilterCss = validCancelFilter.join(", "); const configuredInvertValue = Number(inverseValue) / 100; const invertValue = isDarkMode() ? 0 : configuredInvertValue; const filters = [...baseFilters, `invert(${invertValue})`]; const safeBrightness = Number(brightnessVal) > 0 ? 100 / Number(brightnessVal) : 1; const safeContrast = Number(contrastVal) > 0 ? 25 / Number(contrastVal) : 1; const safeSaturate = Number(saturateVal) > 0 ? 100 / Number(saturateVal) : 1; const mediaCompensateFilters = [ `brightness(${safeBrightness})`, `contrast(${safeContrast})`, `saturate(${safeSaturate})`, `hue-rotate(${-Number(hueRotateVal)}deg)`, `invert(${configuredInvertValue > 0.5 ? 1 : 0})`, ]; markCancelFilterElements(validCancelFilter); document.documentElement.setAttribute("data-night-mode", "true"); GM_setValue("filters", filters); GM_setValue("cancelFilter", cancelFilter); GM_setValue("firstTime", false); if (isMediaFullscreenActive()) { setNightModeStyle(` html{ -webkit-filter:none !important; filter:none !important; } :fullscreen, :-webkit-full-screen{ -webkit-filter:none !important; filter:none !important; } `); return; } setNightModeStyle(` html{ -webkit-filter:${filters.join(" ")}; filter:${filters.join(" ")}; } ${cancelFilterCss}, .advanced-night-mode-cancel-filter, .advanced-night-mode-cancel-filter *{ -webkit-filter:${mediaCompensateFilters.join(" ")} !important; filter:${mediaCompensateFilters.join(" ")} !important; } `); } function updateSliderStyle(slider, valSpan) { const min = parseInt(slider.min, 10); const max = parseInt(slider.max, 10); const value = parseInt(slider.value, 10); const percentage = ((value - min) * 100) / (max - min); slider.style.setProperty("--val", `${percentage}%`); valSpan.innerText = slider.value; GM_setValue(slider.getAttribute("name"), slider.value); parameterSetting(); checkCurrentMode(); } function checkCurrentMode() { //*检查当前模式*/ const currentSettings = settings.reduce((obj, setting) => { obj[setting.name] = GM_getValue(setting.name, setting.defaultValue); return obj; }, {}); let matchedModeName = "默认/自定义模式"; const savedCustomModes = GM_getValue("customModes", {}); for (const modeName in savedCustomModes) { if (savedCustomModes.hasOwnProperty(modeName)) { const modeSettings = savedCustomModes[modeName]; const isMatch = Object.keys(modeSettings).every( (key) => modeSettings[key] == currentSettings[key], ); if (isMatch) { matchedModeName = modeName; break; } } } for (const modeName in customModes) { //遍历预设模式 if (customModes.hasOwnProperty(modeName)) { const modeSettings = customModes[modeName]; const isMatch = Object.keys(modeSettings).every( (key) => modeSettings[key] == currentSettings[key], ); if (isMatch) { matchedModeName = modeName; break; } } } const displayCurrentModeElement = document.querySelector( ".displayCurrentMode", ); if (displayCurrentModeElement) { displayCurrentModeElement.innerText = "当前模式:" + matchedModeName; } else { console.error("未找到 displayCurrentMode 元素"); } } function debounce(func, wait, immediate) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } function createSettingLine(setting) { /*创建设置行*/ const settingLine = createElementWithStylesAndAttributes( "div", { display: "flex", margin: "0", marginBottom: "5px", fontSize: "14px", fontWeight: "bold", textAlign: "left", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", color: "#4B5563", marginLeft: "6px", marginRight: "6px", justifyContent: "space-between", whiteSpace: "nowrap", }, { className: "settingLine", innerText: setting.name, }, ); if (setting.name === "自动开启与关闭") { const [defaultStart, defaultEnd] = setting.defaultValue.split(","); console.log(defaultStart, defaultEnd); const timeContainer = document.createElement("div"); timeContainer.className = "timeContainer"; addStyles(timeContainer, { display: "flex", marginTop: "-2px", flexDirection: "row", alignItems: "center", }); const timeInputWrapper_start = document.createElement("div"); addStyles(timeInputWrapper_start, { width: "70px", overflow: "hidden", }); const startTimePicker = createElementWithStylesAndAttributes( "input", null, { //设置时间输入框 className: "startTimePicker", type: "time", value: GM_getValue(setting.name + "_start", defaultStart), }, ); startTimePicker.onchange = function () { GM_setValue(setting.name + "_start", this.value); }; timeInputWrapper_start.appendChild(startTimePicker); timeContainer.append(timeInputWrapper_start); const toLabel = document.createElement("span"); toLabel.innerText = " 到 "; timeContainer.append(toLabel); const timeInputWrapper_end = document.createElement("div"); addStyles(timeInputWrapper_end, { width: "70px", overflow: "hidden", }); const endTimePicker = createElementWithStylesAndAttributes( "input", null, { className: "endTimePicker", type: "time", value: GM_getValue(setting.name + "_end", defaultEnd), }, ); endTimePicker.onchange = function () { GM_setValue(setting.name + "_end", this.value); }; timeInputWrapper_end.appendChild(endTimePicker); timeContainer.append(timeInputWrapper_end); if ( GM_getValue(setting.name + "_switchState", false) !== (true || false) ) { GM_setValue(setting.name + "_switchState", false); } const autoOpen = createSwitch( GM_getValue(setting.name + "_switchState", false), ); autoOpen .querySelector('input[type="checkbox"]') .addEventListener("change", function () { GM_setValue(setting.name + "_switchState", this.checked); console.log(this.checked); }); timeContainer.appendChild(autoOpen); settingLine.append(timeContainer); setInterval(function () { //每分钟检查时间是否符合自动开启与关闭条件 const currentTime = new Date(); const [startHour, startMinute] = startTimePicker.value.split(":"); const [endHour, endMinute] = endTimePicker.value.split(":"); if ( currentTime.getHours() >= startHour && currentTime.getMinutes() >= startMinute && currentTime.getHours() <= endHour && currentTime.getMinutes() <= endMinute && autoOpen.checked ) { return; } }, 60000); } else { const sliderLine = document.createElement("div"); sliderLine.className = "sliderLine"; addStyles(sliderLine, { display: "flex" }); const incrementButton = createElementWithStylesAndAttributes( "button", { backgroundColor: "#E0E5EC", border: "none", width: "20px", height: "20px", borderRadius: "5px", color: "#000", boxShadow: "2px 2px 8px #bbb, -2px -2px 8px #FFFFFF", webkitTouchCallout: "none", userSelect: "none", display: "flex", alignItems: "center", justifyContent: "center", }, { className: "incrementButton", innerText: "\u2795", }, ); incrementButton.onclick = () => { if (parseInt(slider.value) < slider.max) { slider.stepUp(); updateSliderStyle(slider, valSpan); } }; const slider = document.createElement("input"); slider.className = "slider"; const valSpan = document.createElement("span"); valSpan.className = "valSpan"; addStyles(valSpan, { width: "50px", display: "inline-block", textAlign: "right", }); slider.setAttribute("name", setting.name); valSpan.setAttribute("name", setting.name); slider.type = "range"; switch ( setting.name //根据参数名设置滑块范围 ) { case "亮度": slider.min = "20"; slider.max = "100"; break; case "色相": slider.min = "0"; slider.max = "360"; break; case "对比度": slider.min = "20"; slider.max = "100"; break; case "饱和度": slider.min = "0"; slider.max = "250"; break; default: slider.min = "0"; slider.max = "100"; } slider.value = GM_getValue(setting.name, setting.defaultValue); valSpan.innerText = slider.value; updateSliderStyle(slider, valSpan); addStyles(slider, { webkitAppearance: "none", width: "120px", height: "20px", borderRadius: "5px", background: "#d3d3d3", outline: "none", opacity: "0.7", zIndex: "99999", margin: "0 10px", }); const style = document.createElement("style"); //自定义样式 style.innerHTML = ` input[type=range]::-moz-range-thumb { -moz-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #E0E5EC; box-shadow: 2px 2px 2px #babecc, -4px -4px 5px #ffffff; border: 1px solid #babecc; } input[type=range]::-moz-range-track { -moz-transform: perspective(135px) rotateX(25deg); background: linear-gradient(to right, #E0E5EC var(--val, 100%), #d3d3d3 100%); box-shadow: -3px -3px 10px #FFFFFF, 3px 3px 10px #BECBD8; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #E0E5EC; box-shadow: 2px 2px 2px #babecc, -4px -4px 5px #ffffff; border: 1px solid #babecc; } input[type=range]::-webkit-slider-runnable-track { -webkit-transform: perspective(135px) rotateX(25deg); background: linear-gradient(to right, #E0E5EC var(--val, 100%), #d3d3d3 100%); box-shadow: -3px -3px 10px #FFFFFF, 3px 3px 10px #BECBD8; }`; slider.oninput = debounce(function () { valSpan.innerText = this.value; GM_setValue(setting.name, this.value); const percentage = ((this.value - this.min) / (this.max - this.min)) * 100; this.style.setProperty("--val", percentage + "%"); parameterSetting(); checkCurrentMode(); }, 50); document.head.appendChild(style); const decrementButton = createElementWithStylesAndAttributes( "button", { backgroundColor: "#E0E5EC", border: "none", width: "20px", height: "20px", borderRadius: "5px", color: "#000", boxShadow: "2px 2px 8px #bbb, -2px -2px 8px #FFFFFF", webkitTouchCallout: "none", userSelect: "none", display: "flex", alignItems: "center", justifyContent: "center", }, { className: "decrementButton", innerText: "\u2796", }, ); decrementButton.onclick = () => { if (parseInt(slider.value) > slider.min) { slider.stepDown(); updateSliderStyle(slider, valSpan); } }; sliderLine.append(decrementButton, slider, incrementButton, valSpan); settingLine.appendChild(sliderLine); let holdInterval; let isLongPress = false; function handleIncrementButton() { slider.stepUp(); valSpan.innerText = slider.value; updateSliderStyle(slider, valSpan); } function handleDecrementButton() { slider.stepDown(); valSpan.innerText = slider.value; updateSliderStyle(slider, valSpan); } function handleButtonPress(e) { holdInterval = setTimeout(() => { isLongPress = true; holdInterval = setInterval(() => { e === incrementButton ? handleIncrementButton() : handleDecrementButton(); }, 50); }, 500); } function handleButtonRelease() { clearInterval(holdInterval); clearTimeout(holdInterval); isLongPress = false; } addEventListenerMulti(incrementButton, "mousedown touchstart", () => handleButtonPress(incrementButton), ); addEventListenerMulti(incrementButton, "mouseup touchend", () => handleButtonRelease(), ); addEventListenerMulti(decrementButton, "mousedown touchstart", () => handleButtonPress(decrementButton), ); addEventListenerMulti(decrementButton, "mouseup touchend", () => handleButtonRelease(), ); addEventListenerMulti(document, "mouseup touchend", handleButtonRelease); } advancedNightModeSettingsPanel.appendChild(settingLine); } function addEventListenerMulti(element, events, fn) { //多事件绑定 events .split(" ") .forEach((event) => element.addEventListener(event, fn, false)); } function createNewSettingsLine() { //创建新的设置行 const newSettingsLine = createElementWithStylesAndAttributes( "div", { display: "flex", marginTop: "10px", justifyContent: "space-between", }, { className: "newSettingsLine", }, ); function CreateButton(buttonText, ButtonFunction) { //创建按钮 const button = createElementWithStylesAndAttributes( "button", { backgroundColor: "#E0E5EC", border: "none", width: "140px", borderRadius: "5px", boxShadow: "2px 2px 8px #bbb, -2px -2px 8px #FFFFFF", padding: "5px", fontWeight: "bold", fontSize: "14px", textAlign: "center", color: "#4B5563", marginLeft: "6px", marginRight: "6px", }, { innerText: buttonText, }, ); button.onclick = ButtonFunction; return button; } const defaultSettingButton = CreateButton("恢复默认模式", function () { //恢复默认模式 settings.forEach(function (setting) { GM_setValue(setting.name, setting.defaultValue); const inputElement = document.querySelector( `.settingLine input[name='${setting.name}']`, ); const valueElement = document.querySelector( `.settingLine span[name='${setting.name}']`, ); let percentage = calculatePercentageForSetting( setting.name, setting.defaultValue, ); if (inputElement) { inputElement.value = setting.defaultValue; inputElement.style.setProperty("--val", `${percentage}%`); } if (valueElement) { valueElement.innerText = setting.defaultValue; } }); parameterSetting(); checkCurrentMode(); }); const presetButton = CreateButton("保存当前参数到模板", function () { showModal(); }); let currentMode = "默认模式"; const displayCurrentMode = document.createElement("div"); displayCurrentMode.className = "displayCurrentMode"; settings.forEach((setting) => { if (GM_getValue(setting.name) != setting.defaultValue) { currentMode = "自定义模式"; for (const mode in customModes) { if ( JSON.stringify(customModes[mode]) === JSON.stringify(settings.map((setting) => GM_getValue(setting.name))) ) currentMode = mode; } } }); displayCurrentMode.innerText = "当前模式:" + currentMode; addStyles(displayCurrentMode, { color: "#4B5563", textAlign: "left", position: "absolute", bottom: "0px", fontSize: "12px", fontWeight: "bold", marginLeft: "6px", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", }); newSettingsLine.appendChild(defaultSettingButton); newSettingsLine.appendChild(presetButton); advancedNightModeSettingsPanel.appendChild(newSettingsLine); advancedNightModeSettingsPanel.appendChild(displayCurrentMode); advancedNightModeSettingsPanel.appendChild(versionInfoElement); } function showModal() { const modal = document.createElement("div"); addStyles(modal, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", backgroundColor: "#E0E5EC", borderRadius: "15px", boxShadow: "14px 14px 22px #cbced1, -14px -14px 22px #ffffff", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-between", width: "305px", zIndex: "10000", }); const closeButton = document.createElement("button"); closeButton.innerHTML = "✖"; addStyles(closeButton, { position: "absolute", top: "10px", right: "10px", backgroundColor: "transparent", border: "none", cursor: "pointer", fontSize: "20px", color: "#4B5563", }); closeButton.addEventListener("click", function () { document.body.removeChild(modal); }); modal.appendChild(closeButton); const title = document.createElement("h2"); title.textContent = "请输入当前配置模板的名称"; addStyles(title, { margin: "0", fontSize: "16px", fontWeight: "bold", textAlign: "center", }); modal.appendChild(title); const inputField = document.createElement("input"); addStyles(inputField, { marginTop: "20px", width: "80%", height: "30px", border: "none", borderRadius: "8px", boxShadow: "inset 4px 4px 6px #cbced1", outset: "-4px -4px 6px #ffffff", }); modal.appendChild(inputField); const submitButton = document.createElement("button"); submitButton.textContent = "提交"; addStyles(submitButton, { cursor: "pointer", marginTop: "20px", width: "80%", height: "30px", borderRadius: "8px", boxShadow: "2px 2px 5px #cbced1", textDecoration: "none", outline: "none", border: "none", backgroundColor: "#4B5563", color: "#FFFFFF", }); submitButton.addEventListener("click", function () { document.body.removeChild(modal); const name = inputField.value.trim(); let currentCustomModes = GM_getValue("customModes", {}); if ( Object.hasOwnProperty.call(currentCustomModes, name) || initialCustomModes.includes(name) ) { showAlert("当前名称已存在,请重新输入其他名称"); document.body.appendChild(modal); return; } let settingsValues = { 亮度: GM_getValue("亮度", "75"), 灰度: GM_getValue("灰度", "0"), 色相: GM_getValue("色相", "0"), 对比度: GM_getValue("对比度", "30"), 反转度: GM_getValue("反转度", "10"), 饱和度: GM_getValue("饱和度", "250"), 棕褐色: GM_getValue("棕褐色", "30"), }; GM_setValue(name, JSON.stringify(settingsValues)); if (name.length > 0) { const settingsValues = settings.reduce((obj, setting) => { obj[setting.name] = GM_getValue(setting.name, setting.defaultValue); return obj; }, {}); const customModes = GM_getValue("customModes", {}); customModes[name] = settingsValues; GM_setValue("customModes", customModes); nameDisplay(name, settingsValues); } else { showAlert("请输入配置名称"); } }); modal.appendChild(submitButton); document.body.appendChild(modal); } function nameDisplay( name, settingsValues, isCheckButton = true, isCloseButton = true, ) { //显示已保存的模板 const nameElement = createElementWithStylesAndAttributes("div", { position: "relative", width: "auto", height: "30px", display: "flex", alignItems: "center", justifyContent: "space-between", padding: "5px", borderRadius: "15px", margin: "10px", backgroundColor: "#E0E5EC", boxShadow: "5px 5px 5px #cbced1, -5px -5px 5px #ffffff", }); const nameText = createElementWithStylesAndAttributes( "p", { margin: "0", color: "#4B5563", fontWeight: "bold", fontSize: "16px", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", }, { innerText: name }, ); const closeButton_modal = document.createElement("button"); closeButton_modal.innerHTML = "✖"; addStyles(closeButton_modal, { position: "absolute", top: "-10px", right: "-10px", backgroundColor: "transparent", border: "none", cursor: "pointer", fontSize: "20px", color: "#4B5563", }); closeButton_modal.onclick = function () { nameDisplayPanel.removeChild(nameElement); handleModalCloseButtonClick(name); GM_deleteValue(name); }; const checkButton_modal = document.createElement("button"); checkButton_modal.innerHTML = "✔"; addStyles(checkButton_modal, { position: "absolute", bottom: "-10px", right: "-10px", backgroundColor: "transparent", border: "none", cursor: "pointer", fontSize: "20px", color: "#4B5563", }); checkButton_modal.onclick = function () { Object.keys(settingsValues).forEach(function (key) { GM_setValue(key, settingsValues[key]); const inputElement = document.querySelector( `.settingLine input[name='${key}']`, ); const valueElement = document.querySelector( `.settingLine span[name='${key}']`, ); let percentage = calculatePercentageForSetting( key, settingsValues[key], ); if (inputElement) { inputElement.value = settingsValues[key]; inputElement.style.setProperty("--val", `${percentage}%`); } if (valueElement) { valueElement.innerText = settingsValues[key]; } }); parameterSetting(); checkCurrentMode(); }; nameElement.appendChild(nameText); if (isCloseButton) { nameElement.appendChild(closeButton_modal); } if (isCheckButton) { nameElement.appendChild(checkButton_modal); } nameDisplayPanel.appendChild(nameElement); closeButton_modal.onclick = function () { nameDisplayPanel.removeChild(nameElement); handleModalCloseButtonClick(name); GM_deleteValue(name); }; parameterSetting(); checkCurrentMode(); } function handleModalCloseButtonClick(modeName) { let customModes = GM_getValue("customModes", {}); delete customModes[modeName]; GM_setValue("customModes", customModes); const allNames = nameDisplayPanel.querySelectorAll(".mode-name"); allNames.forEach((nameElement) => { if (nameElement.innerText === modeName) { nameDisplayPanel.removeChild(nameElement.parentElement); } }); } let nameDisplayPanel = createElementWithStylesAndAttributes( "div", { //显示已保存的模板 position: "relative", width: "290px", height: "80px", borderRadius: "15px", display: "flex", alignItems: "center", justifyContent: "center", margin: "10px auto", overflowY: "auto", flexWrap: "wrap", background: "#E0E5EC", boxShadow: "inset 9px 9px 16px #BABEC6, inset -9px -9px 16px #FFFFFF", }, { className: "neumorphic-name-display" }, ); function showAlert(message) { //显示消息提示框 let alertBox = createElementWithStylesAndAttributes("div", { position: "fixed", bottom: "10px", left: "50%", transform: "translateX(-50%)", backgroundColor: "rgba(236, 241, 247, 0.96)", padding: "14px 18px", border: "1px solid rgba(255, 255, 255, 0.72)", borderRadius: "12px", boxShadow: "0 12px 28px rgba(31, 41, 55, 0.2)", backdropFilter: "blur(10px)", zIndex: "9999999", }); let alertText = createElementWithStylesAndAttributes( "div", { margin: "0", fontSize: "16px", fontWeight: "bold", textAlign: "center", textShadow: "none", color: "#4B5563", }, { innerText: message, }, ); alertBox.appendChild(alertText); document.body.appendChild(alertBox); setTimeout(function () { document.body.removeChild(alertBox); }, 1500); } function initialize() { //初始化脚本 const settingButton = createElementWithStylesAndAttributes( "span", { cursor: "pointer", marginLeft: "10px", marginRight: "10px", width: "14px", heigth: "14px", display: "inline-block", verticalAlign: "middle", }, { innerHTML: ``, }, ); const title = createElementWithStylesAndAttributes( "h1", { color: "#4B5563", textAlign: "center", marginBottom: "10px", fontSize: "24px", fontWeight: "bold", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", }, { className: "title", innerText: "高级夜间模式设置" }, ); const closeSettingpanel = createElementWithStylesAndAttributes( "span", { cursor: "pointer", marginLeft: "30px", maginRight: "10px", width: "11px", heigth: "11px", display: "inline-block", verticalAlign: "middle", }, { innerHTML: ``, }, ); settingButton.addEventListener("click", () => { const infoPanel = createInfoPanel(); togglePanel(advancedNightModeSettingsPanel, infoPanel); }); closeSettingpanel.addEventListener("click", () => { advancedNightModeSettingsPanel.style.display = "none"; }); title.appendChild(settingButton); title.appendChild(closeSettingpanel); advancedNightModeSettingsPanel.appendChild(title); settings.forEach(function (setting) { createSettingLine(setting); }); createNewSettingsLine(); parameterSetting(); advancedNightModeSettingsPanel.appendChild(nameDisplayPanel); document.body.appendChild(advancedNightModeSettingsPanel); GM_registerMenuCommand("显示/隐藏高级夜间模式设置", function () { advancedNightModeSettingsPanel.style.display = advancedNightModeSettingsPanel.style.display === "none" ? "block" : "none"; }); } function createInfoPanel() { //创建信息面板 const infoPanel = createElementWithStylesAndAttributes( "div", { position: "fixed", width: "305px", height: "410px", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "10000", backgroundColor: "rgba(236, 241, 247, 0.96)", border: "1px solid rgba(255, 255, 255, 0.72)", borderRadius: "18px", boxShadow: "0 18px 42px rgba(31, 41, 55, 0.18), inset 1px 1px 0 rgba(255, 255, 255, 0.72)", backdropFilter: "blur(12px)", display: "block", transition: "transform 0.3s", }, { className: "infoPanel", }, ); const closeIcon = createElementWithStylesAndAttributes( "svg", { //关闭按钮 position: "absolute", top: "10px", left: "10px", width: "1em", height: "1em", fill: "currentColor", cursor: "pointer", zIndex: "10001", }, { innerHTML: ``, }, ); closeIcon.addEventListener("click", () => { document.body.removeChild(infoPanel); advancedNightModeSettingsPanel.style.display = "block"; }); infoPanel.appendChild(closeIcon); document.body.appendChild(infoPanel); addUsageInstructionsToInfoPanel(infoPanel); return infoPanel; } function getUniqueSelector(element) { //获取元素的唯一选择器 if (!(element instanceof Element)) { return ""; } const escapeSelector = (value) => window.CSS && CSS.escape ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&"); if (element.id) { return "#" + escapeSelector(element.id); } const testId = element.getAttribute("data-testid"); if (testId) { return `${element.tagName.toLowerCase()}[data-testid="${escapeSelector(testId)}"]`; } if (element.classList && element.classList.length > 0) { return ( element.tagName.toLowerCase() + "." + Array.from(element.classList) .slice(0, 3) .map(escapeSelector) .join(".") ); } return element.tagName.toLowerCase(); } function addUsageInstructionsToInfoPanel(infoPanel) { //添加使用说明到信息面板 const instructions = createElementWithStylesAndAttributes("div", { display: "flex", flexDirection: "column", padding: "15px", overflowY: "auto", height: "100%", boxSizing: "border-box", }); const titleContainer = createElementWithStylesAndAttributes("div", { display: "flex", justifyContent: "space-between", borderBottom: "1px solid #ccc", paddingBottom: "10px", marginBottom: "10px", }); const usageTitle = createElementWithStylesAndAttributes( "h2", { marginTop: "15px", fontsize: "18px" }, { innerText: "使用说明" }, ); const excludeTitle = createElementWithStylesAndAttributes( "h2", { marginTop: "15px", fontsize: "18px" }, { innerText: "暗夜排除元素", cursor: "pointer" }, ); const selectVisualBtn = createElementWithStylesAndAttributes( "button", { border: "1px solid rgba(148, 163, 184, 0.5)", backgroundColor: "#F8FAFC", borderRadius: "8px", padding: "8px 15px", fontSize: "14px", cursor: "pointer", marginTop: "10px", color: "#334155", boxShadow: "0 2px 6px rgba(15, 23, 42, 0.08)", }, { innerText: "可视化选择排除元素" }, ); let isVisualSelectionActive = false; selectVisualBtn.addEventListener("click", (event) => { event.stopPropagation(); isVisualSelectionActive = true; showAlert("请点击你想排除的元素(点击元素的类元素都会被排除)"); infoPanel.style.visibility = "hidden"; const links = document.querySelectorAll("a[href]"); links.forEach((link) => { link.setAttribute("data-old-href", link.href); link.removeAttribute("href"); }); }); document.body.addEventListener("click", (event) => { if (!isVisualSelectionActive) return; isVisualSelectionActive = false; event.preventDefault(); const selectedElement = event.target; const selectedElementSelector = getUniqueSelector(selectedElement); const selectors = normalizeSelectorList([ ...normalizeSelectorList(instructionsTextarea.value), selectedElementSelector, ]); instructionsTextarea.value = selectors.join(", "); GM_setValue("cancelFilter", selectors); parameterSetting(); infoPanel.style.visibility = "visible"; showAlert("您点击的元素已添加到输入框最后"); const links = document.querySelectorAll("a[data-old-href]"); links.forEach((link) => { link.href = link.getAttribute("data-old-href"); link.removeAttribute("data-old-href"); }); }); usageTitle.style.fontWeight = "bold"; excludeTitle.style.fontWeight = "normal"; excludeTitle.addEventListener("click", () => { if (usageTitle.style.fontWeight === "bold") { usageTitle.style.fontWeight = "normal"; excludeTitle.style.fontWeight = "bold"; instructionList.style.display = "none"; instructionsTextarea.style.display = "block"; instructions.appendChild(saveBtn); instructions.appendChild(selectVisualBtn); } }); usageTitle.addEventListener("click", () => { if (excludeTitle.style.fontWeight === "bold") { usageTitle.style.fontWeight = "bold"; excludeTitle.style.fontWeight = "normal"; instructionList.style.display = "block"; instructionsTextarea.style.display = "none"; instructions.removeChild(saveBtn); instructions.removeChild(selectVisualBtn); } }); const saveBtn = createElementWithStylesAndAttributes( "button", { border: "1px solid rgba(148, 163, 184, 0.5)", backgroundColor: "#F8FAFC", borderRadius: "8px", padding: "8px 15px", fontSize: "14px", cursor: "pointer", marginTop: "10px", color: "#334155", boxShadow: "0 2px 6px rgba(15, 23, 42, 0.08)", }, { innerText: "保存更改" }, ); saveBtn.addEventListener("click", () => { GM_setValue("cancelFilter", normalizeSelectorList(instructionsTextarea.value)); instructionsTextarea.value = normalizeSelectorList(instructionsTextarea.value).join(", "); parameterSetting(); showAlert("更改已保存!"); }); titleContainer.appendChild(usageTitle); titleContainer.appendChild(excludeTitle); instructions.appendChild(titleContainer); const instructionList = createElementWithStylesAndAttributes("ul", null, { className: "instructionList", }); const instructionsContent = [ //使用说明 "使用快捷键ALT+Shift+N或在手机上使用'三指触摸屏幕后再单击屏幕'来启动高级夜间模式设置。", "点击'暗夜排除元素'可以选择哪些页面元素不受夜间模式影响。也可使用'可视化选择排除'直接点击网页中欲排除的元素。", "在设置面板中自由调整参数。建议保留默认预设的三个模式,这些是经过测试的最佳参数。", "自动开启/关闭功能让您可以设置特定时间段自动应用夜间模式,关闭则持续开启。", "点击'恢复默认模式'会将设置重置为出厂默认值。", "通过点击'保存当前参数到模板',您可以将当前设置保存为新模式。", "在自定义模式中,可通过输入字段来保存您的设置。", ]; instructionsContent.forEach((text) => { const li = createElementWithStylesAndAttributes("li", null, { innerText: text, }); instructionList.appendChild(li); }); instructions.appendChild(instructionList); const instructionsTextarea = createElementWithStylesAndAttributes( "textarea", { flexBasis: "60%", width: "100%", border: "1px solid rgba(148, 163, 184, 0.55)", borderRadius: "10px", backgroundColor: "#F8FAFC", color: "#334155", padding: "10px", boxSizing: "border-box", boxShadow: "inset 0 1px 4px rgba(15, 23, 42, 0.08)", }, ); instructionsTextarea.value = getCancelFilterSelectors().join(", "); instructionsTextarea.addEventListener("input", (e) => { GM_setValue("cancelFilter", normalizeSelectorList(e.target.value)); parameterSetting(); }); instructionsTextarea.style.display = "none"; instructions.appendChild(instructionsTextarea); infoPanel.appendChild(instructions); } function togglePanel(fromPanel, toPanel) { //用于不同面板直接的动画切换特效 const fromPanelRect = fromPanel.getBoundingClientRect(); updateStylesWithAttributes(toPanel, { width: `${fromPanelRect.width}px`, height: `${fromPanelRect.height}px`, top: `${fromPanelRect.top}px`, left: `${fromPanelRect.left}px`, }); toPanel.style.opacity = "0"; toPanel.style.transform = "scale(0.95)"; toPanel.style.display = "block"; requestAnimationFrame(() => { toPanel.style.opacity = "1"; toPanel.style.transform = "scale(1)"; }); setTimeout(() => { fromPanel.style.display = "none"; }, 300); } //为页面加载和设置快捷方式添加事件侦听器 function waitForElementToDisplay( selector, callback, checkFrequencyInMs, timeoutInMs, ) { const startTimeInMs = Date.now(); (function loopSearch() { if (document.querySelector(selector) != null) { callback(); return; } else { setTimeout(function () { if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs) return; loopSearch(); }, checkFrequencyInMs); } })(); } waitForElementToDisplay( "body", function () { parameterSetting(); initialize(); const savedCustomModes = GM_getValue("customModes", {}); Object.entries(savedCustomModes).forEach(([name, settingsValues]) => { nameDisplay(name, settingsValues); }); initialCustomModes.forEach((modeName) => { const settingsValues = customModes[modeName]; nameDisplay(modeName, settingsValues, true, false); }); //桌面快捷键 let touchStartCount = 0; //用于在移动设备上启用夜间模式交互的其他逻辑 window.addEventListener( "touchstart", function (event) { // 检测是否为三指触摸 if (event.touches.length === 3) { touchStartCount = 3; } }, false, ); window.addEventListener( "touchend", function (event) { // 如果之前检测到三指触摸,并且当前没有触摸点(所有手指都被抬起) if (touchStartCount === 3 && event.touches.length === 0) { // 触发显示或隐藏设置面板 advancedNightModeSettingsPanel.style.display = advancedNightModeSettingsPanel.style.display === "none" ? "block" : "none"; event.preventDefault(); } touchStartCount = 0; }, false, ); }, 10, 5000, ); [ "fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange", ].forEach((eventName) => { document.addEventListener(eventName, parameterSetting, false); }); document.addEventListener("keydown", function (event) { if (event.altKey && event.shiftKey && event.key === "N") { if ( advancedNightModeSettingsPanel.style.display === "none" || advancedNightModeSettingsPanel.style.display === "" ) { advancedNightModeSettingsPanel.style.display = "block"; } else { advancedNightModeSettingsPanel.style.display = "none"; } } }); })();