├── .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 | 91 | ${chars + paths + circles} 92 | 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 | 120 | ${icons[btn.getAttribute('icon')]} 121 | 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 | --------------------------------------------------------------------------------