├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .prettierrc
├── LICENCE.txt
├── README.md
├── cubemania-auto-select-inspection.user.js
├── dailymotion-disable-autoplay.user.js
├── dndbeyond-jumping.user.js
├── gcpedia-ace-editor.user.js
├── github-editor-default-settings.user.js
├── google-12-hour.user.js
├── google-middle-click-search.user.js
├── greasyfork-default-sort.user.js
├── greasyfork-enable-syntax-highlighting.user.js
├── imgur-mirror.user.js
├── json-formatter.user.js
├── kitsu-external-links.user.js
├── kitsu-fansub-info.user.js
├── kitsu-mangadex.user.js
├── libs
├── README.md
└── gm_config.js
├── lingodeer-write-myself.user.js
├── loft-board-game-filter.user.js
├── mogul-tv-channel-points-claimer.user.js
├── myanimelist-external-kitsu-links.user.js
├── newspaper-paywall-bypasser.user.js
├── nintendo-store-canada.user.js
├── package-lock.json
├── package.json
├── pokemondb-default-version.user.js
├── prevent-wikia-ads.user.js
├── reddit-disable-no-participation.user.js
├── reddit-flair-linkifier.user.js
├── reddit-prevent-middle-click.user.js
├── retailmenot-enhancer.user.js
├── rulu-ad-block.user.js
├── soundcloud-toggle-continuous-play.user.js
├── telegram-emojione.user.js
├── tou-tv-srt.user.js
├── umbraco-ace-editor.user.js
├── userstyle-auto-enable-source-editor.user.js
├── userstyles-auto-keep-me-logged-in.user.js
├── wanikani-kana-search.user.js
├── wanikani-kanjidamage-mnemonics.user.js
├── wanikani-pitch-accent.user.js
├── works-burger-chooser.user.js
├── youtube-middle-click-search.user.js
├── youtube-unblocker.user.js
├── youtube-view-more.user.js
└── youtube-volume.user.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["adpyke-es6", "prettier"],
3 | "env": {
4 | "greasemonkey": true
5 | },
6 | "globals": {
7 | "waitForElems": true,
8 | "waitForUrl": true,
9 | "GM_config": true,
10 | "m": true,
11 | "wanakana": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | ignore
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "useTabs": false,
4 | "tabWidth": 2,
5 | "trailingComma": "none",
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Adrien Pyke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # My UserScripts
2 |
3 | A collection of user scripts that I made to make my experience on the web a little nicer
4 |
5 | ## List of UserScripts
6 |
7 | - [Cubemania - Auto Select Inspection](https://greasyfork.org/en/scripts/27009-cubemania-auto-select-inspection)
8 | - [Dailymotion disable autoplay](https://greasyfork.org/en/scripts/33276-dailymotion-disable-autoplay)
9 | - [D&D Beyond - Jumping Distance](https://greasyfork.org/en/scripts/40734-d-d-beyond-jumping-distance)
10 | - [GCPedia Ace Editor](https://greasyfork.org/en/scripts/31427-gcpedia-ace-editor)
11 | - [GitHub Editor - Change Default Settings](https://greasyfork.org/en/scripts/21964-github-editor-change-default-settings)
12 | - [Google, 12 hour date-time picker](https://greasyfork.org/en/scripts/32011-google-12-hour-date-time-picker)
13 | - [Google - Middle Click Search](https://greasyfork.org/en/scripts/22183-google-middle-click-search)
14 | - [Greasy Fork - Auto Enable Syntax-Highlighting Source Editor](https://greasyfork.org/en/scripts/22223-greasy-fork-auto-enable-syntax-highlighting-source-editor)
15 | - [Greasy Fork - Change Default Script Sort on User Profiles](https://greasyfork.org/en/scripts/22202-greasy-fork-change-default-script-sort-on-user-profiles)
16 | - [Hummingbird User Compare](https://greasyfork.org/en/scripts/5680-hummingbird-user-compare)
17 | - [Imgur Mirror](https://greasyfork.org/en/scripts/18806-imgur-mirror)
18 | - [JSON Formatter](https://greasyfork.org/en/scripts/407804-json-formatter?sort=daily-installs)
19 | - [Kitsu MangaDex Links](https://greasyfork.org/en/scripts/22888-kitsu-mangadex-links)
20 | - [Kitsu Fansub Info](https://greasyfork.org/en/scripts/22500-hummingbird-fansub-info)
21 | - [LingoDeer Auto Write Myself](https://greasyfork.org/en/scripts/396684-lingodeer-auto-write-myself)
22 | - [Loft Lounge Board Game Filters](https://greasyfork.org/en/scripts/22579-loft-lounge-board-game-filters)
23 | - [MyAnimeList, External Kitsu Links](https://greasyfork.org/en/scripts/23163-myanimelist-external-hummingbird-links)
24 | - [Newspaper Paywall Bypasser](https://greasyfork.org/en/scripts/18585-newspaper-paywall-bypasser)
25 | - [New reddit: Prevent middle click scroll](https://greasyfork.org/en/scripts/48534-new-reddit-prevent-middle-click-scroll)
26 | - [Nintendo Store Canada](https://greasyfork.org/en/scripts/25423-nintendo-store-canada)
27 | - [PokemonDB Default Version](https://greasyfork.org/en/scripts/25060-pokemondb-default-version)
28 | - [Prevent Wikia Ads](https://greasyfork.org/en/scripts/22420-prevent-wikia-ads)
29 | - [Reddit Disable No Participation](https://greasyfork.org/en/scripts/23529-reddit-disable-no-participation)
30 | - [Reddit Flair Linkifier](https://greasyfork.org/en/scripts/706-reddit-flair-linkifier)
31 | - [RetailMeNot Enhancer](https://greasyfork.org/en/scripts/23203-retailmenot-enhancer)
32 | - [Rulu.co remove ads](https://greasyfork.org/en/scripts/40136-rulu-co-remove-ads)
33 | - [SoundCloud Toggle Continuous Play and Autoplay](https://greasyfork.org/en/scripts/22549-soundcloud-toggle-continuous-play-and-autoplay)
34 | - [Telegram Web Emojione](https://greasyfork.org/en/scripts/38330-telegram-web-emojione)
35 | - [The Works Burger Chooser](https://greasyfork.org/en/scripts/22582-the-works-burger-chooser)
36 | - [userstyles.org - Auto Enable Source Editor](https://greasyfork.org/en/scripts/22361-userstyles-org-auto-enable-source-editor)
37 | - [userstyles.org - auto select keep me logged in](https://greasyfork.org/en/scripts/22419-userstyles-org-auto-select-keep-me-logged-in)
38 | - [View More Videos by Same YouTube Channel](https://greasyfork.org/en/scripts/370637-view-more-videos-by-same-youtube-channel)
39 | - [WaniKani Kana Search](https://greasyfork.org/en/scripts/396166-wanikani-kana-search?sort=daily-installs)
40 | - [WaniKani Kanjidamage Mnemonics](https://greasyfork.org/en/scripts/20106-wanikani-kanjidamage-mnemonics)
41 | - [Youtube Middle Click Search](https://greasyfork.org/en/scripts/6031-youtube-middle-click-search)
42 | - [Youtube Scroll Volume](https://greasyfork.org/en/scripts/376155-youtube-scroll-volume)
43 | - [Youtube Unblocker](https://greasyfork.org/en/scripts/24163-youtube-unblocker)
44 |
--------------------------------------------------------------------------------
/cubemania-auto-select-inspection.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Cubemania - Auto Select Inspection
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.1
5 | // @description auto checks the 15 second inspection checkbox on the cubemania timer
6 | // @author Adrien Pyke
7 | // @match *://www.cubemania.org/puzzles/*/timer
8 | // @grant GM_getValue
9 | // @grant GM_setValue
10 | // @grant GM_registerMenuCommand
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | const Config = {
17 | getAutoSelect(key) {
18 | const value = GM_getValue(key);
19 | if (typeof value === 'undefined') return true;
20 | return value;
21 | },
22 | setAutoSelect(key, value) {
23 | GM_setValue(key, value);
24 | }
25 | };
26 |
27 | const puzzle = location.pathname.match(/^\/puzzles\/(.+)\//iu)[1];
28 | const autoselect = Config.getAutoSelect(puzzle);
29 | if (autoselect) {
30 | document.querySelector('input.inspection-toggle').click();
31 | }
32 |
33 | GM_registerMenuCommand(
34 | `${
35 | autoselect ? 'Disable' : 'Enable'
36 | } "Auto Select Inspection" for ${puzzle}`,
37 | () => {
38 | Config.setAutoSelect(puzzle, !autoselect);
39 | location.reload();
40 | }
41 | );
42 | })();
43 |
--------------------------------------------------------------------------------
/dailymotion-disable-autoplay.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Dailymotion disable autoplay
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.4
5 | // @description Disables autoplay and auto next vid on dailymotion
6 | // @author Adrien Pyke
7 | // @match *://www.dailymotion.com/video/*
8 | // @grant none
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Util = {
16 | q(query, context = document) {
17 | return context.querySelector(query);
18 | },
19 | qq(query, context = document) {
20 | return Array.from(context.querySelectorAll(query));
21 | }
22 | };
23 |
24 | waitForElems({
25 | sel: 'body.has-skyscraper',
26 | stop: true,
27 | context: document,
28 | onmatch() {
29 | Util.q('.dmp_PlaybackButton').click();
30 | }
31 | });
32 |
33 | waitForElems({
34 | sel: '.dmp_ComingUpEndScreen:not(.dmp_is-hidden) .dmp_ComingUpEndScreen-cancel',
35 | stop: true,
36 | onmatch(cancel) {
37 | cancel.click();
38 | }
39 | });
40 | })();
41 |
--------------------------------------------------------------------------------
/dndbeyond-jumping.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name D&D Beyond - Jumping Distance
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.10
5 | // @description Adds a jumping distance section to D&D Beyond
6 | // @author Adrien Pyke
7 | // @match *://www.dndbeyond.com/profile/*/characters/*
8 | // @grant GM_getValue
9 | // @grant GM_setValue
10 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | const SCRIPT_NAME = 'D&D Beyond - Jumping Distance';
17 |
18 | const Util = {
19 | log(...args) {
20 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
21 | console.log(...args);
22 | },
23 | q(query, context = document) {
24 | return context.querySelector(query);
25 | },
26 | qq(query, context = document) {
27 | return Array.from(context.querySelectorAll(query));
28 | }
29 | };
30 |
31 | const App = {
32 | createSpeedManagerItem(label, amount) {
33 | const div = document.createElement('div');
34 | div.classList.add('speed-manager-item');
35 |
36 | const lblSpan = document.createElement('span');
37 | lblSpan.classList.add('speed-manager-item-label');
38 | lblSpan.textContent = label;
39 | div.appendChild(lblSpan);
40 |
41 | const lblAmount = document.createElement('span');
42 | lblAmount.classList.add('speed-manager-item-amount');
43 | lblAmount.textContent = `${amount} ft.`;
44 | div.appendChild(lblAmount);
45 |
46 | return div;
47 | },
48 | createOverrideItem(label, key) {
49 | const div = document.createElement('div');
50 | div.classList.add('speed-manager-override-item');
51 | div.dataset.key = key;
52 |
53 | const lblDiv = document.createElement('div');
54 | lblDiv.classList.add('speed-manager-override-item-label');
55 | lblDiv.textContent = label;
56 | div.appendChild(lblDiv);
57 |
58 | const inputDiv = document.createElement('div');
59 | inputDiv.classList.add('speed-manager-override-item-input');
60 |
61 | const value = document.createElement('input');
62 | value.type = 'number';
63 | value.min = 0;
64 | value.value = GM_getValue(`${Character.id}-${key}`) || '';
65 | inputDiv.appendChild(value);
66 |
67 | div.appendChild(inputDiv);
68 |
69 | const sourceDiv = document.createElement('div');
70 | sourceDiv.classList.add('speed-manager-override-item-source');
71 |
72 | const source = document.createElement('input');
73 | source.type = 'text';
74 | source.value = GM_getValue(`${Character.id}-${key}-source`) || '';
75 | sourceDiv.appendChild(source);
76 |
77 | div.appendChild(sourceDiv);
78 |
79 | return div;
80 | },
81 | saveOverrideItem(item) {
82 | if (item) {
83 | const key = item.dataset.key;
84 | const value = Util.q(
85 | '.speed-manager-override-item-input > input',
86 | item
87 | ).value;
88 | const source = Util.q(
89 | '.speed-manager-override-item-source > input',
90 | item
91 | ).value;
92 | GM_setValue(`${Character.id}-${key}`, value);
93 | GM_setValue(`${Character.id}-${key}-source`, source);
94 | }
95 | }
96 | };
97 |
98 | const Character = {
99 | id: parseInt(location.pathname.match(/\/([0-9]+)$/u)[1]),
100 | get strength() {
101 | return parseInt(
102 | Util.q('.character-ability-strength > .character-ability-score')
103 | .textContent
104 | );
105 | },
106 | get strengthModifier() {
107 | return parseInt(
108 | Util.q(
109 | '.character-ability-strength > .character-ability-modifier > .character-ability-stat-value'
110 | ).textContent
111 | );
112 | },
113 | get longJump() {
114 | return parseInt(
115 | GM_getValue(`${Character.id}-long-jump`) || Character.strength
116 | );
117 | },
118 | get highJump() {
119 | return parseInt(
120 | GM_getValue(`${Character.id}-high-jump`) ||
121 | Math.max(Character.strengthModifier + 3, 0)
122 | );
123 | }
124 | };
125 |
126 | waitForElems({
127 | sel: '.speed-manager-view',
128 | onmatch(view) {
129 | const items = Util.q('.speed-manager-items', view);
130 | items.appendChild(
131 | App.createSpeedManagerItem('Long Jump', Character.longJump)
132 | );
133 | items.appendChild(
134 | App.createSpeedManagerItem('High Jump', Character.highJump)
135 | );
136 |
137 | Util.q('.fullscreen-modal-accept > button').addEventListener(
138 | 'click',
139 | () => {
140 | Util.qq('.speed-manager-override-item[data-key]').forEach(item =>
141 | App.saveOverrideItem(item)
142 | );
143 | }
144 | );
145 | }
146 | });
147 |
148 | waitForElems({
149 | sel: '.speed-manager-override-list',
150 | onmatch(overrides) {
151 | overrides.appendChild(App.createOverrideItem('Long Jump', 'long-jump'));
152 | overrides.appendChild(App.createOverrideItem('High Jump', 'high-jump'));
153 | }
154 | });
155 | })();
156 |
--------------------------------------------------------------------------------
/gcpedia-ace-editor.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name GCPedia Ace Editor
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.3.1
5 | // @description Use the Ace Editor when editing things on GCPedia
6 | // @author Adrien Pyke
7 | // @match *://www.gcpedia.gc.ca/*
8 | // @grant unsafeWindow
9 | // @grant GM_addStyle
10 | // @grant GM_getValue
11 | // @grant GM_setValue
12 | // @grant GM_registerMenuCommand
13 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
14 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
15 | // ==/UserScript==
16 |
17 | (() => {
18 | 'use strict';
19 |
20 | const SCRIPT_NAME = 'GCPedia Ace Editor';
21 |
22 | const Util = {
23 | log(...args) {
24 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
25 | console.log(...args);
26 | },
27 | q(query, context = document) {
28 | return context.querySelector(query);
29 | },
30 | qq(query, context = document) {
31 | return Array.from(context.querySelectorAll(query));
32 | },
33 | addScript(src, onload) {
34 | const s = document.createElement('script');
35 | s.onload = onload;
36 | s.src = src;
37 | document.body.appendChild(s);
38 | },
39 | addScriptText(code, onload) {
40 | const s = document.createElement('script');
41 | s.onload = onload;
42 | s.textContent = code;
43 | document.body.appendChild(s);
44 | },
45 | appendAfter(elem, elemToAppend) {
46 | elem.parentNode.insertBefore(elemToAppend, elem.nextElementSibling);
47 | }
48 | };
49 |
50 | const Config = GM_config([
51 | {
52 | key: 'theme',
53 | label: 'Theme',
54 | default: 'monokai',
55 | type: 'dropdown',
56 | values: [
57 | 'ambiance',
58 | 'chaos',
59 | 'chrome',
60 | 'clouds',
61 | 'clouds_midnight',
62 | 'cobalt',
63 | 'crimson_editor',
64 | 'dawn',
65 | 'dreamweaver',
66 | 'eclipse',
67 | 'github',
68 | 'gob',
69 | 'idle_fingers',
70 | 'iplastic',
71 | 'katzenmilch',
72 | 'kr_theme',
73 | 'kuroir',
74 | 'merbivore',
75 | 'merbivore_soft',
76 | 'mono_industrial',
77 | 'monokai',
78 | 'solarized_dark',
79 | 'solarized_light',
80 | 'sqlserver',
81 | 'terminal',
82 | 'textmate',
83 | 'tomorrow',
84 | 'tomorrow_night',
85 | 'tomorrow_night_blue',
86 | 'tomorrow_night_bright',
87 | 'tomorrow_night_eighties',
88 | 'twilight',
89 | 'vibrant_ink',
90 | 'xcode'
91 | ]
92 | }
93 | ]);
94 |
95 | waitForElems({
96 | sel: '#wpTextbox1',
97 | stop: true,
98 | onmatch(textArea) {
99 | const wrapper = document.createElement('div');
100 | wrapper.id = 'ace';
101 | wrapper.textContent = textArea.value;
102 |
103 | Util.appendAfter(textArea, wrapper);
104 |
105 | GM_addStyle(`
106 | .ace_editor {
107 | height: 600px;
108 | }
109 | .wikiEditor-ui,
110 | #wpTextbox1 {
111 | display: none;
112 | }
113 | `);
114 |
115 | Util.addScript(
116 | 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.8/ace.js',
117 | () => {
118 | const editor = unsafeWindow.ace.edit('ace');
119 | editor.setTheme(`ace/theme/${Config.load().theme}`);
120 | editor.getSession().setMode('ace/mode/html');
121 | editor.resize();
122 |
123 | unsafeWindow.aceEditor = editor;
124 | unsafeWindow.originalTextArea = textArea;
125 |
126 | Util.addScriptText(
127 | 'aceEditor.getSession().on("change", () => originalTextArea.value = aceEditor.getValue())'
128 | );
129 |
130 | GM_registerMenuCommand('GCPedia Ace Editor Settings', () =>
131 | Config.setup(editor)
132 | );
133 | Config.onchange = (key, value) =>
134 | editor.setTheme(`ace/theme/${value}`);
135 | Config.oncancel = cfg => editor.setTheme(`ace/theme/${cfg.theme}`);
136 | }
137 | );
138 | }
139 | });
140 | })();
141 |
--------------------------------------------------------------------------------
/github-editor-default-settings.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name GitHub Editor - Change Default Settings
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.21
5 | // @description change default settings for the github editor
6 | // @author Adrien Pyke
7 | // @match *://github.com/*/new/*
8 | // @match *://github.com/*/edit/*
9 | // @grant GM_getValue
10 | // @grant GM_setValue
11 | // @grant GM_registerMenuCommand
12 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
13 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
14 | // ==/UserScript==
15 |
16 | (() => {
17 | 'use strict';
18 |
19 | const Config = GM_config([
20 | {
21 | key: 'indentMode',
22 | label: 'Indent mode',
23 | default: 'tab',
24 | type: 'dropdown',
25 | values: [
26 | { value: 'space', text: 'Spaces' },
27 | { value: 'tab', text: 'Tabs' }
28 | ]
29 | },
30 | {
31 | key: 'indentWidth',
32 | label: 'Indent size',
33 | default: 4,
34 | type: 'dropdown',
35 | values: [2, 4, 8]
36 | },
37 | {
38 | key: 'wrapMode',
39 | label: 'Line wrap mode',
40 | default: 'off',
41 | type: 'dropdown',
42 | values: [
43 | { value: 'off', text: 'No wrap' },
44 | { value: 'on', text: 'Soft wrap' }
45 | ]
46 | }
47 | ]);
48 |
49 | const updateDropdown = function (dropdown, value) {
50 | dropdown.value = value;
51 | const evt = document.createEvent('HTMLEvents');
52 | evt.initEvent('change', false, true);
53 | dropdown.dispatchEvent(evt);
54 | };
55 |
56 | const applySettings = function (cfg) {
57 | const indentMode = document.querySelector('.js-code-indent-mode');
58 | const indentWidth = document.querySelector('.js-code-indent-width');
59 | const wrapMode = document.querySelector('.js-code-wrap-mode');
60 |
61 | if (location.href.match(/^https?:\/\/github.com\/[^/]*\/[^/]*\/new\/.*/u)) {
62 | // new file
63 | updateDropdown(indentMode, cfg.indentMode);
64 | updateDropdown(indentWidth, cfg.indentWidth);
65 | updateDropdown(wrapMode, cfg.wrapMode);
66 | } else if (
67 | location.href.match(/^https?:\/\/github.com\/[^/]*\/[^/]*\/edit\/.*/u)
68 | ) {
69 | // edit file
70 | // if the file is using space indentation we don't want to change it
71 | if (indentMode.value === 'tab') {
72 | updateDropdown(indentWidth, cfg.indentWidth);
73 | }
74 | updateDropdown(wrapMode, cfg.wrapMode);
75 | }
76 | };
77 |
78 | GM_registerMenuCommand('GitHub Editor Settings', Config.setup);
79 | const settings = Config.load();
80 |
81 | waitForElems({
82 | sel: '.CodeMirror-code',
83 | onmatch() {
84 | applySettings(settings);
85 | }
86 | });
87 | })();
88 |
--------------------------------------------------------------------------------
/google-12-hour.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Google, 12 hour date-time picker
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.6
5 | // @description Switches the date time picker on google searches to a 12 hour clock
6 | // @author Adrien Pyke
7 | // @include /^https?:\/\/www\.google\.[a-zA-Z]+\/.*$/
8 | // @grant none
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Util = {
16 | q(query, context = document) {
17 | return context.querySelector(query);
18 | },
19 | qq(query, context = document) {
20 | return Array.from(context.querySelectorAll(query));
21 | }
22 | };
23 |
24 | waitForElems({
25 | sel: '.tdu-datetime-picker > div.tdu-t > div:nth-child(1) > div > ul',
26 | onmatch(hourSelector) {
27 | Util.qq('li', hourSelector).forEach(hour => {
28 | const value = parseInt(hour.dataset.value);
29 | if (value === 0) {
30 | hour.textContent = 'AM 12';
31 | } else if (value === 12) {
32 | hour.textContent = 'PM 12';
33 | } else {
34 | hour.textContent = (value < 12 ? 'AM ' : 'PM ') + (value % 12);
35 | }
36 | });
37 | }
38 | });
39 | })();
40 |
--------------------------------------------------------------------------------
/google-middle-click-search.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Google - Middle Click Search
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.2.1
5 | // @description Opens search results in new tab when you middle click
6 | // @author Adrien Pyke
7 | // @include /^https?:\/\/www\.google\.[a-zA-Z]+\/?(?:\?.*)?$/
8 | // @include /^https?:\/\/www\.google\.[a-zA-Z]+\/search\/?\?.*$/
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // @grant GM_openInTab
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | const setQueryParam = function (key, value, url = location.href) {
17 | const regex = new RegExp(`([?&])${key}=.*?(&|#|$)(.*)`, 'giu');
18 | const hasValue =
19 | typeof value !== 'undefined' && value !== null && value !== '';
20 | if (regex.test(url)) {
21 | if (hasValue) {
22 | return url.replace(regex, `$1${key}=${value}$2$3`);
23 | } else {
24 | const [path, hash] = url.split('#');
25 | url = path.replace(regex, '$1$3').replace(/(&|\?)$/u, '');
26 | if (hash) url += `#${hash[1]}`;
27 | return url;
28 | }
29 | } else if (hasValue) {
30 | const separator = url.includes('?') ? '&' : '?';
31 | const [path, hash] = url.split('#');
32 | url = `${path + separator + key}=${value}`;
33 | if (hash) url += `#${hash[1]}`;
34 | return url;
35 | } else return url;
36 | };
37 |
38 | const getUrl = function (value) {
39 | if (
40 | window.location.href.match(
41 | /^https?:\/\/www\.google\.[a-zA-Z]+\/search\/?\?.*$/u
42 | )
43 | ) {
44 | return setQueryParam('q', encodeURIComponent(value));
45 | } else {
46 | return `${location.protocol}//${
47 | location.host
48 | }/search?q=${encodeURIComponent(value)}`;
49 | }
50 | };
51 |
52 | waitForElems({
53 | sel: '#_fZl',
54 | onmatch(btn) {
55 | const input = document.querySelector('#lst-ib');
56 |
57 | btn.onmousedown = e => {
58 | if (e.button === 1) {
59 | e.preventDefault();
60 | }
61 | };
62 |
63 | btn.onclick = e => {
64 | if (e.button === 1 && input.value.trim()) {
65 | e.preventDefault();
66 | e.stopImmediatePropagation();
67 | const url = getUrl(input.value);
68 | GM_openInTab(url, true);
69 | return false;
70 | }
71 | };
72 |
73 | btn.onauxclick = btn.onclick;
74 | }
75 | });
76 |
77 | waitForElems({
78 | sel: '.sbsb_b li .sbqs_c, .sbsb_b li .sbpqs_d',
79 | onmatch(elem) {
80 | elem.onclick = e => {
81 | if (e.button === 1) {
82 | e.preventDefault();
83 | e.stopImmediatePropagation();
84 | const text = elem.classList.contains('sbpqs_d')
85 | ? elem.querySelector('span').textContent
86 | : elem.textContent;
87 | const url = getUrl(text);
88 | GM_openInTab(url, true);
89 | return false;
90 | }
91 | };
92 | elem.onauxclick = elem.onclick;
93 | }
94 | });
95 | })();
96 |
--------------------------------------------------------------------------------
/greasyfork-default-sort.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Greasy Fork - Change Default Script Sort
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.3.5
5 | // @description Change default script sort on GreasyFork
6 | // @author Adrien Pyke
7 | // @match *://greasyfork.org/*/users/*
8 | // @match *://greasyfork.org/*/scripts*
9 | // @grant GM_getValue
10 | // @grant GM_setValue
11 | // @grant GM_registerMenuCommand
12 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
13 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
14 | // @run-at document-start
15 | // ==/UserScript==
16 |
17 | (() => {
18 | 'use strict';
19 |
20 | const commonValues = [
21 | { value: 'daily-installs', text: 'Daily installs' },
22 | { value: 'total_installs', text: 'Total installs' },
23 | { value: 'ratings', text: 'Ratings' },
24 | { value: 'created', text: 'Created date' },
25 | { value: 'updated', text: 'Updated date' },
26 | { value: 'name', text: 'Name' }
27 | ];
28 | const Config = GM_config([
29 | {
30 | key: 'all',
31 | label: 'All Scripts Sort',
32 | default: 'daily-installs',
33 | type: 'dropdown',
34 | values: commonValues
35 | },
36 | {
37 | key: 'search',
38 | label: 'Search Sort',
39 | default: 'relevance',
40 | type: 'dropdown',
41 | values: [{ value: 'relevance', text: 'Relevance' }, ...commonValues]
42 | },
43 | {
44 | key: 'user',
45 | label: 'User Profile Sort',
46 | default: 'daily-installs',
47 | type: 'dropdown',
48 | values: commonValues
49 | }
50 | ]);
51 | GM_registerMenuCommand('GreasyFork Sort Settings', Config.setup);
52 |
53 | const onSearch = location.href.match(
54 | /^https?:\/\/greasyfork\.org\/.+?\/scripts\/?.*\?.*q=/iu
55 | );
56 | const onScripts = location.href.match(
57 | /^https?:\/\/greasyfork\.org\/.+?\/scripts\/?/iu
58 | );
59 | const onProfile = location.href.match(
60 | /^https?:\/\/greasyfork\.org\/.+?\/users\//iu
61 | );
62 |
63 | waitForElems({
64 | sel: '#script-list-sort > ul > li:first-of-type > a',
65 | stop: true,
66 | onmatch(defaultSort) {
67 | const url = new URL(defaultSort.href);
68 | url.searchParams.set('sort', onSearch ? 'relevance' : 'daily-installs');
69 | defaultSort.href = url.href;
70 | }
71 | });
72 |
73 | const url = new URL(location.href);
74 | const sort = url.searchParams.get('sort');
75 | if (!sort) {
76 | const cfg = Config.load();
77 | let cfgSort;
78 | if (onSearch) {
79 | cfgSort = cfg.search;
80 | } else if (onScripts) {
81 | cfgSort = cfg.all;
82 | } else if (onProfile) {
83 | cfgSort = cfg.user;
84 | }
85 | if (cfgSort) {
86 | url.searchParams.set('sort', cfgSort);
87 | window.location.replace(url.href);
88 | }
89 | }
90 | })();
91 |
--------------------------------------------------------------------------------
/greasyfork-enable-syntax-highlighting.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Greasy Fork - Auto Enable Syntax-Highlighting Source Editor
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.5
5 | // @description Auto enables greasy fork's syntax-highlighting source editor
6 | // @author Adrien Pyke
7 | // @match *://greasyfork.org/*/script_versions/new*
8 | // @match *://greasyfork.org/*/scripts/*/versions/new*
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // @grant none
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | waitForElems({
17 | sel: '#enable-source-editor-code',
18 | stop: true,
19 | onmatch(checkbox) {
20 | checkbox.click();
21 | }
22 | });
23 | })();
24 |
--------------------------------------------------------------------------------
/imgur-mirror.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Imgur Mirror
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.7
5 | // @description Switches all imgur links to the mirror site http://kageurufu.net/imgur
6 | // @author Adrien Pyke
7 | // @include http*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant none
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const regex =
16 | /imgur\.com\/(?!a\/|gallery\/)(?:r\/[a-z0-9_]+\/)?([a-z0-9]+)(\.+[a-z0-9]+)?/iu;
17 | const extensions = [
18 | '.jpg',
19 | '.jpeg',
20 | '.png',
21 | '.gif',
22 | '.gifv',
23 | '.webm',
24 | '.mp4'
25 | ];
26 |
27 | const getNewLink = function (imgurLink, useGif) {
28 | const match = imgurLink.match(regex);
29 | if (match) {
30 | const file = match[1];
31 | let extension = match[2].toLowerCase();
32 | if (!extension || !extensions.includes(extension)) {
33 | extension = '.png';
34 | } else if (
35 | extension === '.gifv' ||
36 | extension === '.gif' ||
37 | extension === '.webm'
38 | ) {
39 | extension = '.mp4';
40 | }
41 | if (useGif && extension === '.mp4') {
42 | extension = '.gif';
43 | }
44 | return `http://kageurufu.net/imgur/?${file + extension}`;
45 | } else {
46 | return null;
47 | }
48 | };
49 |
50 | waitForElems({
51 | sel: 'img,a',
52 | onmatch(node) {
53 | const isImg = node.nodeName === 'IMG';
54 | const prop = isImg ? 'src' : 'href';
55 | const newLink = getNewLink(node[prop], isImg);
56 | if (newLink) {
57 | node[prop] = newLink;
58 | if (node.dataset.hrefUrl) {
59 | node.dataset.hrefUrl = newLink;
60 | }
61 | if (node.dataset.outboundUrl) {
62 | node.dataset.outboundUrl = newLink;
63 | }
64 | }
65 | }
66 | });
67 | })();
68 |
--------------------------------------------------------------------------------
/json-formatter.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name JSON Formatter
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.1
5 | // @description auto format JSON files
6 | // @author Adrien Pyke
7 | // @include /^.*\.json(\?.*)?$/
8 | // @grant GM_getValue
9 | // @grant GM_setValue
10 | // @grant GM_registerMenuCommand
11 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@c7f613292672252995cb02a0cab3b6acb18ccac5/libs/gm_config.js
12 | // ==/UserScript==
13 |
14 | (() => {
15 | 'use strict';
16 |
17 | const Config = GM_config([
18 | { key: 'tabSize', label: 'Tab Size', type: 'number', min: 0, default: 2 },
19 | { key: 'wordWrap', label: 'Word Wrap', type: 'bool', default: true }
20 | ]);
21 | GM_registerMenuCommand('JSON Formatter: Config', Config.setup);
22 |
23 | const format = ({ tabSize, wordWrap }) => {
24 | const formatted = JSON.stringify(
25 | JSON.parse(document.body.textContent),
26 | null,
27 | Number(tabSize)
28 | );
29 | document.body.innerHTML = `
`;
32 | document.getElementById('jsonArea').textContent = formatted;
33 | };
34 | format(Config.load());
35 | Config.onsave = format;
36 | })();
37 |
--------------------------------------------------------------------------------
/kitsu-external-links.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Kitsu External Links
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.1
5 | // @description adds a link to myanimelist and anilist on Kitsu entries
6 | // @author Adrien Pyke
7 | // @match *://kitsu.io/*
8 | // @grant none
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Api = {
16 | cache: {},
17 | getId: () =>
18 | document
19 | .querySelector('.cover-photo')
20 | .getAttribute('style')
21 | .match(/cover_images\/(?\d+)/iu).groups.id,
22 | getType: () =>
23 | location.href.match(/kitsu\.io\/(?anime|manga)/iu).groups.type,
24 | getLinks: async (id, type) => {
25 | const endpoint = `https://kitsu.io/api/edge/${type}/${id}?include=mappings`;
26 | if (Api.cache[endpoint]) return Api.cache[endpoint];
27 | const response = await fetch(endpoint);
28 | const json = await response.json();
29 | const mappings = json.included
30 | .filter(i =>
31 | i.attributes.externalSite.match(
32 | /^(myanimelist|anilist)\/(anime|manga)$/iu
33 | )
34 | )
35 | .map(i => i.attributes);
36 | Api.cache[endpoint] = mappings;
37 | return mappings;
38 | }
39 | };
40 |
41 | const getLinksContainer = container => {
42 | const node = document.createElement('div');
43 | node.classList.add('where-to-watch-widget');
44 | const links = document.createElement('ul');
45 | links.classList.add('nav');
46 | node.appendChild(links);
47 | container.appendChild(node);
48 | return links;
49 | };
50 |
51 | const getLinkNode = mapping => {
52 | const { site, type } = mapping.externalSite.match(
53 | /^(?myanimelist|anilist)\/(?anime|manga)$/iu
54 | ).groups;
55 | const webSite =
56 | site === 'anilist' ? 'https://anilist.co' : 'https://myanimelist.net';
57 | const href = `${webSite}/${type}/${mapping.externalId}`;
58 |
59 | const node = document.createElement('li');
60 | const link = document.createElement('a');
61 | link.classList.add('hint--top', 'hint--bounce', 'hint--rounded');
62 | Object.assign(link, { href, target: '_blank', rel: 'noopener noreferrer' });
63 | link.setAttribute('aria-label', site);
64 | node.appendChild(link);
65 | const logo = document.createElement('img');
66 | Object.assign(logo, {
67 | src:
68 | site === 'anilist'
69 | ? 'https://anilist.co/img/icons/android-chrome-512x512.png'
70 | : 'https://upload.wikimedia.org/wikipedia/commons/7/7a/MyAnimeList_Logo.png',
71 | width: '20',
72 | height: '20'
73 | });
74 | link.appendChild(logo);
75 | return node;
76 | };
77 |
78 | waitForElems({
79 | sel: '.media-sidebar',
80 | onmatch(container) {
81 | const links = getLinksContainer(container);
82 | const id = Api.getId();
83 | const type = Api.getType();
84 | Api.getLinks(id, type).then(list =>
85 | list.forEach(m => links.appendChild(getLinkNode(m)))
86 | );
87 | }
88 | });
89 | })();
90 |
--------------------------------------------------------------------------------
/kitsu-fansub-info.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Kitsu Fansub Info
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.1.25
5 | // @description Show MAL fansub info on Kitsu anime pages
6 | // @author Adrien Pyke
7 | // @match *://kitsu.io/*
8 | // @match *://myanimelist.net/anime/*
9 | // @grant GM_xmlhttpRequest
10 | // @grant GM_openInTab
11 | // @grant GM_getValue
12 | // @grant GM_setValue
13 | // @grant GM_registerMenuCommand
14 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
15 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
16 | // ==/UserScript==
17 |
18 | /*
19 |
20 | Evil Icons
21 | https://github.com/evil-icons/evil-icons
22 |
23 | Copyright (c) 2014 Alexander Madyankin , Roman Shamin
24 |
25 | MIT License
26 |
27 | Permission is hereby granted, free of charge, to any person obtaining
28 | a copy of this software and associated documentation files (the
29 | "Software"), to deal in the Software without restriction, including
30 | without limitation the rights to use, copy, modify, merge, publish,
31 | distribute, sublicense, and/or sell copies of the Software, and to
32 | permit persons to whom the Software is furnished to do so, subject to
33 | the following conditions:
34 |
35 | The above copyright notice and this permission notice shall be
36 | included in all copies or substantial portions of the Software.
37 |
38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
39 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
40 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
41 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
42 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
43 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
44 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 |
46 | */
47 |
48 | (() => {
49 | 'use strict';
50 |
51 | const SCRIPT_NAME = 'Kitsu Fansub Info';
52 | const API = 'https://kitsu.io/api/edge';
53 | const REGEX = /^https?:\/\/kitsu\.io\/anime\/([^/]+)\/?(?:\?.*)?$/u;
54 | const SECTION_ID = 'kitsu-fansubs';
55 |
56 | const Icon = {
57 | extLink:
58 | '',
59 | link: '',
60 | minus:
61 | '',
62 | plus: '',
63 | thumbsUp:
64 | ''
65 | };
66 |
67 | const Colors = {
68 | like: '#16a085',
69 | dislike: 'db2409',
70 | neutral: '#b4b4b4'
71 | };
72 |
73 | const Util = {
74 | log(...args) {
75 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
76 | console.log(...args);
77 | },
78 | q(query, context = document) {
79 | return context.querySelector(query);
80 | },
81 | qq(query, context = document) {
82 | return Array.from(context.querySelectorAll(query));
83 | },
84 | getQueryParam(name, url = location.href) {
85 | name = name.replace(/[[\]]/gu, '\\$&');
86 | const regex = new RegExp(`[?&]${name}(=([^]*)|&|#|$)`, 'u');
87 | const results = regex.exec(url);
88 | if (!results) return null;
89 | if (!results[2]) return '';
90 | return decodeURIComponent(results[2].replace(/\+/gu, ' '));
91 | },
92 | setQueryParam(key, value, url = location.href) {
93 | const regex = new RegExp(`([?&])${key}=.*?(&|#|$)(.*)`, 'giu');
94 | const hasValue =
95 | typeof value !== 'undefined' && value !== null && value !== '';
96 | if (regex.test(url)) {
97 | if (hasValue) {
98 | return url.replace(regex, `$1${key}=${value}$2$3`);
99 | } else {
100 | const [path, hash] = url.split('#');
101 | url = path.replace(regex, '$1$3').replace(/(&|\?)$/u, '');
102 | if (hash) url += `#${hash[1]}`;
103 | return url;
104 | }
105 | } else if (hasValue) {
106 | const separator = url.includes('?') ? '&' : '?';
107 | const [path, hash] = url.split('#');
108 | url = `${path + separator + key}=${value}`;
109 | if (hash) url += `#${hash[1]}`;
110 | return url;
111 | } else return url;
112 | },
113 | setNewTab(node) {
114 | node.target = '_blank';
115 | node.rel = 'noopener noreferrer';
116 | },
117 | icon(name, color, size = 20, flip = false) {
118 | const newIcon = document.createElementNS(
119 | 'http://www.w3.org/2000/svg',
120 | 'svg'
121 | );
122 | newIcon.innerHTML = Icon[name];
123 | newIcon.setAttribute('viewBox', '0 0 50 50');
124 | newIcon.setAttribute('width', size);
125 | newIcon.setAttribute('height', size);
126 | if (color) newIcon.setAttribute('fill', color);
127 | if (flip) newIcon.setAttribute('transform', 'scale(-1, -1)');
128 | newIcon.style.verticalAlign = 'sub';
129 | return newIcon;
130 | },
131 | createModal(title, bodyDiv) {
132 | const div = document.createElement('div');
133 | const modal = document.createElement('div');
134 | modal.classList.add('modal');
135 | modal.style.display = 'block';
136 | modal.style.overflowY = 'auto';
137 | div.appendChild(modal);
138 | const backdrop = document.createElement('div');
139 | backdrop.classList.add('modal-backdrop', 'fade', 'in');
140 | div.appendChild(backdrop);
141 | const dialog = document.createElement('div');
142 | dialog.classList.add('modal-dialog');
143 | modal.appendChild(dialog);
144 | const content = document.createElement('div');
145 | content.classList.add('modal-content');
146 | dialog.appendChild(content);
147 | const header = document.createElement('div');
148 | header.classList.add('modal-header');
149 | content.appendChild(header);
150 | const body = document.createElement('div');
151 | body.classList.add('modal-body');
152 | content.appendChild(body);
153 | const wrapper = document.createElement('div');
154 | wrapper.classList.add('modal-wrapper');
155 | body.appendChild(wrapper);
156 |
157 | const h4 = document.createElement('h4');
158 | h4.classList.add('modal-title');
159 | h4.textContent = title;
160 | header.appendChild(h4);
161 |
162 | wrapper.appendChild(bodyDiv);
163 |
164 | div.onclick = e => {
165 | if (e.target === modal || e.target === backdrop) {
166 | div.remove();
167 | }
168 | };
169 | document.body.appendChild(div);
170 | }
171 | };
172 |
173 | const App = {
174 | fansubCache: {},
175 | websiteCache: {},
176 | votingTabs: {},
177 | getKitsuInfo(id, cb) {
178 | // Util.log('Loading Kitsu info...');
179 | GM_xmlhttpRequest({
180 | method: 'GET',
181 | url: `${API}/anime?filter[slug]=${id}&include=mappings`,
182 | headers: {
183 | Accept: 'application/vnd.api+json'
184 | },
185 | onload(response) {
186 | Util.log('Loaded Kitsu info.');
187 | cb(JSON.parse(response.responseText));
188 | },
189 | onerror() {
190 | Util.log('Error loading Kitsu info.');
191 | }
192 | });
193 | },
194 | getMALFansubInfo(malid, cb) {
195 | // Util.log('Loading MAL info...');
196 | const url = `https://myanimelist.net/anime/${malid}`;
197 | GM_xmlhttpRequest({
198 | method: 'GET',
199 | url,
200 | onload(response) {
201 | Util.log('Loaded MAL info.');
202 | const tempDiv = document.createElement('div');
203 | tempDiv.innerHTML = response.responseText;
204 |
205 | let fansubDiv = Util.q('#inlineContent', tempDiv);
206 | if (fansubDiv) {
207 | fansubDiv = fansubDiv.parentNode;
208 | const fansubs = Util.qq('.spaceit_pad', fansubDiv)
209 | .filter(node => !node.id)
210 | .map(node => {
211 | const id = Util.q('a:nth-of-type(1)', node).dataset.groupId;
212 | const link = Util.q('a:nth-of-type(4)', node);
213 | const tagNode = Util.q('small:nth-of-type(1)', node);
214 | const tag =
215 | tagNode && tagNode.textContent !== '[]'
216 | ? tagNode.textContent
217 | : null;
218 | const langNode = Util.q('small:nth-of-type(2)', node);
219 | const lang = langNode
220 | ? langNode.textContent.substring(
221 | 1,
222 | langNode.textContent.length - 1
223 | )
224 | : null;
225 | const voteUpButton = Util.q(`#good${id}`, node);
226 | const voteDownButton = Util.q(`#bad${id}`, node);
227 | const approvalNode = Util.q('a:nth-of-type(5) > small', node);
228 | let totalApproved = 0;
229 | let totalVotes = 0;
230 | let comments = [];
231 | if (approvalNode) {
232 | const match = approvalNode.textContent.match(
233 | /([0-9]+)[^0-9]*([0-9]+)/u
234 | );
235 | if (match) {
236 | totalApproved = match[1];
237 | totalVotes = match[2];
238 | comments = Util.qq(
239 | `#fsgComments${id} > .spaceit`,
240 | node
241 | ).map(comment => ({
242 | text: comment.textContent,
243 | approves: !comment.hasAttribute('style')
244 | }));
245 | }
246 | }
247 | let value = 3;
248 | if (voteUpButton.src.match('good-on.gif$')) {
249 | value = 1;
250 | } else if (voteDownButton.src.match('bad-on.gif$')) {
251 | value = 2;
252 | }
253 | return {
254 | id,
255 | malid,
256 | name: link.textContent,
257 | url: `https://myanimelist.net${link.pathname}${link.search}`,
258 | tag,
259 | lang,
260 | totalVotes,
261 | totalApproved,
262 | value,
263 | comments
264 | };
265 | });
266 | cb({
267 | url: `${url}#inlineContent`,
268 | fansubs
269 | });
270 | } else {
271 | alert(
272 | "Failed to get MAL Fansub info. Please make sure you're logged into myanimelist.net."
273 | );
274 | }
275 | },
276 | onerror() {
277 | Util.log('Error loading MAL info.');
278 | }
279 | });
280 | },
281 | getFansubs(id, cb) {
282 | const self = this;
283 | if (self.fansubCache[id]) {
284 | cb(self.fansubCache[id]);
285 | return;
286 | }
287 | self.getKitsuInfo(id, anime => {
288 | let mal_id;
289 | if (anime.included) {
290 | for (let i = 0; i < anime.included.length; i++) {
291 | if (
292 | anime.included[i].attributes.externalSite === 'myanimelist/anime'
293 | ) {
294 | mal_id = anime.included[i].attributes.externalId;
295 | }
296 | }
297 | }
298 | if (mal_id) {
299 | self.getMALFansubInfo(mal_id, fansubs => {
300 | self.fansubCache[id] = fansubs;
301 | cb(fansubs);
302 | });
303 | } else {
304 | Util.log('MAL ID not found');
305 | const section = Util.q(`#${SECTION_ID}`);
306 | if (section) section.remove();
307 | }
308 | });
309 | },
310 | getFansubSection() {
311 | const container = document.createElement('section');
312 | container.classList.add('m-b-1');
313 | container.id = SECTION_ID;
314 |
315 | const title = document.createElement('h5');
316 | title.id = 'fansubs-title';
317 | title.textContent = 'Fansubs';
318 | container.appendChild(title);
319 |
320 | const list = document.createElement('ul');
321 | list.classList.add('media-list', 'w-100');
322 | container.appendChild(list);
323 |
324 | return container;
325 | },
326 | vote(malid, groupid, value, comment) {
327 | if (App.votingTabs.malid) {
328 | App.votingTabs.malid.close();
329 | App.votingTabs.malid = null;
330 | }
331 | let url = `https://myanimelist.net/anime/${malid}`;
332 | url = Util.setQueryParam('US_VOTE', true, url);
333 | url = Util.setQueryParam('groupid', groupid, url);
334 | url = Util.setQueryParam('value', value, url);
335 | url = Util.setQueryParam('comment', comment, url);
336 | App.votingTabs.malid = GM_openInTab(url, true);
337 | App.votingTabs.malid.onbeforeunload = () => {
338 | App.votingTabs.malid = null;
339 | };
340 | },
341 | createVotingButtons(fansub) {
342 | const votingButtons = document.createElement('div');
343 | const voteUp = document.createElement('a');
344 | const voteDown = document.createElement('a');
345 | votingButtons.appendChild(voteUp);
346 | votingButtons.appendChild(voteDown);
347 | voteUp.href = '#';
348 | voteDown.href = '#';
349 | voteUp.dataset.value = 1;
350 | voteDown.dataset.value = 2;
351 |
352 | const setVoteIcons = () => {
353 | voteUp.innerHTML = voteDown.innerHTML = '';
354 | voteUp.appendChild(
355 | Util.icon(
356 | 'thumbsUp',
357 | fansub.value === 1 ? Colors.like : Colors.neutral,
358 | 23
359 | )
360 | );
361 | voteDown.appendChild(
362 | Util.icon(
363 | 'thumbsUp',
364 | fansub.value === 2 ? Colors.dislike : Colors.neutral,
365 | 23,
366 | true
367 | )
368 | );
369 | };
370 |
371 | const voteHandler = e => {
372 | e.preventDefault();
373 | let clickedNode = e.target;
374 | if (clickedNode.nodeName === 'svg') {
375 | clickedNode = clickedNode.parentNode;
376 | } else if (clickedNode.nodeName === 'path') {
377 | clickedNode = clickedNode.parentNode.parentNode;
378 | }
379 | let value = parseInt(clickedNode.dataset.value);
380 | if (value === fansub.value) {
381 | value = 3;
382 | }
383 | App.vote(fansub.malid, fansub.id, value);
384 | fansub.value = value;
385 |
386 | setVoteIcons();
387 | };
388 | voteUp.onclick = voteHandler;
389 | voteDown.onclick = voteHandler;
390 | setVoteIcons();
391 |
392 | return votingButtons;
393 | },
394 | getFansubOutput(fansub) {
395 | const fansubDiv = document.createElement('div');
396 | fansubDiv.classList.add('stream-item', 'row');
397 |
398 | const streamWrap = document.createElement('div');
399 | streamWrap.classList.add('stream-item-wrapper', 'col-sm-12');
400 | fansubDiv.appendChild(streamWrap);
401 |
402 | const titleBlock = document.createElement('div');
403 | titleBlock.classList.add('stream-item--title-block');
404 | streamWrap.appendChild(titleBlock);
405 |
406 | const authorInfo = document.createElement('div');
407 | authorInfo.classList.add('author-info');
408 | titleBlock.appendChild(authorInfo);
409 |
410 | const streamContent = document.createElement('div');
411 | streamContent.classList.add('stream-content');
412 | streamWrap.appendChild(streamContent);
413 |
414 | const streamContentPost = document.createElement('div');
415 | streamContentPost.classList.add('stream-content-post');
416 | streamContent.appendChild(streamContentPost);
417 |
418 | const streamActivity = document.createElement('div');
419 | streamActivity.classList.add('stream-item-activity');
420 | streamWrap.appendChild(streamActivity);
421 |
422 | const streamOptions = document.createElement('div');
423 | streamOptions.classList.add('stream-item-options');
424 | streamWrap.appendChild(streamOptions);
425 |
426 | const nameLink = document.createElement('a');
427 | nameLink.classList.add('author-name');
428 | nameLink.textContent = fansub.name;
429 | nameLink.href = fansub.url;
430 | Util.setNewTab(nameLink);
431 | authorInfo.appendChild(nameLink);
432 |
433 | if (fansub.lang) {
434 | const lang = document.createElement('small');
435 | lang.classList.add('secondary-text');
436 | lang.textContent = fansub.lang;
437 | authorInfo.appendChild(lang);
438 | }
439 |
440 | const approvals = document.createElement('p');
441 | approvals.textContent = `${fansub.totalApproved} of ${fansub.totalVotes} users approve.`;
442 | streamContentPost.appendChild(approvals);
443 |
444 | streamActivity.appendChild(App.createVotingButtons(fansub));
445 |
446 | if (fansub.comments && fansub.comments.length > 0) {
447 | const commentsWrap = document.createElement('span');
448 | commentsWrap.classList.add('more-wrapper');
449 | streamOptions.appendChild(commentsWrap);
450 | const commentsLink = document.createElement('a');
451 | commentsLink.classList.add('more-drop');
452 | commentsLink.href = '#';
453 | commentsLink.textContent = 'Comments...';
454 | commentsWrap.appendChild(commentsLink);
455 | commentsLink.onclick = e => {
456 | e.preventDefault();
457 | const commentsDiv = document.createElement('div');
458 | fansub.comments.forEach(comment => {
459 | const div = document.createElement('div');
460 | div.classList.add('author-header');
461 |
462 | const smileContainer = document.createElement('div');
463 | smileContainer.classList.add('review-avatar');
464 | div.appendChild(smileContainer);
465 | smileContainer.appendChild(
466 | comment.approves
467 | ? Util.icon('plus', Colors.like, 25)
468 | : Util.icon('minus', Colors.dislike, 25)
469 | );
470 |
471 | const commentContainer = document.createElement('div');
472 | commentContainer.classList.add('comment-body');
473 | div.appendChild(commentContainer);
474 | const commentText = document.createElement('p');
475 | commentText.textContent = comment.text;
476 | commentContainer.appendChild(commentText);
477 |
478 | commentsDiv.appendChild(div);
479 | });
480 | Util.q('.author-header:last-child', commentsDiv);
481 | Util.createModal(fansub.name, commentsDiv);
482 | return false;
483 | };
484 | }
485 |
486 | return fansubDiv;
487 | },
488 | filterFansubs(fansubs, langs) {
489 | langs = langs.split(',').map(lang => lang.trim().toLowerCase());
490 | return fansubs.filter(({ lang }) => {
491 | lang = lang || 'english';
492 | return langs.includes(lang.trim().toLowerCase());
493 | });
494 | }
495 | };
496 |
497 | const Config = GM_config([
498 | {
499 | key: 'lang',
500 | label: 'Languages',
501 | placeholder: 'Languages (Comma Separated)',
502 | type: 'text'
503 | }
504 | ]);
505 |
506 | if (location.hostname === 'kitsu.io') {
507 | GM_registerMenuCommand('Kitsu Fansub Info Settings', Config.setup);
508 |
509 | const cfg = Config.load();
510 | waitForUrl(REGEX, () => {
511 | waitForElems({
512 | sel: '.media-container > .row > .col-sm-8',
513 | stop: true,
514 | onmatch(container) {
515 | const reviews = Util.qq('section.m-b-1', container)[1];
516 |
517 | let section = Util.q(`#${SECTION_ID}`, container);
518 | if (section) section.remove();
519 | section = App.getFansubSection();
520 | reviews.parentNode.insertBefore(section, reviews.nextSibling);
521 |
522 | const slug = location.href.match(REGEX)[1];
523 | const url = location.href;
524 | App.getFansubs(slug, response => {
525 | if (location.href === url) {
526 | if (cfg.lang) {
527 | response.fansubs = App.filterFansubs(
528 | response.fansubs,
529 | cfg.lang
530 | );
531 | }
532 |
533 | const extLink = Util.q('h5#fansubs-title', section);
534 | const malLink = document.createElement('a');
535 | malLink.href = response.url;
536 | Util.setNewTab(malLink);
537 | extLink.appendChild(malLink);
538 | malLink.appendChild(Util.icon('extLink'));
539 |
540 | const list = Util.q('.media-list', section);
541 |
542 | if (response.fansubs.length > 0) {
543 | const hiddenSpan = document.createElement('span');
544 | hiddenSpan.hidden = true;
545 | let addViewMore = false;
546 |
547 | response.fansubs.forEach((fansub, i) => {
548 | const fansubDiv = App.getFansubOutput(fansub);
549 | if (i < 4) {
550 | list.appendChild(fansubDiv);
551 | } else {
552 | hiddenSpan.appendChild(fansubDiv);
553 | addViewMore = true;
554 | }
555 | });
556 |
557 | if (addViewMore) {
558 | list.appendChild(hiddenSpan);
559 | const viewMoreDiv = document.createElement('div');
560 | viewMoreDiv.classList.add('text-xs-center', 'w-100');
561 |
562 | const viewMore = document.createElement('button');
563 | viewMore.classList.add('button', 'button--secondary');
564 | viewMore.textContent = 'View More Fansubs';
565 | viewMoreDiv.appendChild(viewMore);
566 |
567 | viewMore.onclick = e => {
568 | e.preventDefault();
569 | if (hiddenSpan.hidden) {
570 | hiddenSpan.hidden = false;
571 | viewMore.textContent = 'View Less Fansubs';
572 | } else {
573 | hiddenSpan.hidden = true;
574 | viewMore.textContent = 'View More Fansubs';
575 | }
576 | return false;
577 | };
578 |
579 | list.appendChild(viewMoreDiv);
580 | }
581 | } else {
582 | const p = document.createElement('p');
583 | p.textContent = 'No fansubs found.';
584 | p.style.textAlign = 'center';
585 | p.style.marginTop = '5px';
586 | list.appendChild(p);
587 | }
588 | }
589 | });
590 | }
591 | });
592 | });
593 | } else if (Util.getQueryParam('US_VOTE')) {
594 | const groupid = Util.getQueryParam('groupid');
595 | const value = Util.getQueryParam('value');
596 | const comment = Util.getQueryParam('comment');
597 | const button = Util.q(
598 | `.js-fansub-set-vote-button[data-type="${value}"][data-group-id="${groupid}"]`
599 | );
600 | button.click();
601 | if (value === '3') {
602 | setTimeout(window.close, 0);
603 | } else {
604 | waitForElems({
605 | sel: '#fancybox-inner',
606 | stop: true,
607 | onmatch(node) {
608 | const commentBox = Util.q('#fsgcomm', node);
609 | const submit = Util.q('.js-fansub-comment-button', node);
610 | commentBox.value = comment;
611 | setTimeout(() => {
612 | Util.log(submit);
613 | submit.click();
614 | setTimeout(window.close, 0);
615 | }, 300);
616 | }
617 | });
618 | }
619 | }
620 | })();
621 |
--------------------------------------------------------------------------------
/kitsu-mangadex.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Kitsu MangaDex Links
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 3.0.8
5 | // @description Adds MangaDex links to Kitsu manga pages
6 | // @author Adrien Pyke
7 | // @match *://kitsu.io/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant GM_xmlhttpRequest
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const SCRIPT_NAME = 'Kitsu MangaDex Links';
16 | const REGEX = /^https?:\/\/kitsu\.io\/manga\/[^/]+\/?(?:\?.*)?$/u;
17 |
18 | const Util = {
19 | log(...args) {
20 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
21 | console.log(...args);
22 | },
23 | q(query, context = document) {
24 | return context.querySelector(query);
25 | },
26 | qq(query, context = document) {
27 | return Array.from(context.querySelectorAll(query));
28 | },
29 | encodeQuery(query) {
30 | return encodeURIComponent(query.trim().replace(/\s+/gu, ' '));
31 | }
32 | };
33 |
34 | const App = {
35 | cache: {},
36 | getMangaDexPage(title, cb) {
37 | const self = this;
38 | if (self.cache[title]) {
39 | Util.log('Loading cached info');
40 | cb(self.cache[title]);
41 | } else {
42 | const url = `https://mangadex.org/quick_search/${Util.encodeQuery(
43 | title
44 | )}`;
45 | Util.log('Searching MangaDex:', url);
46 | GM_xmlhttpRequest({
47 | method: 'GET',
48 | url,
49 | onload(response) {
50 | const tempDiv = document.createElement('div');
51 | tempDiv.innerHTML = response.responseText;
52 |
53 | const manga = Util.q('#search_manga .manga_title', tempDiv);
54 | if (manga) {
55 | manga.href = `https://mangadex.org${manga.getAttribute('href')}`;
56 | Util.log('Link:', manga.href);
57 | self.cache[title] = manga.href;
58 | cb(manga.href);
59 | } else {
60 | Util.log('No results found');
61 | self.cache[title] = null;
62 | cb(null);
63 | }
64 | },
65 | onerror() {
66 | Util.log('Error searching MangaDex');
67 | }
68 | });
69 | }
70 | }
71 | };
72 |
73 | waitForUrl(REGEX, () => {
74 | waitForElems({
75 | sel: '.media-sidebar',
76 | stop: true,
77 | onmatch(node) {
78 | const title = Util.q('.media--title h3');
79 | const url = location.href;
80 | App.getMangaDexPage(title.textContent, manga => {
81 | const check = Util.q('.where-to-watch-widget');
82 | if (!manga && check) check.remove();
83 |
84 | if (location.href === url && manga) {
85 | if (check) {
86 | const updateLink = Util.q('#mangadex-link');
87 | updateLink.href = manga;
88 | } else {
89 | const section = document.createElement('div');
90 | section.className = 'where-to-watch-widget';
91 |
92 | const header = document.createElement('span');
93 | header.className = 'where-to-watch-header';
94 | const headerText = document.createElement('span');
95 | headerText.textContent = 'Read Online';
96 | header.appendChild(headerText);
97 | section.appendChild(header);
98 |
99 | const listWrap = document.createElement('ul');
100 | listWrap.className = 'nav';
101 | const list = document.createElement('li');
102 | listWrap.appendChild(list);
103 | section.appendChild(listWrap);
104 |
105 | const link = document.createElement('a');
106 | link.id = 'mangadex-link';
107 | link.href = manga;
108 | link.target = '_blank';
109 | link.rel = 'noopener noreferrer';
110 | link.setAttribute('aria-label', 'MangaDex');
111 | link.className = 'hint--top hint--bounce hint--rounded';
112 | const img = document.createElement('img');
113 | img.src = 'https://mangadex.org/images/misc/navbar.svg';
114 | img.style.verticalAlign = 'text-bottom';
115 | link.appendChild(img);
116 | list.appendChild(link);
117 |
118 | node.appendChild(section);
119 | }
120 | }
121 | });
122 | }
123 | });
124 | });
125 | })();
126 |
--------------------------------------------------------------------------------
/libs/README.md:
--------------------------------------------------------------------------------
1 | # Libraries
2 |
3 | Any libs I made to help with userscript development.
4 |
5 | ## GM_config
6 |
7 | A lib that provides an API to store and retrieve userscript settings, and also provides a UI for users to modify them.
8 |
9 | ```javascript
10 | GM_config(settings, (storage = 'cfg'));
11 | ```
12 |
13 | ### Usage
14 |
15 | To use this library, require `gm_config.js`. You must also grant `GM_getValue` and `GM_setValue` for it to function. If you want to hook it up to a GreaseMonkey menu command you should also grant `GM_registerMenuCommand`.
16 |
17 | Example:
18 |
19 | ```javascript
20 | // @grant GM_getValue
21 | // @grant GM_setValue
22 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@c7f613292672252995cb02a0cab3b6acb18ccac5/libs/gm_config.js
23 |
24 | const Config = GM_config([
25 | {
26 | key: 'opt1'
27 | label: 'Textbox Option',
28 | type: 'text'
29 | }, {
30 | key: 'opt2',
31 | label: 'Checkbox Option',
32 | type: 'bool',
33 | }, {
34 | key: 'opt3',
35 | label: 'Dropdown Option',
36 | default: 4,
37 | type: 'dropdown',
38 | values: [1, 2, 3, 4, 5]
39 | }
40 | ]);
41 | ```
42 |
43 | ### Parameters
44 |
45 | **`settings`**: An array of settings objects.
46 | **`storage`**: Optional. Defines what variable the settings will be stored under. Default is `cfg`.
47 |
48 | ### Settings Objects
49 |
50 | **Common Options:**
51 |
52 | ```javascript
53 | {
54 | // The key for the setting.
55 | key: 'my-setting',
56 |
57 | // The label that'll be used for the setting in the UI.
58 | label: 'Enter Value',
59 |
60 | // Optional. The default value for the setting.
61 | default: 'default',
62 |
63 | // What type of setting it is.
64 | type: 'text|number|dropdown|bool|keybinding|hidden'
65 | }
66 | ```
67 |
68 | **Type Specific Options:**
69 |
70 | **`text:`** Shows a textbox.
71 |
72 | ```javascript
73 | {
74 | // Optional. Placeholder text for the textbox.
75 | placeholder: 'Placeholder',
76 |
77 | // Optional. Sets the max length of the textbox.
78 | maxLength: 10,
79 |
80 | // Optional. If true, shows a textarea instead of a text input. Defaults to false.
81 | multiline: true,
82 |
83 | // Optional. Only applicable when multiline is true. If true the textarea will be resizable. Defaults to false.
84 | resizable: true
85 | }
86 | ```
87 |
88 | **`number:`** Show a number spinner.
89 |
90 | ```javascript
91 | {
92 | // Optional. Placeholder text for the number spinner.
93 | placeholder: 'Placeholder',
94 |
95 | // Optional. The minimum value.
96 | min: 0,
97 |
98 | // Optional. The maximum value.
99 | max: 10,
100 |
101 | // Optional. The increment size. Defaults to 1.
102 | step: 0.01
103 | }
104 | ```
105 |
106 | **`dropdown:`** Shows a dropdown list.
107 |
108 | ```javascript
109 | {
110 | // The list of possible options for the dropdown. Each entry can be a value, an object with a text and value property, or an optgroup object.
111 | values: [
112 | 1,
113 | { value: 2, text: 'Option 2'},
114 | {
115 | optgroup: 'Group',
116 | values: [
117 | 3,
118 | { value: 4, text: 'Option 4'},
119 | ]
120 | }
121 | ],
122 |
123 | // Optional. If true show a blank option. Defaults to false.
124 | showBlank: true
125 | }
126 | ```
127 |
128 | **`bool:`** Shows a checkbox.
129 |
130 | **`keybinding:`** Shows a textbox where the user can set a keybinding.
131 |
132 | ```javascript
133 | {
134 | // set the default keybinding. boolean properties ctrlKey, altKey, shiftKey, and metaKey to set modifiers. char property key to set the key.
135 | default: { ctrlKey: true, shiftKey: true, key: 'I' },
136 |
137 | // Optional. Require either ctrl, alt, shift, or meta to be pressed. Defaults to false.
138 | requireModifier: true,
139 |
140 | // Optional. Require a non-modifier key to be pressed. Defaults to false.
141 | requireKey: true
142 | }
143 | ```
144 |
145 | **`hidden:`** Hide the setting from the UI.
146 |
147 | ### Functions
148 |
149 | **`load()`**: Returns an object containing the currently stored settings.
150 | **`save(cfg)`**: Takes a configuration object and saves it to storage.
151 | **`setup()`**: Initializes a UI for the user to modify the settings.
152 |
153 | ### Using the UI
154 |
155 | You can hook the setup to a GreaseMonkey menu command by granting `GM_registerMenuCommand` and doing the following:
156 |
157 | ```javascript
158 | GM_registerMenuCommand('Command Text', Config.setup);
159 | ```
160 |
161 | ### Events
162 |
163 | GM_config has the following events:
164 |
165 | **`onchange(key, value)`**: Fires when a user changes a setting, but before saving.
166 | **`onsave(cfg)`**: Fires when the user clicks save.
167 | **`oncancel(cfg)`**: Fires when the user clicks cancel.
168 |
169 | Example:
170 |
171 | ```javascript
172 | Config.onchange = (key, value) => console.log(key, value);
173 | ```
174 |
175 | ### Other Configuration
176 |
177 | **Change storage key:** By default GM_config stores your configuration to the key `cfg`. This can be customized by passing in a second parameter.
178 |
179 | ```javascript
180 | const Config = GM_config(items, 'conf');
181 | ```
182 |
183 | **Change CSS class:** By default GM_config uses the CSS class `gm-config` to style the setup window. This can be customized by passing in a third parameter.
184 |
185 | ```javascript
186 | const Config = GM_config(items, 'cfg', 'custom-class');
187 | ```
188 |
--------------------------------------------------------------------------------
/libs/gm_config.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | 'use strict';
3 |
4 | const fromEntries =
5 | Object.fromEntries ||
6 | (iterable =>
7 | [...iterable].reduce((obj, [key, val]) => ((obj[key] = val), obj), {}));
8 |
9 | const makeElem = (type, { classes, ...opts } = {}) => {
10 | const node = Object.assign(
11 | document.createElement(type),
12 | fromEntries(Object.entries(opts).filter(([_, value]) => value != null))
13 | );
14 | classes && classes.forEach(c => node.classList.add(c));
15 | return node;
16 | };
17 |
18 | const zip = (parts, args) =>
19 | parts.reduce((acc, c, i) => acc + c + (args[i] == null ? '' : args[i]), '');
20 |
21 | const css = (parts, ...args) => {
22 | const style = zip(parts, args);
23 | window.GM_addStyle == null
24 | ? document.head.appendChild(makeElem('style', { textContent: style }))
25 | : GM_addStyle(style);
26 | };
27 |
28 | const preventDefault = e => {
29 | e.preventDefault();
30 | e.stopImmediatePropagation();
31 | e.stopPropagation();
32 | return false;
33 | };
34 |
35 | const stylesAdded = {};
36 |
37 | window.GM_config = (settings, storage = 'cfg', prefix = 'gm-config') => {
38 | let ret = null;
39 |
40 | const addStyle = () => {
41 | if (stylesAdded[prefix]) return;
42 | css`
43 | .${prefix} {
44 | display: grid;
45 | align-items: center;
46 | grid-row-gap: 5px;
47 | grid-column-gap: 10px;
48 | background-color: white;
49 | border: 1px solid black;
50 | padding: 5px;
51 | position: fixed;
52 | top: 0;
53 | right: 0;
54 | z-index: 2147483647;
55 | }
56 |
57 | .${prefix} label {
58 | grid-column: 1 / 2;
59 | color: black;
60 | text-align: right;
61 | font-size: small;
62 | font-weight: bold;
63 | }
64 |
65 | .${prefix} input,
66 | .${prefix} textarea,
67 | .${prefix} select {
68 | grid-column: 2 / 4;
69 | }
70 |
71 | .${prefix} .${prefix}-save {
72 | grid-column: 2 / 3;
73 | }
74 |
75 | .${prefix} .${prefix}-cancel {
76 | grid-column: 3 / 4;
77 | }
78 | `;
79 | stylesAdded[prefix] = true;
80 | };
81 |
82 | const load = () => {
83 | const defaults = {};
84 | settings.forEach(({ key, default: def }) => (defaults[key] = def));
85 |
86 | let cfg =
87 | window.GM_getValue != null
88 | ? GM_getValue(storage)
89 | : localStorage.getItem(storage);
90 | if (!cfg) return defaults;
91 |
92 | cfg = JSON.parse(cfg);
93 | Object.entries(defaults).forEach(([key, value]) => {
94 | if (cfg[key] == null) {
95 | cfg[key] = value;
96 | }
97 | });
98 |
99 | return cfg;
100 | };
101 |
102 | const save = cfg => {
103 | const data = JSON.stringify(cfg);
104 | window.GM_setValue != null
105 | ? GM_setValue(storage, data)
106 | : localStorage.setItem(storage, data);
107 | };
108 |
109 | const setup = () => {
110 | const createContainer = () => makeElem('form', { classes: [prefix] });
111 |
112 | const createTextbox = (
113 | name,
114 | value,
115 | placeholder,
116 | maxLength,
117 | multiline,
118 | resize
119 | ) => {
120 | const input = makeElem(multiline ? 'textarea' : 'input', {
121 | type: multiline ? null : 'text',
122 | name,
123 | value,
124 | placeholder,
125 | maxLength
126 | });
127 | if (multiline) {
128 | input.style.resize = resize ? 'vertical' : 'none';
129 | }
130 | return input;
131 | };
132 |
133 | const createNumber = (name, value, placeholder, min, max, step) =>
134 | makeElem('input', {
135 | type: 'number',
136 | value,
137 | placeholder,
138 | min,
139 | max,
140 | step
141 | });
142 |
143 | const createSelect = (name, options, value, showBlank) => {
144 | const select = makeElem('select', { name });
145 |
146 | const createOption = val => {
147 | const { value = val, text = val } = val;
148 | return makeElem('option', { value, textContent: text });
149 | };
150 |
151 | if (showBlank) {
152 | select.appendChild(createOption(''));
153 | }
154 |
155 | options.forEach(opt => {
156 | if (opt.optgroup != null) {
157 | const optgroup = makeElem('optgroup', { label: opt.optgroup });
158 | select.appendChild(optgroup);
159 | opt.values.forEach(value =>
160 | optgroup.appendChild(createOption(value))
161 | );
162 | } else {
163 | select.appendChild(createOption(opt));
164 | }
165 | });
166 |
167 | select.value = value;
168 | return select;
169 | };
170 |
171 | const createCheckbox = (name, checked) =>
172 | makeElem('input', {
173 | type: 'checkbox',
174 | id: `${prefix}-${name}`,
175 | name,
176 | checked
177 | });
178 |
179 | const createKeybinding = (
180 | name,
181 | keybinding,
182 | requireModifier,
183 | requireKey
184 | ) => {
185 | const textbox = makeElem('input', {
186 | type: 'text',
187 | name,
188 | readOnly: true,
189 | placeholder: 'Press Keybinding'
190 | });
191 |
192 | const META_KEYS = ['CONTROL', 'ALT', 'SHIFT', 'META'];
193 |
194 | const setText = () => {
195 | const parts = [];
196 | const d = textbox.dataset;
197 | if (d.ctrlKey === 'true') parts.push('CTRL');
198 | if (d.altKey === 'true') parts.push('ALT');
199 | if (d.shiftKey === 'true') parts.push('SHIFT');
200 | if (d.metaKey === 'true') parts.push('META');
201 | if (d.key && !META_KEYS.includes(d.key)) parts.push(d.key);
202 | textbox.value = parts.join('+');
203 | };
204 |
205 | const setDataset = ({
206 | ctrlKey = false,
207 | altKey = false,
208 | shiftKey = false,
209 | metaKey = false,
210 | key = ''
211 | } = {}) => {
212 | Object.assign(textbox.dataset, {
213 | ctrlKey,
214 | altKey,
215 | shiftKey,
216 | metaKey,
217 | key: key.toUpperCase()
218 | });
219 | setText();
220 | };
221 |
222 | setDataset(keybinding);
223 |
224 | textbox.addEventListener(
225 | 'keydown',
226 | e => {
227 | preventDefault(e);
228 | if (
229 | requireModifier &&
230 | !e.ctrlKey &&
231 | !e.altKey &&
232 | !e.shiftKey &&
233 | !e.metaKey
234 | )
235 | return false;
236 | const key = (e.key || '').toUpperCase();
237 | if (requireKey && (!key || META_KEYS.includes(key))) return false;
238 | setDataset(e);
239 | return false;
240 | },
241 | true
242 | );
243 | textbox.addEventListener('keypress', preventDefault, true);
244 |
245 | return textbox;
246 | };
247 |
248 | const createButton = (text, onclick, classname) =>
249 | makeElem('button', {
250 | textContent: text,
251 | onclick,
252 | classes: [`${prefix}-${classname}`]
253 | });
254 |
255 | const createLabel = (label, htmlFor) =>
256 | makeElem('label', { htmlFor, textContent: label });
257 |
258 | const init = cfg => {
259 | const controls = {};
260 |
261 | const getValue = (type, control) => {
262 | const getKeybindingValue = () => {
263 | const ctrlKey = control.dataset.ctrlKey === 'true';
264 | const altKey = control.dataset.altKey === 'true';
265 | const shiftKey = control.dataset.shiftKey === 'true';
266 | const metaKey = control.dataset.metaKey === 'true';
267 | const key = control.dataset.key;
268 | return { ctrlKey, altKey, shiftKey, metaKey, key };
269 | };
270 | return type === 'bool'
271 | ? control.checked
272 | : type === 'keybinding'
273 | ? getKeybindingValue()
274 | : control.value;
275 | };
276 |
277 | const div = createContainer();
278 | settings
279 | .filter(({ type }) => type !== 'hidden')
280 | .forEach(setting => {
281 | const value = cfg[setting.key];
282 |
283 | let control;
284 | if (setting.type === 'text') {
285 | control = createTextbox(
286 | setting.key,
287 | value,
288 | setting.placeholder,
289 | setting.maxLength,
290 | setting.multiline,
291 | setting.resizable
292 | );
293 | } else if (setting.type === 'number') {
294 | control = createNumber(
295 | setting.key,
296 | value,
297 | setting.placeholder,
298 | setting.min,
299 | setting.max,
300 | setting.step
301 | );
302 | } else if (setting.type === 'dropdown') {
303 | control = createSelect(
304 | setting.key,
305 | setting.values,
306 | value,
307 | setting.showBlank
308 | );
309 | } else if (setting.type === 'bool') {
310 | control = createCheckbox(setting.key, value);
311 | } else if (setting.type === 'keybinding') {
312 | control = createKeybinding(
313 | setting.key,
314 | value,
315 | setting.requireModifier,
316 | setting.requireKey
317 | );
318 | }
319 |
320 | div.appendChild(createLabel(setting.label, control.id));
321 | div.appendChild(control);
322 | controls[setting.key] = control;
323 |
324 | control.addEventListener(
325 | setting.type === 'dropdown' ? 'change' : 'input',
326 | () => {
327 | if (!ret.onchange) return;
328 | const control = controls[setting.key];
329 | ret.onchange(setting.key, getValue(setting.type, control));
330 | }
331 | );
332 | });
333 |
334 | div.appendChild(
335 | createButton(
336 | 'Save',
337 | () => {
338 | settings
339 | .filter(({ type }) => type !== 'hidden')
340 | .forEach(({ key, type }) => {
341 | const control = controls[key];
342 | cfg[key] = getValue(type, control);
343 | });
344 | save(cfg);
345 |
346 | if (ret.onsave) ret.onsave(cfg);
347 |
348 | div.remove();
349 | },
350 | 'save'
351 | )
352 | );
353 |
354 | div.appendChild(
355 | createButton(
356 | 'Cancel',
357 | () => {
358 | if (ret.oncancel) {
359 | ret.oncancel(cfg);
360 | }
361 | div.remove();
362 | },
363 | 'cancel'
364 | )
365 | );
366 |
367 | document.body.appendChild(div);
368 | };
369 |
370 | init(load());
371 | };
372 |
373 | addStyle();
374 |
375 | ret = {
376 | load,
377 | save,
378 | setup
379 | };
380 | return ret;
381 | };
382 | })();
383 |
--------------------------------------------------------------------------------
/lingodeer-write-myself.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name LingoDeer Auto Write Myself
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.3
5 | // @description Auto switch to "Write Myself", and adds press enter to continue.
6 | // @author Adrien Pyke
7 | // @match *://www.lingodeer.com/learn-languages/*
8 | // @grant none
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | let helpClicked = false;
16 | waitForElems({
17 | sel: '.switchBtn',
18 | onmatch: btn => {
19 | if (
20 | btn.textContent.trim() === 'I want to write it myself' &&
21 | !helpClicked
22 | )
23 | btn.click();
24 | else btn.addEventListener('click', () => (helpClicked = true));
25 | helpClicked = false;
26 | }
27 | });
28 | waitForElems({
29 | sel: '.textAreaInput textarea',
30 | onmatch: input => (
31 | input.addEventListener('keydown', e => {
32 | if (e.key !== 'Enter') return;
33 | const btn = document.querySelector(
34 | '.checkBtn.active, .continueBtn:not(.wrong)'
35 | );
36 | btn && btn.click();
37 | e.preventDefault();
38 | return false;
39 | }),
40 | input.focus()
41 | )
42 | });
43 | waitForElems({
44 | sel: '.signBtn',
45 | stop: true,
46 | onmatch: btn => btn.click()
47 | });
48 | })();
49 |
--------------------------------------------------------------------------------
/loft-board-game-filter.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Loft Lounge Board Game Filters
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.6
5 | // @description Adds Filters to the Loft Lounge board game page
6 | // @author Adrien Pyke
7 | // @match *://www.theloftlounge.ca/pages/board-games*
8 | // @match *://www.theloftlounge.ca/pages/new-games*
9 | // @grant none
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const SCRIPT_NAME = 'Loft Lounge Board Game Filters';
16 |
17 | const Util = {
18 | log(...args) {
19 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
20 | console.log(...args);
21 | },
22 | q(query, context = document) {
23 | return context.querySelector(query);
24 | },
25 | qq(query, context = document) {
26 | return Array.from(context.querySelectorAll(query));
27 | },
28 | prepend(parent, child) {
29 | parent.insertBefore(child, parent.firstChild);
30 | },
31 | createTextbox() {
32 | const input = document.createElement('input');
33 | input.type = 'text';
34 | return input;
35 | },
36 | createCheckbox(lbl) {
37 | const label = document.createElement('label');
38 | const checkbox = document.createElement('input');
39 | checkbox.type = 'checkbox';
40 | label.appendChild(checkbox);
41 | label.appendChild(document.createTextNode(lbl));
42 | return label;
43 | },
44 | createButton(text, onclick) {
45 | const button = document.createElement('button');
46 | button.textContent = text;
47 | button.onclick = onclick;
48 | return button;
49 | },
50 | toTitleCase(str) {
51 | return str.replace(
52 | /[a-z0-9]+/giu,
53 | word => word.slice(0, 1).toUpperCase() + word.slice(1)
54 | );
55 | },
56 | appendStyle(str) {
57 | const style = document.createElement('style');
58 | style.textContent = str;
59 | document.head.appendChild(style);
60 | }
61 | };
62 |
63 | Util.appendStyle(/* css */ `
64 | /* Fix Ctrl+F */
65 | #sidebar-holder, #content-holder {
66 | position: static!important;
67 | height: auto!important;
68 | }
69 | #content {
70 | position: static!important;
71 | margin-left: 290px;
72 | width: auto;
73 | }
74 | #content-holder {
75 | width: 100%!important;
76 | float: right;
77 | }
78 | @media (max-width: 900px), (max-device-width: 1024px) {
79 | #content {
80 | margin-left: 0px;
81 | }
82 | }
83 |
84 | /* Additional Styles */
85 | .category-list {
86 | border: 1px solid black;
87 | border-radius: 6px;
88 | padding: 6px;
89 | background-color: white;
90 | color: black;
91 | position: absolute;
92 | z-index: 9999;
93 | }
94 |
95 | .rte table tr td:nth-of-type(1) {
96 | width: 75%;
97 | }
98 |
99 | .rte table tr td:nth-of-type(2) {
100 | width: 25%;
101 | }
102 | `);
103 |
104 | const table = Util.q('#page-content > div > table > tbody');
105 | const rows = Util.qq('tr:not(:first-of-type)', table);
106 | const categories = new Set(
107 | rows
108 | .map(row => {
109 | const typos = {
110 | Triva: 'Trivia'
111 | };
112 | const td = Util.q('td:last-of-type', row);
113 | let category = Util.toTitleCase(td.textContent.trim());
114 | if (typos[category]) {
115 | td.textContent = category = typos[category];
116 | }
117 | return category;
118 | })
119 | .sort()
120 | );
121 |
122 | const tr = document.createElement('tr');
123 | const td1 = document.createElement('td');
124 | const td2 = document.createElement('td');
125 | tr.appendChild(td1);
126 | tr.appendChild(td2);
127 |
128 | const nameFilter = Util.createTextbox();
129 | td1.appendChild(nameFilter);
130 |
131 | const selectedCategories = [];
132 |
133 | const filter = function () {
134 | rows.forEach(row => (row.hidden = true));
135 | let rowsFilter = rows;
136 |
137 | if (selectedCategories.length > 0) {
138 | rowsFilter = rowsFilter.filter(row => {
139 | const category = Util.q('td:last-of-type', row)
140 | .textContent.trim()
141 | .toLowerCase();
142 | return selectedCategories.includes(category);
143 | });
144 | }
145 |
146 | const value = nameFilter.value.trim().toLowerCase();
147 | if (value) {
148 | rowsFilter = rowsFilter.filter(row => {
149 | const name = Util.q('td:first-of-type', row)
150 | .textContent.trim()
151 | .toLowerCase();
152 | return name.includes(value);
153 | });
154 | }
155 |
156 | rowsFilter.forEach(row => (row.hidden = false));
157 | };
158 |
159 | nameFilter.oninput = filter;
160 |
161 | const categoryDiv = document.createElement('div');
162 | categoryDiv.classList.add('category-list');
163 | categoryDiv.hidden = true;
164 |
165 | const categorySpan = document.createElement('span');
166 |
167 | categories.forEach(category => {
168 | const label = Util.createCheckbox(category);
169 | categoryDiv.appendChild(label);
170 | categoryDiv.appendChild(document.createElement('br'));
171 | const check = Util.q('input', label);
172 | check.oninput = () => {
173 | const cat = category.trim().toLowerCase();
174 | const index = selectedCategories.indexOf(cat);
175 | if (check.checked) {
176 | if (index === -1) {
177 | selectedCategories.push(cat);
178 | }
179 | } else if (index !== -1) {
180 | selectedCategories.splice(index, 1);
181 | }
182 | categorySpan.textContent = selectedCategories
183 | .map(category => Util.toTitleCase(category))
184 | .join(', ');
185 | filter();
186 | };
187 | });
188 |
189 | const categoryButton = Util.createButton('Categories...', () => {
190 | if (categoryDiv.hidden) {
191 | categoryDiv.hidden = false;
192 | } else {
193 | categoryDiv.hidden = true;
194 | }
195 | });
196 |
197 | document.body.addEventListener('click', e => {
198 | if (e.target !== categoryButton && !categoryDiv.contains(e.target)) {
199 | categoryDiv.hidden = true;
200 | }
201 | });
202 |
203 | td2.appendChild(categoryButton);
204 | td2.appendChild(document.createElement('br'));
205 | td2.appendChild(categoryDiv);
206 | td2.appendChild(categorySpan);
207 |
208 | Util.prepend(table, tr);
209 | })();
210 |
--------------------------------------------------------------------------------
/mogul-tv-channel-points-claimer.user.js:
--------------------------------------------------------------------------------
1 |
2 | // ==UserScript==
3 | // @name Truffle TV Channel Points Claimer
4 | // @namespace https://greasyfork.org/users/649
5 | // @version 3.0
6 | // @description Auto claim point on Truffle TV enabled streams.
7 | // @author Adrien Pyke
8 | // @match *://new.ludwig.social/channel-points
9 | // @match *://*.spore.build/component-instance/*
10 | // @grant none
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | setInterval(() => {
17 | const root = document.getElementById('root');
18 | const shadowRoot = root && root.firstChild.shadowRoot;
19 | const node = shadowRoot
20 | ? shadowRoot.querySelector('.claim')
21 | : document.querySelector('.claim.is-visible');
22 | if (node) node.click();
23 | }, 1000);
24 | })();
25 |
--------------------------------------------------------------------------------
/myanimelist-external-kitsu-links.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name MyAnimeList, External Kitsu Links
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.2.3
5 | // @description Adds a link to the Kitsu page in the External Links section
6 | // @author Adrien Pyke
7 | // @match *://myanimelist.net/anime/*
8 | // @match *://myanimelist.net/manga/*
9 | // @grant GM_xmlhttpRequest
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const SCRIPT_NAME = 'MyAnimeList, External Kitsu Links';
16 | const API = 'https://kitsu.io/api/edge';
17 |
18 | const Util = {
19 | log(...args) {
20 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
21 | console.log(...args);
22 | },
23 | q(query, context = document) {
24 | return context.querySelector(query);
25 | },
26 | qq(query, context = document) {
27 | return Array.from(context.querySelectorAll(query));
28 | }
29 | };
30 |
31 | const App = {
32 | getKitsuLink(type, malid, cb) {
33 | GM_xmlhttpRequest({
34 | method: 'GET',
35 | url: `${API}/mappings?filter[external_site]=myanimelist/${type}&filter[external_id]=${malid}`,
36 | headers: {
37 | Accept: 'application/vnd.api+json'
38 | },
39 | onload(response) {
40 | try {
41 | const json = JSON.parse(response.responseText);
42 | GM_xmlhttpRequest({
43 | method: 'GET',
44 | url: `${API}/mappings/${json.data[0].id}/item?fields[${type}]=slug`,
45 | headers: {
46 | Accept: 'application/vnd.api+json'
47 | },
48 | onload(response) {
49 | try {
50 | const json = JSON.parse(response.responseText);
51 | if (type === 'anime') {
52 | cb(`https://kitsu.io/anime/${json.data.attributes.slug}`);
53 | } else if (type === 'manga') {
54 | cb(`https://kitsu.io/manga/${json.data.attributes.slug}`);
55 | }
56 | } catch (err) {
57 | Util.log('Failed to parse media API results');
58 | }
59 | },
60 | onerror() {
61 | Util.log('Failed to get Kitsu media slug');
62 | }
63 | });
64 | } catch (err) {
65 | Util.log('Failed to parse mapping API results');
66 | }
67 | },
68 | onerror() {
69 | Util.log('Failed to get Kitsu mapping ID');
70 | }
71 | });
72 | }
73 | };
74 |
75 | const match = location.href.match(
76 | /^https?:\/\/myanimelist\.net\/(anime|manga)\/([0-9]+)/iu
77 | );
78 | if (match) {
79 | const type = match[1];
80 | const id = match[2];
81 | App.getKitsuLink(type, id, href => {
82 | Util.log('Link:', href);
83 | const container = Util.q(
84 | '#content > table > tbody > tr > td.borderClass .pb16'
85 | );
86 | if (container) {
87 | container.appendChild(document.createTextNode(', '));
88 |
89 | const a = document.createElement('a');
90 | a.textContent = 'Kitsu';
91 | a.href = href;
92 | a.target = '_blank';
93 | a.rel = 'noopener';
94 | container.appendChild(a);
95 | } else {
96 | const sidebar = Util.q(
97 | '#content > table > tbody > tr > td.borderClass > div'
98 | );
99 |
100 | const header = document.createElement('h2');
101 | header.textContent = 'External Links';
102 | sidebar.appendChild(header);
103 |
104 | const links = document.createElement('div');
105 | links.classList.add('pb16');
106 | sidebar.appendChild(links);
107 |
108 | const b = document.createElement('a');
109 | b.textContent = 'Kitsu';
110 | b.href = href;
111 | b.target = '_blank';
112 | b.rel = 'noopener';
113 | links.appendChild(b);
114 | }
115 | });
116 | }
117 | })();
118 |
--------------------------------------------------------------------------------
/newspaper-paywall-bypasser.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Newspaper Paywall Bypasser
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.2.6
5 | // @description Bypass the paywall on online newspapers
6 | // @author Adrien Pyke
7 | // @match *://www.thenation.com/article/*
8 | // @match *://www.wsj.com/articles/*
9 | // @match *://blogs.wsj.com/*
10 | // @match *://www.bostonglobe.com/*
11 | // @match *://www.nytimes.com/*
12 | // @match *://myaccount.nytimes.com/mobile/wall/smart/*
13 | // @match *://mobile.nytimes.com/*
14 | // @match *://www.latimes.com/*
15 | // @match *://www.washingtonpost.com/*
16 | // @grant GM_xmlhttpRequest
17 | // @grant GM_getValue
18 | // @grant GM_setValue
19 | // @grant GM_registerMenuCommand
20 | // @grant unsafeWindow
21 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
22 | // @noframes
23 | // ==/UserScript==
24 |
25 | (() => {
26 | 'use strict';
27 |
28 | // short reference to unsafeWindow (or window if unsafeWindow is unavailable e.g. bookmarklet)
29 | const W = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
30 | const SCRIPT_NAME = 'Newspaper Paywall Bypasser';
31 |
32 | const Util = {
33 | log(...args) {
34 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
35 | console.log(...args);
36 | },
37 | q(query, context = document) {
38 | return context.querySelector(query);
39 | },
40 | qq(query, context = document) {
41 | return Array.from(context.querySelectorAll(query));
42 | },
43 | getQueryParameter(name, url = W.location.href) {
44 | name = name.replace(/[[\]]/gu, '\\$&');
45 | const regex = new RegExp(`[?&]${name}(=([^]*)|&|#|$)`, 'u');
46 | const results = regex.exec(url);
47 | if (!results) return null;
48 | if (!results[2]) return '';
49 | return decodeURIComponent(results[2].replace(/\+/gu, ' '));
50 | },
51 | appendStyle(css) {
52 | let out = '';
53 | for (const selector in css) {
54 | out += `${selector}{`;
55 | for (const rule in css[selector]) {
56 | out += `${rule}:${css[selector][rule]}!important;`;
57 | }
58 | out += '}';
59 | }
60 |
61 | const style = document.createElement('style');
62 | style.type = 'text/css';
63 | style.appendChild(document.createTextNode(out));
64 | document.head.appendChild(style);
65 | },
66 | clearAllIntervals() {
67 | const interval_id = window.setInterval(null, 9999);
68 | for (let i = 1; i <= interval_id; i++) {
69 | window.clearInterval(i);
70 | }
71 | },
72 | hijackScrollEvent(cb) {
73 | document.onscroll = e => {
74 | if (cb) {
75 | cb(e);
76 | }
77 | e.preventDefault();
78 | e.stopImmediatePropagation();
79 | return false;
80 | };
81 | },
82 | addScript(src, onload) {
83 | const s = document.createElement('script');
84 | s.onload = onload;
85 | s.src = src;
86 | document.body.appendChild(s);
87 | },
88 | prepend(parent, child) {
89 | parent.insertBefore(child, parent.firstChild);
90 | }
91 | };
92 |
93 | // GM_xmlhttpRequest polyfill
94 | if (typeof GM_xmlhttpRequest === 'undefined') {
95 | Util.log('Adding GM_xmlhttpRequest polyfill');
96 | W.GM_xmlhttpRequest = function (config) {
97 | const xhr = new XMLHttpRequest();
98 | xhr.open(config.method || 'GET', config.url);
99 | if (config.headers) {
100 | for (const header in config.headers) {
101 | xhr.setRequestHeader(header, config.headers[header]);
102 | }
103 | }
104 | if (config.anonymous) {
105 | xhr.setRequestHeader('Authorization', '');
106 | }
107 | if (config.onload) {
108 | xhr.onload = function () {
109 | config.onload(xhr);
110 | };
111 | }
112 | if (config.onerror) {
113 | xhr.onerror = function () {
114 | config.onerror(xhr.status);
115 | };
116 | }
117 | xhr.send();
118 | };
119 | }
120 |
121 | /**
122 | * Sample Implementation:
123 | {
124 | name: 'something', // name of the implementation
125 | match: '^https?://domain.com/.*', // the url to react to
126 | remove: '#element', // css selector to get elements to remove
127 | wait: 3000, // how many ms to wait before running (to wait for elements to load), or a css selector to keep trying until it returns an elem
128 | referer: 'something', // load content in with an xhr using this referrer
129 | replace: '#element', // css selector to get element to replace with xhr
130 | replaceUsing: 'url', // url to use for the replace xhr. If null, it'll use the curren url.
131 | replaceWith: '#element', // css selector to get element to replace the element with. if null, it will use the same seletor as replace.
132 | css: {}, // object, keyed by css selector of css rules
133 | bmmode: function() { }, // function to call before doing anything else if in BM_MODE
134 | fn: function() { }, // a function to run before doing anything else for more complicated logic
135 | afterReplace: function() { } // a function that runs after the replace is done
136 | }
137 | * Any of the CSS selectors can be functions instead that return the desired value.
138 | */
139 |
140 | const implementations = [
141 | {
142 | name: 'The Nation',
143 | match: '^https?://www\\.thenation\\.com/article/.*',
144 | remove: '#paywall',
145 | wait: '#paywall',
146 | bmmode() {
147 | W.Paywall.hide();
148 | }
149 | },
150 | {
151 | name: 'Wall Street Journal',
152 | match: '^https?://.*\\.wsj\\.com/.*',
153 | wait: '.wsj-snippet-login',
154 | referer: 'https://t.co/T1323aaaa',
155 | afterReplace() {
156 | W.loadCSS('//asset.wsj.net/public/extra.production-2a7a40d6.css');
157 | const scripts = Util.qq('script');
158 | const add = function (regex, onload) {
159 | const matching = scripts.filter(script => script.src.match(regex));
160 | if (matching.length > 0) {
161 | Util.addScript(matching[0].src, onload);
162 | } else {
163 | onload();
164 | }
165 | };
166 | add(/\/common\\.js$/iu, () => {
167 | add(/\/article\\.js$/iu, () => {
168 | add(/\/snippet\\.js$/iu);
169 | });
170 | });
171 | }
172 | },
173 | {
174 | name: 'Boston Globe',
175 | match: '^https?://www\\.bostonglobe\\.com/.*',
176 | css: {
177 | 'html, body, #contain': {
178 | overflow: 'visible'
179 | },
180 | '.mfp-wrap, .mfp-ready': {
181 | display: 'none'
182 | }
183 | }
184 | },
185 | {
186 | name: 'NY Times',
187 | match: '^https?://www\\.nytimes\\.com/.*',
188 | css: {
189 | 'html, body': {
190 | overflow: 'visible'
191 | },
192 | '#Gateway_optly, #overlay': {
193 | display: 'none'
194 | },
195 | '.media .image': {
196 | 'margin-bottom': '7px'
197 | },
198 | '.new-story-body-text': {
199 | 'font-size': '1.0625rem',
200 | 'line-height': '1.625rem'
201 | }
202 | },
203 | cleanupStory(story) {
204 | if (story) {
205 | // prevent payywall from finding the elements to remove
206 | Util.qq('figure', story).forEach(figure => {
207 | figure.outerHTML = figure.outerHTML
208 | .replace(/ {
212 | paragraph.classList.remove('story-body-text');
213 | paragraph.classList.add('new-story-body-text');
214 | });
215 | }
216 | return story;
217 | },
218 | bmmode() {
219 | const self = this;
220 | Util.clearAllIntervals();
221 | GM_xmlhttpRequest({
222 | url: W.location.href,
223 | method: 'GET',
224 | onload(response) {
225 | const tempDiv = document.createElement('div');
226 | tempDiv.innerHTML = response.responseText;
227 | const story = self.cleanupStory(Util.q('#story', tempDiv));
228 | if (story) {
229 | Util.q('#story').innerHTML = story.innerHTML;
230 | }
231 | }
232 | });
233 | },
234 | fn() {
235 | // clear intervals once the paywall comes up to prevent changes afterward
236 | waitForElems({
237 | sel: '#Gateway_optly',
238 | stop: true,
239 | onmatch: Util.clearAllIntervals
240 | });
241 |
242 | this.cleanupStory(Util.q('#story'));
243 | setTimeout(() => {
244 | W.require(['jquery/nyt'], $ => {
245 | W.require(['vhs'], vhs => {
246 | Util.qq('.video').forEach(video => {
247 | video.setAttribute('style', 'position: relative');
248 | const bind = document.createElement('div');
249 | bind.classList.add('video-bind');
250 | const div = document.createElement('div');
251 | div.setAttribute(
252 | 'style',
253 | 'padding-bottom: 56.25%; position: relative; overflow: hidden;'
254 | );
255 | bind.appendChild(div);
256 | Util.prepend(video, bind);
257 | vhs.player({
258 | id: video.dataset.videoid,
259 | container: $(div),
260 | width: '100%',
261 | height: '100%',
262 | mode: 'html5',
263 | controlsOverlay: {
264 | mode: 'article'
265 | },
266 | cover: {
267 | mode: 'article'
268 | },
269 | newControls: true
270 | });
271 | });
272 | });
273 | });
274 | }, 0);
275 | }
276 | },
277 | {
278 | name: 'NY Times Mobile Redirect',
279 | match: '^https?://myaccount\\.nytimes\\.com/mobile/wall/smart/.*',
280 | fn() {
281 | const article = Util.getQueryParameter('EXIT_URI');
282 | if (article) {
283 | W.location.replace(
284 | `http://mobile.nytimes.com?LOAD_ARTICLE=${encodeURIComponent(
285 | article
286 | )}`
287 | );
288 | }
289 | }
290 | },
291 | {
292 | name: 'NY Times Mobile Loader',
293 | match: '^https?://mobile\\.nytimes\\.com',
294 | css: {
295 | '.full-art': {
296 | 'font-family': 'Georgia,serif',
297 | color: '#333'
298 | },
299 | '.full-art .article-body': {
300 | 'margin-bottom': '26px',
301 | 'font-size': '1.6em',
302 | 'line-height': '1.4em'
303 | }
304 | },
305 | replaceUsing: Util.getQueryParameter('LOAD_ARTICLE'),
306 | replace() {
307 | if (this.repalceUsing) {
308 | return '.sect';
309 | }
310 | return null;
311 | },
312 | replaceWith() {
313 | if (this.repalceUsing) {
314 | return 'article';
315 | }
316 | return null;
317 | }
318 | },
319 | {
320 | name: 'LA Times',
321 | match: '^https?://www\\.latimes\\.com/.*',
322 | css: {
323 | 'div#reg-overlay': {
324 | display: 'none'
325 | },
326 | 'html, body': {
327 | overflow: 'visible'
328 | }
329 | },
330 | fn: Util.hijackScrollEvent
331 | },
332 | {
333 | name: 'Washington Post',
334 | match: '^https?://www\\.washingtonpost\\.com/.*',
335 | css: {
336 | '.wp_signin, #wp_Signin': {
337 | display: 'none'
338 | },
339 | 'html, body': {
340 | overflow: 'visible'
341 | }
342 | },
343 | fn() {
344 | const handler = e => {
345 | e.stopImmediatePropagation();
346 | };
347 | document.addEventListener('keydown', handler, true);
348 | document.addEventListener('mousewheel', handler, true);
349 | }
350 | }
351 | ];
352 | // END OF IMPLEMENTATIONS
353 |
354 | const Config = {
355 | load() {
356 | const defaults = {
357 | blacklist: {}
358 | };
359 |
360 | let cfg = GM_getValue('cfg');
361 | if (!cfg) return defaults;
362 |
363 | cfg = JSON.parse(cfg);
364 | Object.entries(defaults).forEach(([key, value]) => {
365 | if (typeof cfg[key] === 'undefined') {
366 | cfg[key] = value;
367 | }
368 | });
369 |
370 | return cfg;
371 | },
372 |
373 | save(cfg) {
374 | GM_setValue('cfg', JSON.stringify(cfg));
375 | },
376 |
377 | toggleBlacklist(imp) {
378 | const cfg = Config.load();
379 | if (cfg.blacklist[imp]) {
380 | cfg.blacklist[imp] = false;
381 | } else {
382 | cfg.blacklist[imp] = true;
383 | }
384 | Config.save(cfg);
385 | }
386 | };
387 |
388 | const App = {
389 | currentImpName: null,
390 |
391 | bypass(imp) {
392 | if (W.BM_MODE && imp.bmmode) {
393 | Util.log('Running bookmarkelet specific function');
394 | imp.bmmode();
395 | }
396 | if (imp.fn) {
397 | Util.log('Running site specific function');
398 | imp.fn();
399 | }
400 | if (imp.css) {
401 | Util.log('Adding style');
402 | const cssObj = typeof imp.css === 'function' ? imp.css() : imp.css;
403 | Util.appendStyle(cssObj);
404 | }
405 | if (imp.remove) {
406 | Util.log('Removing elements');
407 | const elemsToRemove =
408 | typeof imp.remove === 'function' ? imp.remove() : Util.qq(imp.remove);
409 | elemsToRemove.forEach(elem => {
410 | elem.remove();
411 | });
412 | }
413 |
414 | const replaceSelector =
415 | typeof imp.replace === 'function' ? imp.replace() : imp.replace;
416 | let replaceUsing =
417 | typeof imp.replaceUsing === 'function'
418 | ? imp.replaceUsing()
419 | : imp.replaceUsing;
420 | const theReferer =
421 | typeof imp.referer === 'function' ? imp.referer() : imp.referer;
422 | if (replaceSelector || replaceUsing || theReferer) {
423 | replaceUsing = replaceUsing || W.location.href;
424 |
425 | Util.log(
426 | `Loading xhr for "${replaceUsing}" with referer: ${theReferer}`
427 | );
428 | GM_xmlhttpRequest({
429 | method: 'GET',
430 | url: replaceUsing,
431 | headers: {
432 | referer: theReferer
433 | },
434 | anonymous: true,
435 | onload(response) {
436 | if (replaceSelector) {
437 | let replaceWithSelector =
438 | typeof imp.replaceWith === 'function'
439 | ? imp.replaceWith()
440 | : imp.replaceWith;
441 | replaceWithSelector = replaceWithSelector || replaceSelector;
442 |
443 | const tempDiv = document.createElement('div');
444 | tempDiv.innerHTML = response.responseText;
445 |
446 | Util.q(replaceSelector).innerHTML = Util.q(
447 | replaceWithSelector,
448 | tempDiv
449 | ).innerHTML;
450 | } else {
451 | document.body.innerHTML = response.responseText;
452 | }
453 | if (imp.afterReplace) {
454 | Util.log('Performing after replace logic');
455 | imp.afterReplace();
456 | }
457 | },
458 | onerror() {
459 | Util.log('error occured when loading xhr');
460 | }
461 | });
462 | }
463 | Util.log('Paywall Bypassed.');
464 | },
465 |
466 | waitAndBypass(imp) {
467 | if (imp.wait) {
468 | const waitType = typeof imp.wait;
469 | if (waitType === 'number') {
470 | setTimeout(App.bypass(imp), imp.wait || 0);
471 | } else {
472 | const wait = waitType === 'function' ? imp.wait() : imp.wait;
473 | waitForElems({
474 | sel: wait,
475 | stop: true,
476 | onmatch() {
477 | Util.log('Condition fulfilled, bypassing');
478 | App.bypass(imp);
479 | }
480 | });
481 | }
482 | } else {
483 | App.bypass(imp);
484 | }
485 | },
486 |
487 | start(imps) {
488 | Util.log('starting...');
489 | const success = imps.some(imp => {
490 | if (imp.match && new RegExp(imp.match, 'iu').test(W.location.href)) {
491 | App.currentImpName = imp.name;
492 | if (W.BM_MODE) {
493 | App.waitAndBypass(imp);
494 | } else {
495 | let menuCommandText;
496 | if (!Config.load().blacklist[imp.name]) {
497 | menuCommandText = `Disable ${SCRIPT_NAME} for ${imp.name}`;
498 | App.waitAndBypass(imp);
499 | } else {
500 | menuCommandText = `Enable ${SCRIPT_NAME} for ${imp.name}`;
501 | Util.log(`${imp.name} blacklisted`);
502 | }
503 | GM_registerMenuCommand(menuCommandText, () => {
504 | Config.toggleBlacklist(imp.name);
505 | location.reload();
506 | });
507 | }
508 | return true;
509 | }
510 | });
511 |
512 | if (!success) {
513 | Util.log(`no implementation for ${W.location.href}`, 'error');
514 | }
515 | }
516 | };
517 |
518 | App.start(implementations);
519 | })();
520 |
--------------------------------------------------------------------------------
/nintendo-store-canada.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Nintendo Store Canada
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.3
5 | // @description Auto redirect nintendo store to canada store
6 | // @author Adrien Pyke
7 | // @match https://store.nintendo.com/ng3/*
8 | // @grant none
9 | // @run-at document-start
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | if (!location.href.match(/\/ca\//iu)) {
16 | if (location.href.match(/\/us\//iu)) {
17 | location.replace(location.href.replace(/\/us\//iu, '/ca/'));
18 | } else {
19 | location.replace(location.href.replace(/\/ng3/iu, '/ng3/ca/po'));
20 | }
21 | }
22 | })();
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-userscripts",
3 | "version": "1.0.0",
4 | "description": "A collection of user scripts",
5 | "private": true,
6 | "scripts": {
7 | "lint": "eslint .",
8 | "format": "prettier --write \"**/*.{js,jsx,md,json,css,prettierrc,eslintrc,html}\"",
9 | "check:format": "prettier --check \"**/*.{js,jsx,md,json,css,prettierrc,eslintrc,html}\"",
10 | "check": "npm run lint && npm run check:format"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/kufii/My-UserScripts.git"
15 | },
16 | "author": "Adrien Pyke",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/kufii/My-UserScripts/issues"
20 | },
21 | "homepage": "https://github.com/kufii/My-UserScripts#readme",
22 | "devDependencies": {
23 | "eslint": "^8.8.0",
24 | "eslint-config-adpyke-es6": "^1.4.13",
25 | "eslint-config-prettier": "^8.3.0",
26 | "prettier": "^2.5.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pokemondb-default-version.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PokemonDB Default Version
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.1.5
5 | // @description Auto selects the chosen version in the Moves section on PokemonDB
6 | // @author Adrien Pyke
7 | // @match *://pokemondb.net/pokedex/*
8 | // @grant GM_getValue
9 | // @grant GM_setValue
10 | // @grant GM_registerMenuCommand
11 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
12 | // ==/UserScript==
13 |
14 | (() => {
15 | 'use strict';
16 |
17 | const Config = GM_config([
18 | {
19 | key: 1,
20 | label: 'Gen 1',
21 | type: 'dropdown',
22 | showBlank: true,
23 | values: ['Red/Blue', 'Yellow']
24 | },
25 | {
26 | key: 2,
27 | label: 'Gen 2',
28 | type: 'dropdown',
29 | showBlank: true,
30 | values: ['Gold/Silver', 'Crystal']
31 | },
32 | {
33 | key: 3,
34 | label: 'Gen 3',
35 | type: 'dropdown',
36 | showBlank: true,
37 | values: ['Ruby/Sapphire', 'FireRed/LeafGreen', 'Emerald']
38 | },
39 | {
40 | key: 4,
41 | label: 'Gen 4',
42 | type: 'dropdown',
43 | showBlank: true,
44 | values: ['Diamond/Pearl', 'Platinum', 'HeartGold/SoulSilver']
45 | },
46 | {
47 | key: 5,
48 | label: 'Gen 5',
49 | type: 'dropdown',
50 | showBlank: true,
51 | values: ['Black/White', 'Black 2/White 2']
52 | },
53 | {
54 | key: 6,
55 | label: 'Gen 6',
56 | type: 'dropdown',
57 | showBlank: true,
58 | values: ['X/Y', 'Omega Ruby/Alpha Sapphire']
59 | },
60 | {
61 | key: 7,
62 | label: 'Gen 7',
63 | type: 'dropdown',
64 | showBlank: true,
65 | values: [
66 | 'Sun/Moon',
67 | 'Ultra Sun/Ultra Moon',
68 | "Let's Go Pikachu/Let's Go Eevee"
69 | ]
70 | }
71 | ]);
72 | GM_registerMenuCommand('Select default PokemonDB versions', Config.setup);
73 |
74 | const match = location.href.match(
75 | /^https?:\/\/pokemondb\.net\/pokedex\/.*?\/moves\/(\d+)/iu
76 | );
77 | const currentGen = match ? match[1] : 7;
78 | const defaultVersion = Config.load()[currentGen];
79 | const tabs = Array.from(
80 | document.querySelectorAll('.tabs-tab-list > a.tabs-tab')
81 | );
82 |
83 | if (defaultVersion) {
84 | const [tab] = tabs.filter(tab => tab.textContent === defaultVersion);
85 | if (tab) {
86 | tab.click();
87 | }
88 | }
89 |
90 | let changing = false;
91 | tabs.forEach(tab =>
92 | tab.addEventListener('click', () => {
93 | if (changing) return;
94 | changing = true;
95 | tabs
96 | .filter(tabB => tabB.textContent === tab.textContent)
97 | .forEach(tabB => tabB.click());
98 | changing = false;
99 | })
100 | );
101 | })();
102 |
--------------------------------------------------------------------------------
/prevent-wikia-ads.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Prevent Wikia Ads
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.9
5 | // @description Prevent the ads that pop up when clicking a link to an external page on Wikias
6 | // @author Adrien Pyke
7 | // @match *://*.wikia.com/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant GM_openInTab
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | waitForElems({
16 | sel: 'a.exitstitial',
17 | onmatch(link) {
18 | link.onclick = e => {
19 | e.preventDefault();
20 | e.stopImmediatePropagation();
21 | if (e.button === 0) {
22 | location.href = link.href;
23 | } else if (e.button === 1) {
24 | GM_openInTab(link.href, true);
25 | }
26 | return false;
27 | };
28 | link.onauxclick = link.onclick;
29 | }
30 | });
31 | })();
32 |
--------------------------------------------------------------------------------
/reddit-disable-no-participation.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Reddit Disable No Participation
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.8
5 | // @description Disables No Participation on Reddit
6 | // @author Adrien Pyke
7 | // @match *://*.reddit.com/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @run-at document-start
10 | // @grant none
11 | // ==/UserScript==
12 |
13 | (() => {
14 | 'use strict';
15 |
16 | const MATCH = /^https?:\/\/np\.reddit\.com/iu;
17 | const REPLACE = {
18 | regex: /^(https?:\/\/)np(.+)/iu,
19 | replaceWith: '$1www$2'
20 | };
21 |
22 | if (location.href.match(MATCH)) {
23 | location.replace(location.href.replace(REPLACE.regex, REPLACE.replaceWith));
24 | }
25 |
26 | document.addEventListener('DOMContentLoaded', () => {
27 | waitForElems({
28 | sel: 'a',
29 | onmatch(link) {
30 | if (link.href.match(MATCH)) {
31 | link.href = link.href.replace(REPLACE.regex, REPLACE.replaceWith);
32 | }
33 | }
34 | });
35 | });
36 | })();
37 |
--------------------------------------------------------------------------------
/reddit-flair-linkifier.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Reddit Flair Linkifier
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.1.7
5 | // @description Turns the text in various subreddits' flair into links
6 | // @author Adrien Pyke
7 | // @match *://*.reddit.com/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant GM_addStyle
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | GM_addStyle(`
16 | .flair-link {
17 | text-decoration: none;
18 | }
19 | .flair-link:hover {
20 | text-decoration: underline;
21 | }
22 | `);
23 |
24 | const newLayoutId = '#SHORTCUT_FOCUSABLE_DIV';
25 |
26 | waitForElems({
27 | sel: [
28 | // old reddit
29 | 'span.flair',
30 | 'span.Comment__authorFlair',
31 |
32 | // new reddit
33 | `${newLayoutId} span`
34 | ].join(','),
35 | onmatch(flair) {
36 | if (
37 | flair.childNodes.length !== 1 ||
38 | flair.childNodes[0].nodeType !== Node.TEXT_NODE ||
39 | flair.closest('.DraftEditor-root')
40 | )
41 | return;
42 | const newhtml = flair.textContent
43 | .split(' ')
44 | .map(segment =>
45 | segment.match(/^https?:\/\//u)
46 | ? `${segment}`
47 | : segment
48 | )
49 | .join(' ');
50 | if (flair.innerHTML !== newhtml) flair.innerHTML = newhtml;
51 | }
52 | });
53 | })();
54 |
--------------------------------------------------------------------------------
/reddit-prevent-middle-click.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name New reddit: Prevent middle click scroll
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.8
5 | // @description Prevents the middle click scroll when middle clicking posts on the new reddit layout
6 | // @author Adrien Pyke
7 | // @match *://*.reddit.com/*
8 | // @grant GM_openInTab
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Util = {
16 | q(query, context = document) {
17 | return context.querySelector(query);
18 | },
19 | qq(query, context = document) {
20 | return Array.from(context.querySelectorAll(query));
21 | }
22 | };
23 |
24 | const mousedown = e => {
25 | if (e.button === 1) return false;
26 | };
27 |
28 | waitForElems({
29 | sel: '.Post',
30 | onmatch(post) {
31 | post.onmousedown = mousedown;
32 |
33 | const links = Util.qq('a[data-click-id="comments"]', post);
34 | if (links.length) {
35 | const link = links[links.length - 1];
36 | if (link) {
37 | post.onclick = post.onauxclick = e => {
38 | if (
39 | e.button === 1 &&
40 | e.target.tagName !== 'A' &&
41 | e.target.parentNode.tagName !== 'A'
42 | ) {
43 | e.preventDefault();
44 | e.stopImmediatePropagation();
45 | GM_openInTab(link.href, true);
46 | }
47 | };
48 | }
49 | }
50 | }
51 | });
52 | })();
53 |
--------------------------------------------------------------------------------
/retailmenot-enhancer.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name RetailMeNot Enhancer
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 3.1.9
5 | // @description Auto shows coupons and stops pop-unders on RetailMeNot
6 | // @author Adrien Pyke
7 | // @match *://www.retailmenot.com/*
8 | // @match *://www.retailmenot.ca/*
9 | // @match *://www.retailmenot.de/*
10 | // @match *://www.retailmenot.es/*
11 | // @match *://www.retailmenot.it/*
12 | // @match *://www.retailmenot.pl/*
13 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
14 | // @grant GM_openInTab
15 | // ==/UserScript==
16 |
17 | (() => {
18 | 'use strict';
19 |
20 | const SCRIPT_NAME = 'RetailMeNot Auto Show Coupons';
21 |
22 | const Util = {
23 | log(...args) {
24 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
25 | console.log(...args);
26 | },
27 | q(query, context = document) {
28 | return context.querySelector(query);
29 | },
30 | qq(query, context = document) {
31 | return Array.from(context.querySelectorAll(query));
32 | },
33 | getQueryParam(name, url = location.href) {
34 | name = name.replace(/[[\]]/gu, '\\$&');
35 | const regex = new RegExp(`[?&]${name}(=([^]*)|&|#|$)`, 'u');
36 | const results = regex.exec(url);
37 | if (!results) return null;
38 | if (!results[2]) return '';
39 | return decodeURIComponent(results[2].replace(/\+/gu, ' '));
40 | },
41 | setQueryParam(key, value, url = location.href) {
42 | const regex = new RegExp(`([?&])${key}=.*?(&|#|$)(.*)`, 'giu');
43 | const hasValue =
44 | typeof value !== 'undefined' && value !== null && value !== '';
45 | if (regex.test(url)) {
46 | if (hasValue) {
47 | return url.replace(regex, `$1${key}=${value}$2$3`);
48 | } else {
49 | const [path, hash] = url.split('#');
50 | url = path.replace(regex, '$1$3').replace(/(&|\?)$/u, '');
51 | if (hash) url += `#${hash[1]}`;
52 | return url;
53 | }
54 | } else if (hasValue) {
55 | const separator = url.includes('?') ? '&' : '?';
56 | const [path, hash] = url.split('#');
57 | url = `${path + separator + key}=${value}`;
58 | if (hash) url += `#${hash[1]}`;
59 | return url;
60 | } else return url;
61 | },
62 | removeQueryParam(key, url) {
63 | return Util.setQueryParam(key, null, url);
64 | },
65 | changeUrl(url) {
66 | window.history.replaceState({ path: url }, '', url);
67 | },
68 | createCookie(name, value, days) {
69 | let expires;
70 | if (days) {
71 | const date = new Date();
72 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
73 | expires = `; expires=${date.toGMTString()}`;
74 | } else expires = '';
75 | document.cookie = `${name}=${value}${expires}; path=/`;
76 | }
77 | };
78 |
79 | // remove force reload param
80 | Util.changeUrl(Util.removeQueryParam('r'));
81 | if (window.location.href.match(/^https?:\/\/www\.retailmenot\.com/iu)) {
82 | // US
83 | Util.log('Enhancing US site');
84 | // Show Coupons
85 | document.body.classList.add('ctc');
86 |
87 | // Disable pop unders
88 | waitForElems({
89 | sel: '.js-outclick, .js-title > a, .js-triggers-outclick, .js-coupon-square, .offer-item-in-list',
90 | onmatch(button) {
91 | const path =
92 | button.dataset.newTab && !button.dataset.newTab.match(/^\/out/iu)
93 | ? button.dataset.newTab
94 | : button.dataset.mainTab;
95 | const href = `${window.location.protocol}//${window.location.host}${path}`;
96 | if (path) {
97 | const handler = e => {
98 | e.preventDefault();
99 | e.stopImmediatePropagation();
100 | if (e.button === 1) {
101 | GM_openInTab(href, true);
102 | } else if (window.location.pathname === path) {
103 | window.location.replace(href);
104 | } else {
105 | window.location.href = href;
106 | }
107 | return false;
108 | };
109 | if (button.classList.contains('offer-item-in-list')) {
110 | const offerButton = Util.q('.offer-button', button);
111 | if (offerButton) {
112 | offerButton.onclick = handler;
113 | }
114 | const offerTitle = Util.q('.offer-title', button);
115 | if (offerTitle) {
116 | offerTitle.href = href;
117 | offerTitle.onclick = handler;
118 | }
119 | } else {
120 | if (button.tagname === 'A') {
121 | button.href = href;
122 | }
123 | button.onclick = handler;
124 | Util.qq('*', button).forEach(elem => {
125 | elem.onclick = handler;
126 | });
127 | }
128 | }
129 | }
130 | });
131 | } else if (window.location.href.match(/^https?:\/\/www\.retailmenot\.ca/iu)) {
132 | // CANADA
133 | Util.log('Enhancing Canadian site');
134 | // Show Coupons
135 | Util.qq('.crux > .cover').forEach(cover => {
136 | cover.remove();
137 | });
138 |
139 | // Disable Pop Unders
140 | waitForElems({
141 | sel: '.offer, .stage .coupon',
142 | onmatch(offer) {
143 | const href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?c=${offer.dataset.offerid}`;
144 |
145 | const clickHandler = e => {
146 | e.preventDefault();
147 | e.stopImmediatePropagation();
148 | if (e.button === 1) {
149 | GM_openInTab(href, true);
150 | } else {
151 | window.location.replace(href);
152 | }
153 | return false;
154 | };
155 |
156 | if (!offer.parentNode.classList.contains('stage')) {
157 | waitForElems({
158 | context: offer,
159 | sel: 'a.offer-title',
160 | stop: true,
161 | onmatch(title) {
162 | title.href = href;
163 | title.onclick = clickHandler;
164 | }
165 | });
166 | }
167 |
168 | Util.qq(
169 | '.action-button, .crux, .caterpillar-title, .caterpillar-code',
170 | offer
171 | ).forEach(elem => {
172 | elem.onclick = clickHandler;
173 | });
174 | }
175 | });
176 |
177 | // disable pop unders on the exclusive tags
178 | Util.qq('.exclusive_icon').forEach(tag => {
179 | tag.onclick = e => {
180 | e.preventDefault();
181 | e.stopImmediatePropagation();
182 | };
183 | });
184 | } else {
185 | // GERMANY, SPAIN, ITALY, POLAND
186 | Util.log('Enhancing international site');
187 | // Remove hash after modal comes up
188 | if (window.location.href.indexOf('#') !== -1) {
189 | waitForElems({
190 | sel: '#modal-coupon',
191 | stop: true,
192 | onmatch() {
193 | Util.changeUrl(window.location.href.split('#')[0]);
194 | }
195 | });
196 | }
197 | // disable pop unders
198 | waitForElems({
199 | sel: '.coupon',
200 | onmatch(coupon) {
201 | const id = coupon.dataset.suffix;
202 | const href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?r=1#${id}`;
203 | const clickHandler = e => {
204 | e.preventDefault();
205 | e.stopImmediatePropagation();
206 | Util.createCookie(`click_${id}`, true);
207 | if (e.button === 1) {
208 | GM_openInTab(href, true);
209 | } else {
210 | window.location.replace(href);
211 | }
212 | return false;
213 | };
214 | Util.qq('.outclickable', coupon).forEach(elem => {
215 | if (elem.tagName === 'A') {
216 | elem.href = href;
217 | }
218 | elem.onclick = clickHandler;
219 | });
220 | }
221 | });
222 | }
223 | // human checks
224 | const regex = /^https?:\/\/www\.retailmenot\.[^/]+\/humanCheck\.php/iu;
225 | Util.qq('a')
226 | .filter(link => link.href.match(regex))
227 | .forEach(link => {
228 | const url = Util.getQueryParam('url', link.href);
229 | if (url) {
230 | link.href = `${window.location.protocol}//${window.location.host}${url}`;
231 | }
232 | });
233 |
234 | // remove coupon query param so reloads work properly
235 | Util.changeUrl(Util.removeQueryParam('c'));
236 | })();
237 |
--------------------------------------------------------------------------------
/rulu-ad-block.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Rulu.co remove ads
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.6
5 | // @description Removes ad links from Rulu.co
6 | // @author Adrien Pyke
7 | // @match *://www.rulu.co/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant none
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Util = {
16 | qq(query, context = document) {
17 | return Array.from(context.querySelectorAll(query));
18 | }
19 | };
20 |
21 | const REGEX = /^https?:\/\/rulu\.io\/j\//iu;
22 |
23 | Util.qq('a')
24 | .filter(link => link.href.match(REGEX))
25 | .forEach(link => (link.href = link.href.replace(REGEX, '')));
26 |
27 | waitForElems({
28 | sel: '#d0bf',
29 | onmatch(overlay) {
30 | overlay.remove();
31 | }
32 | });
33 | })();
34 |
--------------------------------------------------------------------------------
/soundcloud-toggle-continuous-play.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SoundCloud Toggle Continuous Play and Autoplay
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.21
5 | // @description Adds options to toggle continuous play and autoplay in SoundCloud
6 | // @author Adrien Pyke
7 | // @match *://soundcloud.com/*
8 | // @grant GM_getValue
9 | // @grant GM_setValue
10 | // @grant GM_registerMenuCommand
11 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
12 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
13 | // ==/UserScript==
14 |
15 | (() => {
16 | 'use strict';
17 |
18 | const SCRIPT_NAME = 'SoundCloud Toggle Continuous Play and Autoplay';
19 |
20 | const Util = {
21 | log(...args) {
22 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
23 | console.log(...args);
24 | },
25 | q(query, context = document) {
26 | return context.querySelector(query);
27 | },
28 | qq(query, context = document) {
29 | return Array.from(context.querySelectorAll(query));
30 | },
31 | createCheckbox(lbl) {
32 | const label = document.createElement('label');
33 | const checkbox = document.createElement('input');
34 | checkbox.type = 'checkbox';
35 | label.appendChild(checkbox);
36 | label.appendChild(document.createTextNode(lbl));
37 | return label;
38 | }
39 | };
40 |
41 | const Config = GM_config([
42 | {
43 | key: 'autoplay',
44 | label: 'Autoplay',
45 | default: false,
46 | type: 'bool'
47 | },
48 | {
49 | key: 'continuousPlay',
50 | label: 'Continuous Play',
51 | default: false,
52 | type: 'bool'
53 | }
54 | ]);
55 | GM_registerMenuCommand('SoundCloud Autoplay', Config.setup);
56 |
57 | const App = {
58 | playButton: Util.q('.playControl'),
59 | isPlaying() {
60 | return App.playButton.classList.contains('playing');
61 | },
62 | pause() {
63 | if (App.isPlaying()) {
64 | App.playButton.click();
65 | }
66 | },
67 | play() {
68 | if (!App.isPlaying()) {
69 | App.playButton.click();
70 | }
71 | },
72 | getPlaying() {
73 | const link = Util.q('a.playbackSoundBadge__title');
74 | if (link) {
75 | return link.href;
76 | }
77 | return null;
78 | },
79 | addAutoplayControl() {
80 | const container = Util.q('.playControls__inner');
81 | const label = Util.createCheckbox('Autoplay');
82 | label.setAttribute(
83 | 'style',
84 | 'position: absolute; bottom: 0; right: 0; z-index: 1;'
85 | );
86 | container.appendChild(label);
87 | const check = Util.q('input', label);
88 | check.checked = Config.load().continuousPlay;
89 | check.onchange = () => {
90 | const cfg = Config.load();
91 | cfg.continuousPlay = check.checked;
92 | Config.save(cfg);
93 | };
94 | return check;
95 | }
96 | };
97 |
98 | // disable autoplay
99 | if (!Config.load().autoplay) {
100 | App.pause();
101 | }
102 |
103 | const autoplayControl = App.addAutoplayControl();
104 | let current = App.getPlaying();
105 | let timeout;
106 | // every time the song changes
107 | waitForElems({
108 | context: Util.q('.playControls__soundBadge'),
109 | onchange() {
110 | const next = App.getPlaying();
111 | if (!autoplayControl.checked && current && next !== current) {
112 | timeout = setTimeout(() => {
113 | Util.log('Pausing...');
114 | App.pause();
115 | }, 0);
116 | }
117 | current = next;
118 | }
119 | });
120 |
121 | // override the click event for elements that shouldn't trigger a pause
122 | waitForElems({
123 | sel: '.skipControl, .playButton, .compactTrackList__item, .fullListenHero__foreground',
124 | onmatch(elem) {
125 | elem.addEventListener('click', () => {
126 | if (timeout) {
127 | clearTimeout(timeout);
128 | timeout = null;
129 | }
130 | });
131 | }
132 | });
133 |
134 | // waveforms need to be handled differently
135 | waitForElems({
136 | sel: '.waveform__layer',
137 | onmatch(elem) {
138 | elem.addEventListener('click', () => {
139 | setTimeout(() => {
140 | if (!App.isPlaying()) {
141 | Util.log('Playing via Waveform');
142 | App.play();
143 | }
144 | }, 0);
145 | });
146 | }
147 | });
148 |
149 | // fix for buttons constantly showing buffering
150 | waitForElems({
151 | sel: '.sc-button-buffering',
152 | onmatch(button) {
153 | if (!App.isPlaying()) {
154 | button.classList.remove('sc-button-buffering');
155 | button.title = button.textContent = 'Play';
156 | }
157 | }
158 | });
159 | })();
160 |
--------------------------------------------------------------------------------
/telegram-emojione.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Telegram Web Emojione
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.12
5 | // @description Replaces old iOS emojis with Emojione on Telegram Web
6 | // @author Adrien Pyke
7 | // @match *://web.telegram.org/*
8 | // @grant GM_addStyle
9 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
10 | // @require https://cdn.rawgit.com/emojione/emojione/9a81e8462ea5c1efc8e4f2947944d0a248b8ec73/lib/js/emojione.min.js
11 | // ==/UserScript==
12 | /* global emojione */
13 |
14 | (() => {
15 | 'use strict';
16 |
17 | const SCRIPT_NAME = 'Telegram Web Emojione';
18 |
19 | const Util = {
20 | log(...args) {
21 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
22 | console.log(...args);
23 | },
24 | q(query, context = document) {
25 | return context.querySelector(query);
26 | },
27 | qq(query, context = document) {
28 | return Array.from(context.querySelectorAll(query));
29 | },
30 | regexEscape(str) {
31 | return str.replace(/[-[\]/{}()*+?.\\^$|]/gu, '\\$&');
32 | }
33 | };
34 |
35 | const EmojiHelper = {
36 | replacements: {
37 | ':+1:': ':thumbsup:',
38 | ':facepunch:': ':punch:',
39 | ':hand:': ':raised_hand:',
40 | ':moon:': ':waxing_gibbous_moon:',
41 | ':phone:': ':telephone:',
42 | ':hocho:': ':knife:',
43 | ':boat:': ':sailboat:',
44 | ':car:': ':red_car:',
45 | ':large_blue_circle:': ':blue_circle:',
46 | '\uD83C\uDFF3': '\uD83C\uDFF3\uFE0F', // Flag
47 | '\uD83D\uDECF': '\uD83D\uDECF\uFE0F', // Bed
48 | '\u2640': '\u2640\uFE0F', // Female Sign
49 | '\u2764\uFE0F': '\u2764\uFE0F\u200B', // Red Heart
50 | '\uFE0F\uFE0F': '\uFE0F' // Fix for ZWJ
51 | },
52 | sizes: [
53 | {
54 | size: 20
55 | },
56 | {
57 | class: ['im_short_message_text', 'im_short_message_media'],
58 | size: 16
59 | },
60 | {
61 | class: ['composer_emoji_tooltip', 'stickerset_modal_sticker_alt'],
62 | size: 26
63 | }
64 | ],
65 | addStyles() {
66 | GM_addStyle(
67 | EmojiHelper.sizes
68 | .map(size => {
69 | let output = '.emoji';
70 | if (size.class) {
71 | output = size.class.map(c => `.${c} .emoji`).join(', ');
72 | }
73 | return `${output} {width: ${size.size}px; height: ${size.size}px; vertical-align: middle;}`;
74 | })
75 | .join('')
76 | );
77 | },
78 | makeReplacements(str) {
79 | Object.entries(EmojiHelper.replacements).forEach(([key, value]) => {
80 | str = str.replace(new RegExp(Util.regexEscape(key), 'gu'), value);
81 | });
82 | return str;
83 | },
84 | buildEmoji(img, src) {
85 | img.removeAttribute('style');
86 | img.removeAttribute('class');
87 | img.classList.add('emoji', 'e1-converted');
88 | img.style.backgroundImage = `url(${src})`;
89 | img.style.backgroundSize = 'cover';
90 | img.src = 'img/blank.gif';
91 | },
92 | toEmoji(text) {
93 | const tempDiv = document.createElement('div');
94 | tempDiv.innerHTML = emojione.toImage(EmojiHelper.makeReplacements(text));
95 |
96 | Util.qq('img', tempDiv).forEach(emoji => (emoji.outerHTML = emoji.alt));
97 | tempDiv.innerHTML = emojione.toImage(
98 | EmojiHelper.makeReplacements(tempDiv.textContent)
99 | );
100 |
101 | Util.qq('img', tempDiv).forEach(emoji =>
102 | EmojiHelper.buildEmoji(emoji, emoji.src)
103 | );
104 |
105 | return tempDiv.innerHTML;
106 | },
107 | shortnameToSrc(shortname) {
108 | const tempDiv = document.createElement('div');
109 | tempDiv.innerHTML = emojione.toImage(
110 | EmojiHelper.replacements[shortname] || shortname
111 | );
112 | return Util.q('img', tempDiv).src;
113 | },
114 | convert(node) {
115 | if (node.childNodes && node.childNodes.length > 0) {
116 | Util.qq('span.emoji', node).forEach(
117 | emoji => (emoji.outerHTML = emoji.textContent)
118 | );
119 | }
120 | if (node.nodeType === Node.TEXT_NODE) {
121 | const tempDiv = document.createElement('div');
122 | tempDiv.innerHTML = EmojiHelper.toEmoji(node.textContent);
123 |
124 | if (Util.q('img', tempDiv)) {
125 | Array.from(tempDiv.childNodes).forEach(tempChild =>
126 | node.parentNode.insertBefore(tempChild, node)
127 | );
128 | node.remove();
129 | }
130 | } else if (node.tagName === 'IMG') {
131 | if (!node.classList.contains('e1-converted')) {
132 | EmojiHelper.buildEmoji(node, EmojiHelper.shortnameToSrc(node.alt));
133 | }
134 | } else if (node.childNodes) {
135 | Array.from(node.childNodes).forEach(EmojiHelper.convert);
136 | }
137 | }
138 | };
139 |
140 | EmojiHelper.addStyles();
141 |
142 | const convertAndWatch = function (node, config) {
143 | EmojiHelper.convert(node);
144 | const changes = waitForElems({
145 | context: node,
146 | config,
147 | onchange() {
148 | changes.stop();
149 | EmojiHelper.convert(node);
150 | changes.resume();
151 | }
152 | });
153 | };
154 |
155 | waitForElems({
156 | sel: [
157 | '.im_message_author',
158 | '.im_message_webpage_site',
159 | '.im_message_webpage_title > a',
160 | '.im_message_webpage_description',
161 | '.im_dialog_peer > span',
162 | '.stickerset_modal_sticker_alt',
163 | '.im_message_photo_caption',
164 | '.im_message_document_caption',
165 | '.reply_markup_button'
166 | ].join(','),
167 | onmatch: EmojiHelper.convert
168 | });
169 |
170 | waitForElems({
171 | sel: [
172 | '.im_message_text',
173 | '.im_short_message_text',
174 | '.im_short_message_media > span > span > span'
175 | ].join(','),
176 | onmatch: convertAndWatch
177 | });
178 |
179 | convertAndWatch(Util.q('.composer_rich_textarea'), {
180 | characterData: true,
181 | childList: true,
182 | subtree: true
183 | });
184 |
185 | waitForElems({
186 | sel: '.composer_emoji_btn',
187 | onmatch(btn) {
188 | btn.innerHTML = EmojiHelper.toEmoji(btn.title);
189 | }
190 | });
191 |
192 | waitForElems({
193 | sel: '.composer_emoji_option',
194 | onmatch(option) {
195 | Util.q('.emoji', option).outerHTML = EmojiHelper.toEmoji(
196 | Util.q('span', option).textContent
197 | );
198 | }
199 | });
200 | })();
201 |
--------------------------------------------------------------------------------
/umbraco-ace-editor.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Parks Canada Umbraco Ace Editor
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.1
5 | // @description Use the Ace Editor when editing things on Umbraco on Parks Canada intranet
6 | // @author Adrien Pyke
7 | // @match *://*.apca2.gc.ca/umbraco_client/tinymce3/themes/umbraco/source_editor.htm
8 | // @match *://intranet2/umbraco_client/tinymce3/themes/umbraco/source_editor.htm
9 | // @grant unsafeWindow
10 | // @grant GM_addStyle
11 | // @grant GM_getValue
12 | // @grant GM_setValue
13 | // @grant GM_registerMenuCommand
14 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
15 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
16 | // @require https://unpkg.com/prettier@2.6.2/standalone.js
17 | // @require https://unpkg.com/prettier@2.6.2/parser-html.js
18 | // ==/UserScript==
19 |
20 | /* global prettier, prettierPlugins */
21 |
22 | (() => {
23 | 'use strict';
24 |
25 | const SCRIPT_NAME = 'Parks Canada Umbraco Ace Editor';
26 |
27 | const Util = {
28 | log(...args) {
29 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
30 | console.log(...args);
31 | },
32 | q(query, context = document) {
33 | return context.querySelector(query);
34 | },
35 | qq(query, context = document) {
36 | return Array.from(context.querySelectorAll(query));
37 | },
38 | addScript(src, onload) {
39 | const s = document.createElement('script');
40 | s.onload = onload;
41 | s.src = src;
42 | document.body.appendChild(s);
43 | },
44 | addScriptText(code, onload) {
45 | const s = document.createElement('script');
46 | s.onload = onload;
47 | s.textContent = code;
48 | document.body.appendChild(s);
49 | },
50 | appendAfter(elem, elemToAppend) {
51 | elem.parentNode.insertBefore(elemToAppend, elem.nextElementSibling);
52 | }
53 | };
54 |
55 | const Config = GM_config([
56 | {
57 | key: 'theme',
58 | label: 'Theme',
59 | default: 'monokai',
60 | type: 'dropdown',
61 | values: [
62 | 'ambiance',
63 | 'chaos',
64 | 'chrome',
65 | 'clouds',
66 | 'clouds_midnight',
67 | 'cobalt',
68 | 'crimson_editor',
69 | 'dawn',
70 | 'dreamweaver',
71 | 'eclipse',
72 | 'github',
73 | 'gob',
74 | 'idle_fingers',
75 | 'iplastic',
76 | 'katzenmilch',
77 | 'kr_theme',
78 | 'kuroir',
79 | 'merbivore',
80 | 'merbivore_soft',
81 | 'mono_industrial',
82 | 'monokai',
83 | 'solarized_dark',
84 | 'solarized_light',
85 | 'sqlserver',
86 | 'terminal',
87 | 'textmate',
88 | 'tomorrow',
89 | 'tomorrow_night',
90 | 'tomorrow_night_blue',
91 | 'tomorrow_night_bright',
92 | 'tomorrow_night_eighties',
93 | 'twilight',
94 | 'vibrant_ink',
95 | 'xcode'
96 | ]
97 | }
98 | ]);
99 |
100 | waitForElems({
101 | sel: '#htmlSource',
102 | stop: true,
103 | onmatch(textArea) {
104 | textArea.value = prettier.format(textArea.value, {
105 | parser: 'html',
106 | plugins: prettierPlugins
107 | });
108 |
109 | const wrapper = document.createElement('div');
110 | wrapper.id = 'ace';
111 | wrapper.textContent = textArea.value;
112 |
113 | Util.appendAfter(textArea, wrapper);
114 |
115 | GM_addStyle(`
116 | .ace_editor {
117 | height: 515px;
118 | }
119 | #htmlSource {
120 | display: none;
121 | }
122 | `);
123 |
124 | Util.addScript(
125 | 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ace.min.js',
126 | () => {
127 | const editor = unsafeWindow.ace.edit('ace');
128 | editor.setTheme(`ace/theme/${Config.load().theme}`);
129 | editor.getSession().setMode('ace/mode/html');
130 | editor.resize();
131 |
132 | unsafeWindow.aceEditor = editor;
133 | unsafeWindow.originalTextArea = textArea;
134 |
135 | Util.addScriptText(
136 | 'aceEditor.getSession().on("change", () => originalTextArea.value = aceEditor.getValue())'
137 | );
138 |
139 | GM_registerMenuCommand(SCRIPT_NAME + ' Settings', () =>
140 | Config.setup(editor)
141 | );
142 | Config.onchange = (key, value) =>
143 | editor.setTheme(`ace/theme/${value}`);
144 | Config.oncancel = cfg => editor.setTheme(`ace/theme/${cfg.theme}`);
145 | }
146 | );
147 | }
148 | });
149 | })();
150 |
--------------------------------------------------------------------------------
/userstyle-auto-enable-source-editor.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name userstyles.org - Auto Enable Source Editor
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.0
5 | // @description auto enables the source editor on userstyles.org
6 | // @author Adrien Pyke
7 | // @match *://userstyles.org/d/styles/*
8 | // @grant none
9 | // ==/UserScript==
10 |
11 | (() => {
12 | 'use strict';
13 |
14 | document.querySelector('#enable-source-editor-code').click();
15 | })();
16 |
--------------------------------------------------------------------------------
/userstyles-auto-keep-me-logged-in.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name userstyles.org - auto select keep me logged in
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1
5 | // @description Auto checks keep me logged in on userstyles.org
6 | // @author Adrien Pyke
7 | // @match *://userstyles.org/d/login*
8 | // @grant none
9 | // ==/UserScript==
10 |
11 | (() => {
12 | 'use strict';
13 |
14 | document.querySelector('#remember-openid').checked = true;
15 | document.querySelector('#remember-normal').checked = true;
16 | })();
17 |
--------------------------------------------------------------------------------
/wanikani-kana-search.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name WaniKani Kana Search
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.1
5 | // @description Search on WaniKani using hiragana
6 | // @author Adrien Pyke
7 | // @match *://www.wanikani.com/*
8 | // @grant none
9 | // @require https://unpkg.com/wanakana@4.0.2/umd/wanakana.min.js
10 | // ==/UserScript==
11 |
12 | (() => {
13 | 'use strict';
14 |
15 | const Util = {
16 | q: (query, context = document) => context.querySelector(query),
17 | qq: (query, context = document) => [...context.querySelectorAll(query)]
18 | };
19 |
20 | const form = Util.q('#search-form');
21 | if (!form) return;
22 |
23 | const input = Util.q('#query', form);
24 |
25 | let kanaActive = false;
26 |
27 | const hookupEvents = btn =>
28 | (btn.onclick = () => {
29 | kanaActive = !kanaActive;
30 | btn.style.color = kanaActive ? '#333' : '#999';
31 | wanakana[kanaActive ? 'bind' : 'unbind'](input);
32 | input.focus();
33 | });
34 |
35 | const addKanaButton = () => {
36 | const span = document.createElement('span');
37 | Object.assign(span.style, {
38 | position: 'absolute',
39 | top: '0.3em',
40 | right: '0.6em',
41 | cursor: 'pointer',
42 | fontWeight: 'bold',
43 | color: '#999',
44 | userSelect: 'none'
45 | });
46 | span.textContent = 'あ';
47 | form.appendChild(span);
48 | hookupEvents(span);
49 | };
50 | addKanaButton();
51 | })();
52 |
--------------------------------------------------------------------------------
/wanikani-kanjidamage-mnemonics.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name WaniKani Kanjidamage Mnemonics
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.0.7
5 | // @description Includes Kanjidamage Mnemonics in WaniKani
6 | // @author Adrien Pyke
7 | // @match *://www.wanikani.com/kanji/*
8 | // @match *://www.wanikani.com/level/*/kanji/*
9 | // @match *://www.wanikani.com/review/session
10 | // @match *://www.wanikani.com/lesson/session
11 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
12 | // @grant GM_xmlhttpRequest
13 | // ==/UserScript==
14 |
15 | (() => {
16 | 'use strict';
17 |
18 | const SCRIPT_NAME = 'WaniKani Kanjidamage Mnemonics';
19 |
20 | const Util = {
21 | log(...args) {
22 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
23 | console.log(...args);
24 | },
25 | fromEntries:
26 | Object.fromEntries ||
27 | (iterable =>
28 | [...iterable].reduce((obj, [key, val]) => ((obj[key] = val), obj), {})),
29 | q: (query, context = document) => context.querySelector(query),
30 | qq: (query, context = document) =>
31 | Array.from(context.querySelectorAll(query)),
32 | appendAfter: (elem, elemToAppend) =>
33 | elem.parentNode.insertBefore(elemToAppend, elem.nextElementSibling),
34 | makeElem: (type, { classes, ...opts } = {}) => {
35 | const node = Object.assign(
36 | document.createElement(type),
37 | Util.fromEntries(
38 | Object.entries(opts).filter(([_, value]) => value != null)
39 | )
40 | );
41 | classes && classes.forEach(c => node.classList.add(c));
42 | return node;
43 | },
44 | fetch: (url, method = 'GET') =>
45 | new Promise((resolve, reject) =>
46 | GM_xmlhttpRequest({
47 | url,
48 | method,
49 | onload: resolve,
50 | onerror: reject
51 | })
52 | ),
53 | newTabLink: { target: '_blank', rel: 'noopener noreferrer' }
54 | };
55 |
56 | const App = {
57 | cachedKanji: [],
58 | getKanjiDamageInfo: async (kanji, inLesson) => {
59 | if (App.cachedKanji[kanji]) {
60 | Util.log(`${kanji} cached`);
61 | return App.cachedKanji[kanji];
62 | }
63 | Util.log(`Loading Kanjidamage information for ${kanji}`);
64 |
65 | try {
66 | const response = await Util.fetch(
67 | `http://www.kanjidamage.com/kanji/search?q=${kanji}`
68 | );
69 |
70 | Util.log(`Found Kanjidamage information for ${kanji}`);
71 |
72 | const tempDiv = Util.makeElem('div', {
73 | innerHTML: response.responseText
74 | });
75 |
76 | const replaceClasses = elem => {
77 | if (elem.classList.contains('onyomi')) {
78 | elem.classList.remove('onyomi');
79 | elem.classList.add(
80 | inLesson ? 'highlight-reading' : 'reading-highlight'
81 | );
82 | }
83 | if (elem.classList.contains('component')) {
84 | elem.classList.remove('component');
85 | elem.classList.add(
86 | inLesson ? 'highlight-radical' : 'radical-highlight'
87 | );
88 | }
89 | if (elem.classList.contains('translation')) {
90 | elem.classList.remove('translation');
91 | elem.classList.add(
92 | inLesson ? 'highlight-kanji' : 'kanji-highlight'
93 | );
94 | }
95 | };
96 |
97 | const readTableHtml = header => {
98 | const section = Util.qq('h2', tempDiv).find(elem =>
99 | elem.textContent.includes(header)
100 | );
101 | if (!section) return;
102 | const content = Util.q('td:nth-child(2)', section.nextElementSibling);
103 | Util.qq('span', content).forEach(replaceClasses);
104 | Util.qq('img', content)
105 | .filter(img => img.getAttribute('src').startsWith('/'))
106 | .forEach(
107 | img =>
108 | (img.src =
109 | 'http://www.kanjidamage.com' + img.getAttribute('src'))
110 | );
111 | return content.innerHTML;
112 | };
113 |
114 | const reading = readTableHtml('Onyomi');
115 | const mnemonic = readTableHtml('Mnemonic');
116 |
117 | App.cachedKanji[kanji] = {
118 | character: kanji,
119 | reading,
120 | mnemonic,
121 | url: response.finalUrl
122 | };
123 |
124 | return App.cachedKanji[kanji];
125 | } catch (e) {
126 | Util.log(`Could not find Kanjidamage information for ${kanji}`);
127 | }
128 | },
129 | createH2() {
130 | const h2 = Util.makeElem('h2');
131 | const link = Util.makeElem('a', {
132 | textContent: 'Kanjidamage',
133 | ...Util.newTabLink
134 | });
135 | h2.appendChild(link);
136 | return { h2, link };
137 | },
138 | createSection(node) {
139 | const { h2, link } = App.createH2();
140 | const section = Util.makeElem('section');
141 | if (node) {
142 | Util.appendAfter(node, h2);
143 | Util.appendAfter(h2, section);
144 | }
145 | return { h2, link, section };
146 | },
147 | createContainer(sel, selNode) {
148 | const container = Util.makeElem('section');
149 | const { h2, link, section } = App.createSection();
150 | container.appendChild(h2);
151 | container.appendChild(section);
152 | if (typeof sel === 'string')
153 | waitForElems({
154 | sel,
155 | onmatch: elem =>
156 | Util.q(selNode).classList.contains('kanji') &&
157 | Util.appendAfter(elem, container)
158 | });
159 | else Util.appendAfter(sel, container);
160 | return { container, h2, link, section };
161 | },
162 | getKanjiObjHtml: ({ reading, mnemonic }) =>
163 | (reading || '') + (mnemonic || ''),
164 | initWatch: (sel, selKanji, cb, cbClear) =>
165 | waitForElems({
166 | context: Util.q(sel),
167 | config: {
168 | attributes: true,
169 | childList: true,
170 | characterData: true,
171 | subtree: true
172 | },
173 | onchange: async () => {
174 | cbClear && cbClear();
175 | if (!Util.q(sel).classList.contains('kanji')) return;
176 | const kanji = Util.q(selKanji).textContent.trim();
177 | const kanjiObj = await App.getKanjiDamageInfo(kanji, true);
178 | kanji === kanjiObj.character && cb && cb(kanjiObj);
179 | }
180 | }),
181 | runOnLesson: () =>
182 | waitForElems({
183 | sel: '#main-info',
184 | stop: true,
185 | onmatch() {
186 | const { link: meaningLink, section: meaningSection } =
187 | App.createSection(Util.q('#supplement-kan-meaning-notes'));
188 | const { link: readingLink, section: readingSection } =
189 | App.createSection(Util.q('#supplement-kan-reading-notes'));
190 | const { link: reviewLink, section: reviewSection } =
191 | App.createContainer('#note-reading', '#main-info');
192 |
193 | const clearOutput = () =>
194 | (meaningLink.href =
195 | readingLink.href =
196 | reviewLink.href =
197 | meaningSection.innerHTML =
198 | readingSection.innerHTML =
199 | reviewSection.innerHTML =
200 | '');
201 |
202 | const outputKanjidamage = kanjiObj => {
203 | meaningLink.href =
204 | readingLink.href =
205 | reviewLink.href =
206 | kanjiObj.url;
207 | meaningSection.innerHTML =
208 | readingSection.innerHTML =
209 | reviewSection.innerHTML =
210 | App.getKanjiObjHtml(kanjiObj);
211 | };
212 |
213 | App.initWatch(
214 | '#main-info',
215 | '#character',
216 | outputKanjidamage,
217 | clearOutput
218 | );
219 | }
220 | }),
221 | runOnReview: () =>
222 | waitForElems({
223 | sel: '#character',
224 | onmatch() {
225 | const { link, section } = App.createContainer(
226 | '#note-reading',
227 | '#character'
228 | );
229 |
230 | const outputKanjidamage = kanjiObj => {
231 | link.href = kanjiObj.url;
232 | section.innerHTML = App.getKanjiObjHtml(kanjiObj);
233 | };
234 |
235 | App.initWatch('#character', '#character > span', outputKanjidamage);
236 | }
237 | }),
238 | runOnKanjiPage: async () => {
239 | const kanji = Util.q('.kanji-icon').textContent;
240 | const kanjiObj = await App.getKanjiDamageInfo(kanji, false);
241 | const { link, section } = App.createContainer(
242 | Util.q('#note-reading').parentNode
243 | );
244 |
245 | link.href = kanjiObj.url;
246 | section.innerHTML = App.getKanjiObjHtml(kanjiObj);
247 | }
248 | };
249 |
250 | const isLesson = window.location.pathname.includes('/lesson/');
251 | const isReview = window.location.pathname.includes('/review/');
252 |
253 | isLesson
254 | ? App.runOnLesson()
255 | : isReview
256 | ? App.runOnReview()
257 | : App.runOnKanjiPage();
258 | })();
259 |
--------------------------------------------------------------------------------
/wanikani-pitch-accent.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name WaniKani Pitch Accent
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.0.1
5 | // @description Show pitch accent data on WaniKani
6 | // @author Adrien Pyke
7 | // @match *://www.wanikani.com/vocabulary/*
8 | // @match *://www.wanikani.com/level/*/vocabulary/*
9 | // @match *://www.wanikani.com/review/session
10 | // @match *://www.wanikani.com/lesson/session
11 | // @require https://cdn.jsdelivr.net/gh/IllDepence/SVG_pitch@295af214b1e3c8add03a31cf022e28033495da08/accdb.js
12 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
13 | // @grant none
14 | // ==/UserScript==
15 | /* global acc_dict */
16 |
17 | (() => {
18 | 'use strict';
19 |
20 | const Util = {
21 | q: (query, context = document) => context.querySelector(query),
22 | qq: (query, context = document) =>
23 | Array.from(context.querySelectorAll(query)),
24 | toMoraArray: kana => kana.match(/.[ゃゅょぁぃぅぇぉャュョァィゥェォ]?/gu),
25 | getAccentData: (kanji, reading) => {
26 | const [kana, pitch] =
27 | (acc_dict[kanji] && acc_dict[kanji].find(([r]) => r === reading)) || [];
28 | if (!kana) return [];
29 | return [Util.toMoraArray(kana), [...pitch.replace(/[lh]/gu, '')]];
30 | }
31 | };
32 |
33 | const Draw = {
34 | textGeneric: (x, text, color = '#666') =>
35 | `${text}`,
36 | text: (x, mora) =>
37 | mora.length === 1
38 | ? Draw.textGeneric(x, mora)
39 | : Draw.textGeneric(x - 5, mora[0]) + Draw.textGeneric(x + 12, mora[1]),
40 | circle: (x, y, empty, color = '#000', emptyColor = '#eee') =>
41 | `
42 |
43 | ` +
44 | (empty
45 | ? ``
46 | : ''),
47 | path: (x, y, type, stepWidth, color = '#000') =>
48 | `
49 |
52 | `,
53 | svg: (kanji, reading) => {
54 | const [mora, pitch] = Util.getAccentData(kanji, reading);
55 | if (!mora) return;
56 |
57 | const stepWidth = 35;
58 | const marginLr = 16;
59 | const positions = Math.max(mora.length, pitch.length);
60 | const svgWidth = Math.max(0, (positions - 1) * stepWidth + marginLr * 2);
61 | const getXCenter = step => marginLr + step * stepWidth;
62 | const getYCenter = type => (type === 'H' ? 5 : 30);
63 |
64 | const chars = mora
65 | .map((kana, i) => Draw.text(getXCenter(i) - 11, kana))
66 | .join('');
67 | const paths = pitch
68 | .slice(1)
69 | .map((type, i) => ({
70 | prevXCenter: getXCenter(i),
71 | prevYCenter: getYCenter(pitch[i]),
72 | yCenter: getYCenter(type)
73 | }))
74 | .map(({ prevXCenter, prevYCenter, yCenter }) =>
75 | Draw.path(
76 | prevXCenter,
77 | prevYCenter,
78 | prevYCenter < yCenter ? 'd' : prevYCenter > yCenter ? 'u' : 's',
79 | stepWidth
80 | )
81 | )
82 | .join('');
83 | const circles = pitch
84 | .map((type, i) =>
85 | Draw.circle(getXCenter(i), getYCenter(type), i >= mora.length)
86 | )
87 | .join('');
88 |
89 | return `
90 |
93 | `.trim();
94 | }
95 | };
96 |
97 | const addSvgToGroup = (group, kanji, marginTop) => {
98 | const svg = Draw.svg(
99 | kanji,
100 | Util.q('.pronunciation-variant', group).textContent
101 | );
102 | if (!svg) return;
103 | const div = document.createElement('div');
104 | div.style.marginTop = marginTop;
105 | div.innerHTML = svg;
106 | group.appendChild(div);
107 | };
108 |
109 | const isLesson = window.location.pathname.includes('/lesson/');
110 | const isReview = window.location.pathname.includes('/review/');
111 | const isVocab = !isLesson && !isReview;
112 |
113 | waitForElems({
114 | sel: '.pronunciation-group',
115 | onmatch: group =>
116 | addSvgToGroup(
117 | group,
118 | Util.q(
119 | isVocab
120 | ? '.vocabulary-icon'
121 | : isLesson
122 | ? '#character'
123 | : '#character > span'
124 | ).textContent,
125 | isVocab ? 0 : '10px'
126 | )
127 | });
128 | })();
129 |
--------------------------------------------------------------------------------
/works-burger-chooser.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name The Works Burger Chooser
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.3.1
5 | // @description Choose a random burger on the works menu
6 | // @author Adrien Pyke
7 | // @match *://worksburger.com/menu/burger-menu/*
8 | // @grant unsafeWindow
9 | // ==/UserScript==
10 |
11 | (() => {
12 | 'use strict';
13 |
14 | const W = unsafeWindow || window;
15 |
16 | const SCRIPT_NAME = 'The Works Burger Chooser';
17 |
18 | const Util = {
19 | log(...args) {
20 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
21 | console.log(...args);
22 | },
23 | q: (query, context = document) => context.querySelector(query),
24 | qq: (query, context = document) =>
25 | Array.from(context.querySelectorAll(query)),
26 | prepend: (parent, child) => parent.insertBefore(child, parent.firstChild),
27 | createCheckbox(text) {
28 | const label = document.createElement('label');
29 | label.textContent = text;
30 | const checkbox = document.createElement('input');
31 | checkbox.type = 'checkbox';
32 | label.appendChild(checkbox);
33 | return label;
34 | },
35 | randomColor() {
36 | const letters = '0123456789ABCDEF'.split('');
37 | let color = '#';
38 | for (let i = 0; i < 6; i++) {
39 | color += letters[Math.floor(Math.random() * letters.length)];
40 | }
41 | return color;
42 | },
43 | createStyle(css) {
44 | const style = document.createElement('style');
45 | style.textContent = css;
46 | document.head.appendChild(style);
47 | }
48 | };
49 |
50 | Util.createStyle(`
51 | .vc_grid-item-mini .vc_col-sm-12 {
52 | width: 100%
53 | }
54 | `);
55 |
56 | const container = document.createElement('div');
57 | container.setAttribute(
58 | 'style',
59 | `
60 | position: fixed;
61 | bottom: 20px;
62 | left: 20px;
63 | padding: 5px;
64 | z-index: 99999;
65 | background-color: white;
66 | display: flex;
67 | flex-direction: column;
68 | `
69 | );
70 |
71 | const vegetarian = Util.createCheckbox('Vegetarian ');
72 | const cbVegetarian = Util.q('input', vegetarian);
73 | container.appendChild(vegetarian);
74 |
75 | const button = document.createElement('button');
76 | button.textContent = 'Choose Random Burger';
77 | container.appendChild(button);
78 |
79 | document.body.appendChild(container);
80 |
81 | const selectBurger = () => {
82 | Util.log('Choosing Random Burger...');
83 |
84 | let burgers = Util.qq('.vc_grid-item-mini');
85 | burgers.forEach(burger => burger.removeAttribute('style'));
86 | if (cbVegetarian.checked) {
87 | burgers = burgers.filter(b =>
88 | Util.q('img[src="/wp-content/uploads/2017/11/veg.png"]', b)
89 | );
90 | }
91 |
92 | const burger = burgers[Math.floor(Math.random() * burgers.length)];
93 |
94 | burger.setAttribute(
95 | 'style',
96 | `
97 | transition: 0.5s;
98 | box-shadow: inset 0 0 100px ${Util.randomColor()};
99 | transform: scale(1.2, 1.2);
100 | border-radius: 20px;
101 | `
102 | );
103 |
104 | setTimeout(() => (burger.style.transform = 'scale(1, 1)'), 500);
105 |
106 | burger.scrollIntoView();
107 | };
108 |
109 | if (W.BM_MODE) {
110 | selectBurger();
111 | }
112 |
113 | button.onclick = selectBurger;
114 | })();
115 |
--------------------------------------------------------------------------------
/youtube-middle-click-search.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Youtube Middle Click Search
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 2.1.7
5 | // @description Middle clicking the search on youtube opens the results in a new tab
6 | // @author Adrien Pyke
7 | // @match *://www.youtube.com/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @grant GM_openInTab
10 | // ==/UserScript==
11 |
12 | (function () {
13 | 'use strict';
14 |
15 | const SCRIPT_NAME = 'YMCS';
16 |
17 | const Util = {
18 | log(...args) {
19 | args.unshift(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: #233c7b;');
20 | console.log(...args);
21 | },
22 | q(query, context = document) {
23 | return context.querySelector(query);
24 | },
25 | qq(query, context = document) {
26 | return Array.from(context.querySelectorAll(query));
27 | },
28 | getQueryParameter(name, url = window.location.href) {
29 | name = name.replace(/[[\]]/gu, '\\$&');
30 | const regex = new RegExp(`[?&]${name}(=([^]*)|&|#|$)`, 'u'),
31 | results = regex.exec(url);
32 | if (!results) return null;
33 | if (!results[2]) return '';
34 | return decodeURIComponent(results[2].replace(/\+/gu, ' '));
35 | },
36 | encodeURIWithPlus(string) {
37 | return encodeURIComponent(string).replace(/%20/gu, '+');
38 | }
39 | };
40 |
41 | waitForElems({
42 | sel: '#search-icon-legacy',
43 | stop: true,
44 | onmatch(btn) {
45 | btn.onmousedown = function (e) {
46 | if (e.button === 1) {
47 | e.preventDefault();
48 | }
49 | };
50 | btn.onclick = function (e) {
51 | e.preventDefault();
52 | e.stopImmediatePropagation();
53 |
54 | const input = Util.q('input#search').value.trim();
55 | if (!input) return false;
56 |
57 | const url = `${
58 | location.origin
59 | }/results?search_query=${Util.encodeURIWithPlus(input)}`;
60 | if (e.button === 1) {
61 | GM_openInTab(url, true);
62 | } else if (e.button === 0) {
63 | window.location.href = url;
64 | }
65 |
66 | return false;
67 | };
68 | btn.onauxclick = btn.onclick;
69 | }
70 | });
71 |
72 | waitForElems({
73 | sel: '.sbsb_c',
74 | onmatch(result) {
75 | result.onclick = function (e) {
76 | if (!e.target.classList.contains('sbsb_i')) {
77 | const search = Util.q('.sbpqs_a, .sbqs_c', result).textContent;
78 |
79 | const url = `${
80 | location.origin
81 | }/results?search_query=${Util.encodeURIWithPlus(search)}`;
82 | if (e.button === 1) {
83 | GM_openInTab(url, true);
84 | } else if (e.button === 0) {
85 | window.location.href = url;
86 | }
87 | } else if (e.button === 1) {
88 | // prevent opening in new tab if they middle click the remove button
89 | e.preventDefault();
90 | e.stopImmediatePropagation();
91 | }
92 | };
93 | result.onauxclick = result.onclick;
94 | }
95 | });
96 | })();
97 |
--------------------------------------------------------------------------------
/youtube-unblocker.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Youtube Unblocker
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 3.0.23
5 | // @description Auto redirects blocked videos to the mirror site hooktube.com
6 | // @author Adrien Pyke
7 | // @match *://www.youtube.com/*
8 | // @match *://hooktube.com/watch*
9 | // @grant GM_getValue
10 | // @grant GM_setValue
11 | // @grant GM_registerMenuCommand
12 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
13 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
14 | // ==/UserScript==
15 |
16 | (() => {
17 | 'use strict';
18 |
19 | const Util = {
20 | q(query, context = document) {
21 | return context.querySelector(query);
22 | },
23 | qq(query, context = document) {
24 | return Array.from(context.querySelectorAll(query));
25 | }
26 | };
27 |
28 | const Config = GM_config([
29 | {
30 | key: 'autoplay',
31 | label: 'Autoplay',
32 | default: true,
33 | type: 'bool'
34 | }
35 | ]);
36 | GM_registerMenuCommand('Youtube Unblocker Settings', Config.setup);
37 |
38 | if (location.hostname === 'www.youtube.com') {
39 | waitForElems({
40 | sel: '#page-manager',
41 | stop: true,
42 | onmatch(page) {
43 | setTimeout(() => {
44 | const redirect = function () {
45 | location.replace(
46 | `${location.protocol}//hooktube.com/watch${location.search}`
47 | );
48 | };
49 | if (page.querySelector('[player-unavailable]')) {
50 | redirect();
51 | } else {
52 | setTimeout(() => {
53 | if (!Util.q('#page-manager').innerHTML.trim()) {
54 | redirect();
55 | }
56 | }, 5);
57 | }
58 | }, 0);
59 | }
60 | });
61 | } else {
62 | const cfg = Config.load();
63 | if (!cfg.autoplay) {
64 | waitForElems({
65 | sel: '#player-obj',
66 | stop: true,
67 | onmatch(video) {
68 | video.pause();
69 | }
70 | });
71 | document.querySelector('#player-obj').pause();
72 | }
73 | }
74 | })();
75 |
--------------------------------------------------------------------------------
/youtube-view-more.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name View More Videos by Same YouTube Channel
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.2.3
5 | // @description Displays a list of more videos by the same channel inline
6 | // @author Adrien Pyke
7 | // @match *://www.youtube.com/*
8 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
9 | // @require https://unpkg.com/mithril
10 | // @resource pageTokens https://cdn.rawgit.com/Quihico/handy.stuff/7e47f4f2/yt.pagetokens.00000-00999
11 | // @grant GM_addStyle
12 | // @grant GM_getResourceText
13 | // ==/UserScript==
14 |
15 | (() => {
16 | 'use strict';
17 |
18 | const CLASS_PREFIX = 'YVM_';
19 |
20 | GM_addStyle(/* css */ `
21 | .${CLASS_PREFIX}slider {
22 | display: flex;
23 | align-items: center;
24 | margin-top: 8px;
25 | }
26 | .${CLASS_PREFIX}thumbnails-wrap {
27 | flex-grow: 1;
28 | overflow-x: hidden;
29 | }
30 | .${CLASS_PREFIX}thumbnails {
31 | display: flex;
32 | position: relative;
33 | top: 0;
34 | transition: left 300ms ease-out;
35 | }
36 | .${CLASS_PREFIX}thumbnail {
37 | display: flex;
38 | flex-direction: column;
39 | margin-right: 8px;
40 | min-width: 168px;
41 | }
42 | .${CLASS_PREFIX}thumbnail ytd-thumbnail {
43 | margin-right: 8px;
44 | height: 94px;
45 | width: 168px;
46 | }
47 | .${CLASS_PREFIX}thumbnail.${CLASS_PREFIX}active {
48 | background-color: var(--yt-thumbnail-placeholder-color);
49 | }
50 | #${CLASS_PREFIX}mount-point {
51 | margin-top: 8px;
52 | }
53 | .${CLASS_PREFIX}slider ytd-thumbnail.ytd-compact-video-renderer {
54 | margin: 0;
55 | }
56 | .${CLASS_PREFIX}slider #video-title.ytd-compact-video-renderer {
57 | max-height: 4.8rem;
58 | }
59 | `);
60 |
61 | const API_URL = 'https://www.googleapis.com/youtube/v3/';
62 | const API_KEY = 'AIzaSyDEAM1cSePslXRBk1Bkoo4FCXYBnEZSsgI';
63 | const RESULTS_PER_FETCH = 50;
64 | const LAZY_LOAD_BUFFER = 10;
65 |
66 | const icons = {
67 | 'chevron-left':
68 | '',
69 | 'chevron-right':
70 | ''
71 | };
72 |
73 | const Util = {
74 | btn(options, text) {
75 | return m(
76 | 'paper-button[role=button][subscribed].style-scope.ytd-subscribe-button-renderer',
77 | options,
78 | text
79 | );
80 | },
81 | iconBtn(icon, options = {}) {
82 | return m(
83 | 'ytd-button-renderer.style-scope.ytd-menu-renderer.force-icon-button.style-default.size-default[button-renderer][is-icon-button]',
84 | { icon },
85 | [m('paper-icon-button', Object.assign(options))]
86 | );
87 | },
88 | initCmp(cmp) {
89 | const oninit = cmp.oninit;
90 | return Object.assign({}, cmp, {
91 | oninit(vnode) {
92 | if (typeof cmp.model === 'function') vnode.state.model = cmp.model();
93 | if (oninit) oninit(vnode);
94 | }
95 | });
96 | },
97 | delayedRedraw(func, delay = 50) {
98 | return new Promise(resolve =>
99 | setTimeout(() => {
100 | func();
101 | m.redraw();
102 | resolve();
103 | }, delay)
104 | );
105 | },
106 | fillIcons(vnode) {
107 | Array.from(
108 | vnode.dom.querySelectorAll('ytd-button-renderer[icon]')
109 | ).forEach(
110 | btn =>
111 | (btn.querySelector('iron-icon').innerHTML = /* html */ `
112 |
122 | `)
123 | );
124 | },
125 | decode(text) {
126 | const elem = document.createElement('textarea');
127 | elem.innerHTML = text;
128 | return elem.value;
129 | }
130 | };
131 |
132 | const Api = {
133 | pageTokens: GM_getResourceText('pageTokens').split('\n'),
134 | request(endpoint, data, method = 'GET') {
135 | return m.request({
136 | method,
137 | background: true,
138 | url: API_URL + endpoint,
139 | params: Object.assign(data, { key: API_KEY })
140 | });
141 | },
142 | parseVideo(data) {
143 | return {
144 | id: data.snippet.resourceId ? data.snippet.resourceId.videoId : data.id,
145 | title: data.snippet.title,
146 | channelId: data.snippet.channelId,
147 | channelTitle: data.snippet.channelTitle,
148 | publishedAt: new Date(data.snippet.publishedAt),
149 | thumbnail: data.snippet.thumbnails.medium.url
150 | };
151 | },
152 | sortVideos(a, b) {
153 | if (a.publishedAt > b.publishedAt) return -1;
154 | else if (a.publishedAt < b.publishedAt) return 1;
155 | return 0;
156 | },
157 | async getVideo(id) {
158 | const data = await Api.request('videos', {
159 | part: 'snippet',
160 | id
161 | });
162 | if (data && data.items.length > 0) return Api.parseVideo(data.items[0]);
163 | },
164 | async getPlaylistId(channelId) {
165 | const data = await Api.request('channels', {
166 | part: 'contentDetails',
167 | id: channelId
168 | });
169 | return data.items[0].contentDetails.relatedPlaylists.uploads;
170 | },
171 | async getVideos(playlistId, pageToken) {
172 | const data = await Api.request('playlistItems', {
173 | part: 'snippet',
174 | maxResults: RESULTS_PER_FETCH,
175 | playlistId,
176 | ...(pageToken ? { pageToken } : {})
177 | });
178 | return {
179 | pageToken: data.nextPageToken,
180 | videos: data.items.map(Api.parseVideo)
181 | };
182 | },
183 | get currentVideoId() {
184 | const url = new URL(location.href);
185 | return url.searchParams.get('v');
186 | }
187 | };
188 |
189 | const Components = {
190 | App: Util.initCmp({
191 | model: () => ({
192 | hidden: true
193 | }),
194 | actions: {
195 | toggle: model => (model.hidden = !model.hidden)
196 | },
197 | view(vnode) {
198 | const { model, actions } = vnode.state;
199 | return m('div', [
200 | Util.btn(
201 | {
202 | onclick: () => actions.toggle(model)
203 | },
204 | 'View More Videos'
205 | ),
206 | m(Components.Slider, {
207 | hidden: model.hidden,
208 | videoId: Api.currentVideoId
209 | })
210 | ]);
211 | }
212 | }),
213 | Slider: Util.initCmp({
214 | model: () => ({
215 | currentVideo: null,
216 | playlistId: null,
217 | videos: [],
218 | pageToken: null,
219 | loading: false,
220 | position: 0,
221 | shiftLeft() {
222 | this.position = Math.max(this.position - 1, 0);
223 | },
224 | shiftRight() {
225 | this.position = Math.min(this.position + 1, this.videos.length - 1);
226 | },
227 | get leftPx() {
228 | return this.position * -176;
229 | }
230 | }),
231 | actions: {
232 | async fetchInitialVideos(model, currentVideoId) {
233 | model.currentVideo = await Api.getVideo(currentVideoId);
234 | model.playlistId = await Api.getPlaylistId(
235 | model.currentVideo.channelId
236 | );
237 | await this.loadVideos(model);
238 | model.position = Math.max(
239 | (model.videos.findIndex(v => v.id === model.currentVideo.id) || 0) -
240 | 1,
241 | 0
242 | );
243 | },
244 | async loadVideos(model) {
245 | model.loading = true;
246 | const { pageToken, videos } = await Api.getVideos(
247 | model.playlistId,
248 | model.pageToken
249 | );
250 | model.videos.push(...videos);
251 | model.pageToken = pageToken;
252 | model.loading = false;
253 | m.redraw();
254 | },
255 | moveLeft(model) {
256 | model.shiftLeft();
257 | m.redraw();
258 | },
259 | async moveRight(model) {
260 | if (model.loading) return;
261 | if (
262 | model.position + LAZY_LOAD_BUFFER > model.videos.length &&
263 | model.pageToken
264 | ) {
265 | await this.loadVideos(model, true);
266 | Util.delayedRedraw(() => {
267 | model.shiftRight();
268 | model.loading = false;
269 | });
270 | } else {
271 | model.shiftRight();
272 | }
273 | }
274 | },
275 | oninit(vnode) {
276 | const { model, actions } = vnode.state;
277 | actions.fetchInitialVideos(model, vnode.attrs.videoId);
278 | },
279 | oncreate: Util.fillIcons,
280 | view(vnode) {
281 | const { model, actions } = vnode.state;
282 | return m(`div.${CLASS_PREFIX}slider`, { hidden: vnode.attrs.hidden }, [
283 | Util.iconBtn('chevron-left', {
284 | onclick: () => actions.moveLeft(model)
285 | }),
286 | m(`div.${CLASS_PREFIX}thumbnails-wrap`, [
287 | m(
288 | `div.${CLASS_PREFIX}thumbnails`,
289 | {
290 | style: `left: ${model.leftPx}px;transition-property:${
291 | model.loading ? 'none' : ''
292 | };`
293 | },
294 | model.videos.map(video =>
295 | m(Components.Thumbnail, {
296 | key: video.id,
297 | active: video.id === model.currentVideo.id,
298 | video
299 | })
300 | )
301 | )
302 | ]),
303 | Util.iconBtn('chevron-right', {
304 | onclick: () => actions.moveRight(model)
305 | })
306 | ]);
307 | }
308 | }),
309 | Thumbnail: Util.initCmp({
310 | model: () => ({
311 | video: null
312 | }),
313 | oninit(vnode) {
314 | vnode.state.model.video = vnode.attrs.video;
315 | },
316 | view(vnode) {
317 | const { model } = vnode.state;
318 | const title = Util.decode(model.video.title);
319 | return m(
320 | `div.${CLASS_PREFIX}thumbnail${
321 | vnode.attrs.active ? `.${CLASS_PREFIX}active` : ''
322 | }`,
323 | [
324 | m(
325 | 'ytd-thumbnail.style-scope.ytd-compact-video-renderer',
326 | { width: 168 },
327 | [
328 | m(
329 | 'a#thumbnail.yt-simple-endpoint.inline-block.style-scope.ytd-thumbnail',
330 | { rel: 'nofollow', href: `/watch?v=${model.video.id}` },
331 | [
332 | m(
333 | 'yt-img-shadow.style-scope.ytd-thumbnail.no-transition[loaded]',
334 | [
335 | m('img.style-scope.yt-img-shadow', {
336 | width: 168,
337 | src: model.video.thumbnail
338 | })
339 | ]
340 | )
341 | ]
342 | )
343 | ]
344 | ),
345 | m(
346 | 'a.yt-simple-endpoint.style-scope.ytd-compact-video-renderer',
347 | { rel: 'nofollow', href: `/watch?v=${model.video.id}` },
348 | [
349 | m('h3.style-scope.ytd-compact-video-renderer', [
350 | m(
351 | 'span#video-title.style-scope.ytd-compact-video-renderer',
352 | { title },
353 | title
354 | )
355 | ])
356 | ]
357 | )
358 | ]
359 | );
360 | }
361 | })
362 | };
363 |
364 | let wait;
365 | const mountId = `${CLASS_PREFIX}mount-point`;
366 | waitForUrl(
367 | () => true,
368 | () => {
369 | if (wait) wait.stop();
370 | const oldMount = document.getElementById(mountId);
371 | if (oldMount) {
372 | m.mount(oldMount, null);
373 | oldMount.remove();
374 | }
375 | wait = waitForElems({
376 | sel: 'ytd-video-secondary-info-renderer > #container',
377 | stop: true,
378 | onmatch(container) {
379 | const mount = document.createElement('div');
380 | mount.id = mountId;
381 | container.prepend(mount);
382 | m.mount(mount, Components.App);
383 | }
384 | });
385 | }
386 | );
387 | })();
388 |
--------------------------------------------------------------------------------
/youtube-volume.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Youtube Scroll Volume
3 | // @namespace https://greasyfork.org/users/649
4 | // @version 1.1.12
5 | // @description Use the scroll wheel to adjust volume of youtube videos
6 | // @author Adrien Pyke
7 | // @match *://www.youtube.com/*
8 | // @grant GM_addStyle
9 | // @grant GM_getValue
10 | // @grant GM_setValue
11 | // @grant GM_registerMenuCommand
12 | // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@22210afba13acf7303fc91590b8265faf3c7eda7/libs/gm_config.js
13 | // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
14 | // @require https://cdn.jsdelivr.net/gh/kufii/quick-query.js@2993f91ae90f3b2aff4af7e9ce0b08504f5c8060/dist/window/qq.js
15 | // ==/UserScript==
16 |
17 | (() => {
18 | 'use strict';
19 |
20 | const { q } = window.QuickQuery;
21 |
22 | const Util = {
23 | bound: (num, min, max) => Math.max(Math.min(num, max), min)
24 | };
25 |
26 | const Config = GM_config([
27 | { key: 'reverse', label: 'Reverse Scroll', default: false, type: 'bool' },
28 | {
29 | key: 'horizontal',
30 | label: 'Use Horizontal Scroll',
31 | default: false,
32 | type: 'bool'
33 | },
34 | {
35 | key: 'step',
36 | label: 'Change By',
37 | default: 5,
38 | type: 'number',
39 | min: 1,
40 | max: 100
41 | },
42 | { key: 'hud', label: 'Display HUD', default: true, type: 'bool' },
43 | {
44 | key: 'requireShift',
45 | label: 'Only handle scroll if holding "Shift" key',
46 | default: false,
47 | type: 'bool'
48 | }
49 | ]);
50 | GM_registerMenuCommand('Youtube Scroll Volume Settings', Config.setup);
51 |
52 | let config = Config.load();
53 | Config.onsave = newConf => (config = newConf);
54 |
55 | GM_addStyle(/* css */ `
56 | .YSV_hud {
57 | display: flex;
58 | flex-direction: column;
59 | justify-content: flex-end;
60 | align-items: center;
61 | position: absolute;
62 | top: 0;
63 | bottom: 0;
64 | left: 0;
65 | right: 0;
66 | opacity: 0;
67 | transition: opacity 500ms ease 0s;
68 | z-index: 10;
69 | pointer-events: none;
70 | }
71 | .YSV_bar {
72 | background-color: #444;
73 | border: 2px solid white;
74 | width: 80%;
75 | max-width: 600px;
76 | margin-bottom: 10%;
77 | }
78 | .YSV_progress {
79 | transition: width 100ms ease-out 0s;
80 | background-color: #888;
81 | height: 20px;
82 | }
83 | .YSV_text {
84 | position: absolute;
85 | text-align: center;
86 | line-height: 20px;
87 | width: 80%;
88 | max-width: 600px;
89 | color: white;
90 | }
91 | `);
92 |
93 | const createHud = () => {
94 | const hud = document.createElement('div');
95 | hud.classList.add('YSV_hud');
96 | hud.innerHTML =
97 | '';
98 | return hud;
99 | };
100 |
101 | waitForElems({
102 | sel: 'ytd-player',
103 | onmatch(node) {
104 | let id;
105 |
106 | const hud = createHud();
107 | const progress = q(hud).q('.YSV_progress');
108 | const text = q(hud).q('.YSV_text');
109 | node.appendChild(hud);
110 |
111 | const showHud = volume => {
112 | clearTimeout(id);
113 | progress.style.width = `${volume}%`;
114 | text.innerHTML = `${volume}%`;
115 | hud.style.opacity = 1;
116 | id = setTimeout(() => (hud.style.opacity = 0), 800);
117 | };
118 |
119 | node.onwheel = e => {
120 | if (config.requireShift && !e.shiftKey) return;
121 | const player = node.getPlayer();
122 | const dir =
123 | ((config.horizontal ? -e.deltaX : e.deltaY) > 0 ? -1 : 1) *
124 | (config.reverse ? -1 : 1);
125 |
126 | const vol = Util.bound(player.getVolume() + config.step * dir, 0, 100);
127 | if (vol > 0 && player.isMuted()) player.unMute();
128 | player.setVolume(vol);
129 | if (config.hud) showHud(vol);
130 |
131 | e.preventDefault();
132 | e.stopImmediatePropagation();
133 | };
134 | }
135 | });
136 | })();
137 |
--------------------------------------------------------------------------------