Skip to content

Instantly share code, notes, and snippets.

@Sasquire
Last active February 21, 2022 21:54
Show Gist options
  • Save Sasquire/22036ea2eb5bee6034622b2f54f2122c to your computer and use it in GitHub Desktop.
Save Sasquire/22036ea2eb5bee6034622b2f54f2122c to your computer and use it in GitHub Desktop.

E621 Extensions Setting Page

This is supposed to be a universal settings-page utility for userscripts on e621. Designed to be simple, quick, and useful, I hope that this script can be integrated with other scripts and achieve a unified feel of userscripts on e621.

How to use

You can include this script in your userscript either by (I) copy-pasting it. (II) Using @require in your userscript's header. (III) Compiling this file into your userscript directly.

Once loaded, you can write custom settings like so

// Example settings
const ex_settings = new window.Setting({
	name: 'Testing',
	url: 'https://e621.net/extensions',
	description: 'things for that one thing that does stuff'
});

ex_settings.checkbox({
	name: 'Good Setting',
	key: 'setting_key', // key to be stored in userscript-manager
	default: false, // default position
	description: 'This is supposed to be a setting that does very good things'
});

ex_settings.list({
	name: 'Things that are listed',
	key: 'list_key',
	description: 'This is the testing for a list option',
	default: 'random', // default selected value
	values: [
		{ name: '1', value: 'option1' },
		{ name: '2', value: 'what' },
		{ name: 'great', value: 'more' },
		{ name: 'bad', value: 'random' },
		{ name: 'ooops', value: 'norepeats' }
	]
});

ex_settings.custom({
	name: 'what',
	key: 'testtttt',
	description: 'does stuff',
	default: 'both', // default text-value
	placeholder: 'ahhhh', // placeholder to use when empty
	is_secret: true // if true, sets input to be of type 'password'
});

The types of settings you have access to are checkbox, list, and custom. checkbox provides a togleable option, list provides a dropdown list, and custom accepts a string supplied by the user.

License

All code in this repository is licensed under the Unlicense and is in the Public Domain.

