您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
R4 Settings Library
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-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,
- }
- }