├── .gitignore ├── 468740-restore-youtube-username.js ├── 469864-youtube-short-channel-name.user.js ├── 469878-youtube-chat.js ├── 470042-youtube-floating-chat.user.js ├── 470428.user.js ├── 470865-pangu.user.js ├── 471018.user.js ├── 471033-sticky-memory.user.js ├── 473130-ytdls.user.js ├── 473830-greasyfork++.user.js ├── 473972.user copy 7.js ├── 473972.user copy 8.js ├── 473972.user.js ├── 474214-fix-brave-yt-live-1.8.user.js ├── 474214-fix-brave-yt-live-2.0.user.js ├── 474214-fix-brave-yt-live-3.0.user.js ├── 475579-youtube-make-autoplay-next-more-than-3-seconds.user.js ├── 482487-greasyfork-dark.user.js ├── 483406-youtube-quality-auto-max.user.js ├── 484611-youtube-audio-only.user.js ├── Info-468740.md ├── LICENSE ├── UseCoderInVM.md ├── UserScript.md ├── UserStyle.md ├── Webhook Deployment.md ├── YouTube-Single-Column-Tamer.user.js ├── YouTube-UserScripts.md ├── a-universal-script-to-re-enable-the-selection-and-copying.user.js ├── blank.html ├── boost-chat.user.js ├── chatgpt-message-records.user.js ├── cookie-manager.user.js ├── demo ├── animated-rolling-number.webm ├── pangu-lines.html └── timer-performance.js ├── disable-youtube-autopause.user.js ├── disable-youtube-av1-vp9.js ├── disable-youtube-av1.js ├── disable-youtube-music-autopause.user.js ├── docs ├── animated-rolling-number.html ├── index.html └── textarea.html ├── force-youtube-av1.js ├── google-go-to-cache.user.js ├── icons ├── YouTube-Audio-Only.png ├── blank-letter.png ├── brave.png ├── cookie-manager.png ├── disable-youtube-autopause.svg ├── drag-drop-image-uploader.svg ├── general-icon.png ├── index.html ├── selection-copier.png ├── super-fast-chat.png ├── twitter-original.svg ├── web-cpu-tamer.svg ├── youtube-cpu-tamper-by-animationframe.webp ├── youtube-minimal.png ├── youtube-unlock-indexedDB.png ├── youtube-video-resize-fix.png └── yt-engine.png ├── images ├── stylus-text-settings-example-01.png ├── youtube-autopause-en.webp ├── youtube-autopause-jp.webp ├── youtube-minimal-preview.png ├── youtube-unlock-indexedDB-1.png └── youtube-unlock-indexedDB-2.png ├── library ├── WinComm.js ├── codejar-cursor.esm.js ├── codejar-cursor.umd.js ├── codejar.esm.js ├── codejar.umd.js ├── default-trusted-type-policy.js ├── html.min.js ├── jmt_setImmediate.js ├── misc.js ├── nextBrowserTick.js ├── pangu-lite.js ├── pangu-webworker.js ├── simple-userjs.js ├── solid-js-prod.js ├── structurize.js ├── ytConfigHacks.js └── ytZara.js ├── lihkg ├── lihkg-instant-drawer.user.css └── lihkg-no-ads.user.css ├── no-ad-iframe.user.js ├── references ├── brave-scriptlet.js ├── desktop_polymer_enable_wil_icons-js.css ├── live_chat_polymer-js.css ├── stampDomArray_Renderer.ref.js ├── webworker-via.html ├── webworker.html └── www-player.css ├── reset-youtube-settings.user.js ├── revised-28678-youtube-play-next-queue.user.js ├── revised-35760-yt-url-at-time.user.js ├── revised-script-41552.user.js ├── revised-script-446849.js ├── tmp-480192.user.js ├── unhold-youtube-resource-locks.user.js ├── web-cpu-tamer.user.js ├── youtube-cpu-tamer-by-animationframe.user.js ├── youtube-cpu-tamer-dm.user.js ├── youtube-live-chat-tamer.js ├── youtube-memory-leakage.user.js ├── youtube-minimal-fixs.user.js ├── youtube-minimal-on-pc.user.js ├── youtube-music-audio-only.user.js ├── youtube-no-leakage-01.user.js ├── youtube-playlist-autoplay-button.user.js ├── youtube-popup-window.user.js ├── youtube-rm3.user.js ├── youtube-video-resize-fix.user.js ├── yt-fadeInChatMessage.user.css └── yt-flag-kevlar_watch_grid-false.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /470865-pangu.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 中英文之间加空白 3 | // @name:zh-TW 中英文之間加空白 4 | 5 | // @version 0.7.13 6 | // @author CY Fung 7 | // @namespace UserScript 8 | // @license MIT 9 | // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@b4f5e6fae19dd9f6d36b09054d0a3e4ef7e7eaa4/library/pangu-lite.js 10 | 11 | // @match http://*/* 12 | // @match https://*/* 13 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 14 | // @exclude /^shttps?://yuzu-emu.org/*$/ 15 | 16 | // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/blank-letter.png 17 | // @grant GM_setValue 18 | // @grant unsafeWindow 19 | // @run-at document-start 20 | // @allFrames true 21 | // @inject-into content 22 | 23 | // @description 自动替你在网页中所有的中文字和半形的英文、数字、符号之间插入空白,让文字变得美观好看。(pangu, 盤古之白) 24 | // @description:zh-TW 自動替你在網頁中所有的中文字和半形的英文、數字、符號之間插入空白,讓文字變得美觀好看。(pangu, 盤古之白) 25 | 26 | // @downloadURL https://update.greasyfork.org/scripts/470865/%E4%B8%AD%E8%8B%B1%E6%96%87%E4%B9%8B%E9%97%B4%E5%8A%A0%E7%A9%BA%E7%99%BD.user.js 27 | // @updateURL https://update.greasyfork.org/scripts/470865/%E4%B8%AD%E8%8B%B1%E6%96%87%E4%B9%8B%E9%97%B4%E5%8A%A0%E7%A9%BA%E7%99%BD.meta.js 28 | // ==/UserScript== 29 | 30 | 31 | ((__CONTEXT__) => { 32 | 33 | const { pangu } = __CONTEXT__; 34 | 35 | const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : (this instanceof Window ? this : window); 36 | 37 | // Create a unique key for the script and check if it is already running 38 | const hkey_script = 'depcyxozwnig'; 39 | if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting 40 | win[hkey_script] = true; 41 | 42 | /** @type {globalThis.PromiseConstructor} */ 43 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 44 | const cleanContext = async (win) => { 45 | const waitFn = requestAnimationFrame; // shall have been binded to window 46 | try { 47 | let mx = 16; // MAX TRIAL 48 | const frameId = 'vanillajs-iframe-v1' 49 | let frame = document.getElementById(frameId); 50 | let removeIframeFn = null; 51 | if (!frame) { 52 | frame = document.createElement('iframe'); 53 | frame.id = frameId; 54 | const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash 55 | frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe 56 | let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting) 57 | n.appendChild(frame); 58 | while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine 59 | const root = document.documentElement; 60 | root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL 61 | if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL)); 62 | 63 | removeIframeFn = (setTimeout) => { 64 | const removeIframeOnDocumentReady = async (e) => { 65 | e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false); 66 | e = n; 67 | n = win = removeIframeFn = 0; 68 | if (setTimeout) await new Promise(resolve => setTimeout(resolve, 200)); 69 | try { 70 | e.remove(); 71 | } catch (e) { } 72 | } 73 | if (!setTimeout || document.readyState !== 'loading') { 74 | removeIframeOnDocumentReady(); 75 | } else { 76 | win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false); 77 | } 78 | } 79 | } 80 | 81 | while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn); 82 | const fc = frame.contentWindow; 83 | if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL 84 | try { 85 | const { requestAnimationFrame, setTimeout, clearTimeout } = fc; 86 | const res = { requestAnimationFrame, setTimeout, clearTimeout }; 87 | for (let k in res) res[k] = res[k].bind(win); // necessary 88 | if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn); 89 | return res; 90 | } catch (e) { 91 | if (removeIframeFn) removeIframeFn(); 92 | return null; 93 | } 94 | } catch (e) { 95 | console.warn(e); 96 | return null; 97 | } 98 | }; 99 | 100 | cleanContext(win).then(__CONTEXT__ => { 101 | if (!__CONTEXT__) return null; 102 | 103 | const { requestAnimationFrame } = __CONTEXT__; 104 | 105 | let rafPromise = null; 106 | 107 | const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => { 108 | requestAnimationFrame(hRes => { 109 | rafPromise = null; 110 | resolve(hRes); 111 | }); 112 | })); 113 | 114 | class Mutex { 115 | 116 | constructor() { 117 | this.p = Promise.resolve() 118 | } 119 | 120 | lockWith(f) { 121 | this.p = this.p.then(() => new Promise(f).catch(console.warn)) 122 | } 123 | 124 | } 125 | 126 | let busy = false; 127 | 128 | const mutex = new Mutex(); 129 | 130 | function executor(f) { 131 | mutex.lockWith(unlock => { 132 | if (busy) { 133 | unlock(); 134 | return; 135 | } 136 | busy = true; 137 | Promise.resolve().then(() => { 138 | f(); 139 | }).then(() => { 140 | busy = false; 141 | }).then(unlock); 142 | }); 143 | } 144 | 145 | let np_ = null; 146 | try { 147 | np_ = Object.getOwnPropertyDescriptor(Node.prototype, 'parentNode').get; 148 | } catch (e) { } 149 | const np = np_ || function () { return this.parentNode }; 150 | np_ = null; 151 | 152 | const nativeContains = Node.prototype.contains; 153 | 154 | /** @param {Node} n */ 155 | const myw = new Set(); 156 | const addTheEvent = () => { 157 | document.addEventListener('DOMNodeInserted', function (e) { 158 | if (!busy) { 159 | myw.add(e.target); 160 | } 161 | }, { capture: false, passive: true }); 162 | }; 163 | 164 | let cachedTitle = null; 165 | 166 | function f77(commonParent_) { 167 | 168 | executor(() => { 169 | const node = commonParent_; 170 | if (node instanceof Node) { 171 | pangu.spacingNode(node); 172 | } 173 | }); 174 | 175 | } 176 | 177 | const delayForSiteContentReady = (location.hostname.endsWith('nga.cn') || location.pathname.includes('/code')) ? getRafPromise : () => 0; 178 | 179 | async function onReady() { 180 | window.removeEventListener("DOMContentLoaded", onReady, false); 181 | 182 | let bodyDOM = null; 183 | try { 184 | bodyDOM = document.body; 185 | let maxLoopCount = 16; 186 | while (!bodyDOM && --maxLoopCount >= 0) { 187 | await getRafPromise(); 188 | bodyDOM = document.body; 189 | } 190 | } catch (e) { 191 | bodyDOM = null; 192 | } 193 | 194 | if (!bodyDOM) return; 195 | 196 | if (await delayForSiteContentReady() !== 0) await new Promise(r => setTimeout(r, 177)); 197 | 198 | executor(() => { 199 | pangu.spacingPageTitle(); 200 | cachedTitle = document.title; 201 | pangu.spacingPageBody(); 202 | }); 203 | 204 | const config = { 205 | childList: true, 206 | subtree: true 207 | }; 208 | let observer = null; 209 | function getCommonParent(elements) { 210 | 211 | 212 | let commonParent_ = null; 213 | try { 214 | for (n of elements) { 215 | // checking of body contains 216 | // 1. complete the algo logic 217 | // 2. prevent the element is added and then removed from the DOM tree 218 | if (nativeContains.call(document.body, n)) { 219 | if (commonParent_ === null) { 220 | commonParent_ = np.call(n); 221 | // myz.contains(n) === true 222 | } else if (commonParent_ instanceof Node) { 223 | let maxLooping = 600; 224 | while (!nativeContains.call(commonParent_, n) && --maxLooping > 0) { // worst case: myz = document.body 225 | commonParent_ = np.call(commonParent_); 226 | } 227 | if (maxLooping <= 0) commonParent_ = document.body; // rare case 228 | // myz.contains(n) === true 229 | } 230 | } 231 | } 232 | } catch (e) { commonParent_ = null; } 233 | return commonParent_; 234 | } 235 | const callback = async () => { 236 | let elements = null; 237 | if (myw.size > 0) { 238 | elements = [...myw]; 239 | myw.clear(); 240 | } 241 | Promise.resolve().then(() => { 242 | let cachedTitle_ = document.title; 243 | if (cachedTitle !== cachedTitle_) { 244 | cachedTitle = cachedTitle_; 245 | pangu.spacingPageTitle(); 246 | cachedTitle = document.title; 247 | } 248 | }); 249 | let tmp = false; 250 | try { 251 | if (elements) { 252 | const commonParent = await Promise.resolve(elements).then(getCommonParent) 253 | if (commonParent instanceof Node) f77(commonParent); 254 | } 255 | if (!observer) return; 256 | tmp = document.body; 257 | } catch (e) { 258 | } 259 | if (tmp != bodyDOM) { 260 | observer.takeRecords(); 261 | observer.disconnect(); 262 | if (tmp === false) { 263 | // Facebook - cross-frame error 264 | observer = null; 265 | bodyDOM = null; 266 | } else { 267 | bodyDOM = tmp; 268 | if (bodyDOM) { 269 | observer.observe(bodyDOM, config); 270 | callback(); 271 | } 272 | } 273 | } 274 | 275 | }; 276 | if (nativeContains && np) { 277 | 278 | addTheEvent(); 279 | observer = new MutationObserver(callback); 280 | observer.observe(bodyDOM, config); 281 | } 282 | // callback(); 283 | 284 | } 285 | 286 | 287 | 288 | Promise.resolve().then(() => { 289 | 290 | if (document.readyState !== 'loading') { 291 | onReady(); 292 | } else { 293 | window.addEventListener("DOMContentLoaded", onReady, false); 294 | } 295 | 296 | }); 297 | 298 | }); 299 | 300 | 301 | })({ pangu }); 302 | -------------------------------------------------------------------------------- /471018.user.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright 2023 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | // ==UserScript== 27 | // @name YouTube Native - Vanilla Engine 28 | // @namespace UserScript 29 | // @match https://www.youtube.com/* 30 | // @grant none 31 | // @version 0.1.19 32 | // @license MIT License 33 | // @author CY Fung 34 | // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/yt-engine.png 35 | // @run-at document-start 36 | // @unwrap 37 | // @inject-into page 38 | // @allFrames true 39 | // @description (YouTube Experimental) Disable the YouTube engine hacks and just use the native APIs 40 | // ==/UserScript== 41 | 42 | ((__CONTEXT01__) => { 43 | 44 | 45 | const win = this instanceof Window ? this : window; 46 | 47 | // Create a unique key for the script and check if it is already running 48 | const hkey_script = 'ikkaorpwuzvt'; 49 | if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting 50 | win[hkey_script] = true; 51 | 52 | /** @type {globalThis.PromiseConstructor} */ 53 | const Promise = ((async () => { })()).constructor; 54 | 55 | const cleanContext = async (win) => { 56 | 57 | 58 | const waitFn = requestAnimationFrame; // shall have been binded to window 59 | try { 60 | let mx = 16; // MAX TRIAL 61 | const frameId = 'vanillajs-iframe-v1' 62 | let frame = document.getElementById(frameId); 63 | let removeIframeFn = null; 64 | if (!frame) { 65 | frame = document.createElement('iframe'); 66 | frame.id = frameId; 67 | const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash 68 | frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe 69 | let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting) 70 | n.appendChild(frame); 71 | while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine 72 | const root = document.documentElement; 73 | root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL 74 | if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL)); 75 | 76 | removeIframeFn = (setTimeout) => { 77 | const removeIframeOnDocumentReady = (e) => { 78 | e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false); 79 | e = n; 80 | n = win = removeIframeFn = 0; 81 | setTimeout ? setTimeout(() => e.remove(), 200) : e.remove(); 82 | } 83 | if (!setTimeout || document.readyState !== 'loading') { 84 | removeIframeOnDocumentReady(); 85 | } else { 86 | win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false); 87 | } 88 | } 89 | } 90 | while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn); 91 | const fc = frame.contentWindow; 92 | if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL 93 | try { 94 | const { requestAnimationFrame, cancelAnimationFrame, getComputedStyle, setInterval, clearInterval, setTimeout, clearTimeout } = fc; 95 | const res = { requestAnimationFrame, cancelAnimationFrame, getComputedStyle, setInterval, clearInterval, setTimeout, clearTimeout }; 96 | for (let k in res) res[k] = res[k].bind(win); // necessary 97 | res.animate = fc.Element.prototype.animate; 98 | if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn); 99 | return res; 100 | } catch (e) { 101 | if (removeIframeFn) removeIframeFn(); 102 | return null; 103 | } 104 | } catch (e) { 105 | console.warn(e); 106 | return null; 107 | } 108 | }; 109 | 110 | cleanContext(win).then(__CONTEXT02__ => { 111 | 112 | const { requestAnimationFrame, cancelAnimationFrame, getComputedStyle, setInterval, clearInterval, setTimeout, clearTimeout } = __CONTEXT02__; 113 | 114 | const { animate } = __CONTEXT02__; 115 | 116 | const { frames, defineProperty, window, CDATASection, ProcessingInstruction, FocusEvent } = __CONTEXT01__; 117 | 118 | const ENABLE_NATIVE_CONSTRUCTOR_CHECK = false; 119 | let cids = {}; 120 | function cleanCId(k) { 121 | Promise.resolve().then(() => clearInterval(cids[k])); 122 | } 123 | 124 | 125 | Object.defineProperty = function (o, p, opts) { 126 | 127 | if (arguments.length !== 3) return defineProperty.apply(this, arguments); 128 | 129 | if (o instanceof Window) { 130 | if (p === 'getComputedStyle') return; 131 | if (p === 'Promise' && (p in o)) return; // WaterFox Classic 132 | if (p === 'customElements' || p === 'Polymer') { 133 | if (p in o) return; // duplicate declaration? 134 | } 135 | const value = opts.value; 136 | if (value) { 137 | opts.writable = true; 138 | opts.configurable = true; 139 | opts.enumerable = true; 140 | } 141 | if (p === 'ytInitialPlayerResponse' || p === 'playerResponse') { 142 | // Firefox Chatroom? TBC 143 | } else { 144 | console.log(923, 'window[p]=', p, opts); 145 | } 146 | return defineProperty.call(this, o, p, opts); 147 | } 148 | 149 | 150 | const nativeConstructorCheck = ENABLE_NATIVE_CONSTRUCTOR_CHECK ? `${o.constructor}`.indexOf('native code') > 0 : true; 151 | 152 | if (p.startsWith('__shady_')) { 153 | 154 | const { get, value } = opts; 155 | if (!get) { 156 | o[p] = value; 157 | return; 158 | } 159 | 160 | if (p === '__shady_native_eventPhase') { 161 | // Event -> __shady_native_eventPhase 162 | return defineProperty.call(this, o, p, opts); 163 | } 164 | 165 | let constructor = o instanceof Node ? Node : o instanceof DocumentFragment ? DocumentFragment : o instanceof Document ? Document : null; 166 | 167 | if (!constructor) { 168 | let constructorName = (o.constructor || 0).name; 169 | if (constructorName === 'Node') { 170 | constructor = Node; 171 | } 172 | } 173 | 174 | if (constructor && opts && (typeof opts.get === 'function')) { 175 | 176 | if (!(p in o.constructor.prototype) && !(p in o)) { 177 | defineProperty.call(this, o.constructor.prototype, p, opts); 178 | } 179 | return; 180 | 181 | } 182 | console.log(926, o, p, opts, !!constructor, !!opts, !!(typeof opts.get === 'function')) 183 | // return; 184 | } 185 | 186 | if ((p in o) && nativeConstructorCheck) { 187 | if (o instanceof Text) return; 188 | if (o instanceof Comment) return; 189 | if (CDATASection && o instanceof CDATASection) return; 190 | if (ProcessingInstruction && o instanceof ProcessingInstruction) return; 191 | if (o instanceof Event) return; 192 | if (FocusEvent && o instanceof FocusEvent) return; 193 | } 194 | 195 | return defineProperty.call(this, o, p, opts); 196 | } 197 | 198 | const asserter = (f) => Promise.resolve().then(() => console.assert(f(), `${f}`)); 199 | 200 | const setVJS = () => { 201 | if (window.Promise !== Promise) window.Promise = Promise; 202 | if (window.getComputedStyle !== getComputedStyle) window.getComputedStyle = getComputedStyle; 203 | if (Element.prototype.animate !== animate) Element.prototype.animate = animate; 204 | if (window.requestAnimationFrame !== requestAnimationFrame) window.requestAnimationFrame = requestAnimationFrame 205 | if (window.cancelAnimationFrame !== cancelAnimationFrame) window.cancelAnimationFrame = cancelAnimationFrame 206 | }; 207 | 208 | const finishFn = () => { 209 | cids.finish = 0; 210 | setVJS(); 211 | try { 212 | document.getElementById('zihrS').remove(); 213 | } catch (e) { } 214 | cleanCId('timeVJS'); 215 | if (document.isConnected === false) return; 216 | setTimeout(() => { 217 | if (document.isConnected === false) return; 218 | asserter(() => window.Promise === Promise); 219 | asserter(() => window.getComputedStyle === getComputedStyle); 220 | asserter(() => Element.prototype.animate === animate); 221 | asserter(() => window.requestAnimationFrame === requestAnimationFrame); 222 | asserter(() => window.cancelAnimationFrame === cancelAnimationFrame); 223 | 224 | }, 800); 225 | 226 | }; 227 | 228 | function fastenFinishFn() { 229 | if (cids.finish > 0) { 230 | clearInterval(cids.finish); 231 | cids.finish = setTimeout(finishFn, 40); 232 | } 233 | } 234 | 235 | function preFinishFn() { 236 | let mo = new MutationObserver(function () { 237 | Promise.resolve().then(fastenFinishFn) 238 | mo.disconnect(); 239 | mo.takeRecords(); 240 | mo = null; 241 | }); 242 | mo.observe(document, { subtree: true, childList: true }); 243 | return setTimeout(finishFn, 400); 244 | } 245 | 246 | cids.timeVJS = setInterval(() => { 247 | if (!cids.finish && ('Polymer' in window)) cids.finish = preFinishFn(); 248 | setVJS(); 249 | }, 1); 250 | 251 | 252 | let isInnerFrame = false; 253 | try { 254 | isInnerFrame = window !== top && window.document.domain === top.document.domain; 255 | } catch (e) { } 256 | 257 | if (!isInnerFrame) { 258 | 259 | console.groupCollapsed( 260 | "%cYouTube Native - Vanilla Engine (Experimental)", 261 | "background-color: #e0005a ; color: #ffffff ; font-weight: bold ; padding: 4px ;" 262 | ); 263 | 264 | console.log("Script is loaded."); 265 | console.log("This is an experimental script."); 266 | console.log("If you found any issue in using YouTube, please disable this script to check whether the issue is due to this script or not."); 267 | 268 | console.groupEnd(); 269 | 270 | } 271 | 272 | 273 | }); 274 | 275 | })({ 276 | frames, 277 | defineProperty: Object.defineProperty, 278 | window, 279 | CDATASection, ProcessingInstruction, FocusEvent 280 | }); 281 | -------------------------------------------------------------------------------- /471033-sticky-memory.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube: Force html5_exponential_memory_for_sticky 3 | // @namespace Violentmonkey Scripts 4 | // @match https://www.youtube.com/* 5 | // @version 0.5.8 6 | // @license MIT 7 | // @author CY Fung 8 | // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/yt-engine.png 9 | // @description To prevent YouTube to change the video quality automatically during YouTube Live Streaming. 10 | // @run-at document-start 11 | // @grant none 12 | // @unwrap 13 | // @allFrames true 14 | // @inject-into page 15 | // @require https://update.greasyfork.org/scripts/475632/1361351/ytConfigHacks.js 16 | // ==/UserScript== 17 | 18 | // html5_exponential_memory_for_sticky 19 | /* "YouTube to change the video quality automatically during YouTube Live Streaming" refers to the following code: 20 | 21 | k_=function(a){if(a.Tf){var b=a.Zi;var c=a.Tf;a=a.Dv();if(b.va.qt().isInline())var d=gO;else b.N("html5_exponential_memory_for_sticky")?d=.5>Jwa(b.Z.Wf,"sticky-lifetime")?"auto":TH[xL()]:d=TH[xL()],d=g.RH("auto",d,!1,"s");if(SH(d)){d=n_a(b,c);var e=d.compose,f;a:if((f=c.j)&&f.videoInfos.length){for(var h=g.u(f.videoInfos),l=h.next();!l.done;l=h.next()){l=l.value;var m=void 0;if(null==(m=l.u)?0:m.smooth){f=l.video.j;break a}}f=f.videoInfos[0].video.j}else f=0;Tma()&&!g.MM(b.Z)&&hI(c.j.videoInfos[0])&& 22 | (f=Math.min(f,g.QH.large));d=e.call(d,new PH(0,f,!1,"o"));e=d.compose;f=4320;!b.Z.u||g.FM(b.Z)||b.Z.N("hls_for_vod")||b.Z.N("mweb_remove_360p_cap")||(f=g.QH.medium);(h=g.AL(b.Z.experiments,"html5_default_quality_cap"))&&c.j.j&&!c.videoData.aj&&!c.videoData.me&&(f=Math.min(f,h));h=g.AL(b.Z.experiments,"html5_random_playback_cap");l=/[a-h]$/;h&&l.test(c.videoData.clientPlaybackNonce)&&(f=Math.min(f,h));if(l=h=g.AL(b.Z.experiments,"html5_hfr_quality_cap"))a:{l=c.j;if(l.j)for(l=g.u(l.videoInfos),m=l.next();!m.done;m= 23 | l.next())if(32 { 29 | 30 | const win = this instanceof Window ? this : window; 31 | 32 | // Create a unique key for the script and check if it is already running 33 | const hkey_script = 'ezinmgkfbpgh'; 34 | if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting 35 | win[hkey_script] = true; 36 | 37 | /** @type {globalThis.PromiseConstructor} */ 38 | const Promise = ((async () => { })()).constructor; 39 | 40 | let isMainWindow = false; 41 | try { 42 | isMainWindow = window.document === window.top.document 43 | } catch (e) { } 44 | 45 | window._ytConfigHacks.add((config_) => { 46 | 47 | let obj = null; 48 | try { 49 | obj = config_.WEB_PLAYER_CONTEXT_CONFIGS.WEB_PLAYER_CONTEXT_CONFIG_ID_KEVLAR_WATCH; 50 | } catch (e) { } 51 | 52 | if (obj) { 53 | 54 | const sflags = obj.serializedExperimentFlags 55 | if (typeof sflags === 'string') { 56 | if (sflags.includes('&h5_expr_b9Nkc=true')) return; 57 | let s = sflags.replace(/(^|&)(html5_exponential_memory_for_sticky|html5_perf_cap_override_sticky|html5_ustreamer_cap_override_sticky|html5_always_apply_default_quality_cap|html5_apply_pbr_cap_for_drm|manifestless_post_live_ufph|manifestless_post_live|hls_for_vod|mweb_remove_360p_cap)=\w+/, '') + '&html5_exponential_memory_for_sticky=true&h5_expr_b9Nkc=true'; 58 | obj.serializedExperimentFlags = s.charCodeAt(0) === 38 ? s.substring(1) : s; 59 | } 60 | 61 | } 62 | 63 | }); 64 | 65 | })(); 66 | -------------------------------------------------------------------------------- /473130-ytdls.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YtDLS: YouTube Dual Language Subtitle (Modified) 3 | // @name:zh-CN YtDLS: Youtube 双语字幕(改) 4 | // @name:zh-TW YtDLS: Youtube 雙語字幕(改) 5 | // @version 2.1.4 6 | // @description Enhances YouTube with dual language subtitles. 7 | // @description:zh-CN 为YouTube添加双语字幕增强功能。 8 | // @description:zh-TW 增強YouTube的雙語字幕功能。 9 | // @author CY Fung 10 | // @author Coink Wang 11 | // @match https://www.youtube.com/* 12 | // @match https://m.youtube.com/* 13 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini|webp|webm)[^\/]*$/ 14 | // @exclude /^https?://\S+_live_chat*$/ 15 | // @require https://cdn.jsdelivr.net/gh/culefa/xhProxy@eaa2e84b40290fc63af1ca777f3f545008bf79bb/dist/xhProxy.min.js 16 | // @grant none 17 | // @inject-into page 18 | // @allFrames true 19 | // @run-at document-start 20 | // @namespace Y2BDoubleSubs 21 | // @license MIT 22 | // @supportURL https://github.com/cyfung1031/Y2BDoubleSubs/tree/translation-api 23 | // ==/UserScript== 24 | 25 | /* global xhProxy */ 26 | 27 | /* 28 | 29 | original script: https://greasyfork.org/scripts/397363 30 | based on v1.8.0 + PR#18 ( https://github.com/CoinkWang/Y2BDoubleSubs/pull/18 ) [v2.0.0] 31 | added m.youtube.com support based on two scripts (https://greasyfork.org/scripts/457476 & https://greasyfork.org/scripts/464879 ) which are fork from v1.8.0 32 | 33 | */ 34 | 35 | 36 | 37 | 38 | (() => { 39 | 40 | let localeLangFn = () => document.documentElement.lang || navigator.language || 'en' // follow the language used in YouTube Page 41 | // localeLangFn = () => 'zh' // uncomment this line to define the language you wish here 42 | 43 | function isValidForHook() { 44 | try { 45 | if (location.pathname === '/live_chat' || location.pathname === '/live_chat_replay') return false; 46 | return true; 47 | } catch (e) { 48 | return false; 49 | } 50 | } 51 | if (!isValidForHook()) return; 52 | 53 | const Promise = (async () => { })().constructor; 54 | const fetch = window.fetch.bind(window) 55 | 56 | let enableFullWidthSpaceSeparation = true 57 | function encodeFullwidthSpace(text) { 58 | if (!enableFullWidthSpaceSeparation) return text 59 | return text.replace(/\n/g, '\n®\n').replace(/\u3000/g, '\n©\n') 60 | } 61 | function decodeFullwidthSpace(text) { 62 | if (!enableFullWidthSpaceSeparation) return text 63 | return text.replace(/\n©\n/g, '\u3000').replace(/\n®\n/g, '\n') 64 | } 65 | let requestDeferred = Promise.resolve(); 66 | 67 | const inPlaceArrayPush = (() => { 68 | // for details, see userscript-supports/library/misc.js 69 | const LIMIT_N = typeof AbortSignal !== 'undefined' && typeof (AbortSignal||0).timeout === 'function' ? 50000 : 10000; 70 | return function (dest, source) { 71 | let index = 0; 72 | const len = source.length; 73 | while (index < len) { 74 | let chunkSize = len - index; // chunkSize > 0 75 | if (chunkSize > LIMIT_N) { 76 | chunkSize = LIMIT_N; 77 | dest.push(...source.slice(index, index + chunkSize)); 78 | } else if (index > 0) { // to the end 79 | dest.push(...source.slice(index)); 80 | } else { // normal push.apply 81 | dest.push(...source); 82 | } 83 | index += chunkSize; 84 | } 85 | } 86 | 87 | })(); 88 | 89 | xhProxy.hook({ 90 | onConfig(xhr, config) { 91 | 92 | const originalReqUrl = config.url; 93 | 94 | if (typeof ytcfg !== 'object' || !originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')){ 95 | this.byPassRequest = true; 96 | 97 | } 98 | 99 | 100 | // config.byPassRequest = true; 101 | 102 | 103 | 104 | // console.log(xhr, config) 105 | 106 | }, 107 | onRequest(xhr, config) { 108 | 109 | // console.log(xhr, config) 110 | }, 111 | async onResponse(xhr, config) { 112 | 113 | const o = {} 114 | try { 115 | 116 | const originalReqUrl = config.url; 117 | if (!originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')) return; 118 | 119 | if (typeof ytcfg !== 'object') return; // not a valid youtube page 120 | let defaultJson = null 121 | const jsonResponse = xhr.xhJson; 122 | if (jsonResponse && jsonResponse.events) defaultJson = jsonResponse; 123 | if (defaultJson === null) return; 124 | const localeLang = localeLangFn() 125 | const langIdx = originalReqUrl.indexOf('lang=') 126 | if (langIdx > 5) { 127 | 128 | // &key=yt8&lang=en&fmt=json3&xorb=2&xobt=3&xovt=3 129 | // &key=yt8&lang=ja&fmt=json3&xorb=2&xobt=3&xovt=3 130 | // &key=yt8&lang=ja&name=Romaji&fmt=json3&xorb=2&xobt=3 131 | 132 | let ulc = originalReqUrl.charAt(langIdx - 1) 133 | if (ulc === '?' || ulc === '&') { 134 | let usp = new URLSearchParams(originalReqUrl.substring(langIdx)) 135 | let uspLang = usp.get('lang') 136 | let uspName = usp.get('name') 137 | if (uspName === 'Romaji') return defaultAction() 138 | if (typeof uspLang === 'string' && uspLang.toLocaleLowerCase() === localeLang.toLocaleLowerCase()) return; 139 | } 140 | 141 | } 142 | 143 | const lines = [] 144 | for (const event of defaultJson.events) { 145 | for (const seg of event.segs) { 146 | if (seg && typeof seg.utf8 === 'string') { 147 | inPlaceArrayPush(lines, seg.utf8.split('\n')); 148 | } 149 | } 150 | } 151 | if (lines.length === 0) return defaultAction() 152 | let linesText = lines.join('\n') 153 | linesText = encodeFullwidthSpace(linesText) 154 | const q = encodeURIComponent(linesText) 155 | o.defaultJson = defaultJson 156 | o.lines = lines 157 | o.requestURL = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${localeLang}&dj=1&dt=t&dt=rm&q=${q}` 158 | 159 | 160 | } catch (e) { 161 | 162 | console.warn(e) 163 | return; 164 | } 165 | 166 | 167 | return new Promise(xhrResolve => { 168 | 169 | 170 | function fetchData() { 171 | return new Promise(requestDeferredResolve => { 172 | 173 | fetch(o.requestURL, { 174 | method: "GET", 175 | headers: { 176 | "Accept": "application/json", 177 | "Accept-Encoding": "gzip, deflate, br" 178 | }, 179 | credentials: "omit", 180 | referrerPolicy: "no-referrer", 181 | redirect: "error", 182 | keepalive: false, 183 | cache: "default" 184 | }) 185 | .then(res => { 186 | requestDeferredResolve() 187 | return res.json() 188 | }) 189 | .then(result => { 190 | let resultText = result.sentences.map((function (s) { 191 | return "trans" in s ? s.trans : "" 192 | })).join("") 193 | resultText = decodeFullwidthSpace(resultText) 194 | return resultText.split("\n") 195 | }) 196 | .then(translatedLines => { 197 | const { lines, defaultJson } = o 198 | o.lines = null 199 | o.defaultJson = null 200 | const addTranslation = (line, idx) => { 201 | if (line !== lines[i + idx]) return line 202 | let translated = translatedLines[i + idx] 203 | if (line === translated) return line 204 | return `${line}\n${translated}` 205 | } 206 | let i = 0 207 | for (const event of defaultJson.events) { 208 | for (const seg of event.segs) { 209 | if (seg && typeof seg.utf8 === 'string') { 210 | let s = seg.utf8.split('\n') 211 | let st = s.map(addTranslation) 212 | seg.utf8 = st.join('\n') 213 | i += s.length 214 | } 215 | } 216 | } 217 | xhr.xhJson = defaultJson; 218 | 219 | 220 | xhrResolve() 221 | }).catch(e => { 222 | console.warn(e) 223 | xhrResolve() 224 | }) 225 | 226 | }) 227 | 228 | } 229 | 230 | requestDeferred = requestDeferred.then(fetchData) 231 | 232 | 233 | }) 234 | 235 | 236 | 237 | } 238 | }) 239 | 240 | })(); -------------------------------------------------------------------------------- /474214-fix-brave-yt-live-1.8.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Fix Brave Bug for YouTube Live Chat 3 | // @namespace UserScripts 4 | // @version 1.8 5 | // @description To Fix Brave Bug for YouTube Live Chat 6 | // @author CY Fung 7 | // @license MIT 8 | // @icon https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@main/icons/brave.png 9 | // @match https://www.youtube.com/* 10 | // @grant none 11 | // @run-at document-start 12 | // @unwrap 13 | // @inject-into page 14 | // ==/UserScript== 15 | 16 | (async () => { 17 | 'use strict'; 18 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 19 | 20 | await customElements.whenDefined('ytd-live-chat-frame'); 21 | 22 | const chat = document.getElementById('chat') || (await new Promise(resolve => { 23 | let mo = new MutationObserver(entries => { 24 | const chat = document.getElementById('chat'); 25 | if (chat && mo) { 26 | mo.disconnect(); 27 | mo.takeRecords(); 28 | mo = null; 29 | resolve(chat); 30 | } 31 | }); 32 | mo.observe(document, { childList: true, subtree: true }) 33 | })); 34 | 35 | if (!chat || chat.is !== 'ytd-live-chat-frame') return; 36 | 37 | const chatWR = new WeakRef(chat); 38 | 39 | /** @param {HTMLIFrameElement} chatframe */ 40 | const onChatFrameFound = (chatframe) => { 41 | try { 42 | const body = chatframe.contentDocument.body; 43 | let io = new IntersectionObserver(function () { 44 | if (io) { 45 | io.disconnect(); 46 | io.takeRecords(); 47 | io = null; 48 | const chat = chatWR.deref(); 49 | if (chat) { 50 | const frameLocation = chatframe.contentWindow.location; 51 | let src = chatframe.src || ''; 52 | if (src === '' || src === 'about:blank') { 53 | const cnt = insp(chat); 54 | src = (cnt.url || ''); 55 | if (!src.includes('/live_chat')) src = cnt.liveChatPageUrl(cnt.baseUrl, cnt.collapsed, cnt.data, cnt.forceDarkTheme); 56 | } 57 | if (body.firstChild === null && src.includes('/live_chat') && frameLocation.href === 'about:blank') { 58 | frameLocation.replace(src.replace(/&\d+$/, '') + "&1"); 59 | } 60 | } 61 | chatframe = null; 62 | } 63 | }); 64 | io.observe(body); 65 | } catch (e) { 66 | console.warn(e); 67 | } 68 | } 69 | 70 | const f = () => { 71 | const chatframe = ((insp(chat).$ || chat.$ || chat).chatframe || 0); 72 | if (chatframe instanceof HTMLIFrameElement && chatframe.isConnected === true) { 73 | if (!chatframe.__b375__) { 74 | chatframe.__b375__ = 1; 75 | Promise.resolve(chatframe).then(onChatFrameFound); 76 | } 77 | } 78 | } 79 | 80 | const mo = new MutationObserver(f); 81 | 82 | mo.observe(chat, { 83 | attributes: true, 84 | attributeFilter: ['collapsed', 'hidden'] 85 | }); 86 | 87 | f(); 88 | })(); -------------------------------------------------------------------------------- /474214-fix-brave-yt-live-2.0.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Fix Brave Bug for YouTube Live Chat 3 | // @namespace UserScripts 4 | // @version 2.0 5 | // @description To Fix Brave Bug for YouTube Live Chat 6 | // @author CY Fung 7 | // @license MIT 8 | // @icon https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@main/icons/brave.png 9 | // @match https://www.youtube.com/* 10 | // @grant none 11 | // @run-at document-start 12 | // @unwrap 13 | // @inject-into page 14 | // ==/UserScript== 15 | 16 | (() => { 17 | 18 | const setTimeout = window.setTimeout.bind(typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); 19 | const delayPn = delay => new Promise((fn => setTimeout(fn, delay))); 20 | 21 | /** @type {globalThis.PromiseConstructor} */ 22 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 23 | 24 | const PromiseExternal = ((resolve_, reject_) => { 25 | const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject }; 26 | return class PromiseExternal extends Promise { 27 | constructor(cb = h) { 28 | super(cb); 29 | if (cb === h) { 30 | /** @type {(value: any) => void} */ 31 | this.resolve = resolve_; 32 | /** @type {(reason?: any) => void} */ 33 | this.reject = reject_; 34 | } 35 | } 36 | }; 37 | })(); 38 | 39 | 40 | let um = null; 41 | let ur = null; 42 | 43 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 44 | 45 | document.addEventListener('load', function (evt) { 46 | const target = (evt || 0).target; 47 | if (target instanceof HTMLIFrameElement) { 48 | if (target.id === 'chatframe') { 49 | um && um.resolve(); 50 | um = null; 51 | } 52 | } 53 | }, true); 54 | 55 | 56 | (async () => { 57 | 'use strict'; 58 | 59 | await customElements.whenDefined('ytd-live-chat-frame'); 60 | 61 | const chat = document.createElement('ytd-live-chat-frame'); 62 | 63 | if (!chat || chat.is !== 'ytd-live-chat-frame') return; 64 | 65 | const cnt = insp(chat); 66 | 67 | const liveChatPageUrl66 = cnt.__proto__.liveChatPageUrl; 68 | cnt.__proto__.liveChatPageUrl = function (baseUrl, collapsed, data, forceDarkTheme) { 69 | 70 | let r = liveChatPageUrl66.apply(this, arguments); 71 | if (r !== ur && typeof r === 'string') { 72 | ur = r; 73 | if (!um) um = new PromiseExternal(); 74 | } 75 | 76 | return r; 77 | } 78 | 79 | 80 | const urlChanged66 = cnt.__proto__.urlChanged; 81 | 82 | cnt.__proto__.urlChanged = function () { 83 | 84 | const url = this.url; 85 | const pm = (typeof url === 'string' && url === ur) ? um : null; 86 | (pm ? Promise.race([pm, delayPn(1800)]) : Promise.resolve()).then(() => { 87 | urlChanged66.apply(this, arguments) 88 | }); 89 | } 90 | 91 | })(); 92 | })(); 93 | -------------------------------------------------------------------------------- /474214-fix-brave-yt-live-3.0.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Fix Brave Bug for YouTube Live Chat 3 | // @namespace UserScripts 4 | // @version 3.38 5 | // @description To Fix Brave Bug for YouTube Live Chat 6 | // @author CY Fung 7 | // @license MIT 8 | // @icon https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@main/icons/brave.png 9 | // @match https://www.youtube.com/* 10 | // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@c2b707e4977f77792042d4a5015fb188aae4772e/library/nextBrowserTick.min.js 11 | // @grant none 12 | // @run-at document-start 13 | // @unwrap 14 | // @inject-into page 15 | // ==/UserScript== 16 | 17 | (async () => { 18 | 'use strict'; 19 | 20 | /** @type {globalThis.PromiseConstructor} */ 21 | const Promise = (async () => { })().constructor; 22 | 23 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 24 | 25 | const setTimeout_ = setTimeout.bind(window); 26 | 27 | await customElements.whenDefined('ytd-live-chat-frame'); 28 | 29 | const chat = document.querySelector('ytd-live-chat-frame') || document.createElement('ytd-live-chat-frame'); 30 | 31 | if (!chat || chat.is !== 'ytd-live-chat-frame') return; 32 | 33 | const cnt = insp(chat); 34 | const cProto = cnt.constructor.prototype || 0; 35 | 36 | if (typeof cProto.urlChanged === 'function' && !cProto.urlChanged66 && !cProto.urlChangedAsync12 && cProto.urlChanged.length === 0) { 37 | cProto.urlChanged66 = cProto.urlChanged; 38 | let ath = 0; 39 | cProto.urlChangedAsync12 = async function () { 40 | await this.__urlChangedAsyncT689__; 41 | const t = ath = (ath & 1073741823) + 1; 42 | const chatframe = this.chatframe || (this.$ || 0).chatframe || 0; 43 | if (chatframe instanceof HTMLIFrameElement) { 44 | if (chatframe.contentDocument === null) { 45 | await Promise.resolve('#').catch(console.warn); 46 | if (t !== ath) return; 47 | } 48 | await new Promise(resolve => setTimeout_(resolve, 1)).catch(console.warn); // neccessary for Brave 49 | if (t !== ath) return; 50 | const isBlankPage = !this.data || this.collapsed; 51 | const p1 = new Promise(resolve => setTimeout_(resolve, 706)).catch(console.warn); 52 | const p2 = new Promise(resolve => { 53 | (new IntersectionObserver((entries, observer) => { 54 | for (const entry of entries) { 55 | const rect = entry.boundingClientRect || 0; 56 | if (isBlankPage || (rect.width > 0 && rect.height > 0)) { 57 | observer.disconnect(); 58 | resolve('#'); 59 | break; 60 | } 61 | } 62 | })).observe(chatframe); 63 | }).catch(console.warn); 64 | await Promise.race([p1, p2]); 65 | if (t !== ath) return; 66 | } 67 | this.urlChanged66(); 68 | } 69 | cProto.urlChanged = function () { 70 | const t = this.__urlChangedAsyncT688__ = (this.__urlChangedAsyncT688__ & 1073741823) + 1; 71 | nextBrowserTick(() => { 72 | if (t !== this.__urlChangedAsyncT688__) return; 73 | this.urlChangedAsync12(); 74 | }); 75 | } 76 | } 77 | 78 | })(); 79 | -------------------------------------------------------------------------------- /Info-468740.md: -------------------------------------------------------------------------------- 1 | * Markdown updated on: 2023.06.28 2 | * Userscript: [Restore YouTube Username from Handle to Custom](https://greasyfork.org/en/scripts/468740-restore-youtube-username-from-handle-to-custom) 3 | * License: MIT License 4 | * Similar Userscript / Extension: 5 | 1. [yakisova41's Return YouTube Comment Username](https://github.com/yakisova41/return-youtube-comment-username) 6 | 7 | * This userscript is independently developed by CY Fung using another implementation to do the similar feature. 8 | * This userscript supports Desktop & Mobile YouTube but Studio and Kids are excluded. 9 | 10 | -------------------- 11 | 12 | * *Similar Feature as [Return YouTube Comment Username](https://greasyfork.org/en/scripts/460361-return-youtube-comment-username) [aka [YouTubeコメント欄の名前を元に戻す](https://greasyfork.org/ja/scripts/460361-return-youtube-comment-username)], but completely different implementation* 13 | 14 | * *Full Compatible with ALL UserScripts, Plugins, and Extensions* 15 | 16 | * *Full Compatible with Tampermonkey, Violentmonkey, FireMonkey* 17 | 18 | * *Support Mentions inside Comments* 19 | 20 | * *Support Mobile Layout (YouTube Mobile: m.youtube.com) since v0.5.0* 21 | 22 | * Recommend for Android: Firefox + Tampermonkey 23 | 24 | 25 | 26 | ### Minimum Browser Versions: 27 | 28 | 29 | 30 | ### Android: 31 | 32 | [Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox) + [Tampermonkey](https://na.cx/i/4qzDUiG.png) 33 | 34 | ### IOS: 35 | 36 | [Stay](https://apps.apple.com/app/id1591620171) * Not yet tested for compatibility 37 | 38 | # Restore YouTube Username from Handle to Custom 39 | 40 | This user script, named "Restore YouTube Username from Handle to Custom," is designed to restore the traditional custom name on YouTube. It aims to replace the handle-based usernames with the custom usernames that were previously used on the platform. 41 | 42 | ## Description 43 | 44 | The script utilizes the Tampermonkey extension and runs on the YouTube website. It fetches the necessary data to restore the custom username and replaces the handle-based usernames displayed on the page. 45 | 46 | ## Author 47 | 48 | This user script is developed by CY Fung. 49 | 50 | ## Compatibility 51 | 52 | The script is compatible with the YouTube websites (https://www.youtube.com/* & https://m.youtube.com/*). 53 | * www.youtube.com : OK 54 | * m.youtube.com : OK (>=v0.5.0) 55 | * youtube.com : NG 56 | * studio.youtube.com : NG 57 | 58 | ## Functionality 59 | 60 | The script works by making requests to the YouTube API to retrieve the necessary data for each channel. It fetches the channel's metadata, including the custom username, and replaces the handle-based username displayed on the page. 61 | 62 | 63 | 64 | **Important:** Before installing any user script, ensure that you review the script's source and verify its authenticity to ensure your safety and security. 65 | 66 | [Example 1](https://www.youtube.com/watch?v=Yo83M-KOc7k) 67 | ![img](https://na.cx/i/bp81ktL.png) 68 | 69 | [Example 2](https://www.youtube.com/watch?v=dYzgZVhiqmU) 70 | ![img](https://na.cx/i/2aVWeLk.png) 71 | 72 | 73 | [Example 3 (Mobile)](https://www.youtube.com/watch?v=IKKar5SS29E) 74 | ![img](https://na.cx/i/404gyCz.png) 75 | 76 | [Example 4 (Mobile)](https://www.youtube.com/watch?v=IKKar5SS29E) 77 | ![img](https://na.cx/i/6mrOpKh.png) 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 cyfung1031 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 | -------------------------------------------------------------------------------- /UseCoderInVM.md: -------------------------------------------------------------------------------- 1 | * Use them in chrome-extension://jinjaccalgkegednnccohejagnlnfdag/options/index.html (Editor Mode) 2 | 3 | ## Setup 4 | 5 | 1. Open Dev Tools (Meta+Opt+I in Mac) 6 | 2. Sources -> Snippet -> Add a new Snippet 7 | 3. Copy and Paste 8 | ```js 9 | javascript:((script,ci,b,bu)=>{(ci='4472e5a6f788cf6e6460624269160ce50b43e9ff')&&fetch(`https://raw.githubusercontent.com/cyfung1031/userscript-supports/${ci}/tools/coder.js`).then(r=>r.text()).then(t=>[(b=new Blob([t],{type:'text/javascript; charset=UTF-8'})),(bu=URL.createObjectURL(b)),(script.src=bu),document.head.appendChild(script)]&&new Promise(r=>script.onload=r)).then(k=>URL.revokeObjectURL(bu)).then(e=>console.log('JS Injected'))})(document.createElement('script')); 10 | ``` 11 | 4. Save with name "0" 12 | 13 | 14 | ![img](https://na.cx/i/udzA65H.png) 15 | 16 | ## Usage 17 | 18 | 1. Open Dev Tools (Meta+Opt+I in Mac) 19 | 2. CMD+O 20 | 3. Type "0" and Enter 21 | 4. Ctrl+Enter to execute 22 | -------------------------------------------------------------------------------- /UserScript.md: -------------------------------------------------------------------------------- 1 | # UserScript 2 | 3 | ## UserScript Manager 4 | 5 | ### Violentmonkey (v2.15.4 or above) 6 | * Can be used in Chrome, Brave, Edge, Firefox, Opera, etc. 7 | * Open source 8 | * GitHub: https://github.com/violentmonkey/violentmonkey/ (Actively Maintained) 9 | * Highly Recommended 10 | * (Be aware of the [WTF behavior](https://github.com/violentmonkey/violentmonkey/issues/1901) - see https://github.com/violentmonkey/violentmonkey/issues/1023) 11 | * 85% userscripts (for China userscripts, 65%) can run in Violentmonkey without issues 12 | 13 | ### Tampermonkey 14 | * Can be used in Chrome, Brave, Edge, Firefox, Opera, etc. 15 | * Closed source (`This repository contains the source of the Tampermonkey extension up to version 2.9. All newer versions are distributed under a proprietary license.`) 16 | * GitHub: https://github.com/Tampermonkey/tampermonkey/ (Inactively Maintained) 17 | * Recommended 18 | * (This is the most popular one but actually this is closed source) 19 | * 99% userscripts can run in Tampermonkey without issues 20 | 21 | ### Firemonkey 22 | * Can be ONLY used in Firefox 23 | * [Open source](https://github.com/erosman/support/tree/FireMonkey) 24 | * GitHub: https://github.com/erosman/support/issues (Actively Maintained) 25 | * NOT Recommended due to its intented to be highly secured. 26 | * 65% userscripts (for China userscripts, 30%) can run in Firemonkey without issues 27 | 28 | ### Stay 29 | * MacOS, Safari, iPhone, iPad, etc. 30 | * Open Source 31 | * GitHub: https://github.com/shenruisi/Stay 32 | * By China developer, and with Chinese Community 33 | * (You have no other good choice in Apple's stuff) 34 | 35 | ### Userscripts 36 | * MacOS 37 | * Not Recommended because Stay is better 38 | 39 | ### ScriptCat 40 | * Can be used in Chrome, Brave, Edge, Firefox, Opera, etc. 41 | * Open Source 42 | * GitHub: https://github.com/scriptscat/scriptcat 43 | * By China developer, and with Chinese Community 44 | * https://docs.scriptcat.org/ 45 | 46 | ### Greasemonkey 47 | * Can be ONLY used in Firefox 48 | * Highly NOT recommended 49 | * Just a shit 50 | * GitHub: https://github.com/greasemonkey/greasemonkey (no one maintain) 51 | * 25% userscripts (for China userscripts, 10%) can run in Greasemonkey without issues 52 | 53 | 54 | 55 | ## UserScript Website 56 | * https://greasyfork.org/ (Highly Recommended) 57 | * https://sleazyfork.org/ (Recommended) 58 | * https://openuserjs.org/ 59 | * https://scriptcat.org/ (for China Users) 60 | * https://github.com/shenruisi/Stay-Offical-Userscript 61 | 62 | ## Guidelines 63 | * https://www.tampermonkey.net/documentation.php?locale=en 64 | * https://violentmonkey.github.io/ 65 | * https://erosman.github.io/support/content/help.html 66 | 67 | 68 | ## Standard Template 69 | 70 | ```js 71 | // ==UserScript== 72 | // @name Hello World 73 | // @namespace UserScripts 74 | // @match https://*/* 75 | // @grant none 76 | // @version 0.1.0 77 | // @author Author 78 | // @license MIT 79 | // @description Description Here 80 | // @allFrames true 81 | // @unwrap 82 | // @run-at document-start 83 | // @inject-into page 84 | // ==/UserScript== 85 | (()=>{ 86 | // TODO 87 | })(); 88 | ``` 89 | -------------------------------------------------------------------------------- /UserStyle.md: -------------------------------------------------------------------------------- 1 | # UserStyle 2 | 3 | ## UserStyle Manager 4 | 5 | ### Stylus 6 | * Highly Recommended 7 | * It is created after Stylish become shit. 8 | * Open Source 9 | * GitHub: https://github.com/openstyles/stylus 10 | * Works with Chrome, Brave, Edge, Firefox, Opera, etc 11 | * (Auto format and format checker is not working really well. Just ignore the warnings and errors) 12 | 13 | ### Firemonkey 14 | * NOT Recommended 15 | * It cannot handle `@preprocessor stylus` 16 | 17 | ### Stylish 18 | * DON'T USE ! 19 | * SHIT! 20 | 21 | ## Guidelines 22 | https://github.com/openstyles/stylus/wiki/Writing-UserCSS 23 | 24 | ## Preprocessor 25 | ### default 26 | * [CSS documentation](https://learn.freecodecamp.org/responsive-web-design/basic-css/) 27 | ### uso 28 | * [userstyles.org](https://userstyles.org/help/coding) 29 | ### less 30 | * [less documentation](http://lesscss.org/#overview) 31 | ### stylus 32 | * [stylus-lang documentation](http://stylus-lang.com/) 33 | * Highly Recommended 34 | 35 | ## UserStyle Websites 36 | 37 | ### UserStyles.world 38 | * https://userstyles.world/ 39 | * Highly Recommended 40 | * developed after UserStyles.org 41 | * Partnership: Stylus = Great 42 | 43 | ### UserStyles.org 44 | * https://userstyles.org/ 45 | * Not user-friendly 46 | * Partnership: Stylish = Shit 47 | 48 | ### Greasy Fork 49 | * https://greasyfork.org/ 50 | * Recommended 51 | 52 | ## Template 53 | 54 | ```scss 55 | 56 | /* ==UserStyle== 57 | @name Hello World 58 | @version 0.1.0 59 | @namespace github.com/openstyles 60 | @license MIT 61 | @description Description Here 62 | @author Author 63 | @preprocessor stylus 64 | @var color color-light "Color (Light Theme)" #0cb8da 65 | @var color color-dark "Color (Dark Theme)" #0c74e4 66 | @var number text-font-weight "Text Font-Weight" [400, 100, 900, 100] 67 | @var select text-option "Text Option" { 68 | "none": "none", 69 | "option 1": "option-1" 70 | } 71 | 72 | ==/UserStyle== */ 73 | 74 | dummy = 1 75 | 76 | 77 | 78 | 79 | // ----- Main Frame ------ 80 | 81 | @-moz-document url-prefix("https://www.youtube.com/") { 82 | 83 | 84 | html{ 85 | --my-color: color-light; 86 | } 87 | html[dark]{ 88 | --my-color: color-dark; 89 | } 90 | 91 | 92 | 93 | } 94 | 95 | // ----- Iframe ------ 96 | 97 | @-moz-document url-prefix("https://www.youtube.com/live_chat") { 98 | 99 | 100 | html{ 101 | --my-iframe-color: color-light; 102 | } 103 | html[dark]{ 104 | --my-iframe-color: color-dark; 105 | } 106 | 107 | } 108 | 109 | 110 | ``` 111 | -------------------------------------------------------------------------------- /Webhook Deployment.md: -------------------------------------------------------------------------------- 1 | See this https://github.com/JasonBarnabe/greasyfork/issues/1211#issuecomment-1826291455 2 | 3 | Reference Repos: 4 | 5 | https://github.com/a1mersnow/aliyundrive-rename 6 | https://github.com/Ex124OJ/Ex124OJ 7 | https://github.com/cyfung1031/Tabview-Youtube 8 | 9 | ## Tabview-Youtube 10 | 11 | 1. Create a placeholder file `generated/Tabview-Youtube.user.js` in `[master]` branch 12 | 2. Set the `[generated-files]` branch based on `[master]` branch 13 | 3. Write the content to `generated/Tabview-Youtube.user.js` in `[generated-files]` branch 14 | 4. Trigger webhook with modified file `generated/Tabview-Youtube.user.js` in `[generated-files]` branch 15 | 16 | 17 | 18 | ## Ex124OJ 19 | 20 | 1. Set the `[build]` branch based on `[master]` branch 21 | 2. Create an empty js file `dist/ex124oj.user.js` in `[build]` branch (1st commit) 22 | 3. Write the content to `dist/ex124oj.user.js` in `[build]` branch (2nd commit) 23 | 4. Trigger webhook with modified file `dist/ex124oj.user.js` in `[build]` branch 24 | 25 | 26 | ## aliyundrive-rename 27 | 28 | 1. The js file `dist/aliyundrive-rename.user.js` exists in `[master]` branch. 29 | 2. When there is release, generate the file to dist. 30 | 4. Trigger webhook with modified file `dist/aliyundrive-rename.user.js` in `[master]` branch 31 | 32 | 33 | -------------------------------------------------------------------------------- /YouTube-Single-Column-Tamer.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube: Single Column Tamer 3 | // @namespace UserScripts 4 | // @match https://www.youtube.com/* 5 | // @grant none 6 | // @unwrap 7 | // @inject-into page 8 | // @version 0.1.8 9 | // @author CY Fung 10 | // @description Re-adoption of Single Column Detection against video and browser sizes 11 | // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@8fac46500c5a916e6ed21149f6c25f8d1c56a6a3/library/ytZara.js 12 | // @require https://update.greasyfork.org/scripts/475632/1361351/ytConfigHacks.js 13 | // @license MIT 14 | // ==/UserScript== 15 | 16 | (() => { 17 | const ENABLE_WHEN_CONTENT_OCCUPY_MORE_THAN = 0.2 // 20% or more of other content can be displayed in your browser 18 | 19 | // protait screen & vertical live 20 | 21 | let _isSingleColumnPreferred = false; 22 | let bypass = false; 23 | let videoRatio = null; 24 | let _forceTwoCols = 0; 25 | let cachedSCUsage = null; 26 | 27 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 28 | 29 | const Promise = (async () => { })().constructor; 30 | 31 | const createPipeline = () => { 32 | let pipelineMutex = Promise.resolve(); 33 | const pipelineExecution = fn => { 34 | return new Promise((resolve, reject) => { 35 | pipelineMutex = pipelineMutex.then(async () => { 36 | let res; 37 | try { 38 | res = await fn(); 39 | } catch (e) { 40 | console.log('error_F1', e); 41 | reject(e); 42 | } 43 | resolve(res); 44 | }).catch(console.warn); 45 | }); 46 | }; 47 | return pipelineExecution; 48 | }; 49 | 50 | let rafPromise = null; 51 | const rafFn = (typeof webkitRequestAnimationFrame !== 'undefined' ? webkitRequestAnimationFrame : requestAnimationFrame).bind(window); // eslint-disable-line no-undef, no-constant-condition 52 | 53 | const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => { 54 | rafFn(hRes => { 55 | rafPromise = null; 56 | resolve(hRes); 57 | }); 58 | })); 59 | 60 | const getProto = (element) => { 61 | if (element) { 62 | const cnt = insp(element); 63 | return cnt.constructor.prototype || null; 64 | } 65 | return null; 66 | }; 67 | 68 | function toQueryForcedTwoCols(q) { 69 | if (q && typeof q === 'string') { 70 | q = q.replace('1000px', '200.2px'); 71 | q = q.replace('629px', '129.2px'); 72 | q = q.replace('657px', '157.2px'); 73 | q = q.replace('630px', '130.2px'); 74 | q = q.replace('1327px', '237.2px'); 75 | } 76 | return q; 77 | } 78 | 79 | function toQueryForcedOneCol(q) { 80 | if (q && typeof q === 'string') { 81 | q = q.replace('1000px', '998200.3px'); 82 | q = q.replace('629px', '998129.3px'); 83 | q = q.replace('657px', '998157.3px'); 84 | q = q.replace('630px', '998130.3px'); 85 | q = q.replace('1327px', '998237.3px'); 86 | } 87 | return q; 88 | } 89 | 90 | function getShouldSingleColumn() { 91 | if (typeof cachedSCUsage == 'boolean') return cachedSCUsage; 92 | const { clientHeight, clientWidth } = document.documentElement; 93 | if (clientHeight > clientWidth) { 94 | const referenceVideoHeight = clientWidth * videoRatio; 95 | const belowSpace = clientHeight - referenceVideoHeight; 96 | if (belowSpace > -1e-3 && belowSpace - ENABLE_WHEN_CONTENT_OCCUPY_MORE_THAN * clientHeight > -1e-3 && belowSpace > 65) { 97 | return (cachedSCUsage = true); 98 | } 99 | } 100 | return (cachedSCUsage = false); 101 | } 102 | 103 | /** @type {Set>} */ 104 | const querySet = new Set(); 105 | const protoFnQueryChanged = async () => { 106 | 107 | await customElements.whenDefined('iron-media-query'); 108 | const dummy = document.querySelector('iron-media-query') || document.createElement('iron-media-query'); 109 | const cProto = getProto(dummy); 110 | 111 | if (typeof cProto.queryChanged !== 'function') return; 112 | if (cProto.queryChanged71) return; 113 | if (cProto.queryChanged.length !== 0) return; 114 | cProto.queryChanged71 = cProto.queryChanged; 115 | 116 | cProto.queryChanged = function () { 117 | 118 | /** @type {string} */ 119 | let q = this.query; 120 | 121 | if (q) { 122 | 123 | if (!this.addedToSet53_) { 124 | this.addedToSet53_ = 1; 125 | querySet.add(new WeakRef(this)); 126 | } 127 | 128 | if (!bypass) { 129 | if (q.length > 3 && !q.includes('.')) { 130 | this.lastQuery53_ = q; 131 | } 132 | } 133 | 134 | 135 | if (this.lastQuery53_) { 136 | 137 | if (_isSingleColumnPreferred) { 138 | q = toQueryForcedOneCol(this.lastQuery53_); 139 | } else if (_forceTwoCols) { 140 | q = toQueryForcedTwoCols(this.lastQuery53_); 141 | } else { 142 | q = this.lastQuery53_; 143 | } 144 | 145 | } 146 | 147 | if (q !== this.query && typeof q === 'string' && q) { 148 | this.query = q; 149 | } 150 | 151 | } 152 | 153 | return this.queryChanged71(); 154 | 155 | } 156 | 157 | }; 158 | 159 | const createCSSElement = ()=>{ 160 | 161 | const cssElm = document.createElement('style'); 162 | cssElm.id = 'oh7T7lsvcHJQ'; 163 | document.head.appendChild(cssElm); 164 | 165 | cssElm.textContent = ` 166 | 167 | ytd-watch-flexy[flexy][is-two-columns_] { 168 | --ytd-watch-flexy-min-player-height-ss: 10px; 169 | } 170 | ytd-watch-flexy[flexy][is-two-columns_] #primary.ytd-watch-flexy { 171 | min-width: calc(var(--ytd-watch-flexy-min-player-height-ss)*1.7777777778); 172 | } 173 | ytd-watch-flexy[flexy][is-two-columns_]:not([is-four-three-to-sixteen-nine-video_]):not([is-extra-wide-video_]):not([full-bleed-player][full-bleed-no-max-width-columns]):not([fixed-panels]) #primary.ytd-watch-flexy { 174 | min-width: calc(var(--ytd-watch-flexy-min-player-height-ss)*1.7777777778); 175 | } 176 | `; 177 | return cssElm; 178 | } 179 | 180 | const protoFnRatioChanged = async () => { 181 | 182 | await customElements.whenDefined('ytd-watch-flexy'); 183 | const dummy = document.querySelector('ytd-watch-flexy') || document.createElement('ytd-watch-flexy'); 184 | const cProto = getProto(dummy); 185 | 186 | if (typeof cProto.videoHeightToWidthRatioChanged_ !== 'function') return; 187 | if (cProto.videoHeightToWidthRatioChanged23_) return; 188 | // if (cProto.videoHeightToWidthRatioChanged_.length !== 2) return; 189 | 190 | cProto.videoHeightToWidthRatioChanged23_ = cProto.videoHeightToWidthRatioChanged_; 191 | const ratioQueryFix24_ = () => { 192 | 193 | if (videoRatio > 1e-5) { } else return; 194 | let changeCSS = false; 195 | 196 | const changedSingleColumn = _isSingleColumnPreferred !== (_isSingleColumnPreferred = getShouldSingleColumn()); 197 | let action = 0; 198 | if (changedSingleColumn) { 199 | action |= 4; 200 | } 201 | if (!_isSingleColumnPreferred) { 202 | const isVerticalRatio = videoRatio > 1.6 && videoRatio < 2.7; 203 | if (isVerticalRatio && !_forceTwoCols) { 204 | changeCSS = true; 205 | _forceTwoCols = 1; 206 | action |= 1; 207 | } else if (!isVerticalRatio && _forceTwoCols) { 208 | changeCSS = true; 209 | _forceTwoCols = 0; 210 | action |= 2; 211 | } 212 | } 213 | if (action) { 214 | for (const p of querySet) { 215 | const qnt = p.deref(); 216 | if (!qnt || !qnt.lastQuery53_) continue; 217 | if (action & 4) { 218 | if (!qnt.q00 && !qnt.q02 && _isSingleColumnPreferred) { 219 | qnt.q00 = qnt.lastQuery53_; 220 | qnt.q02 = toQueryForcedOneCol(qnt.q00); 221 | } 222 | action |= 8; 223 | } 224 | if (action & 1) { 225 | if (!qnt.q00 && !qnt.q01) { 226 | qnt.q00 = qnt.lastQuery53_; 227 | qnt.q01 = toQueryForcedTwoCols(qnt.q00); 228 | } 229 | if (qnt.q00 && qnt.q01) { 230 | action |= 8; 231 | } 232 | } else if (action & 2) { 233 | if (qnt.q00 && qnt.q01) { 234 | action |= 8; 235 | } 236 | } 237 | } 238 | 239 | if (action & 8) { 240 | bypass = true; 241 | for (const p of querySet) { 242 | const qnt = p.deref(); 243 | if (qnt && qnt.lastQuery53_ && qnt.query) { 244 | qnt.queryChanged(); 245 | } 246 | } 247 | bypass = false; 248 | } 249 | 250 | } 251 | 252 | let cssElm = null; 253 | 254 | if (changeCSS) { 255 | cssElm = cssElm || document.querySelector('style#oh7T7lsvcHJQ') || createCSSElement(); 256 | } else { 257 | cssElm = cssElm || document.querySelector('style#oh7T7lsvcHJQ'); 258 | } 259 | 260 | if (cssElm) { 261 | if (_forceTwoCols && cssElm.disabled) cssElm.disabled = false; 262 | else if (!_forceTwoCols && !cssElm.disabled) cssElm.disabled = true; 263 | } 264 | }; 265 | 266 | const resizePipeline = createPipeline(); 267 | 268 | cProto.videoHeightToWidthRatioChanged_ = function () { 269 | try { 270 | cachedSCUsage = null; 271 | videoRatio = this.videoHeightToWidthRatio_; 272 | resizePipeline(ratioQueryFix24_); 273 | } catch (e) { 274 | } 275 | return this.videoHeightToWidthRatioChanged23_(...arguments); 276 | }; 277 | 278 | let rzid = 0; 279 | Window.prototype.addEventListener.call(window, 'resize', function () { 280 | cachedSCUsage = null; 281 | if (videoRatio > 1e-5) { } else return; 282 | if (rzid > 1e9) rzid = 9; 283 | const t = ++rzid; 284 | resizePipeline(async () => { 285 | if (t !== rzid) return; 286 | await getRafPromise(); 287 | if (t !== rzid) return; 288 | let k = getShouldSingleColumn(); 289 | if (_isSingleColumnPreferred !== k) { 290 | resizePipeline(ratioQueryFix24_); 291 | } 292 | }); 293 | }, { capture: false, passive: true }); 294 | 295 | }; 296 | 297 | window._ytConfigHacks.add((config_) => { 298 | 299 | const EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS; 300 | 301 | if (EXPERIMENT_FLAGS) { 302 | 303 | EXPERIMENT_FLAGS.kevlar_set_internal_player_size = false; // vertical live -> schedulePlayerSizeUpdate_ 304 | 305 | } 306 | 307 | }); 308 | 309 | (async () => { 310 | 311 | if (!document.documentElement) await ytZara.docInitializedAsync(); // wait for document.documentElement is provided 312 | 313 | await ytZara.promiseRegistryReady(); // wait for YouTube's customElement Registry is provided (old browser only) 314 | 315 | protoFnQueryChanged(); 316 | protoFnRatioChanged(); 317 | 318 | })(); 319 | 320 | })(); 321 | -------------------------------------------------------------------------------- /YouTube-UserScripts.md: -------------------------------------------------------------------------------- 1 | ## YouTube UserScript 2 | 3 | ### YouTube Watch, Desktop 4 | 5 | #### Tabview Youtube (BETA) 6 | 7 | * Make comments, lists, meta info into tabs for YouTube Watch Page 8 | * Various fixes to YouTube Watch Layout (fix YouTube Native Bugs) 9 | 10 | ### YouTube Dekstop & Mobile 11 | 12 | #### Restore YouTube Username from Handle to Custom (STABLE) 13 | 14 | * To restore YouTube Username to the traditional custom name 15 | 16 | ### YouTube Watch Chats 17 | 18 | #### YouTube Super Fast Chat (BETA) 19 | 20 | * Aggressive change on the core rendering process to make the chats become ultimate responsive 21 | * Suitable for watching hololive vtuber's livestreams. 22 | 23 | #### Disable all YouTube EXPERIMENT_FLAGS (ALPHA) 24 | 25 | * Disable All Experiment features in YouTube that might be obsolete or not published officially. 26 | 27 | #### YouTube: Force html5\_exponential\_memory\_for\_sticky (STABLE) 28 | 29 | * To prevent YouTube to change the video quality automatically during YouTube Live Streaming. 30 | 31 | #### YouTube Native - Vanilla Engine (ALPHA) 32 | 33 | * (YouTube Experimental) Disable the YouTube engine hacks and just use the native APIs 34 | 35 | #### YouTube Live DateTime Tooltip (STABLE) 36 | * Make a tooltip to show the actual date and time for livestream 37 | 38 | #### YouTube Video Resize Fix (STABLE) 39 | * This Userscript can fix the video sizing issue. Please use it with other Userstyles / Userscripts. 40 | * Mainly for YouTube Live Borderless 41 | 42 | #### Play Single Video Only (STABLE) 43 | * Pause your background video when you play another video in another tab. 44 | 45 | ### YouTube Background 46 | 47 | #### Disable YouTube AutoPause 48 | * For YouTube Desktop 49 | * "Video paused. Continue watching?" and "Still watching? Video will pause soon" will not appear anymore. 50 | 51 | #### Disable YouTube Music AutoPause 52 | * For YouTube Music 53 | * "Video paused. Continue watching?" and "Still watching? Video will pause soon" will not appear anymore. 54 | 55 | ## YouTube UserStyles 56 | 57 | #### YouTube Live Borderless 58 | * It can also for non-live stream videos. 59 | 60 | 61 | -------------------------------------------------------------------------------- /blank.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/blank.html -------------------------------------------------------------------------------- /cookie-manager.user.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright 2022 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | // ==UserScript== 27 | // @name Cookie Manager 28 | // @namespace http://tampermonkey.net/ 29 | // @version 0.5 30 | // @description For Developers Only. Control Cookies everywhere via DevTools 31 | // @author CY Fung 32 | // @supportURL https://github.com/cyfung1031/userscript-supports 33 | // @match https://*/* 34 | // @match http://*/* 35 | // @icon https://github.com/cyfung1031/userscript-supports/blob/main/icons/cookie-manager.png?raw=true 36 | // @grant unsafeWindow 37 | // @license MIT 38 | // @require https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js#sha512=wT7uPE7tOP6w4o28u1DN775jYjHQApdBnib5Pho4RB0Pgd9y7eSkAV1BTqQydupYDB9GBhTcQQzyNMPMV3cAew== 39 | // ==/UserScript== 40 | 41 | /* global Cookies */ 42 | 43 | /* 44 | usage: 45 | 46 | cook.set('hello-world',100) 47 | console.log(cook.get('hello-world')) 48 | cook.remove('hello-world') 49 | 50 | cook.myvar = 'abc' 51 | console.log(cook.myvar) 52 | cook.myvar = null 53 | 54 | cook.get() 55 | 56 | const api = cook.chef(null, { path: '/', domain: '.example.com' }) 57 | const api = cook.chef({ 58 | write: function (value, name) { 59 | return value.toUpperCase() 60 | } 61 | }, null) 62 | 63 | */ 64 | 65 | (function (Cookies) { 66 | 'use strict'; 67 | // Your code here... 68 | if (unsafeWindow.cook) return 69 | const { get, set, remove } = Cookies 70 | function chefFunc(converter, attributes) { 71 | converter = converter ? Object.assign({}, this.converter, converter) : this.converter 72 | attributes = attributes ? Object.assign({}, this.attributes, attributes) : this.attributes 73 | return init(converter, attributes) 74 | } 75 | const target = { 76 | set: set.bind(Cookies), 77 | get: get.bind(Cookies), 78 | remove: remove.bind(Cookies), 79 | chef: chefFunc.bind(Cookies), 80 | replaceChef: (chef) => { 81 | Cookies = chef 82 | target.set = set.bind(Cookies) 83 | target.get = get.bind(Cookies) 84 | target.remove = remove.bind(Cookies) 85 | target.chef = chefFunc.bind(Cookies) 86 | } 87 | } 88 | unsafeWindow.cook = new Proxy(target, { 89 | get(target, prop) { 90 | if (prop in target) { 91 | return target[prop] 92 | } 93 | return Cookies.get(prop) 94 | }, 95 | set(target, prop, val) { 96 | if (val === null) Cookies.remove(prop); 97 | else Cookies.set(prop, val); 98 | return true 99 | } 100 | }) 101 | })(Cookies); 102 | -------------------------------------------------------------------------------- /demo/animated-rolling-number.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/demo/animated-rolling-number.webm -------------------------------------------------------------------------------- /demo/pangu-lines.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Random Line Generator 7 | 15 | 16 | 17 | 18 |
19 | 20 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /demo/timer-performance.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | (() => { 4 | 5 | 6 | if (!document.getElementById('afscript')) { 7 | const style = document.createElement('style'); 8 | style.id = 'afscript'; 9 | style.textContent = ` 10 | @keyFrames aF1 { 11 | 0% { 12 | order: 0; 13 | } 14 | 100% { 15 | order: 1; 16 | } 17 | } 18 | #a-f[id] { 19 | visibility: collapse !important; 20 | position: fixed !important; 21 | display: block !important; 22 | top: -100px !important; 23 | left: -100px !important; 24 | margin:0 !important; 25 | padding:0 !important; 26 | outline:0 !important; 27 | border:0 !important; 28 | z-index:-1 !important; 29 | width: 0px !important; 30 | height: 0px !important; 31 | contain: strict !important; 32 | pointer-events: none !important; 33 | animation: 1ms steps(2, jump-none) 0ms infinite alternate forwards running aF1 !important; 34 | } 35 | `; 36 | (document.head || document.documentElement).appendChild(style); 37 | } 38 | 39 | /** @type {HTMLVideoElement} */ 40 | const vv = document.querySelector('#movie_player video[src]'); 41 | if (!vv) throw 'VIDEO is not found'; 42 | const asc = document.createElement('a-f'); 43 | asc.id = 'a-f'; 44 | window.p44 = 0; 45 | window.p45 = 0; 46 | window.p46 = 0; 47 | let qr = null; 48 | asc.onanimationiteration = function () { 49 | if (qr !== null) qr = (qr(), null); 50 | } 51 | const pn1 = (resolve) => (qr = resolve); 52 | 53 | const pn2 = (resolve) => { 54 | vv.requestVideoFrameCallback(resolve); 55 | }; 56 | const pn3 = (resolve) => { 57 | requestAnimationFrame(resolve); 58 | }; 59 | document.documentElement.insertBefore(asc, document.documentElement.firstChild); 60 | 61 | let t0 = Date.now(); 62 | (async () => { 63 | 64 | while (Date.now() - t0 < 5000) { 65 | window.p44++; 66 | await new Promise(pn1); 67 | } 68 | 69 | })(); 70 | 71 | (async () => { 72 | 73 | while (Date.now() - t0 < 5000) { 74 | window.p45++; 75 | await new Promise(pn2); 76 | } 77 | 78 | })(); 79 | 80 | (async () => { 81 | 82 | while (Date.now() - t0 < 5000) { 83 | window.p46++; 84 | await new Promise(pn3); 85 | } 86 | 87 | })(); 88 | 89 | 90 | })(); -------------------------------------------------------------------------------- /disable-youtube-autopause.user.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright 2022 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | // ==UserScript== 27 | // @name Disable YouTube AutoPause 28 | // @name:en Disable YouTube AutoPause 29 | // @name:ja Disable YouTube AutoPause 30 | // @name:zh-TW Disable YouTube AutoPause 31 | // @name:zh-CN Disable YouTube AutoPause 32 | // @namespace http://tampermonkey.net/ 33 | // @version 2024.02.21.0 34 | // @license MIT License 35 | // @description "Video paused. Continue watching?" and "Still watching? Video will pause soon" will not appear anymore. 36 | // @description:en "Video paused. Continue watching?" and "Still watching? Video will pause soon" will not appear anymore. 37 | // @description:ja 「動画が一時停止されました。続きを視聴しますか?」と「視聴を続けていますか?動画がまもなく一時停止されます」は二度と起こりません。 38 | // @description:zh-TW 「影片已暫停,要繼續觀賞嗎?」和「你還在螢幕前嗎?影片即將暫停播放」不再顯示。 39 | // @description:zh-CN 「视频已暂停。是否继续观看?」和「仍在观看?视频即将暂停」不再显示。 40 | // @author CY Fung 41 | // @match https://www.youtube.com/* 42 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 43 | // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/disable-youtube-autopause.svg 44 | // @supportURL https://github.com/cyfung1031/userscript-supports 45 | // @run-at document-start 46 | // @grant none 47 | // @unwrap 48 | // @allFrames true 49 | // @inject-into page 50 | // ==/UserScript== 51 | 52 | /* jshint esversion:8 */ 53 | 54 | (function (__Promise__) { 55 | 'use strict'; 56 | 57 | /** @type {globalThis.PromiseConstructor} */ 58 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 59 | 60 | const youThereDataHashMapPauseDelay = new WeakMap(); 61 | const youThereDataHashMapPromptDelay = new WeakMap(); 62 | const youThereDataHashMapLactThreshold = new WeakMap(); 63 | const websiteName = 'YouTube'; 64 | let noDelayLogUntil = 0; 65 | 66 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 67 | const indr = o => insp(o).$ || o.$ || 0; 68 | 69 | function delayLog(...args) { 70 | if (Date.now() < noDelayLogUntil) return; 71 | noDelayLogUntil = Date.now() + 280; // avoid duplicated delay log in the same time ticker 72 | console.log(...args); 73 | } 74 | 75 | function defineProp1(youThereData, key, retType, constVal, fGet, fSet, hashMap) { 76 | Object.defineProperty(youThereData, key, { 77 | enumerable: true, 78 | configurable: true, 79 | get() { 80 | Promise.resolve(new Date).then(fGet).catch(console.warn); 81 | const ret = constVal; 82 | return retType === 2 ? `${ret}` : ret; 83 | }, 84 | set(newValue) { 85 | const oldValue = hashMap.get(this); 86 | Promise.resolve([oldValue, newValue, new Date]).then(fSet).catch(console.warn); 87 | hashMap.set(this, newValue); 88 | return true; 89 | } 90 | }); 91 | } 92 | 93 | function defineProp2(youThereData, key, qKey) { 94 | Object.defineProperty(youThereData, key, { 95 | enumerable: true, 96 | configurable: true, 97 | get() { 98 | const r = this[qKey]; 99 | if ((r || 0).length >= 1) r.length = 0; 100 | return r; 101 | }, 102 | set(nv) { 103 | return true; 104 | } 105 | }); 106 | } 107 | 108 | function hookYouThereData(youThereData) { 109 | if (!youThereData || youThereDataHashMapPauseDelay.has(youThereData)) return; 110 | const retPauseDelay = youThereData.playbackPauseDelayMs; 111 | const retPromptDelay = youThereData.promptDelaySec; 112 | const retLactThreshold = youThereData.lactThresholdMs; 113 | const tenPU = Math.floor(Number.MAX_SAFE_INTEGER * 0.1); 114 | const mPU = Math.floor(tenPU / 1000); 115 | 116 | if ('playbackPauseDelayMs' in youThereData && retPauseDelay >= 0 && retPauseDelay < 4 * tenPU) { 117 | youThereDataHashMapPauseDelay.set(youThereData, retPauseDelay); 118 | const retType = typeof retPauseDelay === 'string' ? 2 : +(typeof retPauseDelay === 'number'); 119 | if (retType >= 1) { 120 | defineProp1(youThereData, 'playbackPauseDelayMs', retType, 5 * tenPU, d => { 121 | delayLog(`${websiteName} is trying to pause video...`, d.toLocaleTimeString()); 122 | }, args => { 123 | const [oldValue, newValue, d] = args; 124 | console.log(`${websiteName} is trying to change value 'playbackPauseDelayMs' from ${oldValue} to ${newValue} ...`, d.toLocaleTimeString()); 125 | }, youThereDataHashMapPauseDelay); 126 | } 127 | if (typeof ((youThereData.showPausedActions || 0).length) === 'number' && !youThereData.tvTyh) { 128 | youThereData.tvTyh = []; 129 | defineProp2(youThereData, 'showPausedActions', 'tvTyh'); 130 | } 131 | } 132 | 133 | if ('promptDelaySec' in youThereData && retPromptDelay >= 0 && retPromptDelay < 4 * mPU) { 134 | youThereDataHashMapPromptDelay.set(youThereData, retPromptDelay); 135 | const retType = typeof retPromptDelay === 'string' ? 2 : +(typeof retPromptDelay === 'number'); 136 | // lact -> promptDelaySec -> showDialog -> playbackPauseDelayMs -> pause 137 | if (retType >= 1) { 138 | defineProp1(youThereData, 'promptDelaySec', retType, 5 * mPU, d => { 139 | delayLog(`${websiteName} is trying to pause video...`, d.toLocaleTimeString()); 140 | }, args => { 141 | const [oldValue, newValue, d] = args; 142 | console.log(`${websiteName} is trying to change value 'promptDelaySec' from ${oldValue} to ${newValue} ...`, d.toLocaleTimeString()); 143 | }, youThereDataHashMapPromptDelay); 144 | 145 | } 146 | } 147 | 148 | if ('lactThresholdMs' in youThereData && retLactThreshold >= 0 && retLactThreshold < 4 * tenPU) { 149 | youThereDataHashMapLactThreshold.set(youThereData, retLactThreshold); 150 | const retType = typeof retLactThreshold === 'string' ? 2 : +(typeof retLactThreshold === 'number'); 151 | // lact -> promptDelaySec -> showDialog -> playbackPauseDelayMs -> pause 152 | if (retType >= 1) { 153 | defineProp1(youThereData, 'lactThresholdMs', retType, 5 * tenPU, d => { 154 | // console.log(`${websiteName} is trying to pause video...`, d.toLocaleTimeString()); 155 | }, args => { 156 | const [oldValue, newValue, d] = args; 157 | console.log(`${websiteName} is trying to change value 'lactThresholdMs' from ${oldValue} to ${newValue} ...`, d.toLocaleTimeString()); 158 | }, youThereDataHashMapLactThreshold); 159 | } 160 | } 161 | 162 | } 163 | 164 | // e.performDataUpdate -> f.playerData = a.playerResponse; 165 | // youthereDataChanged_(playerData.messages) 166 | // youthereDataChanged_ -> b.youThereRenderer && fFb(this.youThereManager_, b.youThereRenderer) 167 | // a.youThereData_ = b.configData.youThereData; 168 | // a.youThereData_.playbackPauseDelayMs 169 | function onPageFinished() { 170 | if (arguments.length === 1) noDelayLogUntil = Date.now() + 3400; // no delay log for video changes 171 | Promise.resolve(0).then(() => { 172 | let messages = null; 173 | const pageMgrElm = document.querySelector('#page-manager') || 0; 174 | const pageMgrCnt = insp(pageMgrElm); 175 | try { 176 | messages = pageMgrCnt.data.playerResponse.messages; 177 | } catch (e) { } 178 | if (messages && messages.length > 0) { 179 | for (const message of messages) { 180 | if ((message || 0).youThereRenderer) { 181 | let youThereData = null; 182 | try { 183 | youThereData = message.youThereRenderer.configData.youThereData; 184 | } catch (e) { } 185 | if (youThereData) hookYouThereData(youThereData); 186 | youThereData = null; 187 | break; 188 | } 189 | } 190 | } 191 | 192 | const ytdFlexyElm = document.querySelector('ytd-watch-flexy') || 0; 193 | const ytdFlexyCnt = insp(ytdFlexyElm); 194 | 195 | if (ytdFlexyCnt) { 196 | const youThereManager_ = ytdFlexyCnt.youThereManager_ || ytdFlexyElm.youThereManager_ || 0; 197 | const youThereData_ = (youThereManager_ || 0).youThereData_ || 0; 198 | if (youThereData_) hookYouThereData(youThereData_); 199 | const f = ytdFlexyCnt.youthereDataChanged_; 200 | if (typeof f === 'function' && !f.lq2S7) { 201 | ytdFlexyCnt.youthereDataChanged_ = (function (f) { 202 | return function () { 203 | console.log('youthereDataChanged_()'); 204 | const ret = f.apply(this, arguments); 205 | onPageFinished(); 206 | return ret; 207 | } 208 | })(f); 209 | ytdFlexyCnt.youthereDataChanged_.lq2S7 = 1; 210 | } 211 | } 212 | 213 | }).catch(console.warn) 214 | } 215 | document.addEventListener('yt-page-data-updated', onPageFinished, false); 216 | document.addEventListener('yt-navigate-finish', onPageFinished, false); 217 | document.addEventListener('spfdone', onPageFinished, false); 218 | 219 | })(Promise); 220 | -------------------------------------------------------------------------------- /disable-youtube-av1-vp9.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Disable YouTube AV1 and VP9 3 | // @description Disable AV1 and VP9 for video playback on YouTube 4 | // @name:zh-TW 停用 YouTube AV1 和 VP9 5 | // @description:zh-TW 停用 YouTube 的 AV1 和 VP9 影片播放 6 | // @name:zh-HK 停用 YouTube AV1 和 VP9 7 | // @description:zh-HK 停用 YouTube 的 AV1 和 VP9 影片播放 8 | // @name:zh-CN 停用 YouTube AV1 和 VP9 9 | // @description:zh-CN 停用 YouTube 的 AV1 和 VP9 视频播放 10 | // @name:ja YouTube AV1 と VP9 の停用 11 | // @description:ja YouTube の動画再生に AV1 と VP9 を停用する 12 | // @name:ko YouTube AV1과 VP9 비활성화 13 | // @description:ko YouTube의 동영상 재생에 AV1과 VP9를 비활성화하기 14 | // @name:vi Vô hiệu hóa YouTube AV1 và VP9 15 | // @description:vi Vô hiệu hóa AV1 và VP9 để phát video trên YouTube 16 | // @name:de YouTube AV1 und VP9 deaktivieren 17 | // @description:de Deaktiviert AV1 und VP9 für die Videowiedergabe auf YouTube 18 | // @name:fr Désactiver YouTube AV1 et VP9 19 | // @description:fr Désactivez AV1 et VP9 pour la lecture des vidéos sur YouTube 20 | // @name:it Disabilita YouTube AV1 e VP9 21 | // @description:it Disabilita AV1 e VP9 per la riproduzione dei video su YouTube 22 | // @name:es Desactivar AV1 y VP9 en YouTube 23 | // @description:es Desactivar AV1 y VP9 para la reproducción de videos en YouTube 24 | // @namespace http://tampermonkey.net/ 25 | // @version 2.4.4 26 | // @author CY Fung 27 | // @match https://www.youtube.com/* 28 | // @match https://www.youtube.com/embed/* 29 | // @match https://www.youtube-nocookie.com/embed/* 30 | // @exclude https://www.youtube.com/live_chat* 31 | // @exclude https://www.youtube.com/live_chat_replay* 32 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 33 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 34 | // @grant none 35 | // @run-at document-start 36 | // @license MIT 37 | // @compatible chrome 38 | // @compatible firefox 39 | // @compatible opera 40 | // @compatible edge 41 | // @compatible safari 42 | // @unwrap 43 | // @allFrames true 44 | // @inject-into page 45 | // ==/UserScript== 46 | 47 | 48 | (function () { 49 | 'use strict'; 50 | 51 | console.debug("disable-youtube-av1-and-vp9", "injected"); 52 | 53 | 54 | 55 | const flagConfig = () => { 56 | 57 | let firstDa = true; 58 | let cid = 0; 59 | const { setInterval, clearInterval, setTimeout } = window; 60 | const tn = () => { 61 | 62 | const da = (window.ytcfg && window.ytcfg.data_) ? window.ytcfg.data_ : null; 63 | if (!da) return; 64 | 65 | const isFirstDa = firstDa; 66 | firstDa = false; 67 | 68 | for (const EXPERIMENT_FLAGS of [da.EXPERIMENT_FLAGS, da.EXPERIMENTS_FORCED_FLAGS]) { 69 | 70 | if (EXPERIMENT_FLAGS) { 71 | // EXPERIMENT_FLAGS.html5_disable_av1_hdr = true; 72 | // EXPERIMENT_FLAGS.html5_prefer_hbr_vp9_over_av1 = true; 73 | } 74 | 75 | } 76 | 77 | if (isFirstDa) { 78 | 79 | 80 | let mo = new MutationObserver(() => { 81 | 82 | mo.disconnect(); 83 | mo.takeRecords(); 84 | mo = null; 85 | setTimeout(() => { 86 | cid && clearInterval.call(window, cid); 87 | cid = 0; 88 | tn(); 89 | }) 90 | }); 91 | mo.observe(document, { subtree: true, childList: true }); 92 | 93 | 94 | } 95 | 96 | 97 | }; 98 | cid = setInterval.call(window, tn); 99 | 100 | }; 101 | 102 | const supportedFormatsConfig = () => { 103 | 104 | function typeTest(type) { 105 | 106 | if (typeof type === 'string' && type.startsWith('video/')) { 107 | 108 | if (type.includes('vp9')) { 109 | if (/codecs[\x20-\x7F]+\bvp9\b/.test(type)) return false; 110 | } else if (type.includes('vp09')) { 111 | if (/codecs[\x20-\x7F]+\bvp09\b/.test(type)) return false; 112 | } else if (type.includes('av01')) { 113 | if (/codecs[\x20-\x7F]+\bav01\b/.test(type)) return false; 114 | } else if (type.includes('av1')) { 115 | if (/codecs[\x20-\x7F]+\bav1\b/.test(type)) return false; 116 | } 117 | } 118 | 119 | } 120 | 121 | // return a custom MIME type checker that can defer to the original function 122 | function makeModifiedTypeChecker(origChecker, dx) { 123 | // Check if a video type is allowed 124 | return function (type) { 125 | let res = undefined; 126 | if (type === undefined) res = false; 127 | else res = typeTest(type); 128 | if (res === undefined) res = origChecker.apply(this, arguments); 129 | else res = !dx ? res : (res ? "probably" : ""); 130 | 131 | // console.debug(20, type, res) 132 | 133 | return res; 134 | }; 135 | } 136 | 137 | // Override video element canPlayType() function 138 | const proto = (HTMLVideoElement || 0).prototype; 139 | if (proto && typeof proto.canPlayType == 'function') { 140 | proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType, true); 141 | } 142 | 143 | // Override media source extension isTypeSupported() function 144 | const mse = window.MediaSource; 145 | // Check for MSE support before use 146 | if (mse && typeof mse.isTypeSupported == 'function') { 147 | mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported); 148 | } 149 | 150 | }; 151 | 152 | function disableAV1() { 153 | 154 | 155 | 156 | 157 | // This is the setting to disable AV1 [ 480p (or below) - AV1, above 480p - VP9 ] 158 | // localStorage['yt-player-av1-pref'] = '480'; 159 | try { 160 | Object.defineProperty(localStorage.constructor.prototype, 'yt-player-av1-pref', { 161 | get() { 162 | if (this === localStorage) return '480'; 163 | return this.getItem('yt-player-av1-pref'); 164 | }, 165 | set(nv) { 166 | this.setItem('yt-player-av1-pref', nv); 167 | return true; 168 | }, 169 | enumerable: true, 170 | configurable: true 171 | }); 172 | } catch (e) { 173 | // localStorage['yt-player-av1-pref'] = '480'; 174 | } 175 | 176 | if (localStorage['yt-player-av1-pref'] !== '480') { 177 | 178 | console.warn('Disable YouTube AV1 and VP9', '"yt-player-av1-pref = 480" is not supported in your browser.'); 179 | return; 180 | } 181 | 182 | console.debug("disable-youtube-av1-and-vp9", "AV1 disabled by yt-player-av1-pref = 480"); 183 | 184 | 185 | } 186 | 187 | 188 | disableAV1(); 189 | 190 | // flagConfig(); 191 | supportedFormatsConfig(); 192 | 193 | 194 | 195 | 196 | })(); 197 | 198 | -------------------------------------------------------------------------------- /disable-youtube-av1.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Disable YouTube AV1 3 | // @description Disable AV1 for video playback on YouTube 4 | // @name:zh-TW 停用 YouTube AV1 5 | // @description:zh-TW 停用 YouTube 的 AV1 影片播放 6 | // @name:zh-HK 停用 YouTube AV1 7 | // @description:zh-HK 停用 YouTube 的 AV1 影片播放 8 | // @name:zh-CN 停用 YouTube AV1 9 | // @description:zh-CN 停用 YouTube 的 AV1 视频播放 10 | // @name:ja YouTube AV1 停用 11 | // @description:ja YouTube の動画再生に AV1 を停用する 12 | // @name:ko YouTube AV1 비활성화 13 | // @description:ko YouTube의 동영상 재생에 AV1을 비활성화하기 14 | // @name:vi Vô hiệu hóa YouTube AV1 15 | // @description:vi Vô hiệu hóa AV1 để phát video trên YouTube 16 | // @name:de YouTube AV1 deaktivieren 17 | // @description:de Deaktiviert AV1 für die Videowiedergabe auf YouTube 18 | // @name:fr Désactiver YouTube AV1 19 | // @description:fr Désactivez AV1 pour la lecture des vidéos sur YouTube 20 | // @name:it Disabilita YouTube AV1 21 | // @description:it Disabilita AV1 per la riproduzione dei video su YouTube 22 | // @name:es Desactivar AV1 en YouTube 23 | // @description:es Desactivar AV1 para la reproducción de videos en YouTube 24 | // @namespace http://tampermonkey.net/ 25 | // @version 2.4.4 26 | // @author CY Fung 27 | // @match https://www.youtube.com/* 28 | // @match https://www.youtube.com/embed/* 29 | // @match https://www.youtube-nocookie.com/embed/* 30 | // @exclude https://www.youtube.com/live_chat* 31 | // @exclude https://www.youtube.com/live_chat_replay* 32 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 33 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 34 | // @grant none 35 | // @run-at document-start 36 | // @license MIT 37 | // @compatible chrome 38 | // @compatible firefox 39 | // @compatible opera 40 | // @compatible edge 41 | // @compatible safari 42 | // @unwrap 43 | // @allFrames true 44 | // @inject-into page 45 | // ==/UserScript== 46 | 47 | (function () { 48 | 'use strict'; 49 | 50 | console.debug("disable-youtube-av1", "injected"); 51 | 52 | 53 | 54 | const flagConfig = () => { 55 | 56 | let firstDa = true; 57 | let cid = 0; 58 | const { setInterval, clearInterval, setTimeout } = window; 59 | const tn = () => { 60 | 61 | const da = (window.ytcfg && window.ytcfg.data_) ? window.ytcfg.data_ : null; 62 | if (!da) return; 63 | 64 | const isFirstDa = firstDa; 65 | firstDa = false; 66 | 67 | for (const EXPERIMENT_FLAGS of [da.EXPERIMENT_FLAGS, da.EXPERIMENTS_FORCED_FLAGS]) { 68 | 69 | if (EXPERIMENT_FLAGS) { 70 | // EXPERIMENT_FLAGS.html5_disable_av1_hdr = true; 71 | // EXPERIMENT_FLAGS.html5_prefer_hbr_vp9_over_av1 = true; 72 | } 73 | 74 | } 75 | 76 | if (isFirstDa) { 77 | 78 | 79 | let mo = new MutationObserver(() => { 80 | 81 | mo.disconnect(); 82 | mo.takeRecords(); 83 | mo = null; 84 | setTimeout(() => { 85 | cid && clearInterval.call(window, cid); 86 | cid = 0; 87 | tn(); 88 | }) 89 | }); 90 | mo.observe(document, { subtree: true, childList: true }); 91 | 92 | 93 | } 94 | 95 | 96 | }; 97 | cid = setInterval.call(window, tn); 98 | 99 | }; 100 | 101 | const supportedFormatsConfig = () => { 102 | 103 | function typeTest(type) { 104 | 105 | if (typeof type === 'string' && type.startsWith('video/')) { 106 | if (type.includes('av01')) { 107 | if (/codecs[\x20-\x7F]+\bav01\b/.test(type)) return false; 108 | } else if (type.includes('av1')) { 109 | if (/codecs[\x20-\x7F]+\bav1\b/.test(type)) return false; 110 | } 111 | } 112 | 113 | } 114 | 115 | // return a custom MIME type checker that can defer to the original function 116 | function makeModifiedTypeChecker(origChecker, dx) { 117 | // Check if a video type is allowed 118 | return function (type) { 119 | let res = undefined; 120 | if (type === undefined) res = false; 121 | else res = typeTest(type); 122 | if (res === undefined) res = origChecker.apply(this, arguments); 123 | else res = !dx ? res : (res ? "probably" : ""); 124 | 125 | // console.debug(20, type, res) 126 | 127 | return res; 128 | }; 129 | } 130 | 131 | // Override video element canPlayType() function 132 | const proto = (HTMLVideoElement || 0).prototype; 133 | if (proto && typeof proto.canPlayType == 'function') { 134 | proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType, true); 135 | } 136 | 137 | // Override media source extension isTypeSupported() function 138 | const mse = window.MediaSource; 139 | // Check for MSE support before use 140 | if (mse && typeof mse.isTypeSupported == 'function') { 141 | mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported); 142 | } 143 | 144 | }; 145 | 146 | function disableAV1() { 147 | 148 | 149 | 150 | 151 | // This is the setting to disable AV1 [ 480p (or below) - AV1, above 480p - VP9 ] 152 | // localStorage['yt-player-av1-pref'] = '480'; 153 | try { 154 | Object.defineProperty(localStorage.constructor.prototype, 'yt-player-av1-pref', { 155 | get() { 156 | if (this === localStorage) return '480'; 157 | return this.getItem('yt-player-av1-pref'); 158 | }, 159 | set(nv) { 160 | this.setItem('yt-player-av1-pref', nv); 161 | return true; 162 | }, 163 | enumerable: true, 164 | configurable: true 165 | }); 166 | } catch (e) { 167 | // localStorage['yt-player-av1-pref'] = '480'; 168 | } 169 | 170 | if (localStorage['yt-player-av1-pref'] !== '480') { 171 | 172 | console.warn('Disable YouTube AV1 and VP9', '"yt-player-av1-pref = 480" is not supported in your browser.'); 173 | return; 174 | } 175 | 176 | console.debug("disable-youtube-av1-and-vp9", "AV1 disabled by yt-player-av1-pref = 480"); 177 | 178 | 179 | } 180 | 181 | 182 | disableAV1(); 183 | 184 | // flagConfig(); 185 | supportedFormatsConfig(); 186 | 187 | 188 | 189 | 190 | })(); 191 | -------------------------------------------------------------------------------- /docs/animated-rolling-number.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /docs/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Testing Multiline Textarea 5 | 49 | 50 | 51 |
52 |