((base_html, base_css, GM) => {
/*
Example settings
const a = new Setting({
name: 'Testing',
url: 'https://e621.net/extensions',
description: 'things for that one thing that does stuff'
});
a.checkbox({
name: 'Good Setting',
key: 'setting_key',
default: false,
description: 'This is supposed to be a setting that does very good things'
});
a.list({
name: 'Things that are listed',
key: 'list_key',
description: 'This is the testing for a list option',
default: 'random',
values: [
{ name: '1', value: 'option1' },
{ name: '2', value: 'what' },
{ name: 'great', value: 'more' },
{ name: 'bad', value: 'random' },
{ name: 'ooops', value: 'norepeats' }
]
});
a.custom({
name: 'what',
key: 'testtttt',
description: 'does stuff',
default: 'both',
placeholder: 'ahhhh',
is_secret: true
}); */
function load_page_and_defaults () {
const is_correct_url = window.location.href === 'https://e621.net/extensions';
const is_loaded = document.body.dataset.page_loaded === 'true';
if (is_correct_url && is_loaded === false) {
// This is the first time that the page has been loaded.
clear_page();
init_css();
init_page();
init_common();
}
// Export so it can be used in both browserify and greasemonkey
if (module && module.exports) {
module.exports = Setting;
} else {
window.Setting = Setting;
}
return null;
function init_css () {
const node = document.createElement('style');
node.type = 'text/css';
node.textContent = base_css;
document.head.appendChild(node);
}
function clear_page () {
while (document.head.firstChild) {
document.head.removeChild(document.head.firstChild);
}
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
}
function init_page () {
document.body.innerHTML = base_html;
document.body.dataset.page_loaded = true;
}
function init_common () {
const settings = new Setting({
// Zero width space to ensure this is at the top
name: '\u200BCommon',
description: 'Settings that are common throughout many userscripts'
});
settings.button({
name: 'Update Scripts',
id: 'update_credentials_button',
value: 'Update',
description: 'Pressing this button should update username and API key on all relevant userscripts'
});
settings.custom({
name: 'Username',
placeholder: 'username',
description: 'This should be your username on e621.net',
key: null,
id: 'credentials_username',
is_secret: false
});
settings.custom({
name: 'API Key',
placeholder: 'API Key',
description: 'Your api key which can be found from your <a href="https://e621.net/users/home">homepage</a>',
key: null,
id: 'credentials_api_key',
is_secret: true
});
}
}
function do_constructor (options) {
// Must have options.name
// If options.url is not present, will not link anywhere
// If options.description is not present, will have no description
const container = document.createElement('div');
container.id = options.name;
container.classList.add('setting_section');
// Title
container.appendChild((() => {
const type = options.url !== undefined ? 'a' : 'span';
const title = document.createElement(type);
title.textContent = options.name + '\u200B'; // Add zerowidth space
title.classList.add('setting_header');
if (options.url !== undefined) {
title.href = options.url;
}
return title;
})());
// Description
container.appendChild((() => {
const description = document.createElement('span');
description.classList.add('setting_description');
if (options.description !== undefined) {
description.textContent = options.description;
}
return description;
})());
// Setting values
const settings_div = document.createElement('div');
settings_div.classList.add('setting_values');
['Name', 'Value', 'Description']
.map(e => {
const span = document.createElement('span');
span.textContent = e;
span.classList.add('settings_table_head');
return span;
})
.forEach(e => settings_div.appendChild(e));
container.appendChild(settings_div);
return container;
}
async function get_value (key, default_value) {
return GM.getValue(key)
.then(e => e === undefined ? default_value : e);
}
function do_checkbox (options) {
// Must have options.name, options.key, and options.section
// options.default defaults to false
const checkbox_name = `${options.section}_${options.key}`;
// Title
const title = document.createElement('label');
title.textContent = options.name;
title.htmlFor = checkbox_name;
// Checkbox with load saved setting
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = checkbox_name;
get_value(options.key, options.default || false)
.then(e => (checkbox.checked = e));
checkbox.addEventListener('change', () => {
GM.setValue(options.key, checkbox.checked);
});
return [title, checkbox, options.description || ''];
}
function do_list (options) {
const select = document.createElement('select');
// Fill options
options.values.map(e => {
const option = document.createElement('option');
option.textContent = e.name;
option.value = e.value;
if (e.title) {
option.title = e.title;
}
return option;
}).forEach(e => {
select.appendChild(e);
});
// Listen for change
select.addEventListener('change', () => {
GM.setValue(options.key, select.value);
});
// Set default
get_value(options.key, options.default).then(set_value => {
Array.from(select.getElementsByTagName('option'))
.map((e, i) => ({ index: i, value: e.value }))
.filter(e => e.value === set_value)
.forEach(e => (select.selectedIndex = e.index));
});
return [options.name || '', select, options.description || ''];
}
function do_custom (options) {
const input = document.createElement('input');
input.type = options.is_secret ? 'password' : 'text';
input.placeholder = options.placeholder || '';
// You can set options.key to null to have this be a dummy
// placeholder. If you do then you can set it's id
if (options.key !== null) {
input.addEventListener('change', () => {
GM.setValue(options.key, input.value);
});
get_value(options.key, options.default || '')
.then(e => (input.value = e));
} else {
input.id = options.id;
}
return [options.name || '', input, options.description || ''];
}
class Setting {
constructor (options) {
// Insert the new node in alphabetical order. A nice qol feature
const container = do_constructor(options);
const other_sections = document.getElementsByClassName('setting_section');
const last_item = Array.from(other_sections)
.filter(e => section_compare(e, container) > 0)
.sort(section_compare)[0];
document.getElementById('settings').insertBefore(container, last_item);
this.setting_node = container.getElementsByClassName('setting_values')[0];
this.name = options.name;
function section_compare (a, b) {
const first = a.id.toLowerCase();
const second = b.id.toLowerCase();
return first.localeCompare(second);
}
}
append (node) {
const container = document.createElement('span');
if (typeof node === 'string') {
// innerHTML isn't usually liked, but what is a userscript
// going to do that it can't already do?
container.innerHTML = node;
} else {
container.appendChild(node);
}
this.setting_node.appendChild(container);
}
checkbox (options) {
// options = { name, key, section, default }
do_checkbox({
...options,
section: this.setting_node.parentNode.id
}).forEach(this.append.bind(this));
}
list (options) {
// options = {
// name, description, default, key
// values = [{ name, value, title? }],
// }
do_list(options).forEach(this.append.bind(this));
}
custom (options) {
// options = {
// name, default?, key?, id?
// placeholder, description, is_secret,
// }
do_custom(options).forEach(this.append.bind(this));
}
button (options) {
const button = document.createElement('button');
button.textContent = options.value || '';
button.id = options.id;
[options.name || '', button, options.description || '']
.forEach(this.append.bind(this));
}
};
load_page_and_defaults();
})(`
<h1>e621 Extension Hub</h1>
<div id="settings"></div>
`, `
:root {
--background-blue: #031131;
--home-blue: #012e56;
--standard-blue: #152f56;
--comment-blue: #213a5f;
--quote-blue: #284a81;
--link-blue: #b4c7d9;
--hover-blue: #2e76b4;
--other-blue: #174891;
--yellow: #fdba31;
--light-yellow: #ffde9b;
--dark-yellow: #d8b162;
}
body {
background-color: var(--background-blue);
background-image: url(https://e621.net/images/stripe.png);
}
/* Title at the top of the page */
h1 {
padding: 1rem 3rem;
color: var(--yellow);
background-color: var(--standard-blue);
border-radius: 1rem;
}
.setting_section {
background-color: var(--standard-blue);
margin: 1rem 0px;
padding: 0.5rem 1.5rem 1.5rem 1.5rem;
border-radius: 1rem;
}
/* Header and description */
.setting_header {
color: var(--yellow);
margin: 0px 0.5rem 0.5rem 1rem;
display: inline-block;
font-size: 1.5rem;
font-weight: 600;
}
.setting_description {
color: #ccc;
}
/* Actual settings to be changed */
.setting_values {
display: grid;
grid-template-columns: 1fr 100px 4fr;
color: #ccc;
}
.setting_values > * {
border-bottom: 1px solid white;
margin-bottom: 0.5rem;
padding-bottom: 0.2rem;
}
.setting_values > span > input[type=text],
.setting_values > span > input[type=password] {
width: 90px;
}
.settings_table_head {
color: var(--hover-blue);
text-decoration: underline;
}
.setting_values a, .setting_values a:visited {
color: var(--link-blue);
}
`,
(() => {
// eslint-disable-next-line no-undef
const gm_object = window.GM ? window.GM : GM;
wrap_generic('GM_setValue', 'setValue');
wrap_generic('GM_getValue', 'getValue');
return gm_object;
async function wrap_generic (generic_name, new_name) {
if (gm_object[new_name]) {
return; // Already exists
}
if (window[generic_name] === undefined) {
return; // No old function
}
gm_object[new_name] = async (...args) => new Promise((resolve, reject) => {
try {
resolve(window[generic_name](...args));
} catch (e) {
reject(e);
}
});
}
})());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment