R4 Settings Library
此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/482052/1502912/R4%20Settings.js
// ==UserScript== // @name R4 Settings // @description R4 Settings Library // @version 1.4.9 // @grant GM.info // @grant GM.addStyle // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @grant GM.deleteValue // @require https://update.greasyfork.org/scripts/482042/1298685/R4%20Images.js // @require https://update.greasyfork.org/scripts/482597/1301960/R4%20Utils.js // ==/UserScript== /* ------------------------------------------------- */ /* --------------Polyfills-------------------------- */ /* ------------------------------------------------- */ // Polyfill for Greasemonkey extension function R4_addStyle (aCss) { let head = document.getElementsByTagName('head')[0]; if (head) { let style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.textContent = aCss; head.appendChild(style); return style; } return null; }; if (GM.info?.scriptHandler === 'Userscripts') { console.log("Overriding GM.addStyle for Safari Userscripts extension."); GM.addStyle = R4_addStyle; } // Polyfill for Safari Userscripts extension const original_GM_xmlHttpRequest = GM.xmlHttpRequest; async function R4_xmlHttpRequest(details) { if (details.onload) { const original_onload = details.onload; details.onload = (response) => { if (response.responseURL) { response.finalUrl = response.responseURL; } return original_onload(response); } } return await original_GM_xmlHttpRequest(details); } GM.xmlHttpRequest = R4_xmlHttpRequest; /* ------------------------------------------------- */ /* --------------Settings--------------------------- */ /* ------------------------------------------------- */ function R4Settings(options = {}) { const utils = R4Utils(); const images = R4Images(); GM.addStyle(` /* css */ /* Settings */ .r4-settings { position: relative; } .r4-settings > ul { width: 350px; background: #313131; border-top: 0; position: absolute; top: 50px; left: 0px; white-space: nowrap; box-shadow: 0 5px 20px 0px #000; border-color: #222d33; border-style: solid; border-width: 3px 3px 3px 3px; padding: 5px 0 0 0; } .r4-settings > ul:before { content: ''; display: block; position: absolute; top: -13px; left: 20px; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-bottom: 10px solid #222d33; } .r4-settings > ul:after { content: ''; display: block; position: absolute; top: -9px; left: 21px; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-bottom: 9px solid #313131; } .r4-settings > ul > li, .r4-setting-submenu > ul > li { color: #777; font-size: 10px; font-weight: bold; margin: 0 !important; padding-left: 10px; padding-right: 10px; padding-top: 5px; padding-bottom: 5px; min-height: 30px; } .r4-settings > ul > li .r4-setting, .r4-setting-submenu > ul > li .r4-setting { display: inline-block; width: 100%; } .r4-settings > ul > li .r4-tumbler, .r4-setting-submenu > ul > li .r4-tumbler { float: right; } .r4-settings .r4-setting-header { text-align: center; } .r4-settings .r4-setting-text-value { display: block; opacity: .5; } .r4-settings .r4-setting-text-block { float: left; position: relative; padding-top: 5px; } .r4-setting-submenu { position: relative; cursor: pointer; } .r4-setting-submenu > ul { background: #212121; margin: 30px -10px 0; padding: 10px 0; cursor: auto; } .r4-settings > ul > li:last-child .r4-setting-submenu > ul { margin-bottom: -5px; } .r4-setting-submenu-arrow { float: right; width: 15px; height: 15px; margin-right: 10px; margin-top: 5px; background-size: 15px 15px; background-repeat: no-repeat; background-image: url(${images.arrow}); filter: invert(100%) sepia(95%) saturate(21%) hue-rotate(280deg) brightness(106%) contrast(106%); transform: rotate(180deg); } /* Tumbler */ .r4-tumbler { width: 38px; height: 30px; background-color: #000; border: #1d92b2; border-radius: 30px; display: flex; justify-content: space-between; align-items: center; padding: 0 6px; cursor: pointer; position: relative; user-select: none; box-sizing: content-box; } .r4-tumbler-point { border-radius: 50%; content: ''; display: block; height: 20px; width: 20px; background-color: #999; background-clip: content-box; box-sizing: border-box; border-color: transparent; border-style: solid; border-width: 5px; } .r4-tumbler > .r4-tumbler-dot { position: absolute; height: 20px; width: 20px; border-radius: 50%; background-color: #fff; transition: transform .5s,background-color .5s; will-change: transform; } /* Tumbler On-Off */ .r4-on-of-tumbler .r4-tumbler-point:nth-child(1) { background-color: green; } .r4-on-of-tumbler .r4-tumbler-point:nth-child(2) { background-color: indianred; } /* Tumbler Settings */ .r4-tumbler-settings { width: 40px !important; } .r4-tumbler-settings .r4-tumbler-point { background-size: 15px 15px; background-repeat: no-repeat; background-position: center; border-width: 2px; } .r4-tumbler-settings .r4-tumbler-point:nth-child(1) { background-image: url('${images.settings}'); background-color: transparent !important; } .r4-tumbler-settings .r4-tumbler-point:nth-child(2) { background-image: url('${images.settingsclose}'); background-color: transparent !important; } .r4-tumbler-settings-update, .r4-tumbler-settings-update:hover { height: 30px; background: #f4363630; position: absolute; left: 0; margin-left: 30px; margin-top: 5px; border-radius: 30px; color: #b44b44 !important; line-height: 30px; padding: 0 20px 0 40px; cursor: pointer; text-decoration: none; } /* Tooltip */ .r4-tooltip { position: relative; display: inline-block; } .r4-tooltip .tooltiptext { background: #313131; border-top: 0; position: absolute; top: -10px; left: 35px; white-space: nowrap; box-shadow: 0 5px 20px 0px #000; border-color: #222d33; border-style: solid; border-width: 3px; visibility: hidden; width: 300px; white-space: normal; padding: 15px; position: absolute; z-index: 3; } .r4-tooltip:hover .tooltiptext { visibility: visible; } .r4-tooltip .tooltiptext:before { content: ''; display: block; position: absolute; left: -13px; top: 11px; width: 0; height: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 10px solid #222d33; } .r4-tooltip .tooltiptext:after { content: ''; display: block; position: absolute; left: -9px; top: 12px; width: 0; height: 0; border-top: 9px solid transparent; border-bottom: 9px solid transparent; border-right: 9px solid #222d33; } .r4-tooltip-icon { border-radius: 50%; background: #777; width: 14px; height: 14px; display: inline-block; text-align: center; color: #000; text-transform: lowercase; cursor: pointer; font-family: monospace, monospace; font-size: 13px; margin: 8px; } /* !css */ `); const state = { events: { start: { fired: false, }, end: { fired: false, }, } }; const elements = { tumbler: null, dropdown: null, }; buildSettings(); async function setSetting(name, value) { await GM.setValue(name, value); console.debug(`Saved setting ${name}: ${JSON.stringify(value)}`); } async function deleteSetting(name) { await GM.deleteValue(name); } async function getSetting(name) { const value = await GM.getValue(name); if (value === undefined) { return options.missingSettingHandler?.(name); } console.debug(`Got setting ${name}: ${JSON.stringify(value)}`); return value; } async function setCongigSetting(config, option) { if (option.value === undefined) { await deleteSetting(config.name); } await setSetting(config.name, option.value); } async function getConfigSetting(config) { return await getSetting(config.name); } async function getCurrentOption(config) { const currentSetting = await getConfigSetting(config); for (const tumblerOption of config.options) { const optionSetting = tumblerOption.value; if (optionSetting === currentSetting) { return tumblerOption; } } const option = getDefaultOption(config); await setCongigSetting(config, option); return option; } async function rotateSetting(config) { const currentOption = await getCurrentOption(config); const nextOption = getNextOption(config, currentOption); await setCongigSetting(config, nextOption); setBodyClass(config, nextOption); if (nextOption.reload === true) { document.location.reload(); } if (nextOption.start) { nextOption.start(); } if (nextOption.end) { nextOption.end(); } } function getDefaultOption(config) { for (const tumblerOption of config.options) { if (tumblerOption.default === true) { return tumblerOption; } } return config.options[0]; } function setBodyClass(config, option) { for (const tumblerOption of config.options) { if (tumblerOption.class) { document.body.classList.remove(tumblerOption.class); } } if (option?.class) { document.body.classList.add(option.class); } } function getNextOption(config, option) { let nextOptionIndex; if (option) { const currentOptionIndex = config.options.indexOf(option); if (currentOptionIndex < config.options.length - 1) { nextOptionIndex = currentOptionIndex + 1; } else { nextOptionIndex = 0; } } else { nextOptionIndex = 1; } return config.options[nextOptionIndex]; } function afterStart(callback) { if (state.events.start.fired === true) { callback(); } else { document.addEventListener("R4SettingsStart", callback); } } function afterEnd(callback) { if (state.events.end.fired === true) { callback(); } else { document.addEventListener("R4SettingsEnd", callback); } } async function initSetting(config) { const currentOption = await getCurrentOption(config); afterStart(() => { setBodyClass(config, currentOption); }); if (config?.start) { afterStart(() => { config.start(); }); } if (currentOption?.start) { afterStart(() => { currentOption.start(); }); } if (config?.end) { afterEnd(() => { config.end(); }); } if (currentOption?.end) { afterEnd(() => { currentOption.end(); }); } } function buildSettings() { elements.tumbler = buildTumbler({ handler: toggle, name: "settings", classes: ["r4-settings", "pull-right"], options: [ { class: null, }, { class: "r4-settings-active", }, ], }); elements.dropdown = utils.fromHTML( /* html */ ` <!-- html --> <ul class="hidden"></ul> <!-- !html --> ` ); elements.tumbler.appendChild(elements.dropdown); const header = utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-header"></div> <!-- !html --> ` ); if (options.script_homepage) { header.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-label"> <a href="${options.script_homepage}" target="_blank"> ${GM.info.script.name} </a> </div> <!-- !html --> ` )); header.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-text-value"> <a href="${options.script_homepage}/feedback" target="_blank"> ${options.feedback_text || "Feedback"} </a> </div> <!-- !html --> ` )); GM.xmlHttpRequest({ method: "GET", url: options.script_homepage, onload(response) { console.debug(`Response ${response.status} for ${response.finalUrl}`, {response}); if (response.status === 200) { const patern = /<a class="install-link" [^>]* data-script-version="(?<version>[^"]*)" [^>]* href="(?<href>[^"]*)"[^>]*>/; const results = patern.exec(response.responseText); if (!results?.groups) { console.debug(`Failed to parse install link`); return; } if (results.groups.version == GM.info.script.version) { return; } console.log(`New version ${results.groups.version} is available`); elements.tumbler.insertBefore(utils.fromHTML( /* html */ ` <!-- html --> <a class="r4-tumbler-settings-update" href="${results.groups.href}" target="_blank"> ${options.update_text || "Update"} </a> <!-- !html --> ` ), elements.tumbler.firstChild); } }, onerror(e) { console.debug(`Failed to request install link`); console.debug("Error:", {e}); }, }); } else { header.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-label"> ${GM.info.script.name} </div> <!-- !html --> ` )); } header.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-text-value"> ${options.version_text || "Version"}: ${GM.info.script.version} </div> <!-- !html --> ` )); addElementSetting(header); document.addEventListener("click", close); } function toggle(event) { elements.dropdown.classList.toggle("hidden"); event.stopPropagation(); event.preventDefault(); } function close(event) { if (!event.target.closest(".r4-settings")) { elements.dropdown.classList.add("hidden"); } } function findSubmenu(config) { const submenuAll = elements.tumbler.querySelectorAll(".r4-setting-submenu"); const submenuFiltered = Array.from(submenuAll).find( (el) => el.querySelector(".r4-setting-label").textContent === config.submenu ); if (submenuFiltered) { return submenuFiltered.querySelector("ul"); } } function createSubmenu(config) { const submenuItem = utils.fromHTML( /* html */ ` <!-- html --> <li> <div class="r4-setting r4-setting-submenu"> <span class="r4-setting-submenu-arrow"></span> <ul class="hidden"></ul> </div> </li> <!-- !html --> ` ) const submenuElem = submenuItem.querySelector(".r4-setting-submenu"); submenuElem.insertBefore( buildSettingTextBlock(config.submenu), submenuElem.firstChild ); const submenu = submenuElem.querySelector("ul"); submenu.addEventListener("click", (event) => { event.stopPropagation(); }); submenuItem.addEventListener("click", (event) => { submenu.classList.toggle("hidden"); }); elements.dropdown.appendChild(submenuItem); return submenu; } function addElementSetting(element, config) { let container; if (config?.submenu) { let submenu = findSubmenu(config); if (!submenu) { submenu = createSubmenu(config); } container = submenu; } else { const dropdown = elements.tumbler.querySelector("ul"); container = dropdown; } const item = document.createElement("li"); item.appendChild(element); container.appendChild(item); } function buildTumbler(config) { const optionsLength = config.options.length; const tumblerClassName = `r4-tumbler-${config.name}`; GM.addStyle(` /* css */ .${tumblerClassName} { width: ${optionsLength * 15 + optionsLength * 5}px !important; } /* !css */ `); const tumblerWrapper = utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-tumbler-wrapper ${config.classes.join(" ")}"> <div class="r4-tumbler ${tumblerClassName}"></div> </div> <!-- !html --> ` ); const tumbler = tumblerWrapper.querySelector(".r4-tumbler"); tumbler.addEventListener("click", config.handler); for (let optionIndex = 0; optionIndex < optionsLength; optionIndex++) { const tumblerOption = config.options[optionIndex]; const tumblerPoint = utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-tumbler-point"></div> <!-- !html --> ` ); tumbler.appendChild(tumblerPoint); if (tumblerOption.class) { GM.addStyle(` /* css */ .${tumblerOption.class} .${tumblerClassName} .r4-tumbler-dot { transform: translateX(${optionIndex * 100}%) !important; } /* !css */ `); } else { GM.addStyle(` /* css */ .${tumblerClassName} .r4-tumbler-dot { transform: translateX(${optionIndex * 100}%); } /* !css */ `); } } tumbler.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-tumbler-dot"></div> <!-- !html --> ` )); return tumblerWrapper; } function buildSettingTextBlock(label) { return utils.fromHTML( /* html */ ` <!-- html --> <div class="r4-setting-text-block"> <span class="r4-setting-label">${label}</span> </div> <!-- !html --> ` ); } function buildTumblerSetting(config) { for (const tumplerOption of config.options) { if (tumplerOption.class === undefined && tumplerOption.value !== undefined && tumplerOption.value !== null) { tumplerOption.class = `${config.name}-${tumplerOption.value}`; } if (tumplerOption.value === undefined && tumplerOption.class !== undefined) { tumplerOption.value = tumplerOption.class; } } initSetting(config); const originalHandler = config.handler; config.handler = async (event) => { await rotateSetting(config); originalHandler?.(event); }; const tumblerWrapper = buildTumbler(config); tumblerWrapper.classList.add("r4-setting"); const settingClass = `r4-setting-${config.name}`; tumblerWrapper.classList.add(settingClass); const settingTextBlock = buildSettingTextBlock(config.label); let textValueClassEmpty = null; for (const tumblerOption of config.options) { if (!tumblerOption.class) { const optionIndex = config.options.indexOf(tumblerOption); textValueClassEmpty = `r4-setting-text-value-${optionIndex + 1}`; } } const emptySelectors = []; for (const tumblerOption of config.options) { if (tumblerOption.class) { emptySelectors.push(`body.${tumblerOption.class} .${settingClass} .${textValueClassEmpty}`); } } for (const tumblerOption of config.options) { const optionIndex = config.options.indexOf(tumblerOption); const textValueClass = `r4-setting-text-value-${optionIndex + 1}`; settingTextBlock.appendChild(utils.fromHTML( /* html */ ` <!-- html --> <span class="r4-setting-text-value ${textValueClass}"> ${tumblerOption.text} </span> <!-- !html --> ` )); if (tumblerOption.class) { GM.addStyle(` /* css */ body:not(.${tumblerOption.class}) .${settingClass} .${textValueClass} { display: none !important; } /* !css */ `); } else { GM.addStyle(` /* css */ ${emptySelectors.join(",")} { display: none !important; } /* !css */ `); } } tumblerWrapper.appendChild(settingTextBlock); return tumblerWrapper; } function createTumblerSetting(config, wrapSetting = tumblerSetting => tumblerSetting) { const tumblerSetting = buildTumblerSetting(config); addElementSetting(wrapSetting(tumblerSetting), config); } if (document.body) { state.events.start.fired = true; } else { new MutationObserver((mutationList, observer) => { if (document.body && !state.events.start.fired) { document.dispatchEvent(new Event("R4SettingsStart")); state.events.start.fired = true; observer.disconnect(); } }).observe(document.documentElement, {childList: true}); } if (/complete|interactive|loaded/.test(document.readyState)) { state.events.end.fired = true; } else { document.addEventListener("DOMContentLoaded", () => { document.dispatchEvent(new Event("R4SettingsEnd")); state.events.end.fired = true; }); } return { tumbler: elements.tumbler, buildTumblerSetting, createTumblerSetting, addElementSetting, setSetting, getSetting, afterStart, afterEnd, } }