Testing Multiline Textarea

53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 |
61 |
62 | 63 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /force-youtube-av1.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Use YouTube AV1 3 | // @description Use AV1 for video playback on YouTube 4 | // @name:zh-TW 使用 YouTube AV1 5 | // @description:zh-TW 使用 AV1 進行 YouTube 影片播放 6 | // @name:zh-HK 使用 YouTube AV1 7 | // @description:zh-HK 使用 AV1 進行 YouTube 影片播放 8 | // @name:zh-CN 使用 YouTube AV1 9 | // @description:zh-CN 使用 AV1 进行 YouTube 视频播放 10 | // @name:ja YouTube AV1 の使用 11 | // @description:ja YouTube の動画再生に AV1 を使用する 12 | // @name:ko YouTube AV1 사용 13 | // @description:ko YouTube의 동영상 재생에 AV1을 사용하기 14 | // @name:vi Sử dụng YouTube AV1 15 | // @description:vi Sử dụng AV1 để phát video trên YouTube 16 | // @name:de YouTube AV1 verwenden 17 | // @description:de Verwende AV1 für die Videowiedergabe auf YouTube 18 | // @name:fr Utiliser YouTube AV1 19 | // @description:fr Utiliser AV1 pour la lecture des vidéos sur YouTube 20 | // @name:it Usa YouTube AV1 21 | // @description:it Usa AV1 per la riproduzione dei video su YouTube 22 | // @name:es Usar AV1 en YouTube 23 | // @description:es Usar AV1 para la reproducción de videos en YouTube 24 | // @namespace http://tampermonkey.net/ 25 | // @version 2.4.3 26 | // @author CY Fung 27 | // @match https://www.youtube.com/* 28 | // @match https://www.youtube.com/embed/* 29 | // @match https://www.youtube-nocookie.com/embed/* 30 | // @exclude https://www.youtube.com/live_chat* 31 | // @exclude https://www.youtube.com/live_chat_replay* 32 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 33 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 34 | // @grant none 35 | // @run-at document-start 36 | // @license MIT 37 | // 38 | // @compatible firefox Violentmonkey 39 | // @compatible firefox Tampermonkey 40 | // @compatible firefox FireMonkey 41 | // @compatible chrome Violentmonkey 42 | // @compatible chrome Tampermonkey 43 | // @compatible opera Violentmonkey 44 | // @compatible opera Tampermonkey 45 | // @compatible safari Stay 46 | // @compatible edge Violentmonkey 47 | // @compatible edge Tampermonkey 48 | // @compatible brave Violentmonkey 49 | // @compatible brave Tampermonkey 50 | // 51 | // @unwrap 52 | // @allFrames true 53 | // @inject-into page 54 | // ==/UserScript== 55 | 56 | 57 | 58 | (function (__Promise__) { 59 | 'use strict'; 60 | 61 | /** @type {globalThis.PromiseConstructor} */ 62 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 63 | 64 | console.debug("force-youtube-av1", "injected"); 65 | 66 | 67 | 68 | const flagConfig = () => { 69 | 70 | let firstDa = true; 71 | let cid = 0; 72 | const { setInterval, clearInterval, setTimeout } = window; 73 | const tn = () => { 74 | 75 | const da = (window.ytcfg && window.ytcfg.data_) ? window.ytcfg.data_ : null; 76 | if (!da) return; 77 | 78 | const isFirstDa = firstDa; 79 | firstDa = false; 80 | 81 | for (const EXPERIMENT_FLAGS of [da.EXPERIMENT_FLAGS, da.EXPERIMENTS_FORCED_FLAGS]) { 82 | 83 | if (EXPERIMENT_FLAGS) { 84 | // EXPERIMENT_FLAGS.html5_disable_av1_hdr = false; 85 | // EXPERIMENT_FLAGS.html5_prefer_hbr_vp9_over_av1 = false; 86 | // EXPERIMENT_FLAGS.html5_account_onesie_format_selection_during_format_filter = false; 87 | } 88 | 89 | } 90 | 91 | if (isFirstDa) { 92 | 93 | 94 | let mo = new MutationObserver(() => { 95 | 96 | mo.disconnect(); 97 | mo.takeRecords(); 98 | mo = null; 99 | setTimeout(() => { 100 | cid && clearInterval.call(window, cid); 101 | cid = 0; 102 | tn(); 103 | }) 104 | }); 105 | mo.observe(document, { subtree: true, childList: true }); 106 | 107 | 108 | } 109 | 110 | 111 | }; 112 | cid = setInterval.call(window, tn); 113 | 114 | }; 115 | 116 | 117 | const supportedFormatsConfig = () => { 118 | 119 | 120 | function typeTest(type) { 121 | 122 | if (typeof type === 'string' && type.startsWith('video/')) { 123 | if (type.includes('av01')) { 124 | if (/codecs[\x20-\x7F]+\bav01\b/.test(type)) return true; 125 | } else if (type.includes('av1')) { 126 | if (/codecs[\x20-\x7F]+\bav1\b/.test(type)) return true; 127 | } 128 | } 129 | 130 | } 131 | 132 | // return a custom MIME type checker that can defer to the original function 133 | function makeModifiedTypeChecker(origChecker, dx) { 134 | // Check if a video type is allowed 135 | return function (type) { 136 | let res = undefined; 137 | if (type === undefined) res = false; 138 | else res = typeTest(type); 139 | if (res === undefined) res = origChecker.apply(this, arguments); 140 | else res = !dx ? res : (res ? "probably" : ""); 141 | 142 | // console.debug(20, type, res) 143 | 144 | return res; 145 | }; 146 | } 147 | 148 | // Override video element canPlayType() function 149 | const proto = (HTMLVideoElement || 0).prototype; 150 | if (proto && typeof proto.canPlayType == 'function') { 151 | proto.canPlayType = makeModifiedTypeChecker(proto.canPlayType, true); 152 | } 153 | 154 | // Override media source extension isTypeSupported() function 155 | const mse = window.MediaSource; 156 | // Check for MSE support before use 157 | if (mse && typeof mse.isTypeSupported == 'function') { 158 | mse.isTypeSupported = makeModifiedTypeChecker(mse.isTypeSupported); 159 | } 160 | 161 | 162 | } 163 | 164 | function enableAV1() { 165 | 166 | 167 | // This is the setting to force AV1 168 | // localStorage['yt-player-av1-pref'] = '8192'; 169 | try { 170 | Object.defineProperty(localStorage.constructor.prototype, 'yt-player-av1-pref', { 171 | get() { 172 | if (this === localStorage) return '8192'; 173 | return this.getItem('yt-player-av1-pref'); 174 | }, 175 | set(nv) { 176 | this.setItem('yt-player-av1-pref', nv); 177 | return true; 178 | }, 179 | enumerable: true, 180 | configurable: true 181 | }); 182 | } catch (e) { 183 | // localStorage['yt-player-av1-pref'] = '8192'; 184 | } 185 | 186 | if (localStorage['yt-player-av1-pref'] !== '8192') { 187 | 188 | console.warn('Use YouTube AV1 is not supported in your browser.'); 189 | return; 190 | } 191 | 192 | 193 | console.debug("force-youtube-av1", "AV1 enabled"); 194 | 195 | 196 | // flagConfig(); 197 | supportedFormatsConfig(); 198 | 199 | 200 | 201 | } 202 | 203 | 204 | 205 | 206 | let promise = null; 207 | 208 | try { 209 | promise = navigator.mediaCapabilities.decodingInfo({ 210 | type: "file", 211 | video: { 212 | contentType: "video/mp4; codecs=av01.0.05M.08.0.110.05.01.06.0", 213 | height: 1080, 214 | width: 1920, 215 | framerate: 30, 216 | bitrate: 2826848, 217 | }, 218 | audio: { 219 | contentType: "audio/webm; codecs=opus", 220 | channels: "2.1", 221 | samplerate: 44100, 222 | bitrate: 255236, 223 | } 224 | }); 225 | } catch (e) { 226 | promise = null; 227 | } 228 | 229 | 230 | const callback = (result) => { 231 | 232 | if (result && result.supported && result.smooth) enableAV1(); 233 | else { 234 | console.warn("force-youtube-av1", 'Your browser does not support AV1. You might conside to use the latest version of Google Chrome or Mozilla FireFox.'); 235 | } 236 | }; 237 | 238 | (promise || Promise.resolve(0)).catch(callback).then(callback); 239 | 240 | 241 | 242 | })(Promise); -------------------------------------------------------------------------------- /google-go-to-cache.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Google Search Go To Cache 3 | // @namespace UserScript 4 | // @version 0.2.0 5 | // @description Show a tooltip with Google Cache link for external links in Google search results 6 | // @author CY Fung 7 | // @license MIT 8 | // @match https://www.google.com/search* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | const filterRule = '#search a[href]' 16 | const forceHTTPS = false; 17 | 18 | 19 | function isGoogleHost(hostname) { 20 | 21 | if (hostname === 'www.google.com') return true; 22 | if (hostname === 'google.com') return true; 23 | if (hostname.endsWith('.google.com')) return true; 24 | return false; 25 | } 26 | 27 | let tooltipTimeout; 28 | const tooltip = document.createElement('div'); 29 | let tooltipInner = document.createElement('div'); 30 | tooltipInner.className = 'cache-tooltip-inner' 31 | tooltip.className = 'cache-tooltip'; 32 | tooltipInner.textContent = 'Cache page'; 33 | tooltip.style.position = 'absolute'; 34 | tooltipInner.style.backgroundColor = 'black'; 35 | tooltipInner.style.color = 'white'; 36 | tooltipInner.style.padding = '5px'; 37 | tooltipInner.style.borderRadius = '5px'; 38 | tooltip.style.zIndex = '1000'; 39 | tooltip.style.cursor = 'pointer'; 40 | tooltip.style.display = 'none'; 41 | tooltipInner.style.display='inline-block' 42 | tooltip.appendChild(tooltipInner); 43 | 44 | tooltip.addEventListener('click', function() { 45 | window.location.href = tooltip.dataset.cacheLink; 46 | }); 47 | 48 | document.body.appendChild(tooltip); 49 | 50 | document.addEventListener('mouseenter', function(event) { 51 | if (event.target.tagName === 'A' && event.target.href && event.target.matches(filterRule)) { 52 | const link = event.target.href; 53 | const url = new URL(link); 54 | if (!isGoogleHost(url.hostname)) { 55 | 56 | clearTimeout(tooltipTimeout); 57 | showTooltip(event.target); 58 | } 59 | } 60 | }, true); 61 | 62 | document.addEventListener('mouseleave', function(event) { 63 | if (event.target.tagName === 'A' && event.target.href) { 64 | hideTooltipWithDelay(); 65 | } 66 | }, true); 67 | 68 | tooltip.addEventListener('mouseleave', function() { 69 | hideTooltipWithDelay(); 70 | }); 71 | 72 | tooltip.addEventListener('mouseenter', function() { 73 | clearTimeout(tooltipTimeout); 74 | }); 75 | 76 | let cssAdded = false; 77 | function showTooltip(element) { 78 | 79 | if(!cssAdded){ 80 | cssAdded = true; 81 | document.head.appendChild(document.createElement('style')).textContent = ` 82 | 83 | .cache-tooltip{ 84 | opacity: 0.75; 85 | user-select: none; 86 | z-index: 999; 87 | } 88 | .cache-tooltip:hover { 89 | opacity: 1; 90 | } 91 | 92 | 93 | ` 94 | } 95 | 96 | let link = element.href; 97 | if(forceHTTPS) link = link.replace(/^http\:\/\//, 'https://'); 98 | const cacheLink = `https://webcache.googleusercontent.com/search?q=cache:${encodeURIComponent(link)}`; 99 | 100 | tooltip.dataset.cacheLink = cacheLink; 101 | const rect = element.getBoundingClientRect(); 102 | tooltip.style.top = `${rect.top + window.scrollY + rect.height}px`; 103 | tooltip.style.left = `${rect.left + window.scrollX}px`; 104 | tooltip.style.display = 'block'; 105 | tooltip.style.width = `${rect.width}px`; 106 | } 107 | 108 | function hideTooltipWithDelay() { 109 | clearTimeout(tooltipTimeout); 110 | tooltipTimeout = setTimeout(() => { 111 | tooltip.style.display = 'none'; 112 | }, 160); // Delay before hiding the tooltip 113 | } 114 | })(); 115 | -------------------------------------------------------------------------------- /icons/YouTube-Audio-Only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/YouTube-Audio-Only.png -------------------------------------------------------------------------------- /icons/blank-letter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/blank-letter.png -------------------------------------------------------------------------------- /icons/brave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/brave.png -------------------------------------------------------------------------------- /icons/cookie-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/cookie-manager.png -------------------------------------------------------------------------------- /icons/disable-youtube-autopause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /icons/drag-drop-image-uploader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/general-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/general-icon.png -------------------------------------------------------------------------------- /icons/index.html: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /icons/selection-copier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/selection-copier.png -------------------------------------------------------------------------------- /icons/super-fast-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/super-fast-chat.png -------------------------------------------------------------------------------- /icons/twitter-original.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icons/web-cpu-tamer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/youtube-cpu-tamper-by-animationframe.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/youtube-cpu-tamper-by-animationframe.webp -------------------------------------------------------------------------------- /icons/youtube-minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/youtube-minimal.png -------------------------------------------------------------------------------- /icons/youtube-unlock-indexedDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/youtube-unlock-indexedDB.png -------------------------------------------------------------------------------- /icons/youtube-video-resize-fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/youtube-video-resize-fix.png -------------------------------------------------------------------------------- /icons/yt-engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/icons/yt-engine.png -------------------------------------------------------------------------------- /images/stylus-text-settings-example-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/stylus-text-settings-example-01.png -------------------------------------------------------------------------------- /images/youtube-autopause-en.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/youtube-autopause-en.webp -------------------------------------------------------------------------------- /images/youtube-autopause-jp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/youtube-autopause-jp.webp -------------------------------------------------------------------------------- /images/youtube-minimal-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/youtube-minimal-preview.png -------------------------------------------------------------------------------- /images/youtube-unlock-indexedDB-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/youtube-unlock-indexedDB-1.png -------------------------------------------------------------------------------- /images/youtube-unlock-indexedDB-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfung1031/userscript-supports/eefaf68e3b2fe39728693a265c96b15751e491c3/images/youtube-unlock-indexedDB-2.png -------------------------------------------------------------------------------- /library/WinComm.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 cyfung1031 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | !(function (exports) { 25 | 'use strict'; 26 | 27 | /** 28 | * @typedef {Object} HandlerData 29 | * @prop {string} action 30 | * @prop {string | number} communicationId 31 | * @prop {Object?} data 32 | * @prop {(string | number)?} callbackId 33 | * @typedef {(d: Partial, evt: MessageEvent)} Handler 34 | * @typedef {Object.} Handlers 35 | */ 36 | 37 | /** 38 | * A map to store callback functions. 39 | * @type {Map void>} 40 | */ 41 | const callbacks = new Map(); 42 | let callbackUd = 1; 43 | 44 | /** 45 | * Creates an instance for communication. 46 | * @function 47 | * @memberof WinComm 48 | * @param {string | number} communicationId - ID for communication. 49 | * @returns {WinCommInstance} An instance of communication. 50 | */ 51 | const createInstance = (communicationId) => { 52 | 53 | /** @param {string} key @param {Handlers} handlers @param {string} origin */ 54 | const hook = (key, handlers, origin = location.origin) => { 55 | if (window[key]) { 56 | window.removeEventListener('message', window[key], false); 57 | } else { 58 | window[key] = (evt) => window[key].handleEvent(evt); 59 | } 60 | /** @param {MessageEvent} evt */ 61 | window[key].handleEvent = (evt) => { 62 | if (!evt || evt.origin !== origin) return; 63 | const d = evt.data; 64 | if (d.communicationId !== communicationId) return; 65 | /** @type {Function?} */ 66 | const handler = handlers[d.action]; 67 | if (typeof handler === 'function') { 68 | handler(d, evt); 69 | } 70 | } 71 | window.addEventListener('message', window[key], false); 72 | }; 73 | 74 | /** @param {string} key @param {boolean} removal */ 75 | const unhook = (key, removal = true) => { 76 | window.removeEventListener('message', window[key], false); 77 | removal && (window[key] = null); 78 | } 79 | 80 | /** @param {string} action @param {any} data @param {string} origin */ 81 | const send = (action, data, origin = location.origin) => { 82 | window.postMessage({ 83 | communicationId, 84 | action, 85 | data 86 | }, origin); 87 | } 88 | 89 | /** @param {string} action @param {any} data @param {string} origin */ 90 | const request = (action, data, origin = location.origin) => { 91 | return new Promise(resolve => { 92 | if (callbackUd > 8e5) callbackUd = callbackUd % 1e2; 93 | const callbackId = callbackUd++; 94 | callbacks.set(callbackId, resolve); 95 | window.postMessage({ 96 | communicationId, 97 | callbackId, 98 | action, 99 | data 100 | }, origin); 101 | }); 102 | } 103 | 104 | /** @param {MessageEvent} evt */ 105 | const response = (evt, action, data) => { 106 | evt.source.postMessage({ 107 | communicationId, 108 | action, 109 | data, 110 | callbackId: evt.data.callbackId 111 | }, evt.origin); 112 | } 113 | 114 | /** @param {Partial} d @param {MessageEvent} evt */ 115 | const handleResponse = (d, evt) => { 116 | const c = d.callbackId; 117 | const f = callbacks.get(c); 118 | if (f) { 119 | callbacks.delete(c); 120 | f(d); 121 | } 122 | } 123 | 124 | const res = { 125 | hook, 126 | unhook, 127 | send, 128 | request, 129 | response, 130 | handleResponse 131 | }; 132 | 133 | /** @typedef { typeof res } WinCommInstance */ 134 | 135 | return res 136 | 137 | } 138 | 139 | /** 140 | * Generates a new communication ID. 141 | * @function 142 | * @memberof WinComm 143 | * @returns {string} A new communication ID. 144 | */ 145 | const newCommunicationId = () => `${String.fromCharCode(Date.now() % 26 + 97)}${Math.floor(Math.random() * 982451653 + 982451653).toString(36)}`; 146 | 147 | exports.createInstance = createInstance; 148 | exports.newCommunicationId = newCommunicationId; 149 | 150 | })(this.WinComm || (this.WinComm = {})); 151 | -------------------------------------------------------------------------------- /library/codejar-cursor.esm.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 Anton Medvedev 5 | Copyright (c) 2025 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | /** 27 | * Returns position of cursor on the page. 28 | * @param toStart Position of beginning of selection or end of selection. 29 | */ 30 | export function cursorPosition(toStart = true) { 31 | const s = window.getSelection(); 32 | if (s.rangeCount > 0) { 33 | const cursor = document.createElement("span"); 34 | cursor.textContent = "|"; 35 | const r = s.getRangeAt(0).cloneRange(); 36 | r.collapse(toStart); 37 | r.insertNode(cursor); 38 | const { x, y, height } = cursor.getBoundingClientRect(); 39 | const top = (window.scrollY + y + height) + "px"; 40 | const left = (window.scrollX + x) + "px"; 41 | cursor.parentNode.removeChild(cursor); 42 | return { top, left }; 43 | } 44 | return undefined; 45 | } 46 | /** 47 | * Returns selected text. 48 | */ 49 | export function selectedText() { 50 | const s = window.getSelection(); 51 | if (s.rangeCount === 0) 52 | return ''; 53 | return s.getRangeAt(0).toString(); 54 | } 55 | /** 56 | * Returns text before the cursor. 57 | * @param editor Editor DOM node. 58 | */ 59 | export function textBeforeCursor(editor) { 60 | const s = window.getSelection(); 61 | if (s.rangeCount === 0) 62 | return ''; 63 | const r0 = s.getRangeAt(0); 64 | const r = document.createRange(); 65 | r.selectNodeContents(editor); 66 | r.setEnd(r0.startContainer, r0.startOffset); 67 | return r.toString(); 68 | } 69 | /** 70 | * Returns text after the cursor. 71 | * @param editor Editor DOM node. 72 | */ 73 | export function textAfterCursor(editor) { 74 | const s = window.getSelection(); 75 | if (s.rangeCount === 0) 76 | return ''; 77 | const r0 = s.getRangeAt(0); 78 | const r = document.createRange(); 79 | r.selectNodeContents(editor); 80 | r.setStart(r0.endContainer, r0.endOffset); 81 | return r.toString(); 82 | } 83 | -------------------------------------------------------------------------------- /library/codejar-cursor.umd.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 Anton Medvedev 5 | Copyright (c) 2025 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | (function (global, factory) { 27 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 28 | typeof define === 'function' && define.amd ? define(factory) : 29 | (global.CursorUtils = factory()); 30 | }(this, (function () { 31 | 'use strict'; 32 | 33 | function cursorPosition(toStart = true) { 34 | const s = window.getSelection(); 35 | if (s.rangeCount > 0) { 36 | const cursor = document.createElement("span"); 37 | cursor.textContent = "|"; 38 | const r = s.getRangeAt(0).cloneRange(); 39 | r.collapse(toStart); 40 | r.insertNode(cursor); 41 | const { x, y, height } = cursor.getBoundingClientRect(); 42 | const top = (window.scrollY + y + height) + "px"; 43 | const left = (window.scrollX + x) + "px"; 44 | cursor.parentNode.removeChild(cursor); 45 | return { top, left }; 46 | } 47 | return undefined; 48 | } 49 | 50 | function selectedText() { 51 | const s = window.getSelection(); 52 | if (s.rangeCount === 0) 53 | return ''; 54 | return s.getRangeAt(0).toString(); 55 | } 56 | 57 | function textBeforeCursor(editor) { 58 | const s = window.getSelection(); 59 | if (s.rangeCount === 0) 60 | return ''; 61 | const r0 = s.getRangeAt(0); 62 | const r = document.createRange(); 63 | r.selectNodeContents(editor); 64 | r.setEnd(r0.startContainer, r0.startOffset); 65 | return r.toString(); 66 | } 67 | 68 | function textAfterCursor(editor) { 69 | const s = window.getSelection(); 70 | if (s.rangeCount === 0) 71 | return ''; 72 | const r0 = s.getRangeAt(0); 73 | const r = document.createRange(); 74 | r.selectNodeContents(editor); 75 | r.setStart(r0.endContainer, r0.endOffset); 76 | return r.toString(); 77 | } 78 | 79 | return { 80 | cursorPosition: cursorPosition, 81 | selectedText: selectedText, 82 | textBeforeCursor: textBeforeCursor, 83 | textAfterCursor: textAfterCursor 84 | }; 85 | }))); -------------------------------------------------------------------------------- /library/default-trusted-type-policy.js: -------------------------------------------------------------------------------- 1 | if (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy === null) { 2 | let s = s => s; 3 | trustedTypes.createPolicy('default', { createHTML: s, createScriptURL: s, createScript: s }); 4 | } -------------------------------------------------------------------------------- /library/jmt_setImmediate.js: -------------------------------------------------------------------------------- 1 | // ======================================= setImmediate ======================================= 2 | // 3 | // GitHub: https://github.com/YuzuJS/setImmediate 4 | // based on version 1.0.5 - https://cdnjs.cloudflare.com/ajax/libs/setImmediate/1.0.5/setImmediate.js 5 | // modified by CY Fung => version 1.1.0 6 | // ES2015+ without supporting IE; adjusted `currentlyRunningATask` behavior 7 | /** 8 | * 9 | * 10 | Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | * 31 | * 32 | */ 33 | 34 | (function (global, undefined) { 35 | "use strict"; 36 | 37 | var attachTo = (global.jmt || (global.jmt = {})); 38 | 39 | if (global.setImmediate) { // Node, Deno, Bun, etc 40 | return; 41 | } 42 | 43 | var nextHandle = 1; // Spec says greater than zero 44 | /** 45 | * @typedef {Object} Task 46 | * @property {Function} callback - Callback Function 47 | * @property {any[] | number} args - Callback Arguments 48 | */ 49 | var /** @type { Map } */ tasksByHandle = new Map(); 50 | var currentlyRunningATask = false; 51 | var currentlyRunningATaskEnable = typeof AbortSignal !== 'function'; 52 | // Ignoring currentlyRunningATask in Chrome 66+, Edge 16+, Firefox 57+, Opera 53+, Safari 11.1+ 53 | var doc = global.document; 54 | var /** @type { (handle: number) => void 0 } */ registerImmediate; 55 | 56 | /** @param { Function | string } callback */ 57 | function setImmediate(callback, ...args) { 58 | // Callback can either be a function or a string 59 | if (typeof callback !== "function") { 60 | callback = new Function(`${callback}`); 61 | } 62 | // Store and register the task 63 | var task = { callback: callback, args: args.length ? args : 0 }; 64 | tasksByHandle.set(nextHandle, task); 65 | registerImmediate(nextHandle); 66 | return nextHandle++; 67 | } 68 | 69 | function clearImmediate(handle) { 70 | tasksByHandle.delete(handle); 71 | } 72 | 73 | function run(task) { 74 | var callback = task.callback; 75 | var args = task.args; 76 | switch (args.length) { 77 | case undefined: 78 | case 0: 79 | callback(); 80 | break; 81 | case 1: 82 | callback(args[0]); 83 | break; 84 | case 2: 85 | callback(args[0], args[1]); 86 | break; 87 | case 3: 88 | callback(args[0], args[1], args[2]); 89 | break; 90 | default: 91 | callback.apply(undefined, args); 92 | break; 93 | } 94 | } 95 | 96 | function runIfPresent(handle) { 97 | // From the spec: "Wait until any invocations of this algorithm started before this one have completed." 98 | // So if we're currently running a task, we'll need to delay this invocation. 99 | if (currentlyRunningATaskEnable && currentlyRunningATask) { 100 | // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a 101 | // "too much recursion" error. 102 | setTimeout(runIfPresent, 0, handle); 103 | } else { 104 | var task = tasksByHandle.get(handle); 105 | if (task) { 106 | currentlyRunningATask = true; 107 | try { 108 | run(task); 109 | } finally { 110 | clearImmediate(handle); 111 | currentlyRunningATask = false; 112 | } 113 | } 114 | } 115 | } 116 | 117 | function installNextTickImplementation() { 118 | registerImmediate = function (handle) { 119 | process.nextTick(function () { runIfPresent(handle); }); 120 | }; 121 | } 122 | 123 | function canUsePostMessage() { 124 | // The test against `importScripts` prevents this implementation from being installed inside a web worker, 125 | // where `global.postMessage` means something completely different and can't be used for this purpose. 126 | if (global.postMessage && !global.importScripts) { 127 | var postMessageIsAsynchronous = true; 128 | var mfn = function () { 129 | postMessageIsAsynchronous = false; 130 | } 131 | global.addEventListener('message', mfn, false); 132 | global.postMessage("", "*"); 133 | global.removeEventListener('message', mfn, false); 134 | return postMessageIsAsynchronous; 135 | } 136 | } 137 | 138 | function installPostMessageImplementation() { 139 | // Installs an event handler on `global` for the `message` event: see 140 | // * https://developer.mozilla.org/en/DOM/window.postMessage 141 | // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages 142 | 143 | var messagePrefix = `setImmediate$${Math.random()}$`; 144 | var onGlobalMessage = function (event) { 145 | if (event.source === global) { 146 | var data = event.data; 147 | if (typeof data === "string" && data.startsWith(messagePrefix)) { 148 | runIfPresent(+data.slice(messagePrefix.length)); 149 | } 150 | } 151 | }; 152 | 153 | global.addEventListener("message", onGlobalMessage, false); 154 | 155 | registerImmediate = function (handle) { 156 | global.postMessage(`${messagePrefix}${handle}`, "*"); 157 | }; 158 | } 159 | 160 | function installMessageChannelImplementation() { 161 | var channel = new MessageChannel(); 162 | channel.port1.onmessage = function (event) { 163 | var handle = event.data; 164 | runIfPresent(handle); 165 | }; 166 | 167 | registerImmediate = function (handle) { 168 | channel.port2.postMessage(handle); 169 | }; 170 | } 171 | 172 | function installSetTimeoutImplementation() { 173 | registerImmediate = function (handle) { 174 | setTimeout(runIfPresent, 0, handle); 175 | }; 176 | } 177 | 178 | // Don't get fooled by e.g. browserify environments. 179 | if ({}.toString.call(global.process) === "[object process]") { 180 | // For Node.js before 0.9 181 | installNextTickImplementation(); 182 | 183 | } else if (canUsePostMessage() && global.addEventListener) { 184 | // For modern browsers 185 | installPostMessageImplementation(); 186 | 187 | } else if (global.MessageChannel) { 188 | // For web workers, where supported 189 | installMessageChannelImplementation(); 190 | 191 | } else { 192 | // For older browsers 193 | installSetTimeoutImplementation(); 194 | } 195 | 196 | attachTo.setImmediate = setImmediate; 197 | attachTo.clearImmediate = clearImmediate; 198 | }(typeof self === "undefined" ? typeof global === "undefined" ? this : global : self)); 199 | 200 | // 201 | // ======================================= setImmediate ======================================= 202 | -------------------------------------------------------------------------------- /library/misc.js: -------------------------------------------------------------------------------- 1 | /* general library */ 2 | /* for record purpose */ 3 | 4 | 5 | const inPlaceArrayPush = (() => { 6 | 7 | /* 8 | const n = 110099 9 | let v1 = new Array(n).fill(0).map(e=>Math.random().toFixed(4)); 10 | 11 | let v2 = new Array(n).fill(0).map(e=>Math.random().toFixed(4)); 12 | 13 | 14 | // v2.push.apply(v2, v1) 15 | v2.push(...v1); 16 | 17 | console.log(v2.length) 18 | */ 19 | 20 | 21 | 22 | // chatgpt: The safe number for the length of a list when using Array.prototype.apply with a large array 23 | // to avoid a "RangeError: Maximum call stack size exceeded" error is generally around 100,000. 24 | // However, this limit can vary depending on the JavaScript engine and the environment 25 | // in which it is running (e.g., browser, Node.js). 26 | // For a more conservative and safer approach, it's recommended to use a lower limit, around 50,000 elements. 27 | 28 | 29 | // ---------------------------------------------------------------------------------------------------------------- 30 | 31 | /* 32 | 33 | 34 | let n1 = 50000, n2 = 150000; 35 | 36 | function binarySearch(low, high, evaluate) { 37 | let result = -1; // Default result if no such n is found 38 | 39 | while (low <= high) { 40 | const mid = Math.floor((low + high) / 2); 41 | const value = evaluate(mid); 42 | 43 | if (value) { 44 | result = mid; // Record the largest found n 45 | low = mid + 1; // Continue to search in the upper half 46 | } else { 47 | high = mid - 1; // Search in the lower half 48 | } 49 | } 50 | 51 | return result; // Return the largest n found 52 | } 53 | 54 | const evaluate = (n)=>{ 55 | 56 | 57 | let v1 = new Array(n).fill(0).map(e=>Math.random().toFixed(4)); 58 | 59 | let v2 = new Array(n).fill(0).map(e=>Math.random().toFixed(4)); 60 | 61 | 62 | // v2.push.apply(v2, v1) 63 | try{ 64 | v2.push(...v1); 65 | }catch(e){} 66 | 67 | return v2.length === 2*n; 68 | 69 | } 70 | let res = binarySearch(n1, n2, evaluate); 71 | console.log(res) 72 | 73 | */ 74 | 75 | 76 | // MAX(n) = 110065 in Macbook M1 Pro Brave 77 | // MAX(n) = 110069 in Macbook M1 Pro Chrome 78 | // MAX(n) = 65536 in Macbook M1 Pro Orion 79 | // MAX(n) = 65536 in Macbook M1 Pro Webkit 80 | // MAX(n) = 150000 in Macbook M1 Pro Firefox 81 | // MAX(n) = 110221 in Macbook M1 Pro min 82 | // MAX(n) = 110057 in Macbook M1 Pro Edge 83 | // MAX(n) = 500000 in Macbook M1 Pro Waterfox Classic (n2=950000) 84 | // MAX(n) = 110013 in Macbook M1 Pro Opera 85 | 86 | // 50000 / 65536 = 76.3% > 20% buffer OK 87 | 88 | // ---------------------------------------------------------------------------------------------- 89 | 90 | // Mobile (Android) 91 | 92 | // Brave 93 | // 119289 94 | 95 | // Firefox Focus 96 | // 150000 97 | 98 | // Via 99 | // 110285 100 | 101 | // Kiwi 102 | // 110259 103 | 104 | // ---------------------------------------------------------------------------------------------- 105 | 106 | 107 | 108 | 109 | /* 110 | return function (dest, source) { 111 | const LIMIT_N = 50000; 112 | let len; 113 | while ((len = source.length) > 0) { 114 | if (len > LIMIT_N) { 115 | dest.push(...source.slice(0, LIMIT_N)); 116 | source = source.slice(LIMIT_N); 117 | } else { 118 | dest.push(...source); 119 | break; 120 | } 121 | } 122 | } 123 | */ 124 | 125 | /* 126 | 127 | function optimizedPush(dest, source) { 128 | const LIMIT_N = 50000; 129 | let start = 0; 130 | const len = source.length; 131 | 132 | while (start < len) { 133 | const end = Math.min(start + LIMIT_N, len); 134 | dest.push.apply(dest, source.slice(start, end)); 135 | start = end; 136 | } 137 | } 138 | 139 | */ 140 | 141 | const LIMIT_N = typeof AbortSignal !== 'undefined' && typeof (AbortSignal || 0).timeout === 'function' ? 50000 : 10000; 142 | return function (dest, source) { 143 | let index = 0; 144 | const len = source.length; 145 | while (index < len) { 146 | let chunkSize = len - index; // chunkSize > 0 147 | if (chunkSize > LIMIT_N) { 148 | chunkSize = LIMIT_N; 149 | dest.push(...source.slice(index, index + chunkSize)); 150 | } else if (index > 0) { // to the end 151 | dest.push(...source.slice(index)); 152 | } else { // normal push.apply 153 | dest.push(...source); 154 | } 155 | index += chunkSize; 156 | } 157 | } 158 | 159 | 160 | })(); -------------------------------------------------------------------------------- /library/nextBrowserTick.js: -------------------------------------------------------------------------------- 1 | var nextBrowserTick = typeof nextBrowserTick !== "undefined" && nextBrowserTick.version >= 2 ? nextBrowserTick : (() => { 2 | "use strict"; 3 | const world = typeof self !== "undefined" ? self : typeof global !== "undefined" ? global : this; 4 | 5 | let ok = true; 6 | function canUsePostMessage(e) { 7 | if (e) return (ok = false); 8 | if (world.postMessage && !world.importScripts && world.addEventListener) { 9 | world.addEventListener('message', canUsePostMessage, false); 10 | world.postMessage("$$$", "*"); 11 | world.removeEventListener('message', canUsePostMessage, false); 12 | return ok; 13 | } 14 | } 15 | 16 | if (!canUsePostMessage()) { 17 | console.warn("Your browser environment cannot use nextBrowserTick"); 18 | return; 19 | } 20 | 21 | /** @type {globalThis.PromiseConstructor} */ 22 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 23 | 24 | let promise = null; 25 | const fns = new Map(); 26 | 27 | const {floor, random} = Math; 28 | 29 | let tmp; 30 | do { 31 | tmp = `$$nextBrowserTick$$${(random() + 8).toString().slice(2)}$$`; 32 | } while (tmp in world); 33 | const messageString = tmp; 34 | const p = messageString.length + 9; 35 | world[messageString] = 1; 36 | const mfn = (evt) => { 37 | if (fns.size !== 0) { 38 | const data = (evt || 0).data; 39 | if (typeof data === 'string' && data.length === p && evt.source === (evt.target || 1)) { 40 | const fn = fns.get(data); 41 | if (fn) { 42 | if (data[0] === 'p') promise = null; 43 | fns.delete(data); 44 | fn(); 45 | } 46 | } 47 | } 48 | }; 49 | world.addEventListener('message', mfn, false); 50 | 51 | const g = (f = fns) => { 52 | if (f === fns) { 53 | if (promise) return promise; 54 | let code; 55 | do { 56 | code = `p${messageString}${floor(random() * 314159265359 + 314159265359).toString(36)}`; 57 | } while (fns.has(code)); 58 | promise = new Promise(resolve => { 59 | fns.set(code, resolve); 60 | }); 61 | world.postMessage(code, "*"); 62 | code = null; 63 | return promise; 64 | } else { 65 | let code; 66 | do { 67 | code = `f${messageString}${floor(random() * 314159265359 + 314159265359).toString(36)}`; 68 | } while (fns.has(code)); 69 | fns.set(code, f); 70 | world.postMessage(code, "*"); 71 | } 72 | }; 73 | g.version = 2; 74 | return g; 75 | 76 | })(); -------------------------------------------------------------------------------- /library/simple-userjs.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name SimpleUserJS 3 | // @description To provide stuffs for UserJS 4 | // @author CY Fung 5 | // @version 0.1.0 6 | // @supportURL https://github.com/cyfung1031/userscript-supports/ 7 | // @license MIT 8 | // @match https://*/* 9 | // ==/UserScript== 10 | 11 | /* 12 | 13 | MIT License 14 | 15 | Copyright (c) 2024 cyfung1031 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | */ 36 | 37 | (() => { 38 | 39 | const __win__ = typeof unsafeWindow !== 'undefined' ? unsafeWindow : (this instanceof Window ? this : window); 40 | 41 | if (!__win__.SimpleUserJS) { 42 | const window = __win__; 43 | __win__.SimpleUserJS = (win) => { 44 | win = win || window; 45 | 46 | const observablePromise = (proc, timeoutPromise) => { 47 | let promise = null; 48 | return { 49 | obtain() { 50 | if (!promise) { 51 | promise = new Promise(resolve => { 52 | let mo = null; 53 | const f = () => { 54 | let t = proc(); 55 | if (t) { 56 | mo.disconnect(); 57 | mo.takeRecords(); 58 | mo = null; 59 | resolve(t); 60 | } 61 | } 62 | mo = new MutationObserver(f); 63 | mo.observe(document, { subtree: true, childList: true }) 64 | f(); 65 | timeoutPromise && timeoutPromise.then(() => { 66 | resolve(null) 67 | }); 68 | }); 69 | } 70 | return promise 71 | } 72 | } 73 | } 74 | 75 | win.SimpleUserJS = Object.assign(win.SimpleUserJS || {}, { 76 | css(text) { 77 | const css = document.createElement('style'); 78 | css.textContent = `${text}`; 79 | observablePromise(() => document.head).obtain().then((head) => { 80 | head && head.appendChild(css); 81 | }); 82 | return css; 83 | }, 84 | findAll(selector, callback) { 85 | const wm = new WeakSet(); 86 | observablePromise(() => { 87 | for (const s of document.querySelectorAll(selector)) { 88 | if (wm.has(s)) return; 89 | wm.add(s); 90 | callback(s); 91 | } 92 | }).obtain() 93 | } 94 | }); 95 | 96 | }; 97 | 98 | } 99 | 100 | })(); 101 | -------------------------------------------------------------------------------- /library/structurize.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 CY FUNG 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | */ 11 | 12 | function structurize(htmlText) { 13 | // FOR DEBUG ONLY 14 | const NODE_TYPES = new Map() 15 | for (const [key, desc] of Object.entries(Object.getOwnPropertyDescriptors(Node))) { 16 | if (typeof desc.value === 'number' && desc.writable === false && desc.enumerable === true && desc.configurable === false) { 17 | if (key.endsWith('_NODE')) { 18 | NODE_TYPES.set(desc.value, key) 19 | } 20 | } 21 | } 22 | function prettyElm(/** @type {Element} */ elm) { 23 | if (!elm || !elm.nodeName) return null; 24 | const eId = elm.id || null; 25 | const eClassList = elm.classList || null; 26 | return [elm.nodeName.toLowerCase(), typeof eId == 'string' ? "#" + eId : '', eClassList && eClassList.length > 0 ? '.' + [...eClassList].join('.') : ''].join(''); 27 | } 28 | let template = document.createElement('template') 29 | template.innerHTML = htmlText 30 | let frag = template.content 31 | function looper( /** @type {DocumentFragment | Document | Node | HTMLElement | null} */ elm, parent) { 32 | if (!elm) return 33 | 34 | if (elm.nodeType === 3) { 35 | return { 36 | type: NODE_TYPES.get(elm.nodeType) || elm.nodeType, 37 | text: elm.textContent 38 | } 39 | } else if (elm.nodeType !== 1 && (elm.childNodes || []).length === 0) { 40 | return { 41 | type: NODE_TYPES.get(elm.nodeType) || elm.nodeType, 42 | } 43 | } 44 | 45 | let res = { 46 | type: NODE_TYPES.get(elm.nodeType) || elm.nodeType, 47 | } 48 | 49 | let childs = [] 50 | let noChild = true 51 | if (elm.nodeName === 'SCRIPT' || elm.nodeName === 'STYLE') { 52 | noChild = false 53 | 54 | } else { 55 | if ('childNodes' in elm) { 56 | for (const node of elm.childNodes) { 57 | childs.push(looper(node, elm)) 58 | noChild = false 59 | } 60 | } 61 | } 62 | 63 | if (elm.nodeType === 1) { 64 | res.selector = prettyElm(elm) 65 | /** @type {NamedNodeMap} */ 66 | let attributes = elm.attributes 67 | res.selector += [...attributes].map(attribute => { 68 | if (attribute.name === 'id' || attribute.name === 'class') return null 69 | if (attribute.name === 'src' || attribute.name === 'href') return null 70 | if (attribute.name === 'rel' || attribute.name === 'content') return null 71 | if (attribute.name.length < 10) return attribute.name 72 | else res[`[${attribute.name}]`] = attribute.value 73 | return null 74 | }).filter(e => !!e).map(s => `[${s}]`).join('') 75 | function setResValues(k) { 76 | let t = elm.getAttribute(k) 77 | const N = 220 78 | const M = Math.round(N / 2 - 5) 79 | if (t.length > N) { 80 | t = `${t.substring(0, M)} ... ${t.substring(t.length - M, t.length)}` 81 | } 82 | res[k] = t 83 | } 84 | if (elm.nodeName === 'META' && elm.hasAttribute('content')) setResValues('content') 85 | if (elm.hasAttribute('href')) setResValues('href') 86 | else if (elm.hasAttribute('src')) setResValues('src') 87 | } 88 | 89 | if (noChild === true) { 90 | res.noChild = true 91 | } else if (childs.length > 0) { 92 | if (childs.length === 1 && childs[0].type === 'TEXT_NODE') { 93 | res.text = childs[0].text 94 | } else if (childs.length === 1) { 95 | res.child = childs[0] 96 | } else { 97 | res.childs = childs 98 | } 99 | } 100 | 101 | return res 102 | } 103 | 104 | console.log(JSON.stringify(looper(frag, null), null, 2)) 105 | } 106 | 107 | if (typeof module !== 'undefined') { 108 | module.exports = structurize 109 | } 110 | -------------------------------------------------------------------------------- /library/ytConfigHacks.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ytConfigHacks 3 | // @description To provide a way to hack the yt.config_ such as EXPERIMENT_FLAGS 4 | // @author CY Fung 5 | // @version 0.4.3 6 | // @supportURL https://github.com/cyfung1031/userscript-supports/ 7 | // @license MIT 8 | // @match https://www.youtube.com/* 9 | // ==/UserScript== 10 | 11 | /* 12 | 13 | MIT License 14 | 15 | Copyright (c) 2021-2023 cyfung1031 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | */ 36 | 37 | (() => { 38 | 39 | const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : (this instanceof Window ? this : window); 40 | 41 | if (!win._ytConfigHacks) { 42 | 43 | let remainingCalls = 4; 44 | 45 | /** @extends {Set} */ 46 | class YtConfigHacks extends Set { 47 | add(value) { 48 | if (remainingCalls <= 0) return console.warn("yt.config_ is already applied on the page."); 49 | if (typeof value === 'function') super.add(value); 50 | } 51 | } 52 | 53 | /** @type {globalThis.PromiseConstructor} */ 54 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 55 | 56 | const _ytConfigHacks = win._ytConfigHacks = new YtConfigHacks(); 57 | 58 | let restoreOriginalYtCSI = () => { 59 | const originalYtcsi = win.ytcsi.originalYtcsi; 60 | if (originalYtcsi) { 61 | // delete win.ytcsi; // optional 62 | win.ytcsi = originalYtcsi; 63 | restoreOriginalYtCSI = null; 64 | } 65 | }; 66 | 67 | let isValidConfig = null; 68 | const detectConfigDone = () => { 69 | if (remainingCalls >= 1) { 70 | const config = (win.yt || 0).config_ || (win.ytcfg || 0).data_ || 0; 71 | if (typeof config.INNERTUBE_API_KEY === 'string' && typeof config.EXPERIMENT_FLAGS === 'object') { 72 | if (--remainingCalls <= 0) restoreOriginalYtCSI && restoreOriginalYtCSI(); 73 | isValidConfig = true; 74 | for (const hook of _ytConfigHacks) { 75 | hook(config); 76 | } 77 | } 78 | } 79 | }; 80 | 81 | let restoreOriginalYtCSICount = 1; 82 | const hookIntoYtCSI = (ytcsi) => { 83 | if (ytcsi = (ytcsi || win.ytcsi)) { 84 | // delete win.ytcsi; // optional 85 | win.ytcsi = new Proxy(ytcsi, { 86 | get(target, prop, receiver) { 87 | if (prop === 'originalYtcsi') return target; 88 | detectConfigDone(); 89 | if (isValidConfig && --restoreOriginalYtCSICount <= 0) restoreOriginalYtCSI && restoreOriginalYtCSI(); 90 | return target[prop]; 91 | } 92 | }); 93 | return true; 94 | } 95 | }; 96 | 97 | hookIntoYtCSI() || Object.defineProperty(win, 'ytcsi', { 98 | get() { 99 | return undefined; 100 | }, 101 | set(newValue) { 102 | if (newValue) { 103 | delete win.ytcsi; 104 | hookIntoYtCSI(newValue); 105 | } 106 | return true; 107 | }, 108 | enumerable: false, 109 | configurable: true, 110 | }); 111 | 112 | const { addEventListener, removeEventListener } = Document.prototype; 113 | 114 | new Promise(resolve => { 115 | if (typeof AbortSignal !== "undefined") { 116 | addEventListener.call(document, 'yt-page-data-fetched', resolve, { once: true }); 117 | addEventListener.call(document, 'yt-navigate-finish', resolve, { once: true }); 118 | addEventListener.call(document, 'spfdone', resolve, { once: true }); 119 | } else { 120 | const eventTriggerFn = () => { 121 | resolve(); 122 | removeEventListener.call(document, 'yt-page-data-fetched', eventTriggerFn, false); 123 | removeEventListener.call(document, 'yt-navigate-finish', eventTriggerFn, false); 124 | removeEventListener.call(document, 'spfdone', eventTriggerFn, false); 125 | }; 126 | addEventListener.call(document, 'yt-page-data-fetched', eventTriggerFn, false); 127 | addEventListener.call(document, 'yt-navigate-finish', eventTriggerFn, false); 128 | addEventListener.call(document, 'spfdone', eventTriggerFn, false); 129 | } 130 | }).then(detectConfigDone); 131 | 132 | new Promise(resolve => { 133 | if (typeof AbortSignal !== "undefined") { 134 | addEventListener.call(document, 'yt-action', resolve, { once: true, capture: true }); 135 | } else { 136 | const eventTriggerFn = () => { 137 | resolve(); 138 | removeEventListener.call(document, 'yt-action', eventTriggerFn, true); 139 | }; 140 | addEventListener.call(document, 'yt-action', eventTriggerFn, true); 141 | } 142 | }).then(detectConfigDone); 143 | 144 | function onReady(event) { 145 | detectConfigDone(); 146 | event && win.removeEventListener("DOMContentLoaded", onReady, false); 147 | } 148 | 149 | Promise.resolve().then(() => { 150 | if (document.readyState !== 'loading') { 151 | onReady(); 152 | } else { 153 | win.addEventListener("DOMContentLoaded", onReady, false); 154 | } 155 | }); 156 | 157 | } 158 | 159 | })(); 160 | -------------------------------------------------------------------------------- /library/ytZara.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ytZara 3 | // @namespace http://tampermonkey.net/ 4 | // @version 1.0.1 5 | // @description JS Library for YouTube's UserScripts 6 | // @author CY Fung 7 | // @grant none 8 | // @license MIT 9 | // ==/UserScript== 10 | 11 | /* 12 | 13 | MIT License 14 | 15 | Copyright (c) 2023 cyfung1031 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | */ 36 | 37 | var ytZara = (function () { 38 | 'use strict'; 39 | const VERSION_CONTROL = 1; 40 | const _global_ytZara = window.ytZara; 41 | if (_global_ytZara) { 42 | if (_global_ytZara.VERSION_CONTROL === VERSION_CONTROL) { 43 | return _global_ytZara; 44 | } else if (_global_ytZara.VERSION_CONTROL > VERSION_CONTROL) { 45 | console.warn('A newer ytZara is used.'); 46 | return _global_ytZara; 47 | } else { 48 | console.warn('A newer ytZara will replace your outdated ytZara.') 49 | delete window.ytZara; 50 | } 51 | } 52 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 53 | const indr = o => insp(o).$ || o.$ || 0; 54 | const ytZara = { 55 | VERSION_CONTROL, 56 | insp, 57 | indr, 58 | async ytProtoAsync(tag) { 59 | await this.promiseRegistryReady().then(); 60 | await customElements.whenDefined(tag).then(); 61 | return insp(document.createElement(tag)).constructor.prototype; 62 | }, 63 | docInitializedAsync() { 64 | return new Promise(resolve => { 65 | 66 | let mo = new MutationObserver(() => { 67 | mo.disconnect() 68 | mo.takeRecords() 69 | mo = null 70 | resolve(); 71 | }); 72 | mo.observe(document, { childList: true, subtree: true }); 73 | 74 | }) 75 | }, 76 | isYtHidden(elem) { 77 | if (!(elem instanceof Element)) return null; 78 | return HTMLElement.prototype.closest.call(elem, '[hidden]'); 79 | }, 80 | _promiseRegistryReady: null, 81 | promiseRegistryReady() { 82 | return this._promiseRegistryReady || (this._promiseRegistryReady = new Promise(this.onRegistryReady)); 83 | }, 84 | onRegistryReady(callback) { 85 | const EVENT_KEY_ON_REGISTRY_READY = "ytI-ce-registry-created"; 86 | if (typeof customElements === 'undefined') { 87 | if (!('__CE_registry' in document)) { 88 | // https://github.com/webcomponents/polyfills/ 89 | Object.defineProperty(document, '__CE_registry', { 90 | get() { 91 | // return undefined 92 | }, 93 | set(nv) { 94 | if (typeof nv == 'object') { 95 | delete this.__CE_registry; 96 | this.__CE_registry = nv; 97 | this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY)); 98 | } 99 | return true; 100 | }, 101 | enumerable: false, 102 | configurable: true 103 | }) 104 | } 105 | let eventHandler = (evt) => { 106 | document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false); 107 | const f = callback; 108 | callback = null; 109 | eventHandler = null; 110 | f(); 111 | }; 112 | document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false); 113 | } else { 114 | callback(); 115 | } 116 | } 117 | } 118 | return ytZara; 119 | })(); 120 | -------------------------------------------------------------------------------- /lihkg/lihkg-instant-drawer.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name LIHKG Instant Drawer 3 | @namespace github.com/openstyles/stylus 4 | @version 1.0.1 5 | @description A new userstyle 6 | ==/UserStyle== */ 7 | 8 | @-moz-document url-prefix("https://lihkg.com/") { 9 | 10 | b[class]:first-child:empty{ 11 | opacity: var(--lihkg-opacity, 0) !important; 12 | transition: opacity 0ms; 13 | } 14 | b[class]:first-child:empty+div[class]:last-child[style^="left:"]{ 15 | left: 0px !important; 16 | transition: margin-left 0ms; 17 | margin-left: var(--lihkg-margin-left, -280px); 18 | 19 | } 20 | 21 | 22 | div:not([class$=" "]){ 23 | --lihkg-opacity: 1; 24 | } 25 | div[class$=" "]{ 26 | --lihkg-opacity: 0; 27 | } 28 | 29 | div:not([class$=" "]) > b[class]:first-child:empty+div[class]:last-child[style="left: -280px;"]{ 30 | --lihkg-margin-left: -280px; 31 | } 32 | div:not([class$=" "]) > b[class]:first-child:empty+div[class]:last-child:not([style="left: -280px;"]){ 33 | --lihkg-margin-left: 0px; 34 | } 35 | 36 | 37 | div[class$=" "] > b[class]:first-child:empty+div[class]:last-child[style="left: 0px;"]{ 38 | --lihkg-margin-left: 0px; 39 | } 40 | div[class$=" "] > b[class]:first-child:empty+div[class]:last-child:not([style="left: 0px;"]){ 41 | --lihkg-margin-left: -280px; 42 | } 43 | 44 | 45 | /* Insert code here... */ 46 | } -------------------------------------------------------------------------------- /lihkg/lihkg-no-ads.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name LIHKG NO ADS 3 | @namespace github.com/openstyles/stylus 4 | @version 1.0.0 5 | @description A new userstyle 6 | ==/UserStyle== */ 7 | 8 | @-moz-document domain("lihkg.com") { 9 | 10 | script[src*="https://pb.lihkg.com/"], iframe[src*="https://pb.lihkg.com/"] { 11 | display: none !important; 12 | } 13 | /* Insert code here... */ 14 | } -------------------------------------------------------------------------------- /references/webworker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Count numbers:

