// ==UserScript==
// @name R4 Settings
// @description R4 Settings Library
// @version 1.4.6
// @grant GM.info
// @grant GM.addStyle
// @grant GM.xmlHttpRequest
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @require https://update.gf.qytechs.cn/scripts/482042/1298685/R4%20Images.js
// ==/UserScript==
if (GM.addStyle === undefined) {
// Polyfill for Greasemonkey
GM.addStyle = function (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;
};
}
function R4Settings(options = {}) {
const images = R4Images();
GM.addStyle(`
/* css */
/* Settings */
.r4-settings {
position: relative;
}
.r4-settings > ul {
width: 350px;
display: none;
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;
}
body.r4-settings-active .r4-settings > ul {
display: block !important;
}
.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 */
`);
var tumbler;
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) {
let value = await GM.getValue(name);
if (value !== undefined) {
console.debug(`Got setting ${name}: ${JSON.stringify(value)}`);
} else {
value = options.missingSettingHandler?.(name);
}
return value;
}
async function setCongigSetting(config, option) {
if (option.value !== undefined) {
await setSetting(config.name, option.value);
} else {
await deleteSetting(config.name);
}
}
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];
}
const state = {
events: {
start: {
fired: false,
},
end: {
fired: false,
},
}
}
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() {
tumbler = buildTumbler({
handler: toggle,
name: "settings",
classes: [],
options: [
{
class: null,
},
{
class: "r4-settings-active",
},
],
});
tumbler.classList.add("r4-settings");
tumbler.classList.add("pull-right");
const dropdown = document.createElement("ul");
tumbler.appendChild(dropdown);
const item = document.createElement("div");
item.classList.add("r4-setting-header");
if (options.script_homepage) {
const name = document.createElement("div");
name.classList.add("r4-setting-label");
name.innerHTML = `<a href="${options.script_homepage}" target="_blank">${GM.info.script.name}</a>`;
item.appendChild(name);
const feedback = document.createElement("div");
feedback.classList.add("r4-setting-text-value");
feedback.innerHTML = `<a href="${options.script_homepage}/feedback" target="_blank">${options.feedback_text || "Feedback"}</a>`;
item.appendChild(feedback);
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;
}
const updateURL = results.groups.href;
const updateTumbler = document.createElement("a");
updateTumbler.href = updateURL;
updateTumbler.classList.add("r4-tumbler-settings-update");
updateTumbler.innerText = options.update_text || "Update";
tumbler.insertBefore(updateTumbler, tumbler.firstChild);
}
},
onerror(e) {
console.debug(`Failed to request install link`);
console.debug("Error:", {e});
},
});
} else {
const name = document.createElement("div");
name.classList.add("r4-setting-label");
name.innerHTML = GM.info.script.name;
item.appendChild(name);
}
const version = document.createElement("div");
version.classList.add("r4-setting-text-value");
version.innerHTML = `${options.version_text || "Version"}: ${GM.info.script.version}`;
item.appendChild(version);
addElementSetting(item);
document.addEventListener("click", close);
}
function toggle(event) {
document.body.classList.toggle("r4-settings-active");
event.stopPropagation();
event.preventDefault();
}
function close(event) {
if (!event.target.closest(".r4-settings")) {
document.body.classList.remove("r4-settings-active");
}
}
function findSubmenu(config) {
const submenuAll = 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 settingTextBlock = buildSettingTextBlock(config.submenu);
const submenu = document.createElement("ul");
submenu.addEventListener("click", (event) => {
event.stopPropagation();
});
submenu.classList.add("hidden");
const submenuArrow = document.createElement("span");
submenuArrow.classList.add("r4-setting-submenu-arrow");
const submenuElem = document.createElement("div");
submenuElem.classList.add("r4-setting");
submenuElem.classList.add("r4-setting-submenu");
submenuElem.appendChild(settingTextBlock);
submenuElem.appendChild(submenuArrow);
submenuElem.appendChild(submenu);
const submenuItem = document.createElement("li");
submenuItem.appendChild(submenuElem);
submenuItem.addEventListener("click", (event) => {
submenu.classList.toggle("hidden");
});
const dropdown = tumbler.querySelector("ul");
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 = 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 = document.createElement("div");
tumblerWrapper.classList.add("r4-tumbler-wrapper");
const tumbler = document.createElement("div");
tumbler.classList.add("r4-tumbler");
tumbler.classList.add(tumblerClassName);
tumbler.className += ` ${config.classes.join(" ")}`;
tumbler.addEventListener("click", config.handler);
for (let optionIndex = 0; optionIndex < optionsLength; optionIndex++) {
const tumblerOption = config.options[optionIndex];
const tumblerPoint = document.createElement("div");
tumblerPoint.classList.add("r4-tumbler-point");
tumbler.appendChild(tumblerPoint);
if (tumblerOption.class !== null) {
// Add dot move style for all points except initial
const enabledClassName = tumblerOption.class;
GM.addStyle(`
/* css */
.${enabledClassName} .${tumblerClassName} .r4-tumbler-dot {
transform: translateX(${optionIndex * 100}%);
}
/* !css */
`);
}
}
const tumblerDot = document.createElement("div");
tumblerDot.classList.add("r4-tumbler-dot");
tumbler.appendChild(tumblerDot);
tumblerWrapper.appendChild(tumbler);
return tumblerWrapper;
}
function buildSettingTextBlock(label) {
const settingTextBlock = document.createElement("div");
settingTextBlock.classList.add("r4-setting-text-block");
const labelSpan = document.createElement("span");
labelSpan.classList.add("r4-setting-label");
labelSpan.innerHTML = label;
settingTextBlock.appendChild(labelSpan);
return settingTextBlock;
}
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);
const defaultSelectors = [];
for (const tumplerOption of config.options) {
if (tumplerOption.class !== null) {
defaultSelectors.push(`body.${tumplerOption.class} .${settingClass} .r4-setting-text-value-1`);
}
}
for (const tumplerOption of config.options) {
const optionIndex = config.options.indexOf(tumplerOption);
const textValueClass = `r4-setting-text-value-${optionIndex + 1}`;
const textValueSpan = document.createElement("span");
textValueSpan.classList.add("r4-setting-text-value");
textValueSpan.classList.add(textValueClass);
textValueSpan.innerHTML = tumplerOption.text;
settingTextBlock.appendChild(textValueSpan);
if (optionIndex == 0) {
GM.addStyle(`
/* css */
${defaultSelectors.join(",")} {
display: none !important;
}
/* !css */
`);
} else {
const enabledClassName = tumplerOption.class;
GM.addStyle(`
/* css */
body:not(.${enabledClassName}) .${settingClass} .${textValueClass} {
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,
buildTumblerSetting,
createTumblerSetting,
addElementSetting,
setSetting,
getSetting,
afterStart,
afterEnd,
}
}