6 | 7 | 8 | 9 |

Note: Internet Explorer 9 and earlier versions do not support Web Workers.

10 | 11 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /reset-youtube-settings.user.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright 2022 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | // ==UserScript== 27 | // @name Reset YouTube Settings 28 | // @namespace http://tampermonkey.net/ 29 | // @version 1.1 30 | // @description Due to YouTube making changes to its layout, some obsolete settings might remain and cause some problems to you. Use this to reset them. 31 | // @author CY Fung 32 | // @supportURL https://github.com/cyfung1031/userscript-supports 33 | // @match https://www.youtube.com/* 34 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 35 | // @grant GM_registerMenuCommand 36 | // @grant GM_addValueChangeListener 37 | // @grant unsafeWindow 38 | // @grant GM.setValue 39 | // @grant GM.deleteValue 40 | // @license MIT 41 | // @require https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js#sha512=wT7uPE7tOP6w4o28u1DN775jYjHQApdBnib5Pho4RB0Pgd9y7eSkAV1BTqQydupYDB9GBhTcQQzyNMPMV3cAew== 42 | // ==/UserScript== 43 | 44 | /* global Cookies, GM_addValueChangeListener, GM_registerMenuCommand */ 45 | 46 | (function () { 47 | 'use strict'; 48 | let cb1 = null 49 | GM_registerMenuCommand('Reset YouTube Settings', function () { 50 | if (cb1) return 51 | cb1 = true 52 | 53 | const whitelist = [ 54 | // cookies 55 | 'PREF', 'SID', 'APISID', 'SAPISID', /^__Secure-\w+$/, 'SIDCC', 56 | // localstorage 57 | 'yt-remote-device-id', 'yt-player-headers-readable', 58 | 'ytidb::LAST_RESULT_ENTRY_KEY', 59 | 'yt-remote-connected-devices', 'yt-player-bandwidth', 60 | 'userscript-tabview-settings', // Tabview Youtube 61 | /^[\-\w]*h264ify[\-\w]+$/ // h264ify or enhanced-h264ify 62 | ]; 63 | 64 | const cookiesObject = Cookies.get(); 65 | let keysCookies = Object.keys(cookiesObject) 66 | for (const key of keysCookies) { 67 | let value = cookiesObject[key]; 68 | if (typeof value !== 'string') continue; 69 | if (whitelist.includes(key)) continue; 70 | let isSkip = false; 71 | for (const s of whitelist) { 72 | if (isSkip) break; 73 | if (typeof s === 'object' && s.constructor.name === 'RegExp') { 74 | if (s.test(key)) isSkip = true; 75 | } 76 | } 77 | if (isSkip) continue; 78 | Cookies.remove(key); 79 | Cookies.remove(key, { domain: 'youtube.com' }); // most youtube cookies use youtube.com 80 | Cookies.remove(key, { domain: 'www.youtube.com' }); // some cookies such as 'WEVNSM' and 'WNMCID' use www.youtube.com 81 | Cookies.remove(key, { secure: true }); 82 | Cookies.remove(key, { domain: 'youtube.com', secure: true }); // most youtube cookies use youtube.com 83 | Cookies.remove(key, { domain: 'www.youtube.com', secure: true }); // some cookies such as 'WEVNSM' and 'WNMCID' use www.youtube.com 84 | } 85 | 86 | const lsObject = localStorage; 87 | let keysLS = Object.keys(lsObject) 88 | for (const key of keysLS) { 89 | let value = lsObject[key]; 90 | if (typeof value !== 'string') continue; 91 | if (whitelist.includes(key)) continue; 92 | let isSkip = false; 93 | for (const s of whitelist) { 94 | if (isSkip) break; 95 | if (typeof s === 'object' && s.constructor.name === 'RegExp') { 96 | if (s.test(key)) isSkip = true; 97 | } 98 | } 99 | if (isSkip) continue; 100 | localStorage.removeItem(key); 101 | } 102 | 103 | 104 | function getReduceds() { 105 | 106 | const cookiesObject = Cookies.get(); 107 | let keysCookiesNew = Object.keys(cookiesObject) 108 | const lsObject = localStorage; 109 | let keysLSNew = Object.keys(lsObject) 110 | 111 | 112 | let reduceds = [keysCookies.length - keysCookiesNew.length, keysLS.length - keysLSNew.length] 113 | 114 | 115 | keysCookies = null 116 | keysCookiesNew = null 117 | keysLS = null 118 | keysLSNew = null 119 | 120 | return reduceds 121 | 122 | } 123 | 124 | setTimeout(() => { 125 | 126 | let reduceds = getReduceds() 127 | 128 | if (reduceds[0] || reduceds[1]) { 129 | cb1 = () => { 130 | alert(` 131 | ${reduceds[0]} cookies and ${reduceds[1]} localstorages are deleted. 132 | The settings have been reset. 133 | 134 | Click OK to refresh the browser page. 135 | `.trim()) 136 | reduceds = null 137 | unsafeWindow.location.reload() 138 | } 139 | GM.setValue('reset-youtube-settings-flag', Date.now()) 140 | } else { 141 | alert('No settings to be required for reset.') 142 | cb1 = null 143 | } 144 | 145 | }, 300) 146 | 147 | 148 | }) 149 | 150 | let triggerOnce = 0 151 | GM_addValueChangeListener('reset-youtube-settings-flag', function (name, old_value, new_value, remote) { 152 | if (!(new_value > 0)) return 153 | let mTriggered = !remote && typeof cb1 === 'function' 154 | const tdt = Date.now(); 155 | triggerOnce = tdt 156 | if (mTriggered) { 157 | setTimeout(() => { 158 | if (tdt !== triggerOnce) return 159 | GM.deleteValue('reset-youtube-settings-flag').then(() => { 160 | if (tdt !== triggerOnce) return 161 | setTimeout(() => { 162 | if (tdt !== triggerOnce) return 163 | let pt = Date.now(); 164 | window.requestAnimationFrame(() => { 165 | if (tdt !== triggerOnce) return 166 | let ct = Date.now(); 167 | if (mTriggered && ct - pt < 800) { 168 | cb1(); 169 | cb1 = null 170 | } else { 171 | cb1 = null 172 | unsafeWindow.location.reload() 173 | } 174 | }) 175 | }, 30) 176 | }) 177 | }, 180) 178 | } else { 179 | if (tdt !== triggerOnce) return 180 | window.requestAnimationFrame(() => { 181 | if (tdt !== triggerOnce) return 182 | cb1 = null 183 | unsafeWindow.location.reload() 184 | }) 185 | } 186 | }) 187 | })(); 188 | -------------------------------------------------------------------------------- /revised-35760-yt-url-at-time.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name yt-url-at-time 3 | // @namespace mechalynx/yt-url-at-time 4 | // @license MIT 5 | // @grant none 6 | // @description On youtube, use alt+` to set the url to the current timestamp, for easy bookmarking 7 | // @include https://www.youtube.com/* 8 | // @version 0.2.7.002 9 | // @copyright 2017, MechaLynx (https://github.com/MechaLynx) 10 | // @run-at document-idle 11 | // @author MechaLynx 12 | // ==/UserScript== 13 | // jshint esversion: 6 14 | 15 | 16 | /** 17 | * 18 | * This is the modified version from https://greasyfork.org/scripts/35760-yt-url-at-time/ 19 | * 20 | * MIT, credit to "MechaLynx" 21 | * 22 | */ 23 | 24 | // `video` element utility 25 | var video = { 26 | get currentTime() { 27 | const element = document.querySelector('#movie_player video, #movie_player audio.video-stream.html5-main-video'); 28 | return Math.floor(element.currentTime); 29 | }, 30 | 31 | get _timehash() { 32 | var secs = this.currentTime || 0; 33 | return [(h = ~~(secs / 3600)) && h + 'h' || null, 34 | (m = ~~(secs % 3600 / 60)) && m + 'm' || null, 35 | (s = ~~(secs % 3600 % 60)) && s + 's'].join(''); 36 | }, 37 | get _plaintimehash() { 38 | return `${this.currentTime}`; 39 | }, 40 | 41 | // get timehash() { 42 | // return 't=' + `${this._timehash}`; 43 | // }, 44 | // get plaintimehash() { 45 | // return 't=' + `${this._plaintimehash}` 46 | // }, 47 | // get notimehash() { 48 | // return window.location.origin + 49 | // window.location.pathname + 50 | // window.location.search + 51 | // window.location.hash.replace(/#t=[^=#&]*/g, ''); 52 | // }, 53 | getURL(precise) { 54 | // const hash = precise ? `${video.notimehash}&${video.plaintimehash}` : `${video.notimehash}&${video.timehash}`; 55 | const uo = new URL(window.location.href.replace(/#t=[^=#&]*/g, '')); 56 | uo.searchParams.set('t', `${precise ? this._plaintimehash : this._timehash}`); 57 | return uo.toString(); 58 | } 59 | }; 60 | 61 | // Keep looking for the time indicator span, until it's found 62 | // The `load` event is insufficient 63 | var wait_for_page = window.setInterval(function () { 64 | var current_time_element = document.querySelector('.ytp-time-current'); 65 | if (current_time_element) { 66 | window.clearInterval(wait_for_page); 67 | 68 | // Add CSS for time indicator span 69 | let time_style = document.createElement('style'); 70 | time_style.setAttribute('name', "yt-url-at-time"); 71 | time_style.textContent = ` 72 | .url-at-time-element-hover:hover{ 73 | cursor: pointer; 74 | } 75 | .url-at-time-clipboard-helper{ 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | padding: none; 80 | margin: none; 81 | border: none; 82 | width: 0; 83 | height: 0; 84 | } 85 | `; 86 | document.body.appendChild(time_style); 87 | 88 | // Toggle the class so that it doesn't look clickable 89 | // during ads, which would be confusing 90 | current_time_element.onmouseover = function () { 91 | if (document.querySelector('.videoAdUi')) { 92 | current_time_element.classList.remove('url-at-time-element-hover'); 93 | } else { 94 | current_time_element.classList.add('url-at-time-element-hover'); 95 | } 96 | }; 97 | 98 | current_time_element.addEventListener('click', function (e) { 99 | if (e.altKey) { 100 | hashmodifier(true); 101 | } else { 102 | hashmodifier(false); 103 | } 104 | 105 | if (e.ctrlKey) { 106 | copy_url_to_clipboard(); 107 | } 108 | }); 109 | } 110 | }, 1000); 111 | 112 | 113 | // Add the timestamp to the URL 114 | var hashmodifier = function (precise = false) { 115 | if (/^(\/live\/|\/watch)/.test(location.pathname) && document.querySelector('.videoAdUi') === null) { 116 | const hash = video.getURL(precise); 117 | history.replaceState(history.state, '', hash); 118 | } 119 | }; 120 | 121 | 122 | 123 | var copy_url_to_clipboard = function (attempt_to_restore = false) { 124 | // Current focus and selection cannot be restored 125 | // since clicking on the timer causes the movie player to be focused 126 | // clearing the selection and changing the active element before we arrive here 127 | // However, attempting to restore them is meaningful if called through a hotkey 128 | if (attempt_to_restore) { 129 | var selection = document.getSelection(); 130 | var current_selection = selection.getRangeAt(0); 131 | var current_focus = document.activeElement; 132 | } 133 | 134 | // Add invisible textarea to allow copying the generated URL to clipboard 135 | let clipboard_helper = document.createElement('textarea'); 136 | clipboard_helper.classList.add('url-at-time-clipboard-helper'); 137 | document.body.appendChild(clipboard_helper); 138 | 139 | clipboard_helper.value = window.location.href; 140 | clipboard_helper.select(); 141 | clipboard_helper.setSelectionRange(0, clipboard_helper.value.length); 142 | document.execCommand('copy'); 143 | 144 | document.body.removeChild(clipboard_helper); 145 | 146 | if (attempt_to_restore) { 147 | current_focus.focus(); 148 | 149 | // https://gist.github.com/dantaex/543e721be845c18d2f92652c0ebe06aa 150 | selection.empty(); 151 | selection.addRange(current_selection); 152 | } 153 | }; 154 | 155 | var _alt = false; 156 | var _q = false; 157 | // Listen for the hotkey 158 | document.addEventListener('keydown', z => { 159 | // if you want to change the hotkey 160 | // you can use this: http://mechalynx.github.io/keypress/ 161 | // or another tester if you don't like this one 162 | if (z.code === 'KeyQ') { 163 | _q = true; 164 | } 165 | if (z.altKey && z.code === 'Backquote') { 166 | hashmodifier(_alt); 167 | _alt = true; 168 | } 169 | if (_q && _alt) { 170 | copy_url_to_clipboard(true); 171 | } 172 | }); 173 | 174 | document.addEventListener('keyup', z => { 175 | if (!z.altKey) { 176 | _alt = false; 177 | } 178 | if (z.code === "KeyQ") { 179 | _q = false; 180 | } 181 | }); 182 | -------------------------------------------------------------------------------- /youtube-live-chat-tamer.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube Live Chat Tamer 3 | // @namespace http://tampermonkey.net/ 4 | // @version 2023.07.25.1 5 | // @license MIT License 6 | // @author CY Fung 7 | // @match https://www.youtube.com/live_chat* 8 | // @match https://www.youtube.com/live_chat_replay* 9 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 10 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 11 | // @supportURL https://github.com/cyfung1031/userscript-supports 12 | // @run-at document-start 13 | // @grant none 14 | // @unwrap 15 | // @allFrames true 16 | // @inject-into page 17 | 18 | // @description (Deprecated) to maximize the performance of YouTube Live Chat Refresh 19 | 20 | // ==/UserScript== 21 | 22 | console.warn( 23 | "%cYouTube Live Chat Tamer is no longer maintained (deprecated). \n" + 24 | "Please visit and install https://greasyfork.org/scripts/469878 instead.", 25 | "color: #4ba5dc; font-weight: 600; padding: 4px;"); 26 | -------------------------------------------------------------------------------- /youtube-minimal-on-pc.user.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright 2023 CY Fung 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | // ==UserScript== 27 | // @name YouTube Minimal on PC 28 | // @version 0.5 29 | // @description Watch YouTube with the least CPU usage 30 | // @namespace http://tampermonkey.net/ 31 | // @author CY Fung 32 | // @license MIT 33 | // @supportURL https://github.com/cyfung1031/userscript-supports 34 | // @run-at document-start 35 | // @match https://www.youtube.com/* 36 | // @match https://m.youtube.com/* 37 | // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/youtube-minimal.png 38 | // @grant GM_registerMenuCommand 39 | // @require https://cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js#sha512=wT7uPE7tOP6w4o28u1DN775jYjHQApdBnib5Pho4RB0Pgd9y7eSkAV1BTqQydupYDB9GBhTcQQzyNMPMV3cAew== 40 | // ==/UserScript== 41 | "use strict"; 42 | 43 | /* global Cookies */ 44 | 45 | (function () { 46 | "use strict"; 47 | 48 | function addParam(u, s) { 49 | if (typeof u === 'string') { 50 | u += (u.indexOf('?') > 0 ? '&' : '?') + s 51 | } 52 | return u 53 | } 54 | 55 | let mUrl = Cookies.get("userjs-ym-url", { domain: "youtube.com", secure: true }); 56 | if (mUrl) { 57 | Cookies.remove("userjs-ym-url", { domain: "youtube.com", secure: true }); 58 | mUrl = addParam(mUrl, 'app=' + (mUrl.charAt(8) === 'w' ? 'desktop' : 'm')) 59 | location.replace(mUrl); 60 | return; 61 | } 62 | 63 | function getUrl() { 64 | return location.href.replace(/(?<=[&?])(persist_app|app)=\w+(&|$)/g, '').replace(/[?&]$/, '') 65 | } 66 | 67 | let href = getUrl() || ''; 68 | let hrefC8 = href.charAt(8) || ''; 69 | 70 | let iAmDesktop = hrefC8 === 'w'; 71 | let iAmMobile = hrefC8 === 'm'; 72 | 73 | let redirection = false 74 | 75 | if (href.indexOf(".youtube.com/watch") >= 0) { 76 | redirection = true 77 | } else if (href.endsWith('youtube.com/')) { 78 | redirection = true 79 | } 80 | 81 | function addMenuCommand(s, url, b) { 82 | 83 | GM_registerMenuCommand(s, function () { 84 | if (redirection) { 85 | let h = getUrl() 86 | if (b) h = h.replace("https://www.youtube.com/", "https://m.youtube.com/"); 87 | else h = h.replace("https://m.youtube.com/", "https://www.youtube.com/"); 88 | Cookies.set("userjs-ym-url", h, { domain: "youtube.com", secure: true }); 89 | } 90 | location.replace(url); 91 | }); 92 | 93 | } 94 | if (iAmDesktop) { 95 | addMenuCommand("Switch to YouTube Mobile persistently", "https://m.youtube.com/?persist_app=1&app=m", true); 96 | addMenuCommand("Switch to YouTube Moble temporarily", "https://m.youtube.com/?persist_app=0&app=m", true); 97 | 98 | } else if (iAmMobile) { 99 | addMenuCommand("Switch to YouTube Dekstop persistently", "http://www.youtube.com/?persist_app=1&app=desktop", false); 100 | addMenuCommand("Switch to YouTube Dekstop temporarily", "http://www.youtube.com/?persist_app=0&app=desktop", false); 101 | 102 | } 103 | 104 | addMenuCommand = null 105 | 106 | // Your code here... 107 | })(); -------------------------------------------------------------------------------- /youtube-no-leakage-01.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube: Fix Memory Leakage by usePatchedLifecycles 3 | // @namespace UserScripts 4 | // @match https://*.youtube.com/* 5 | // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/ 6 | // @grant none 7 | // @version 0.0.5 8 | // @author CY Fung 9 | // @license MIT 10 | // @description Some dummy elements leak. 11 | // @run-at document-start 12 | // @inject-into page 13 | // @unwrap 14 | // @license MIT 15 | // @compatible chrome 16 | // @compatible firefox 17 | // @compatible opera 18 | // @compatible edge 19 | // @compatible safari 20 | // @allFrames true 21 | // ==/UserScript== 22 | 23 | 24 | (() => { 25 | 26 | /** @type {globalThis.PromiseConstructor} */ 27 | const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve. 28 | 29 | const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0); 30 | const indr = o => insp(o).$ || o.$ || 0; 31 | 32 | const getThumbnail = (thumbnails) => { 33 | let v = 0, n = (thumbnails || 0).length; 34 | if (!n) return null; 35 | let j = -1; 36 | for (let i = 0; i < n; i++) { 37 | const thumbnail = thumbnails[i]; 38 | let k = thumbnail.width * thumbnail.height; 39 | if (k > v) { 40 | j = i; 41 | v = k; 42 | } 43 | } 44 | if (j >= 0) { 45 | return thumbnails[j]; 46 | } 47 | return null; 48 | } 49 | 50 | // let normal = false; 51 | const ytDOMWM = new WeakMap(); 52 | Object.defineProperty(Element.prototype, 'usePatchedLifecycles', { 53 | get() { 54 | let val = ytDOMWM.get(this); 55 | if (val === 0) val = false; 56 | if (val && !this.isConnected && !this.classList.contains('style-scope')) val = false; 57 | return val; 58 | }, 59 | set(nv) { 60 | let control = false; 61 | const nodeName = (this?.nodeName || '').toLowerCase(); 62 | switch (nodeName) { 63 | case 'yt-attributed-string': 64 | case 'yt-image': 65 | if (this?.classList?.length > 0) { 66 | control = false; 67 | } else { 68 | control = true; 69 | } 70 | break; 71 | 72 | case 'yt-player-seek-continuation': 73 | // case 'yt-iframed-player-events-relay': 74 | case 'yt-payments-manager': 75 | case 'yt-visibility-monitor': 76 | // case 'yt-invalidation-continuation': // live chat loading 77 | case 'yt-live-chat-replay-continuation': 78 | case 'yt-reload-continuation': 79 | case 'yt-timed-continuation': 80 | 81 | control = true; 82 | break; 83 | case 'yt-horizontal-list-renderer': 84 | case 'ytd-rich-grid-slim-media': 85 | case 'ytd-rich-item-renderer': 86 | case 'yt-emoji-picker-renderer': 87 | // if (!normal) { 88 | // control = true; 89 | // } 90 | break; 91 | case 'yt-img-shadow': 92 | if (nv) { 93 | const cnt = insp(this); 94 | const url0 = getThumbnail(cnt?.__data?.thumbnail?.thumbnails)?.url 95 | if (url0 && url0.length > 17) { 96 | // normal = true; 97 | control = true; 98 | Promise.resolve(0).then(() => { 99 | const url = getThumbnail(cnt?.__data?.thumbnail?.thumbnails)?.url || url0; 100 | cnt.$.img.src = `${url}`; 101 | }); 102 | } else { 103 | control = false; 104 | } 105 | } 106 | break; 107 | default: 108 | control = false; 109 | // if (nv) { 110 | // if (!normal) { 111 | // Promise.resolve(0).then(() => { 112 | // if (!normal && (this.classList.contains('style-scope') || this.isConnected === true)) { 113 | // normal = true; 114 | // } 115 | // }); 116 | // } 117 | // } 118 | } 119 | if (control) nv = 0; 120 | ytDOMWM.set(this, nv); 121 | return true; 122 | }, 123 | enumerable: false, 124 | configurable: true 125 | }); 126 | 127 | })(); -------------------------------------------------------------------------------- /youtube-popup-window.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube Popup Window 3 | // @name:zh-TW YouTube Popup Window 4 | // @name:ja YouTube Popup Window 5 | // @namespace http://tampermonkey.net/ 6 | // @version 0.2.2 7 | // @description Enhances YouTube with a popup window feature. 8 | // @description:zh-TW 透過彈出視窗功能增強YouTube。 9 | // @description:ja YouTubeをポップアップウィンドウ機能で強化します。 10 | // @author CY Fung 11 | // @license MIT 12 | // @match https://www.youtube.com/* 13 | // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com 14 | // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1.4.1 15 | // @grant GM_registerMenuCommand 16 | // @allFrames true 17 | // ==/UserScript== 18 | 19 | (async function () { 20 | 'use strict'; 21 | const shortcutKey = 'ctrlcmd-alt-keya'; 22 | 23 | const winName = 'x4tGg'; 24 | const styleName = 'rCbM3'; 25 | 26 | const observablePromise = (proc, timeoutPromise) => { 27 | let promise = null; 28 | return { 29 | obtain() { 30 | if (!promise) { 31 | promise = new Promise(resolve => { 32 | let mo = null; 33 | const f = () => { 34 | let t = proc(); 35 | if (t) { 36 | mo.disconnect(); 37 | mo.takeRecords(); 38 | mo = null; 39 | resolve(t); 40 | } 41 | } 42 | mo = new MutationObserver(f); 43 | mo.observe(document, { subtree: true, childList: true }) 44 | f(); 45 | timeoutPromise && timeoutPromise.then(() => { 46 | resolve(null) 47 | }); 48 | }); 49 | } 50 | return promise 51 | } 52 | } 53 | } 54 | 55 | function getVideo() { 56 | return document.querySelector('.video-stream.html5-main-video'); 57 | } 58 | 59 | function registerKeyboard(o) { 60 | 61 | const { openPopup } = o; 62 | 63 | const { KeyboardService } = VM.shortcut; 64 | 65 | const service = new KeyboardService(); 66 | 67 | service.setContext('activeOnInput', false); 68 | 69 | async function updateActiveOnInput() { 70 | const elm = document.activeElement; 71 | service.setContext('activeOnInput', elm instanceof HTMLInputElement || elm instanceof HTMLTextAreaElement); 72 | } 73 | 74 | document.addEventListener('focus', (e) => { 75 | updateActiveOnInput(); 76 | }, true); 77 | 78 | document.addEventListener('blur', (e) => { 79 | updateActiveOnInput(); 80 | }, true); 81 | 82 | service.register(shortcutKey, openPopup, { 83 | condition: '!activeOnInput', 84 | }); 85 | service.enable(); 86 | 87 | } 88 | 89 | if (window.name === winName && window === top) { 90 | 91 | if (!document.head) await observablePromise(() => document.head).obtain(); 92 | 93 | let style = document.createElement('style'); 94 | style.id = styleName; 95 | 96 | style.textContent = ` 97 | *[class][id].style-scope.ytd-watch-flexy { 98 | min-width: unset !important; 99 | min-height: unset !important; 100 | } 101 | ` 102 | 103 | document.head.appendChild(style); 104 | 105 | } else if (window !== top && top.name === winName) { 106 | 107 | if (!document.head) await observablePromise(() => document.head).obtain(); 108 | 109 | let style = document.createElement('style'); 110 | style.id = styleName; 111 | 112 | style.textContent = ` 113 | * { 114 | min-width: unset !important; 115 | min-height: unset !important; 116 | } 117 | ` 118 | 119 | document.head.appendChild(style); 120 | 121 | 122 | } else if (window === top) { 123 | 124 | function openPopup() { 125 | 126 | const currentUrl = window.location.href; 127 | const ytdAppElm = document.querySelector('ytd-app'); 128 | if (!ytdAppElm) return; 129 | const rect = ytdAppElm.getBoundingClientRect(); 130 | const w = rect.width; 131 | const h = rect.height; 132 | const popupOptions = `toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=${w},height=${h}`; 133 | 134 | let video = getVideo(); 135 | 136 | if (video) { 137 | video.pause(); 138 | } 139 | const win = window.open(currentUrl, '', popupOptions); 140 | win.name = winName; 141 | 142 | const b = document.querySelector('#x4tGg'); 143 | if (b) b.remove(); 144 | 145 | } 146 | 147 | GM_registerMenuCommand('Open Popup Window', function () { 148 | 149 | if (document.querySelector('#x4tGg')) return; 150 | 151 | const div = document.body.appendChild(document.createElement('div')); 152 | div.id = 'x4tGg'; 153 | div.textContent = 'Click to Open Popup' 154 | 155 | Object.assign(div.style, { 156 | 'position': 'fixed', 157 | 'left': '50vw', 158 | 'top': '50vh', 159 | 'padding': '28px', 160 | 'backgroundColor': 'rgb(56, 94, 131)', 161 | 'color': '#fff', 162 | 'borderRadius': '16px', 163 | 'fontSize': '18pt', 164 | 'zIndex': '9999', 165 | 'transform': 'translate(-50%, -50%)' 166 | }) 167 | 168 | div.onclick = function () { 169 | openPopup(); 170 | } 171 | 172 | }); 173 | 174 | registerKeyboard({ openPopup }); 175 | 176 | } 177 | // Your code here... 178 | 179 | })(); 180 | -------------------------------------------------------------------------------- /yt-fadeInChatMessage.user.css: -------------------------------------------------------------------------------- 1 | /* ==UserStyle== 2 | @name YouTube: fadeInChatMessage 3 | @namespace github.com/openstyles/stylus 4 | @version 0.1.2 5 | @description To fade in Chat Messages (with YouTube Super Fast Chat) 6 | @author CY Fung 7 | @preprocessor stylus 8 | @var number from-opacity "From Opacity" [0.3, 0, 1, 0.05] 9 | @var number to-opacity "To Opacity" [1, 0, 1, 0.05] 10 | @var number fade-duration "Fade Duration" [350, 10, 900, 10, 'ms'] 11 | @var select timing-fx "Timing Fx" { 12 | "cubic-bezier(.4,.9,.5,1)": "cubic-bezier(.4,.9,.5,1)", 13 | "linear": "linear", 14 | "ease-in-out": "ease-in-out" 15 | } 16 | ==/UserStyle== */ 17 | 18 | @-moz-document url-prefix("https://www.youtube.com/live_chat") { 19 | 20 | 21 | if 1 { 22 | $animation = fadeInChatMessage fade-duration timing-fx 0s 1 normal forwards 23 | } 24 | 25 | @keyframes fadeInChatMessage { 26 | from{ 27 | opacity:from-opacity; 28 | } 29 | to{ 30 | opacity:to-opacity; 31 | } 32 | } 33 | 34 | 35 | .cyt-chat-last-message { 36 | 37 | animation: $animation; 38 | } 39 | 40 | /* Insert code here... */ 41 | } -------------------------------------------------------------------------------- /yt-flag-kevlar_watch_grid-false.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name YouTube: kevlar_watch_grid = false 3 | // @namespace UserScripts 4 | // @match https://www.youtube.com/* 5 | // @grant none 6 | // @version 0.1.1 7 | // @author CY Fung 8 | // @description Disable kevlar_watch_grid 9 | // @run-at document-start 10 | // @inject-into page 11 | // @unwrap 12 | // @require https://update.greasyfork.org/scripts/475632/1361351/ytConfigHacks.js 13 | // @license MIT 14 | // ==/UserScript== 15 | 16 | (() => { 17 | window._ytConfigHacks.add((config_) => { 18 | const EXPERIMENT_FLAGS = config_.EXPERIMENT_FLAGS; 19 | if (EXPERIMENT_FLAGS) { 20 | EXPERIMENT_FLAGS.kevlar_watch_grid = false; 21 | } 22 | }); 23 | })(); --------------------------------------------------------------------------------