diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b311d28 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root=true + +[*] +end_of_line=lf +charset=utf-8 + +[*.{ts,tsx,js,json}] +indent_style=tab +indent_size=4 +trim_trailing_whitespace=true +insert_final_newline=true diff --git a/.resources/global.d.ts b/.resources/global.d.ts index 8b51306..d212b7a 100644 --- a/.resources/global.d.ts +++ b/.resources/global.d.ts @@ -38,6 +38,8 @@ type GMOpenInTabOptions = { } declare function GM_openInTab(url: string, options?: GMOpenInTabOptions): void +declare type ReactElement = import('react').ReactElement +declare type ReactNode = import('react').ReactNode declare type ChangeEvent = import('react').ChangeEvent declare type Immer = import('immer').Immer @@ -51,7 +53,25 @@ declare const unsafeWindow: UnsafeWindow interface YtdPlayerElement extends HTMLElement { loadVideoWithPlayerVars(options: { videoId: string; start?: number }): void + getPlayer: () => YouTubePlayer } -declare type TrustedTypePolicyFactory = import('trusted-types/lib').TrustedTypePolicyFactory -declare const trustedTypes: TrustedTypePolicyFactory +interface YouTubeMoviePlayerElement extends HTMLElement, YouTubePlayer { + loadVideoByPlayerVars(options: { videoId: string; start?: number }): void + toggleSubtitles(): void + toggleSubtitlesOn(): void + isSubtitlesOn(): boolean +} + +interface YouTubePlayer { + getVideoData: () => YouTubeVideoData + getCurrentTime: () => number + getDuration: () => number + loadVideoById: (videoId: string, startTime?: number) => void +} + +interface YouTubeVideoData { + title: string + video_id: string + isLive: boolean +} diff --git a/.resources/tailwind.min.css b/.resources/tailwind.min.css index 33ce450..3596759 100644 --- a/.resources/tailwind.min.css +++ b/.resources/tailwind.min.css @@ -1 +1 @@ -/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-7{bottom:1.75rem}.left-0{left:0}.left-72{left:18rem}.z-\[999\]{z-index:999}.inline-block{display:inline-block}.flex{display:flex}.h-4{height:1rem}.h-full{height:100%}.w-60{width:15rem}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.cursor-text{cursor:text}.flex-col{flex-direction:column}.items-center{align-items:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.bg-gray-800\/90{background-color:#1f2937e6}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-rose-400{--tw-text-opacity:1;color:rgb(251 113 133/var(--tw-text-opacity))}.text-sky-400{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity))}.text-transparent{color:#0000}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.caret-white{caret-color:#fff}.outline{outline-style:solid}.selection\:bg-blue-700 ::-moz-selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:bg-blue-700 ::selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:text-transparent ::-moz-selection{color:#0000}.selection\:text-transparent ::selection{color:#0000}.selection\:bg-blue-700::-moz-selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:bg-blue-700::selection{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.selection\:text-transparent::-moz-selection{color:#0000}.selection\:text-transparent::selection{color:#0000}.hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))} \ No newline at end of file +/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.pointer-events-none{pointer-events:none}.absolute{position:absolute}.inset-0{inset:0}.z-\[999\]{z-index:999} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a67c02d..8c48c71 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,6 +19,7 @@ }, "files.readonlyInclude": { "scripts/*/{dev,script}.user.js": true, - ".resources/tailwind.min.css": true + ".resources/tailwind.min.css": true, + "pnpm-lock.yaml": true } } diff --git a/README.md b/README.md index 1ff9d57..306634f 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,82 @@

-The userscript collection I wrote makes life easier and better. I use the browser extension [TamperMonkey](https://www.tampermonkey.net) to run and manage userscripts. You need to install it first to use userscript. +The userscript collection I wrote makes life easier and better. I use the browser extension [TamperMonkey][1] to run and manage userscripts. You need to install it first to use userscript. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionDaily installsTotal installs
+ Auto Skip YouTube Ads + + Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. + + + + +
+ YouTube Shorts To Normal Video + + Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. + + + + +
+ No Fullscreen Dropdown + + Real fullscreen instead of fullscreen dropdown, very annoying when playing games. Useful for Microsoft Edge. Press Shift+F11 to toggle fullscreen. + + + + +
+ Tetr.io Improvements + + Provides improvements for Tetr.io game. + + + + +
## 📖 Usage ### Install on GreasyFork (Recommended) -Visit a list of some of my userscripts on the GreasyFork website [here](https://greasyfork.org/en/users/1306283-tientq64). +Visit a list of some of my userscripts on the GreasyFork website [here][2]. ### Install on GitHub @@ -27,30 +96,53 @@ Report bugs [here](https://github.com/tientq64/userscripts/issues). > This section is for developers. If you are a user, you can skip this section. Clone this repository: + ```cmd git clone https://github.com/tientq64/userscripts.git cd userscripts ``` Install `pnpm` if not installed: + ```cmd npm i -g pnpm ``` Install dependencies: + ```cmd pnpm install ``` Start: + ```cmd pnpm run watch ``` -The `script.user.js` file is compiled from the `script.user.ts` file, so do not modify it. +The `script.user.js` file is compiled from the `script.user.ts` or `script.user.tsx` file, so do not modify it. While developing, instead of having to reinstall the `script.user.js` file every time it changes, installing the `dev.user.js` file solves that problem. But note, if you change in the metadata block, you have to reinstall the `dev.user.js` file. +Built-in Tailwind CSS integration, just declare in the metadata block and use: + +```tsx +// ==UserScript== +// @resource TAILWINDCSS +// @grant GM_addStyle +// ==/UserScript== + +const tailwindCss: string = GM_getResourceText('TAILWINDCSS') +GM_addStyle(tailwindCss) + +const el: ReactElement =
+``` + +Common type definitions are written in [`.resources/global.d.ts`](.resources/global.d.ts) file. + ## ⚖️ License All scripts are licensed under the [MIT](./LICENSE) license. + +[1]: https://www.tampermonkey.net +[2]: https://greasyfork.org/en/users/1306283-tientq64 diff --git a/package.json b/package.json index 375ed01..d68521d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "homepage": "https://greasyfork.org/users/1306283-tientq64", "bugs": { - "email": "tientq@gmail.com", + "email": "tientq64@gmail.com", "url": "https://github.com/tientq64/userscripts/issues" }, "license": "MIT", @@ -40,7 +40,6 @@ "@types/node": "^20.14.13", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@types/trusted-types": "2.0.7", "concurrently": "^8.2.2", "eslint": "8.57.0", "eslint-plugin-react": "^7.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df8e21c..87cf79a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,9 +45,6 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 - '@types/trusted-types': - specifier: 2.0.7 - version: 2.0.7 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -357,9 +354,6 @@ packages: '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1906,8 +1900,6 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@types/trusted-types@2.0.7': {} - '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)': diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md index ec84c6f..6bfe5bf 100644 --- a/scripts/Auto-Skip-YouTube-Ads/README.md +++ b/scripts/Auto-Skip-YouTube-Ads/README.md @@ -1,182 +1,211 @@ -## 📰 Introduction - -Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. - -立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 - -Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. - -## 📑 Changelog - -### 6.0.0 - 2025-01-29 - -_Happy Lunar New Year!_ - -- Completely rewritten way to skip ads, more efficient, not detected by YouTube ad blocker warning. -- New way to skip ads is temporarily not working on YouTube Music. - -### 5.3.0 - 2025-01-23 - -- Supports older browser versions. - -### 5.2.0 - 2025-01-21 - -- Support for **YouTube mobile** version 🎉 -- Revisit fix for issue [#2]: Fully resolved the problem where videos couldn't be paused on mobile. The previous fix was incomplete. - -### 5.1.3 - 2025-01-18 - -- Fix ad skipping issue. - -### 5.1.2 - 2025-01-17 - -- Fix issue [#2] where video can't be paused on mobile. - -### 5.1.1 - 2024-12-27 - -- Hide the survey to rate suggested content, located at bottom right. - -### 5.1.0 - 2024-12-26 - -- Skip pie countdown ads 🎉 - -### 5.0.0 - 2024-12-25 - -_Merry Christmas!_ - -- **No need to reload the page** when there is no way to skip the ad anymore 🤯 -- Configuration removed, no longer needed. - -### 4.8.2 - 2024-12-21 - -- Fix timestamp loss when reloading. - -### 4.8.1 - 2024-12-03 - -- Hide survey dialog on home page. - -### 4.8.0 - 2024-11-26 - -- The current video's timestamp will be preserved when the page is reloaded ([#267857]). - -### 4.7.4 - 2024-11-20 - -- Improved ad skipping. - -### 4.7.0 - 2024-10-26 - -- Add option "Don't reload while the user is busy" in Tampermonkey's menu to avoid reloading page when user is busy doing something, like reading comments, entering text. Enabled by default. - -### 4.6.2 - 2024-10-13 - -- Improve hiding of ad banners. - -### 4.6.0 - 2024-10-07 - -- Support skipping ads on **YouTube Music** (PR [#1]). - -### 4.5.2 - 2024-09-30 - -- Fix Shorts reload infinitely ([#258626], [#259545], [#261679]). - -### 4.5.0 - 2024-09-26 - -- Add option to enable/disable "Reload the page when there is no other way to skip ads" feature in Tampermonkey's menu. Enabled by default.\ - ![Screenshot-001] - -### 4.4.0 - 2024-08-30 - -- Automatically reload web page when ad blocker warnings appear. - -### 4.3.13 - 2024-08-26 - -- Fixed bug where video could not be paused using pause/play key on keyboard or media controls ([#257424]). -- Improve the performance. - -### 4.3.9 - 2024-08-21 - -- Fix `@match` invalid syntax ([#256841]). - -### 4.3.8 - 2024-08-20 - -- Fix the issue of removing ad videos in Shorts. - -### 4.3.6 - 2024-08-07 - -- Fix bug where video rewinds a segment after skipping an ad ([#254113]). - -### 4.3.4 - 2024-08-02 - -- Improve the performance. - -### 4.2.1 - 2024-07-30 - -- Fixed video automatically replay when ended. - -### 4.2.0 - 2024-07-30 - -- Videos will now no longer occasionally pause due to ad blocker use. -- Faster ad video skipping speed. - -### 4.1.0 - 2024-07-10 - -- No need to reload the page when the ad blocker warning dialog appears. - -### 4.0.0 - 2024-07-09 - -- The page will now reload if an ad blocker warning dialog appears. Because YouTube now pauses the video at first if an ad blocker is detected. -- Write to the Console every time skip an ad video, etc. Purpose to help debug. To open the Console, press `Ctrl+Shift+J`. - -### 3.1.2 - 2024-07-06 - -- Playing video after clicking dismiss the ad blocker warning popup. - -### 3.1.1 - 2024-07-04 - -- Add a few CSS that hides the ads. - -### 3.1.0 - 2024-07-02 - -- Skip ads faster when the tab is active. -- Fixed bug when set time to end of ad video without the video duration being available. -- Change icon. - -### 3.0.2 - 2024-06-28 - -- Rewriting to only use `setInterval` simplifies things, and fix some bugs. - -### 2.1.3 - 2024-06-21 - -- Fix `popupContainer` not found error. - -### 2.1.0 - 2024-06-20 - -- Auto close YouTube's ad blocker warning popup. - -### 2.0.1 - 2024-06-19 - -- Improved skip ad button detection. -- Fall back to `setInterval` when `MutationObserver` is not supported. - -### 2.0.0 - 2024-06-18 - -- Rewrite the entire code, use `MutationObserver` instead of `setInterval`. - -### 1.0.0 - 2024-06-17 - -- Stable release. - -## 💳 Credits - -Youtube icons created by Ruslan Babkin - Flaticon. - -[#2]: https://github.com/tientq64/userscripts/issues/2 -[#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 -[#258626]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/258626 -[#259545]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/259545 -[#261679]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/261679 -[#257424]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/257424 -[#256841]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/256841 -[#254113]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/254113 -[#1]: https://github.com/tientq64/userscripts/pull/1 -[Screenshot-001]: https://cdn.jsdelivr.net/gh/tientq64/userscripts/scripts/Auto-Skip-YouTube-Ads/assets/screenshot-001.png +## 📰 Introduction + +Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. + +立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。 + +Tự động bỏ qua quảng cáo YouTube ngay lập tức. Không bị phát hiện bởi cảnh báo trình chặn quảng cáo của YouTube. + +## 📑 Changelog + +### 7.3.0 - 2025-06-20 + +- Mute video player while skipping ads ([#15]). +- Automatically re-enable subtitles after skipping ads. + +### 7.2.0 - 2025-03-09 + +- YouTube Music support is back ([#291321], [#12]). + +### 7.1.0 - 2025-02-23 + +- Experiment. +- Fixed the issue where the chapters was not displayed on the right side of the video ([#10], [#279783]). + +### 7.0.0 - 2025-02-15 + +- Version 6 had many bug reports, reverting to the previous version 5 ([#6], [#279168]). + +### 6.0.2 - 2025-02-02 + +- Disable some unnecessary CSS. + +### 6.0.0 - 2025-01-29 + +_Happy Lunar New Year!_ + +- Completely rewritten way to skip ads, more efficient, not detected by YouTube ad blocker warning. +- New way to skip ads is temporarily not working on YouTube Music. + +### 5.3.0 - 2025-01-23 + +- Supports older browser versions. + +### 5.2.0 - 2025-01-21 + +- Support for **YouTube mobile** version 🎉 +- Revisit fix for issue [#2]: Fully resolved the problem where videos couldn't be paused on mobile. The previous fix was incomplete. + +### 5.1.3 - 2025-01-18 + +- Fix ad skipping issue. + +### 5.1.2 - 2025-01-17 + +- Fix issue [#2] where video can't be paused on mobile. + +### 5.1.1 - 2024-12-27 + +- Hide the survey to rate suggested content, located at bottom right. + +### 5.1.0 - 2024-12-26 + +- Skip pie countdown ads 🎉 + +### 5.0.0 - 2024-12-25 + +_Merry Christmas!_ + +- **No need to reload the page** when there is no way to skip the ad anymore 🤯 +- Configuration removed, no longer needed. + +### 4.8.2 - 2024-12-21 + +- Fix timestamp loss when reloading. + +### 4.8.1 - 2024-12-03 + +- Hide survey dialog on home page. + +### 4.8.0 - 2024-11-26 + +- The current video's timestamp will be preserved when the page is reloaded ([#267857]). + +### 4.7.4 - 2024-11-20 + +- Improved ad skipping. + +### 4.7.0 - 2024-10-26 + +- Add option "Don't reload while the user is busy" in Tampermonkey's menu to avoid reloading page when user is busy doing something, like reading comments, entering text. Enabled by default. + +### 4.6.2 - 2024-10-13 + +- Improve hiding of ad banners. + +### 4.6.0 - 2024-10-07 + +- Support skipping ads on **YouTube Music** (PR [#1]). + +### 4.5.2 - 2024-09-30 + +- Fix Shorts reload infinitely ([#258626], [#259545], [#261679]). + +### 4.5.0 - 2024-09-26 + +- Add option to enable/disable "Reload the page when there is no other way to skip ads" feature in Tampermonkey's menu. Enabled by default.\ + ![Screenshot-001] + +### 4.4.0 - 2024-08-30 + +- Automatically reload web page when ad blocker warnings appear. + +### 4.3.13 - 2024-08-26 + +- Fixed bug where video could not be paused using pause/play key on keyboard or media controls ([#257424]). +- Improve the performance. + +### 4.3.9 - 2024-08-21 + +- Fix `@match` invalid syntax ([#256841]). + +### 4.3.8 - 2024-08-20 + +- Fix the issue of removing ad videos in Shorts. + +### 4.3.6 - 2024-08-07 + +- Fix bug where video rewinds a segment after skipping an ad ([#254113]). + +### 4.3.4 - 2024-08-02 + +- Improve the performance. + +### 4.2.1 - 2024-07-30 + +- Fixed video automatically replay when ended. + +### 4.2.0 - 2024-07-30 + +- Videos will now no longer occasionally pause due to ad blocker use. +- Faster ad video skipping speed. + +### 4.1.0 - 2024-07-10 + +- No need to reload the page when the ad blocker warning dialog appears. + +### 4.0.0 - 2024-07-09 + +- The page will now reload if an ad blocker warning dialog appears. Because YouTube now pauses the video at first if an ad blocker is detected. +- Write to the Console every time skip an ad video, etc. Purpose to help debug. To open the Console, press `Ctrl+Shift+J`. + +### 3.1.2 - 2024-07-06 + +- Playing video after clicking dismiss the ad blocker warning popup. + +### 3.1.1 - 2024-07-04 + +- Add a few CSS that hides the ads. + +### 3.1.0 - 2024-07-02 + +- Skip ads faster when the tab is active. +- Fixed bug when set time to end of ad video without the video duration being available. +- Change icon. + +### 3.0.2 - 2024-06-28 + +- Rewriting to only use `setInterval` simplifies things, and fix some bugs. + +### 2.1.3 - 2024-06-21 + +- Fix `popupContainer` not found error. + +### 2.1.0 - 2024-06-20 + +- Auto close YouTube's ad blocker warning popup. + +### 2.0.1 - 2024-06-19 + +- Improved skip ad button detection. +- Fall back to `setInterval` when `MutationObserver` is not supported. + +### 2.0.0 - 2024-06-18 + +- Rewrite the entire code, use `MutationObserver` instead of `setInterval`. + +### 1.0.0 - 2024-06-17 + +- Stable release. + +## 💳 Credits + +Youtube icons created by Ruslan Babkin - Flaticon. + +[#15]: https://github.com/tientq64/userscripts/issues/15 +[#12]: https://github.com/tientq64/userscripts/issues/12 +[#10]: https://github.com/tientq64/userscripts/issues/10 +[#6]: https://github.com/tientq64/userscripts/issues/6 +[#2]: https://github.com/tientq64/userscripts/issues/2 +[#1]: https://github.com/tientq64/userscripts/pull/1 +[#291321]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/291321 +[#279783]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279783 +[#279168]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/279168 +[#267857]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/267857 +[#261679]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/261679 +[#259545]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/259545 +[#258626]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/258626 +[#257424]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/257424 +[#256841]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/256841 +[#254113]: https://greasyfork.org/scripts/498197-auto-skip-youtube-ads/discussions/254113 +[Screenshot-001]: https://cdn.jsdelivr.net/gh/tientq64/userscripts/scripts/Auto-Skip-YouTube-Ads/assets/screenshot-001.png diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.js b/scripts/Auto-Skip-YouTube-Ads/script.user.js index 0ebf0e7..49c85d0 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.js +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.0 +// @version 7.3.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,8 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* +// @exclude https://studio.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -45,70 +47,130 @@ // ==/UserScript== function skipAd() { - const isYouTubeShort = checkIsYouTubeShort() - if (isYouTubeShort) return + if (checkIsYouTubeShorts()) return - const hasAd = checkHasAd() - if (!hasAd) return - - const player = getYouTubePlayer() - if (player === null) return - - const videoData = player.getVideoData() - const videoId = videoData.video_id - const startTime = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) - - console.log('Ad skipped!', videoId, startTime, videoData.title) -} - -/** - * Check if there are any ads interrupting the video. - */ -function checkHasAd() { // This element appears when a video ad appears. const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return true // Timed pie countdown ad. const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown !== null) return true - return false -} + // Survey questions in video player. + const surveyQuestions = document.querySelector('.ytp-ad-survey-questions') -function checkIsYouTubeShort() { - return location.pathname.startsWith('/shorts/') -} + if (adShowing === null && pieCountdown === null && surveyQuestions === null) return -/** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. - */ -function getYouTubePlayer() { + const moviePlayerEl = document.querySelector('#movie_player') + let playerEl let player - if (isYouTubeMobile) { - const playerEl = document.querySelector('#movie_player') + + if (isYouTubeMobile || isYouTubeMusic) { + playerEl = moviePlayerEl player = playerEl } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() + } + + if (playerEl === null || player === null) { + console.log({ + message: 'Player not found', + timeStamp: getCurrentTimeString() + }) + return + } + + // ad.classList.remove('ad-showing') + + let adVideo = null + + if (pieCountdown === null && surveyQuestions === null) { + adVideo = document.querySelector( + '#ytd-player video.html5-main-video, #song-video video.html5-main-video' + ) + + console.table({ + message: 'Ad video', + video: adVideo !== null, + src: adVideo?.src, + paused: adVideo?.paused, + currentTime: adVideo?.currentTime, + duration: adVideo?.duration, + timeStamp: getCurrentTimeString() + }) + + if (adVideo !== null) { + adVideo.muted = true + } + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) { + return + } + + console.log({ + message: 'Ad video has finished loading', + timeStamp: getCurrentTimeString() + }) + } + + if (isYouTubeMusic && adVideo !== null) { + adVideo.currentTime = adVideo.duration + + console.table({ + message: 'Ad skipped', + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) + } else { + const videoData = player.getVideoData() + const videoId = videoData.video_id + const start = Math.floor(player.getCurrentTime()) + + if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) { + window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000) + } + + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.table({ + message: 'Ad skipped', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) } - return player +} + +function checkIsYouTubeShorts() { + return location.pathname.startsWith('/shorts/') +} + +function getCurrentTimeString() { + return new Date().toTimeString().split(' ', 1)[0] } function addCss() { const adsSelectors = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', + '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]', // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', - '.ytp-suggested-action', + // Sponsored ad video items on home page. + // 'ytd-ad-slot-renderer', + + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // Featured product ad banner at the bottom left of the video. @@ -136,39 +198,52 @@ function addCss() { */ function removeAdElements() { const adSelectors = [ - // Ad banner in the upper right corner, above the video playlist. - ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], - // Sponsored ad video items on home page. - ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], + // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], - ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], + // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], - // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + // Ad videos on YouTube Shorts. + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], + // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], // Survey to rate suggested content, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] + // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { const adEl = document.querySelector(adSelector[0]) if (adEl === null) continue - const neededEl = document.querySelector(adSelector[1]) + const neededEl = adEl.querySelector(adSelector[1]) if (neededEl === null) continue adEl.remove() } } const isYouTubeMobile = location.hostname === 'm.youtube.com' +const isYouTubeDesktop = !isYouTubeMobile -window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) +const isYouTubeMusic = location.hostname === 'music.youtube.com' +const isYouTubeVideo = !isYouTubeMusic addCss() + +if (isYouTubeVideo) { + window.setInterval(removeAdElements, 1000) + removeAdElements() +} + +window.setInterval(skipAd, 500) skipAd() + +// const observer = new MutationObserver(skipAd) +// observer.observe(document.body, { +// attributes: true, +// attributeFilter: ['class'], +// childList: true, +// subtree: true +// }) diff --git a/scripts/Auto-Skip-YouTube-Ads/script.user.ts b/scripts/Auto-Skip-YouTube-Ads/script.user.ts index a58de55..a88d35c 100644 --- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts +++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts @@ -14,7 +14,7 @@ // @name:zh-CN 自动跳过 YouTube 广告 // @name:zh-TW 自動跳過 YouTube 廣告 // @namespace https://github.com/tientq64/userscripts -// @version 6.0.0 +// @version 7.3.0 // @description Automatically skip YouTube ads instantly. Undetected by YouTube ad blocker warnings. // @description:ar تخطي إعلانات YouTube تلقائيًا على الفور. دون أن يتم اكتشاف ذلك من خلال تحذيرات أداة حظر الإعلانات في YouTube. // @description:es Omite automáticamente los anuncios de YouTube al instante. Sin que te detecten las advertencias del bloqueador de anuncios de YouTube. @@ -33,6 +33,8 @@ // @icon https://cdn-icons-png.flaticon.com/64/2504/2504965.png // @match https://www.youtube.com/* // @match https://m.youtube.com/* +// @match https://music.youtube.com/* +// @exclude https://studio.youtube.com/* // @grant none // @license MIT // @compatible firefox @@ -43,89 +45,133 @@ // @noframes // ==/UserScript== -interface YtdPlayerElement extends HTMLElement { - getPlayer: () => YouTubePlayer -} +function skipAd(): void { + if (checkIsYouTubeShorts()) return -/** - * YouTube video player. - */ -interface YouTubePlayer { - getVideoData: () => YouTubeVideoData - getCurrentTime: () => number - loadVideoById: (videoId: string, startTime?: number) => void -} + // This element appears when a video ad appears. + const adShowing = document.querySelector('.ad-showing') -interface YouTubeVideoData { - title: string - video_id: string -} + // Timed pie countdown ad. + const pieCountdown = document.querySelector( + '.ytp-ad-timed-pie-countdown-container' + ) -function skipAd(): void { - const isYouTubeShort: boolean = checkIsYouTubeShort() - if (isYouTubeShort) return + // Survey questions in video player. + const surveyQuestions = document.querySelector('.ytp-ad-survey-questions') - const hasAd: boolean = checkHasAd() - if (!hasAd) return + if (adShowing === null && pieCountdown === null && surveyQuestions === null) return - const player: YouTubePlayer | null = getYouTubePlayer() - if (player === null) return + const moviePlayerEl = document.querySelector('#movie_player') + let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null + let player: YouTubePlayer | YouTubeMoviePlayerElement | null - const videoData: YouTubeVideoData = player.getVideoData() - const videoId: string = videoData.video_id - const startTime: number = Math.floor(player.getCurrentTime()) - player.loadVideoById(videoId, startTime) + if (isYouTubeMobile || isYouTubeMusic) { + playerEl = moviePlayerEl + player = playerEl + } else { + playerEl = document.querySelector('#ytd-player') + player = playerEl && playerEl.getPlayer() + } - console.log('Ad skipped!', videoId, startTime, videoData.title) -} + if (playerEl === null || player === null) { + console.log({ + message: 'Player not found', + timeStamp: getCurrentTimeString() + }) + return + } -/** - * Check if there are any ads interrupting the video. - */ -function checkHasAd(): boolean { - // This element appears when a video ad appears. - const adShowing = document.querySelector('.ad-showing') - if (adShowing !== null) return true + // ad.classList.remove('ad-showing') + + let adVideo: HTMLVideoElement | null = null + + if (pieCountdown === null && surveyQuestions === null) { + adVideo = document.querySelector( + '#ytd-player video.html5-main-video, #song-video video.html5-main-video' + ) + + console.table({ + message: 'Ad video', + video: adVideo !== null, + src: adVideo?.src, + paused: adVideo?.paused, + currentTime: adVideo?.currentTime, + duration: adVideo?.duration, + timeStamp: getCurrentTimeString() + }) + + if (adVideo !== null) { + adVideo.muted = true + } + if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) { + return + } + + console.log({ + message: 'Ad video has finished loading', + timeStamp: getCurrentTimeString() + }) + } - // Timed pie countdown ad. - const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container') - if (pieCountdown !== null) return true + if (isYouTubeMusic && adVideo !== null) { + adVideo.currentTime = adVideo.duration - return false + console.table({ + message: 'Ad skipped', + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) + } else { + const videoData: YouTubeVideoData = player.getVideoData() + const videoId: string = videoData.video_id + const start: number = Math.floor(player.getCurrentTime()) + + if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) { + window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000) + } + + if ('loadVideoWithPlayerVars' in playerEl) { + playerEl.loadVideoWithPlayerVars({ videoId, start }) + } else { + playerEl.loadVideoByPlayerVars({ videoId, start }) + } + + console.table({ + message: 'Ad skipped', + videoId, + start, + title: videoData.title, + timeStamp: getCurrentTimeString(), + adShowing: adShowing !== null, + pieCountdown: pieCountdown !== null, + surveyQuestions: surveyQuestions !== null + }) + } } -function checkIsYouTubeShort(): boolean { +function checkIsYouTubeShorts(): boolean { return location.pathname.startsWith('/shorts/') } -/** - * Finds and returns the current YouTube video player. - * - * @returns The current YouTube video player, or `null` if not found. - */ -function getYouTubePlayer(): YouTubePlayer | null { - let player: YouTubePlayer | null - if (isYouTubeMobile) { - const playerEl: unknown = document.querySelector('#movie_player') - player = playerEl as YouTubePlayer - } else { - const playerEl = document.querySelector('#ytd-player') - if (playerEl === null) return null - player = playerEl.getPlayer() - } - return player +function getCurrentTimeString(): string { + return new Date().toTimeString().split(' ', 1)[0] } function addCss(): void { const adsSelectors: string[] = [ // Ad banner in the upper right corner, above the video playlist. '#player-ads', + '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]', // Masthead ad on home page. '#masthead-ad', - 'ytd-ad-slot-renderer', - '.ytp-suggested-action', + // Sponsored ad video items on home page. + // 'ytd-ad-slot-renderer', + + // '.ytp-suggested-action', '.yt-mealbar-promo-renderer', // Featured product ad banner at the bottom left of the video. @@ -153,39 +199,52 @@ function addCss(): void { */ function removeAdElements(): void { const adSelectors: [string, string][] = [ - // Ad banner in the upper right corner, above the video playlist. - ['#panels', 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'], - // Sponsored ad video items on home page. - ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], + // ['ytd-rich-item-renderer', '.ytd-ad-slot-renderer'], - ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], + // ['ytd-rich-section-renderer', '.ytd-statement-banner-renderer'], - // Ad videos on YouTube Short. - ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'], + // Ad videos on YouTube Shorts. + ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer'] // Ad blocker warning dialog. - ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], + // ['tp-yt-paper-dialog', '#feedback.ytd-enforcement-message-view-model'], // Survey dialog on home page, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], + // ['tp-yt-paper-dialog', ':scope > ytd-checkbox-survey-renderer'], // Survey to rate suggested content, located at bottom right. - ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] + // ['tp-yt-paper-dialog', ':scope > ytd-single-option-survey-renderer'] ] for (const adSelector of adSelectors) { - const adEl = document.querySelector(adSelector[0]) + const adEl = document.querySelector(adSelector[0]) if (adEl === null) continue - const neededEl = document.querySelector(adSelector[1]) + const neededEl = adEl.querySelector(adSelector[1]) if (neededEl === null) continue adEl.remove() } } const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com' +const isYouTubeDesktop: boolean = !isYouTubeMobile -window.setInterval(skipAd, 500) -window.setInterval(removeAdElements, 1000) +const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com' +const isYouTubeVideo: boolean = !isYouTubeMusic addCss() + +if (isYouTubeVideo) { + window.setInterval(removeAdElements, 1000) + removeAdElements() +} + +window.setInterval(skipAd, 500) skipAd() + +// const observer = new MutationObserver(skipAd) +// observer.observe(document.body, { +// attributes: true, +// attributeFilter: ['class'], +// childList: true, +// subtree: true +// }) diff --git a/scripts/Diep-io-Improvements/script.user.js b/scripts/Diep-io-Improvements/script.user.js index 4b1d888..cdd8cc3 100644 --- a/scripts/Diep-io-Improvements/script.user.js +++ b/scripts/Diep-io-Improvements/script.user.js @@ -8,7 +8,6 @@ // @match https://diep.io/* // @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js // @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js -// @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js // @resource TAILWINDCSS https://raw.githubusercontent.com/tientq64/userscripts/main/.resources/tailwind.min.css // @grant GM_getResourceText // @grant GM_addStyle @@ -22,280 +21,50 @@ var diepIO ;(function (diepIO) { - const preventDefault = Event.prototype.preventDefault - Event.prototype.preventDefault = () => {} + const seconds = 1000 + const minutes = 60 * seconds const css = ` ${GM_getResourceText('TAILWINDCSS')} - :focus { - outline: none; - } - #canvas { - cursor: crosshair !important; + body { + color: unset; } ` GM_addStyle(css) - const { useState, useEffect, useRef } = React - const { countBy, some } = _ + let grantRewardTimeoutId = undefined function App() { - const [stats] = useState([ - { - key: 1, - color: '#eeb690' - }, - { - key: 2, - color: '#ec6cf0' - }, - { - key: 3, - color: '#9466ea' - }, - { - key: 4, - color: '#6c96f0' - }, - { - key: 5, - color: '#f0d96c' - }, - { - key: 6, - color: '#f06c6c' - }, - { - key: 7, - color: '#98f06c' - }, - { - key: 8, - color: '#6cf0ec' - } - ]) - - const [presets] = useState([ - { - text: '010-7757-6', - upgrade: '456782456784567847586884756457475' - }, - { - text: '000-7777-5', - upgrade: '456788456784567847586664756457475' - }, - { - text: '000-7757-7', - upgrade: '456788456784567847586884756457475' - }, - { - text: '020-5757-7', - upgrade: '456788825678856784758264756457475' - } - ]) - - const [upgrade, setUpgrade] = useState('') - const [isUpgradeShown, setIsUpgradeShown] = useState(false) - const [statsStr, setStatsStr] = useState('') - const upgradeInputRef = useRef(null) - - const getStatBarSlotBackgroundColor = (stat, slotIndex) => { - return slotIndex < countBy(upgrade)[stat.key] - ? stat.color - : slotIndex < 7 - ? '#111827' - : '#030712' - } - - const handleUpgradeChange = (event) => { - const { value } = event.target - if (/[^1-8]/.test(value) || some(countBy(value), (v) => v > 10)) { - const selectionStart = event.target.selectionStart - 1 - setTimeout(() => { - event.target.selectionStart = selectionStart - event.target.selectionEnd = selectionStart - }) - return - } - setUpgrade(value) - } - - const handleUpgradeKeyDown = (event) => { - if (/^((Digit|Numpad)[1-8]|Arrow(Up|Down|Left|Right))$/.test(event.code)) { - event.stopPropagation() - } - if (event.code === 'Enter' || event.code === 'NumpadEnter') { - event.stopPropagation() - input.execute(`game_stats_build ${upgrade}`) - setIsUpgradeShown(false) - } - } - - const handlePresetClick = (preset) => { - setUpgrade(preset.upgrade) - upgradeInputRef.current.focus() - } - - const handleGlobalKeyDown = (event) => { - if (event.repeat || event.ctrlKey || event.shiftKey || event.metaKey) return - if (document.activeElement.localName === 'd-base') return - - switch (event.code) { - case 'KeyT': - setIsUpgradeShown((prev) => !prev) - break - - case 'KeyG': - input.grantReward() - break - } - } - - const handleGlobalContextMenu = (event) => { - if (['input', 'textarea'].includes(event.target.localName)) return - preventDefault.call(event) - } + return React.createElement('div', null) + } - const updateUI = () => { - const newStatStr = ui.__playerAttributes.attributes - .map((attr) => attr.slotsFilled) - .join('/') - if (newStatStr !== statsStr) { - setStatsStr(newStatStr) + function removeEls(delay) { + window.setTimeout(() => { + const els = document.querySelectorAll( + '#ad-holders, #aa-video-holder, #diep-io_300x250, iframe' + ) + for (const el of els) { + el.remove() } - } - - useEffect(() => { - window.addEventListener('keydown', handleGlobalKeyDown) - window.addEventListener('contextmenu', handleGlobalContextMenu) - setInterval(updateUI, 1000) - }, []) - - return React.createElement( - 'div', - { className: 'h-full' }, - isUpgradeShown && - React.createElement( - 'div', - { - className: - 'flex absolute bottom-7 left-72 p-4 gap-4 rounded-lg bg-gray-800/90 pointer-events-auto' - }, - React.createElement( - 'div', - { className: 'flex flex-col gap-3' }, - React.createElement( - 'div', - { className: 'flex-1 w-60' }, - stats.map((stat) => - React.createElement( - 'div', - { key: stat.key, className: 'flex items-center gap-2' }, - React.createElement( - 'div', - { className: 'font-mono text-sm' }, - stat.key - ), - React.createElement( - 'div', - { - className: - 'flex-1 flex gap-1 rounded-lg overflow-hidden' - }, - Array(10) - .fill(0) - .map((_, slotIndex) => - React.createElement('div', { - key: slotIndex, - className: 'flex-1 h-4', - style: { - backgroundColor: - getStatBarSlotBackgroundColor( - stat, - slotIndex - ) - } - }) - ) - ) - ) - ) - ), - React.createElement( - 'div', - { className: 'relative flex font-mono' }, - React.createElement('input', { - ref: upgradeInputRef, - className: - 'min-w-0 px-2 py-1 rounded-lg bg-gray-900 text-transparent caret-white selection:bg-blue-700 selection:text-transparent cursor-text', - autoFocus: true, - maxLength: 33, - size: 31, - value: upgrade, - onChange: handleUpgradeChange, - onKeyDown: handleUpgradeKeyDown - }), - React.createElement( - 'div', - { className: 'absolute inset-0 px-2 py-1 pointer-events-none' }, - Array(33) - .fill(undefined) - .map((_, i) => - React.createElement( - 'div', - { - key: i, - className: `inline-block ${i <= 13 ? 'text-gray-400' : i <= 26 ? 'text-sky-400' : i <= 27 ? 'text-yellow-400' : 'text-rose-400'}` - }, - upgrade[i] ?? '\xb7' - ) - ) - ) - ) - ), - React.createElement( - 'div', - { className: 'flex flex-col gap-1' }, - React.createElement('div', { className: 'px-2' }, 'Presets'), - React.createElement( - 'div', - { className: 'flex-1 flex flex-col overflow-x-hidden' }, - presets.map((preset) => - React.createElement( - 'button', - { - key: preset.upgrade, - className: 'px-2 rounded font-sans hover:bg-gray-600', - onClick: () => handlePresetClick(preset) - }, - preset.text - ) - ) - ) - ) - ), - ui && - React.createElement( - 'div', - { className: 'absolute bottom-7 left-0 flex flex-col' }, - ui.__playerAttributes.attributes - .slice() - .reverse() - .map((attr, i) => { - const stat = stats[i] - return React.createElement( - 'div', - { - key: stat.key, - className: 'px-1 text-black', - style: { background: `${stat.color}cc` } - }, - attr.slotsFilled || '\xa0' - ) - }) - ) - ) + }, delay) } + removeEls() + removeEls(5000) + removeEls(10000) + removeEls(15000) + + const gameOverContinueEl = document.querySelector('#game-over-continue') + gameOverContinueEl?.addEventListener('click', () => { + if (grantRewardTimeoutId !== undefined) return + + input.grantReward() + grantRewardTimeoutId = window.setTimeout( + () => { + grantRewardTimeoutId = undefined + }, + 2 * minutes + 10 * seconds + ) + }) const rootEl = document.createElement('div') rootEl.className = 'absolute inset-0 pointer-events-none z-[999]' diff --git a/scripts/Diep-io-Improvements/script.user.tsx b/scripts/Diep-io-Improvements/script.user.tsx index 6ae2ef2..a44569b 100644 --- a/scripts/Diep-io-Improvements/script.user.tsx +++ b/scripts/Diep-io-Improvements/script.user.tsx @@ -8,7 +8,6 @@ // @match https://diep.io/* // @require https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js // @require https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js -// @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js // @resource TAILWINDCSS // @grant GM_getResourceText // @grant GM_addStyle @@ -20,261 +19,50 @@ // ==/UserScript== namespace diepIO { - type Stat = { - key: number - color: string - } - - type Preset = { - text: string - upgrade: string - } - - const preventDefault = Event.prototype.preventDefault - Event.prototype.preventDefault = () => {} + const seconds: number = 1000 + const minutes: number = 60 * seconds const css: string = ` ${GM_getResourceText('TAILWINDCSS')} - :focus { - outline: none; - } - #canvas { - cursor: crosshair !important; + body { + color: unset; } ` GM_addStyle(css) - const { useState, useEffect, useRef } = React - const { countBy, some } = _ - - function App() { - const [stats] = useState([ - { - key: 1, - color: '#eeb690' - }, - { - key: 2, - color: '#ec6cf0' - }, - { - key: 3, - color: '#9466ea' - }, - { - key: 4, - color: '#6c96f0' - }, - { - key: 5, - color: '#f0d96c' - }, - { - key: 6, - color: '#f06c6c' - }, - { - key: 7, - color: '#98f06c' - }, - { - key: 8, - color: '#6cf0ec' - } - ]) - - const [presets] = useState([ - { - text: '010-7757-6', - upgrade: '456782456784567847586884756457475' - }, - { - text: '000-7777-5', - upgrade: '456788456784567847586664756457475' - }, - { - text: '000-7757-7', - upgrade: '456788456784567847586884756457475' - }, - { - text: '020-5757-7', - upgrade: '456788825678856784758264756457475' - } - ]) - - const [upgrade, setUpgrade] = useState('') - const [isUpgradeShown, setIsUpgradeShown] = useState(false) - const [statsStr, setStatsStr] = useState('') - const upgradeInputRef = useRef(null) - - const getStatBarSlotBackgroundColor = (stat: Stat, slotIndex: number): string => { - return slotIndex < countBy(upgrade)[stat.key] - ? stat.color - : slotIndex < 7 - ? '#111827' - : '#030712' - } - - const handleUpgradeChange = (event: ChangeEvent): void => { - const { value } = event.target - if (/[^1-8]/.test(value) || some(countBy(value), (v) => v > 10)) { - const selectionStart = event.target.selectionStart - 1 - setTimeout(() => { - event.target.selectionStart = selectionStart - event.target.selectionEnd = selectionStart - }) - return - } - setUpgrade(value) - } - - const handleUpgradeKeyDown = (event): void => { - if (/^((Digit|Numpad)[1-8]|Arrow(Up|Down|Left|Right))$/.test(event.code)) { - event.stopPropagation() - } - if (event.code === 'Enter' || event.code === 'NumpadEnter') { - event.stopPropagation() - input.execute(`game_stats_build ${upgrade}`) - setIsUpgradeShown(false) - } - } - - const handlePresetClick = (preset: Preset): void => { - setUpgrade(preset.upgrade) - upgradeInputRef.current.focus() - } - - const handleGlobalKeyDown = (event): void => { - if (event.repeat || event.ctrlKey || event.shiftKey || event.metaKey) return - if (document.activeElement.localName === 'd-base') return - - switch (event.code) { - case 'KeyT': - setIsUpgradeShown((prev) => !prev) - break + let grantRewardTimeoutId: number | undefined = undefined - case 'KeyG': - input.grantReward() - break - } - } - - const handleGlobalContextMenu = (event): void => { - if (['input', 'textarea'].includes(event.target.localName)) return - preventDefault.call(event) - } + function App(): ReactNode { + return
+ } - const updateUI = (): void => { - const newStatStr: string = ui.__playerAttributes.attributes - .map((attr) => attr.slotsFilled) - .join('/') - if (newStatStr !== statsStr) { - setStatsStr(newStatStr) + function removeEls(delay?: number): void { + window.setTimeout(() => { + const els = document.querySelectorAll( + '#ad-holders, #aa-video-holder, #diep-io_300x250, iframe' + ) + for (const el of els) { + el.remove() } - } - - useEffect(() => { - window.addEventListener('keydown', handleGlobalKeyDown) - window.addEventListener('contextmenu', handleGlobalContextMenu) - setInterval(updateUI, 1000) - }, []) - - return ( -
- {isUpgradeShown && ( -
- {/* Cột trái */} -
- {/* Các thanh chỉ số */} -
- {stats.map((stat) => ( -
-
{stat.key}
-
- {Array(10) - .fill(0) - .map((_, slotIndex) => ( -
- ))} -
-
- ))} -
- {/* Ô nhập */} -
- -
- {Array(33) - .fill(undefined) - .map((_, i) => ( -
- {upgrade[i] ?? '\xb7'} -
- ))} -
-
-
- {/* Cột phải */} -
-
Presets
-
- {presets.map((preset) => ( - - ))} -
-
-
- )} - {ui && ( -
- {ui.__playerAttributes.attributes - .slice() - .reverse() - .map((attr, i) => { - const stat: Stat = stats[i] - return ( -
- {attr.slotsFilled || '\xa0'} -
- ) - })} -
- )} -
- ) + }, delay) } + removeEls() + removeEls(5000) + removeEls(10000) + removeEls(15000) + + const gameOverContinueEl = document.querySelector('#game-over-continue') + gameOverContinueEl?.addEventListener('click', () => { + if (grantRewardTimeoutId !== undefined) return + + input.grantReward() + grantRewardTimeoutId = window.setTimeout( + () => { + grantRewardTimeoutId = undefined + }, + 2 * minutes + 10 * seconds + ) + }) const rootEl = document.createElement('div') rootEl.className = 'absolute inset-0 pointer-events-none z-[999]' diff --git a/scripts/Diep-io-Improvements/types.d.ts b/scripts/Diep-io-Improvements/types.d.ts index a36b7e5..ec88683 100644 --- a/scripts/Diep-io-Improvements/types.d.ts +++ b/scripts/Diep-io-Improvements/types.d.ts @@ -1,43 +1,7 @@ namespace diepIO { - declare const input: { - execute(command: string): void + interface Input { grantReward(): void - mouse(x: number, y: number): void - key_down(keyCode: number): void - key_up(keyCode: number): void } - declare const ui: { - screen: 'home' | 'game' | 'stats' - game: UIGame | null - __playerAttributes: UIPlayerAttributes - } - - type UIGame = { - selectedGameMode: UIGameMode - } - - type UIPlayerAttributes = { - pointsLeft: number - attributes: UIAttribute[] - } - - type UIAttribute = { - index: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 - name: UIAttributeName - slotsFilled: number - slotsWanted: number - totalSlots: number - } - - type UIAttributeName = - | 'Movement Speed' - | 'Bullet Damage' - | 'Bullet Penetration' - | 'Bullet Speed' - | 'Body Damage' - | 'Max Health' - | 'Health Regen' - - type UIGameMode = 'ffa' | 'teams' | '4teams' | 'event' | 'maze' | 'sandbox' + declare const input: Input } diff --git a/scripts/Mien-AI-Music/script.user.js b/scripts/Mien-AI-Music/script.user.js index ac390c7..bed56f8 100644 --- a/scripts/Mien-AI-Music/script.user.js +++ b/scripts/Mien-AI-Music/script.user.js @@ -14,18 +14,22 @@ // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle +// @grant window.onurlchange // @license MIT // @noframes // @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Mien-AI-Music // ==/UserScript== -;(async function () { - const simpleUrl = (location.hostname + location.pathname) - .replace(/^www\./, '') - .replace(/\/$/, '') +var MienAIMusic +;(function (MienAIMusic) { + const cleanupCbs = new Set() + + function cleanup(cb) { + cleanupCbs.add(cb) + } let accountEmails = [] - let accountEmailsInput = await GM_getValue('accountEmails', '') + let accountEmailsInput = GM_getValue('accountEmails', '') async function wait(ms) { return new Promise((resolve) => { @@ -59,52 +63,7 @@ el?.click() } - async function switchAccount(accountEmail) { - await GM_setValue('accountEmail', accountEmail) - clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') - await wait(600) - clickIfContentIncludes('button:has(> span)', 'Sign In') - await wait(200) - click('.cl-socialButtonsIconButton__google') - } - - async function inputEmails() { - while (accountEmailsInput === '') { - accountEmailsInput = - prompt( - 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' - ) ?? '' - accountEmailsInput = accountEmailsInput.trim() - if (accountEmailsInput === '') continue - GM_setValue('accountEmails', accountEmailsInput) - } - accountEmails = accountEmailsInput.split(/, */) - } - - async function renderAccountEmails() { - await wait(2000) - const siblingEl = document.querySelector('a[href="/search"]') - const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) - ?.textContent?.replace('@gmail.com', '') - .toLowerCase() - for (const accountEmail of accountEmails.slice().reverse()) { - const el = document.createElement('button') - el.style.paddingLeft = '40px' - el.style.textAlign = 'left' - el.textContent = accountEmail - if (accountEmail === emailName) { - el.style.color = 'limegreen' - el.style.paddingLeft = '26px' - el.textContent = `> ${accountEmail}` - } - el.addEventListener('click', () => { - switchAccount(accountEmail) - }) - siblingEl.after(el) - } - } - - function css(strs, ...vals) { + function embedCode(strs, ...vals) { let result = '' for (let i = 0; i < strs.length - 1; i++) { result += strs[i] + String(vals[i]) @@ -112,115 +71,202 @@ result += strs.at(-1) return result } + const css = embedCode - await inputEmails() - - window.addEventListener('keydown', async (event) => { - if (event.repeat) return - if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return - if (document.activeElement.matches('input, textarea, select')) return - - const audio = document.querySelector('audio#active-audio-play') - - const { code } = event - switch (code) { - case 'F1': - case 'F2': - case 'F3': - case 'F4': - case 'F5': - case 'F6': - { - event.preventDefault() - const accountIndex = Number(code.slice(-1)) - 1 - const accountEmail = accountEmails[accountIndex] - switchAccount(accountEmail) + let oldSimpleUrl = '' + + async function handleUrlChange() { + const simpleUrl = (location.hostname + location.pathname) + .replace(/^www\./, '') + .replace(/\/$/, '') + if (simpleUrl === oldSimpleUrl) return + + oldSimpleUrl = simpleUrl + + cleanupCbs.forEach((cb) => { + cb() + }) + cleanupCbs.clear() + + async function switchAccount(accountEmail) { + GM_setValue('accountEmail', accountEmail) + clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') + await wait(2000) + clickIfContentIncludes('button:has(> span)', 'Sign In') + await wait(1000) + click('.cl-socialButtonsIconButton__google') + } + + async function inputEmails() { + while (accountEmailsInput === '') { + accountEmailsInput = + prompt( + 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' + ) ?? '' + accountEmailsInput = accountEmailsInput.trim() + if (accountEmailsInput === '') continue + GM_setValue('accountEmails', accountEmailsInput) + } + accountEmails = accountEmailsInput.split(/, */) + } + + async function renderAccountEmails() { + if (!simpleUrl.startsWith('suno.com')) return + if (document.body.dataset.renderedAccountEmails !== undefined) return + + document.body.dataset.renderedAccountEmails = '1' + await wait(2000) + const siblingEl = document.querySelector('a[href="/search"]') + if (siblingEl === null) { + document.body.dataset.renderedAccountEmails = undefined + return + } + const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) + ?.textContent?.replace('@gmail.com', '') + .toLowerCase() + for (const accountEmail of accountEmails.slice().reverse()) { + const el = document.createElement('button') + el.style.paddingLeft = '40px' + el.style.textAlign = 'left' + el.textContent = accountEmail + if (accountEmail === emailName) { + el.style.color = 'limegreen' + el.style.paddingLeft = '26px' + el.textContent = `> ${accountEmail}` } - break - - case 'Backquote': - case 'Digit1': - case 'Digit2': - case 'Digit3': - case 'Digit4': - case 'Digit5': - case 'Digit6': - case 'Digit7': - case 'Digit8': - case 'Digit9': - case 'Digit0': - if (audio) { - if (audio.src) { - let part = 0 - if (code.startsWith('Digit')) { - part = Number(code.slice(-1)) + el.addEventListener('click', () => { + switchAccount(accountEmail) + }) + siblingEl.after(el) + } + } + + await inputEmails() + + function handleWindowKeyDown(event) { + if (event.repeat) return + if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return + if (document.activeElement.matches('input, textarea, select')) return + + const audio = document.querySelector('audio#active-audio-play') + + const { code } = event + switch (code) { + case 'F1': + case 'F2': + case 'F3': + case 'F4': + case 'F5': + case 'F6': + { + event.preventDefault() + const accountIndex = Number(code.slice(-1)) - 1 + const accountEmail = accountEmails[accountIndex] + switchAccount(accountEmail) + } + break + + case 'Backquote': + case 'Digit1': + case 'Digit2': + case 'Digit3': + case 'Digit4': + case 'Digit5': + case 'Digit6': + case 'Digit7': + case 'Digit8': + case 'Digit9': + case 'Digit0': + if (audio) { + if (audio.src) { + let part = 0 + if (code.startsWith('Digit')) { + part = Number(code.slice(-1)) + } + audio.currentTime = audio.duration * (part / 10) } - audio.currentTime = audio.duration * (part / 10) } - } - break - - case 'ArrowLeft': - case 'ArrowRight': - if (audio) { - if (audio.src) { - const amount = code === 'ArrowLeft' ? -5 : 5 - audio.currentTime += amount + break + + case 'ArrowLeft': + case 'ArrowRight': + if (audio) { + if (audio.src) { + const amount = code === 'ArrowLeft' ? -5 : 5 + audio.currentTime += amount + } } - } - break + break + } } - }) + window.addEventListener('keydown', handleWindowKeyDown) + cleanup(() => { + window.removeEventListener('keydown', handleWindowKeyDown) + }) - if (simpleUrl === 'suno.com') { - location.pathname = '/me' - } - // - else if (simpleUrl === 'suno.com/me') { - await wait(2000) - clickIfContentIncludes('button', 'Liked') - document.activeElement.blur() - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/create') { - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/search') { - renderAccountEmails() - } - // - else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { - await wait(500) - click('.cl-socialButtonsIconButton__google') - } - // - else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { - const accountEmail = await GM_getValue('accountEmail') - if (typeof accountEmail === 'string') { - await GM_deleteValue('accountEmail') - const emailText = `${accountEmail}@gmail.com` - await wait(600) - click(`[data-identifier="${emailText}"]`) - } - } - // - else if (simpleUrl.startsWith('google.com/search')) { - const els = document.querySelectorAll('[data-lyricid] > div > :nth-child(2) > div') - for (const el of els) { - el.style.marginBottom = '-8px' - const br = document.createElement('br') - el.after(br) + if (simpleUrl.startsWith('suno.com')) { + if (simpleUrl === 'suno.com') { + location.pathname = '/me' + } // + else { + if (simpleUrl === 'suno.com/me') { + await wait(2000) + clickIfContentIncludes('button.bg-transparent', 'Liked') + document.activeElement.blur() + } + renderAccountEmails() + } + } // + else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { + await wait(500) + click('.cl-socialButtonsIconButton__google') + } // + else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { + const accountEmail = await GM_getValue('accountEmail') + if (typeof accountEmail === 'string') { + GM_deleteValue('accountEmail') + const emailText = `${accountEmail}@gmail.com` + await wait(600) + click(`[data-identifier="${emailText}"]`) + } + } // + else if (simpleUrl.startsWith('google.com/search')) { + const els = document.querySelectorAll('[data-lyricid] > div > :nth-child(2) > div') + for (const el of els) { + el.style.marginBottom = '-8px' + const br = document.createElement('br') + el.after(br) + } } } GM_addStyle(css` - body { - font-family: sans-serif; + *, + :before, + :after { + backdrop-filter: none !important; + box-shadow: none !important; + } + body, + .font-sans, + [role='menuitem'] { + font-family: 'IBM Plex Sans', sans-serif !important; + } + .font-serif { + font-family: 'Palatino Linotype', serif; + font-weight: 700; + } + .sticky { + position: unset !important; } .css-1w1f2eu { white-space: normal !important; } + .w-split-bar + :has(.bg-vinylBlack-darker) { + width: 320px !important; + } `) -})() + + window.addEventListener('urlchange', handleUrlChange) + handleUrlChange() +})(MienAIMusic || (MienAIMusic = {})) diff --git a/scripts/Mien-AI-Music/script.user.ts b/scripts/Mien-AI-Music/script.user.ts index 602949b..444622f 100644 --- a/scripts/Mien-AI-Music/script.user.ts +++ b/scripts/Mien-AI-Music/script.user.ts @@ -14,17 +14,20 @@ // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle +// @grant window.onurlchange // @license MIT // @noframes // ==/UserScript== -;(async function () { - const simpleUrl: string = (location.hostname + location.pathname) - .replace(/^www\./, '') - .replace(/\/$/, '') +namespace MienAIMusic { + const cleanupCbs = new Set<() => unknown>() + + function cleanup(cb: () => unknown): void { + cleanupCbs.add(cb) + } let accountEmails: string[] = [] - let accountEmailsInput = await GM_getValue('accountEmails', '') + let accountEmailsInput = GM_getValue('accountEmails', '') async function wait(ms: number): Promise { return new Promise((resolve) => { @@ -66,52 +69,7 @@ el?.click() } - async function switchAccount(accountEmail: string): Promise { - await GM_setValue('accountEmail', accountEmail) - clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') - await wait(600) - clickIfContentIncludes('button:has(> span)', 'Sign In') - await wait(200) - click('.cl-socialButtonsIconButton__google') - } - - async function inputEmails(): Promise { - while (accountEmailsInput === '') { - accountEmailsInput = - prompt( - 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' - ) ?? '' - accountEmailsInput = accountEmailsInput.trim() - if (accountEmailsInput === '') continue - GM_setValue('accountEmails', accountEmailsInput) - } - accountEmails = accountEmailsInput.split(/, */) - } - - async function renderAccountEmails(): Promise { - await wait(2000) - const siblingEl = document.querySelector('a[href="/search"]')! - const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) - ?.textContent?.replace('@gmail.com', '') - .toLowerCase() - for (const accountEmail of accountEmails.slice().reverse()) { - const el = document.createElement('button') - el.style.paddingLeft = '40px' - el.style.textAlign = 'left' - el.textContent = accountEmail - if (accountEmail === emailName) { - el.style.color = 'limegreen' - el.style.paddingLeft = '26px' - el.textContent = `> ${accountEmail}` - } - el.addEventListener('click', (): void => { - switchAccount(accountEmail) - }) - siblingEl.after(el) - } - } - - function css(strs: TemplateStringsArray, ...vals: unknown[]): string { + function embedCode(strs: TemplateStringsArray, ...vals: unknown[]): string { let result: string = '' for (let i = 0; i < strs.length - 1; i++) { result += strs[i] + String(vals[i]) @@ -119,117 +77,204 @@ result += strs.at(-1) return result } + const css = embedCode - await inputEmails() - - window.addEventListener('keydown', async (event) => { - if (event.repeat) return - if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return - if (document.activeElement!.matches('input, textarea, select')) return - - const audio = document.querySelector('audio#active-audio-play') - - const { code } = event - switch (code) { - case 'F1': - case 'F2': - case 'F3': - case 'F4': - case 'F5': - case 'F6': - { - event.preventDefault() - const accountIndex: number = Number(code.slice(-1)) - 1 - const accountEmail: string = accountEmails[accountIndex] - switchAccount(accountEmail) + let oldSimpleUrl: string = '' + + async function handleUrlChange(): Promise { + const simpleUrl: string = (location.hostname + location.pathname) + .replace(/^www\./, '') + .replace(/\/$/, '') + if (simpleUrl === oldSimpleUrl) return + + oldSimpleUrl = simpleUrl + + cleanupCbs.forEach((cb) => { + cb() + }) + cleanupCbs.clear() + + async function switchAccount(accountEmail: string): Promise { + GM_setValue('accountEmail', accountEmail) + clickIfContentIncludes('.chakra-menu__menuitem', 'Sign Out') + await wait(2000) + clickIfContentIncludes('button:has(> span)', 'Sign In') + await wait(1000) + click('.cl-socialButtonsIconButton__google') + } + + async function inputEmails(): Promise { + while (accountEmailsInput === '') { + accountEmailsInput = + prompt( + 'Nhập danh sách email đăng nhập, không bao gồm "@gmail.com", phân tách bởi dấu phẩy:' + ) ?? '' + accountEmailsInput = accountEmailsInput.trim() + if (accountEmailsInput === '') continue + GM_setValue('accountEmails', accountEmailsInput) + } + accountEmails = accountEmailsInput.split(/, */) + } + + async function renderAccountEmails(): Promise { + if (!simpleUrl.startsWith('suno.com')) return + if (document.body.dataset.renderedAccountEmails !== undefined) return + + document.body.dataset.renderedAccountEmails = '1' + await wait(2000) + const siblingEl = document.querySelector('a[href="/search"]') + if (siblingEl === null) { + document.body.dataset.renderedAccountEmails = undefined + return + } + const emailName = queryContentIncludes('.chakra-text', '@gmail.com', true) + ?.textContent?.replace('@gmail.com', '') + .toLowerCase() + for (const accountEmail of accountEmails.slice().reverse()) { + const el = document.createElement('button') + el.style.paddingLeft = '40px' + el.style.textAlign = 'left' + el.textContent = accountEmail + if (accountEmail === emailName) { + el.style.color = 'limegreen' + el.style.paddingLeft = '26px' + el.textContent = `> ${accountEmail}` } - break - - case 'Backquote': - case 'Digit1': - case 'Digit2': - case 'Digit3': - case 'Digit4': - case 'Digit5': - case 'Digit6': - case 'Digit7': - case 'Digit8': - case 'Digit9': - case 'Digit0': - if (audio) { - if (audio.src) { - let part = 0 - if (code.startsWith('Digit')) { - part = Number(code.slice(-1)) + el.addEventListener('click', () => { + switchAccount(accountEmail) + }) + siblingEl.after(el) + } + } + + await inputEmails() + + function handleWindowKeyDown(event: KeyboardEvent): void { + if (event.repeat) return + if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return + if (document.activeElement!.matches('input, textarea, select')) return + + const audio = document.querySelector('audio#active-audio-play') + + const { code } = event + switch (code) { + case 'F1': + case 'F2': + case 'F3': + case 'F4': + case 'F5': + case 'F6': + { + event.preventDefault() + const accountIndex: number = Number(code.slice(-1)) - 1 + const accountEmail: string = accountEmails[accountIndex] + switchAccount(accountEmail) + } + break + + case 'Backquote': + case 'Digit1': + case 'Digit2': + case 'Digit3': + case 'Digit4': + case 'Digit5': + case 'Digit6': + case 'Digit7': + case 'Digit8': + case 'Digit9': + case 'Digit0': + if (audio) { + if (audio.src) { + let part = 0 + if (code.startsWith('Digit')) { + part = Number(code.slice(-1)) + } + audio.currentTime = audio.duration * (part / 10) } - audio.currentTime = audio.duration * (part / 10) } - } - break - - case 'ArrowLeft': - case 'ArrowRight': - if (audio) { - if (audio.src) { - const amount = code === 'ArrowLeft' ? -5 : 5 - audio.currentTime += amount + break + + case 'ArrowLeft': + case 'ArrowRight': + if (audio) { + if (audio.src) { + const amount = code === 'ArrowLeft' ? -5 : 5 + audio.currentTime += amount + } } - } - break + break + } } - }) + window.addEventListener('keydown', handleWindowKeyDown) + cleanup(() => { + window.removeEventListener('keydown', handleWindowKeyDown) + }) - if (simpleUrl === 'suno.com') { - location.pathname = '/me' - } - // - else if (simpleUrl === 'suno.com/me') { - await wait(2000) - clickIfContentIncludes('button', 'Liked') - ;(document.activeElement as HTMLElement).blur() - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/create') { - renderAccountEmails() - } - // - else if (simpleUrl === 'suno.com/search') { - renderAccountEmails() - } - // - else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { - await wait(500) - click('.cl-socialButtonsIconButton__google') - } - // - else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { - const accountEmail = await GM_getValue('accountEmail') - if (typeof accountEmail === 'string') { - await GM_deleteValue('accountEmail') - const emailText: string = `${accountEmail}@gmail.com` - await wait(600) - click(`[data-identifier="${emailText}"]`) - } - } - // - else if (simpleUrl.startsWith('google.com/search')) { - const els = document.querySelectorAll( - '[data-lyricid] > div > :nth-child(2) > div' - ) - for (const el of els) { - el.style.marginBottom = '-8px' - const br: HTMLBRElement = document.createElement('br') - el.after(br) + if (simpleUrl.startsWith('suno.com')) { + if (simpleUrl === 'suno.com') { + location.pathname = '/me' + } // + else { + if (simpleUrl === 'suno.com/me') { + await wait(2000) + clickIfContentIncludes('button.bg-transparent', 'Liked') + ;(document.activeElement as HTMLElement).blur() + } + renderAccountEmails() + } + } // + else if (simpleUrl.startsWith('accounts.suno.com/sign-in')) { + await wait(500) + click('.cl-socialButtonsIconButton__google') + } // + else if (simpleUrl.startsWith('accounts.google.com/o/oauth2/')) { + const accountEmail = await GM_getValue('accountEmail') + if (typeof accountEmail === 'string') { + GM_deleteValue('accountEmail') + const emailText: string = `${accountEmail}@gmail.com` + await wait(600) + click(`[data-identifier="${emailText}"]`) + } + } // + else if (simpleUrl.startsWith('google.com/search')) { + const els = document.querySelectorAll( + '[data-lyricid] > div > :nth-child(2) > div' + ) + for (const el of els) { + el.style.marginBottom = '-8px' + const br: HTMLBRElement = document.createElement('br') + el.after(br) + } } } GM_addStyle(css` - body { - font-family: sans-serif; + *, + :before, + :after { + backdrop-filter: none !important; + box-shadow: none !important; + } + body, + .font-sans, + [role='menuitem'] { + font-family: 'IBM Plex Sans', sans-serif !important; + } + .font-serif { + font-family: 'Palatino Linotype', serif; + font-weight: 700; + } + .sticky { + position: unset !important; } .css-1w1f2eu { white-space: normal !important; } + .w-split-bar + :has(.bg-vinylBlack-darker) { + width: 320px !important; + } `) -})() + + window.addEventListener('urlchange', handleUrlChange) + handleUrlChange() +} diff --git a/scripts/Tetr-io-Improvements/README.md b/scripts/Tetr-io-Improvements/README.md new file mode 100644 index 0000000..dfba14b --- /dev/null +++ b/scripts/Tetr-io-Improvements/README.md @@ -0,0 +1,14 @@ +# Tetr.io Improvements + +## Introducion + +This userscript makes your [tetr.io][1] playing experience better. + +## Features + +- Remove smooth motion effects, making navigation faster. +- Remove ads. +- Press the `Home` key each time to automatically press the login button, enter Tetra League, start finding matches. +- Press the `End` key to press the next button when the match ends. + +[1]: https://tetr.io diff --git a/scripts/Tetr-io-Improvements/script.user.js b/scripts/Tetr-io-Improvements/script.user.js index 2311e29..86df13e 100644 --- a/scripts/Tetr-io-Improvements/script.user.js +++ b/scripts/Tetr-io-Improvements/script.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Tetr.io Improvements // @namespace https://github.com/tientq64/userscripts -// @version 0.1.0 +// @version 1.0.0 // @description Provides improvements for Tetr.io game. // @author tientq64 // @icon https://www.google.com/s2/favicons?sz=64&domain=tetr.io @@ -69,8 +69,8 @@ var TetrIOImprovements window.addEventListener('keydown', handleWindowKeyDown) GM_addStyle(` - * { - transition: none !important; - } -`) + * { + transition: none !important; + } + `) })(TetrIOImprovements || (TetrIOImprovements = {})) diff --git a/scripts/Tetr-io-Improvements/script.user.ts b/scripts/Tetr-io-Improvements/script.user.ts index fee1ad1..b4c4e36 100644 --- a/scripts/Tetr-io-Improvements/script.user.ts +++ b/scripts/Tetr-io-Improvements/script.user.ts @@ -1,7 +1,7 @@ // ==UserScript== // @name Tetr.io Improvements // @namespace https://github.com/tientq64/userscripts -// @version 0.1.0 +// @version 1.0.0 // @description Provides improvements for Tetr.io game. // @author tientq64 // @icon https://www.google.com/s2/favicons?sz=64&domain=tetr.io @@ -72,8 +72,8 @@ namespace TetrIOImprovements { window.addEventListener('keydown', handleWindowKeyDown) GM_addStyle(` - * { - transition: none !important; - } -`) + * { + transition: none !important; + } + `) } diff --git a/scripts/Web-Page-Minimap/README.md b/scripts/Web-Page-Minimap/README.md new file mode 100644 index 0000000..15871aa --- /dev/null +++ b/scripts/Web-Page-Minimap/README.md @@ -0,0 +1,5 @@ +# Web Page Minimap + +## Credits + +Content icons created by DinosoftLabs - Flaticon diff --git a/scripts/Web-Page-Minimap/script.user.js b/scripts/Web-Page-Minimap/script.user.js new file mode 100644 index 0000000..58a0983 --- /dev/null +++ b/scripts/Web-Page-Minimap/script.user.js @@ -0,0 +1,176 @@ +// ==UserScript== +// @name Web Page Minimap +// @namespace https://github.com/tientq64/userscripts +// @version 0.1.0 +// @description A minimap for the current web page. +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/718/718994.png +// @match *://*/* +// @run-at document-idle +// @grant GM_addStyle +// @grant GM_getValue +// @grant GM_setValue +// @license MIT +// @noframes +// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Web-Page-Minimap +// ==/UserScript== + +const namesp = '__Web-Page-Minimap__userscript__' + +const scroller = document.createElement('div') +scroller.className = namesp +document.documentElement.appendChild(scroller) + +const note = document.createElement('div') +note.className = `${namesp}-note` +note.textContent = 'Ctrl+M to show/hide' +note.addEventListener('click', () => setIsShow(false)) +scroller.appendChild(note) + +const canvas = document.createElement('canvas') +canvas.className = `${namesp}-canvas` +scroller.appendChild(canvas) + +const pageWidth = 180 +const factor = pageWidth / (innerWidth - pageWidth) + +let isShow = GM_getValue('isShow', true) + +function setIsShow(value) { + isShow = value + GM_setValue('isShow', isShow) + if (isShow) { + scroller.removeAttribute('hidden') + render() + } else { + scroller.setAttribute('hidden', '') + } +} + +function render(loop) { + if (loop) { + setTimeout(render, loop, loop) + // requestAnimationFrame(render) + } + if (!isShow) return + + const pageHeight = Math.round(document.documentElement.scrollHeight * factor) + canvas.width = pageWidth + canvas.height = pageHeight + + const ctx = canvas.getContext('2d') + if (ctx === null) return + + ctx.imageSmoothingEnabled = false + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, pageWidth, pageHeight) + ctx.scale(factor, factor) + + const htmlRect = document.documentElement.getBoundingClientRect() + ctx.translate(-htmlRect.x, -htmlRect.y) + + const draw = ({ el, style }) => { + if (el.classList.contains(namesp)) return + + const isVisible = el.checkVisibility({ + opacityProperty: true, + visibilityProperty: true + }) + if (!isVisible) return + + const name = el.nodeName + + ctx.fillStyle = style.backgroundColor + + const rects = el.getClientRects() + for (const rect of rects) { + const x = Math.round(rect.x) + const y = Math.round(rect.y) + const width = Math.round(rect.width) + const height = Math.round(rect.height) + + ctx.fillRect(x, y, width, height) + + switch (name) { + case 'IMG': + case 'SVG': + case 'CANVAS': + case 'VIDEO': + { + const img = el + try { + ctx.drawImage(img, x, y, width, height) + } catch { + // + } + } + break + } + } + + const subEls = Array.from(el.children, (subEl) => ({ + el: subEl, + style: getComputedStyle(subEl) + })) + + subEls.sort((subElA, subElB) => { + const zA = subElA.style.zIndex === 'auto' ? 0 : Number(subElA.style.zIndex) + const zB = subElB.style.zIndex === 'auto' ? 0 : Number(subElB.style.zIndex) + return zA - zB + }) + + for (const subEl of subEls) { + draw(subEl) + } + } + + draw({ + el: document.documentElement, + style: getComputedStyle(document.documentElement) + }) +} + +window.addEventListener('keydown', (event) => { + if (event.repeat) return + if (document.activeElement?.matches('input,textarea,select')) return + + const { ctrlKey: ctrl, shiftKey: shift, altKey: alt, code } = event + + if (!ctrl && !shift && alt && code === 'KeyM') { + setIsShow(!isShow) + } +}) + +GM_addStyle(` + .${namesp} { + position: fixed; + top: 50%; + right: 0; + max-height: calc(100% - 16px); + padding: 8px; + overflow: auto; + transform: translate(0, -50%); + scrollbar-width: thin; + z-index: 99999; + } + .${namesp}-note { + font: 13px sans-serif; + line-height: 1.3; + text-align: center; + cursor: pointer; + } + .${namesp}-canvas { + border-radius: 8px; + box-shadow: 0 4px 8px #3338; + image-rendering: pixelated; + } +`) + +setIsShow(isShow) + +setTimeout(render, 1000) +setTimeout(render, 2000) +setTimeout(render, 3000) +setTimeout(render, 4000) + +render(5000) diff --git a/scripts/Web-Page-Minimap/script.user.ts b/scripts/Web-Page-Minimap/script.user.ts new file mode 100644 index 0000000..ad8a268 --- /dev/null +++ b/scripts/Web-Page-Minimap/script.user.ts @@ -0,0 +1,180 @@ +// ==UserScript== +// @name Web Page Minimap +// @namespace https://github.com/tientq64/userscripts +// @version 0.1.0 +// @description A minimap for the current web page. +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/718/718994.png +// @match *://*/* +// @run-at document-idle +// @grant GM_addStyle +// @grant GM_getValue +// @grant GM_setValue +// @license MIT +// @noframes +// ==/UserScript== + +interface El { + el: Element + style: CSSStyleDeclaration +} + +const namesp: string = '__Web-Page-Minimap__userscript__' + +const scroller: HTMLElement = document.createElement('div') +scroller.className = namesp +document.documentElement.appendChild(scroller) + +const note: HTMLDivElement = document.createElement('div') +note.className = `${namesp}-note` +note.textContent = 'Ctrl+M to show/hide' +note.addEventListener('click', () => setIsShow(false)) +scroller.appendChild(note) + +const canvas: HTMLCanvasElement = document.createElement('canvas') +canvas.className = `${namesp}-canvas` +scroller.appendChild(canvas) + +const pageWidth: number = 180 +const factor: number = pageWidth / (innerWidth - pageWidth) + +let isShow: boolean = GM_getValue('isShow', true) + +function setIsShow(value: boolean): void { + isShow = value + GM_setValue('isShow', isShow) + if (isShow) { + scroller.removeAttribute('hidden') + render() + } else { + scroller.setAttribute('hidden', '') + } +} + +function render(loop?: number): void { + if (loop) { + setTimeout(render, loop, loop) + // requestAnimationFrame(render) + } + if (!isShow) return + + const pageHeight: number = Math.round(document.documentElement.scrollHeight * factor) + canvas.width = pageWidth + canvas.height = pageHeight + + const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d') + if (ctx === null) return + + ctx.imageSmoothingEnabled = false + ctx.fillStyle = '#fff' + ctx.fillRect(0, 0, pageWidth, pageHeight) + ctx.scale(factor, factor) + + const htmlRect: DOMRect = document.documentElement.getBoundingClientRect() + ctx.translate(-htmlRect.x, -htmlRect.y) + + const draw = ({ el, style }: El): void => { + if (el.classList.contains(namesp)) return + + const isVisible: boolean = el.checkVisibility({ + opacityProperty: true, + visibilityProperty: true + }) + if (!isVisible) return + + const name: string = el.nodeName + + ctx.fillStyle = style.backgroundColor + + const rects: DOMRectList = el.getClientRects() + for (const rect of rects) { + const x: number = Math.round(rect.x) + const y: number = Math.round(rect.y) + const width: number = Math.round(rect.width) + const height: number = Math.round(rect.height) + + ctx.fillRect(x, y, width, height) + + switch (name) { + case 'IMG': + case 'SVG': + case 'CANVAS': + case 'VIDEO': + { + const img = el as CanvasImageSource + try { + ctx.drawImage(img, x, y, width, height) + } catch { + // + } + } + break + } + } + + const subEls = Array.from(el.children, (subEl) => ({ + el: subEl, + style: getComputedStyle(subEl) + })) + + subEls.sort((subElA, subElB) => { + const zA: number = subElA.style.zIndex === 'auto' ? 0 : Number(subElA.style.zIndex) + const zB: number = subElB.style.zIndex === 'auto' ? 0 : Number(subElB.style.zIndex) + return zA - zB + }) + + for (const subEl of subEls) { + draw(subEl) + } + } + + draw({ + el: document.documentElement, + style: getComputedStyle(document.documentElement) + }) +} + +window.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.repeat) return + if (document.activeElement?.matches('input,textarea,select')) return + + const { ctrlKey: ctrl, shiftKey: shift, altKey: alt, code } = event + + if (!ctrl && !shift && alt && code === 'KeyM') { + setIsShow(!isShow) + } +}) + +GM_addStyle(` + .${namesp} { + position: fixed; + top: 50%; + right: 0; + max-height: calc(100% - 16px); + padding: 8px; + overflow: auto; + transform: translate(0, -50%); + scrollbar-width: thin; + z-index: 99999; + } + .${namesp}-note { + font: 13px sans-serif; + line-height: 1.3; + text-align: center; + cursor: pointer; + } + .${namesp}-canvas { + border-radius: 8px; + box-shadow: 0 4px 8px #3338; + image-rendering: pixelated; + } +`) + +setIsShow(isShow) + +setTimeout(render, 1000) +setTimeout(render, 2000) +setTimeout(render, 3000) +setTimeout(render, 4000) + +render(5000) diff --git a/scripts/YouTube-Shorts-To-Normal-Video/script.user.js b/scripts/YouTube-Shorts-To-Normal-Video/script.user.js index 364ce78..bf803a8 100644 --- a/scripts/YouTube-Shorts-To-Normal-Video/script.user.js +++ b/scripts/YouTube-Shorts-To-Normal-Video/script.user.js @@ -1,47 +1,49 @@ // ==UserScript== -// @name YouTube Shorts To Normal Video -// @name:vi YouTube Shorts Thành Video Bình Thường -// @name:zh-CN YouTube Shorts 转普通视频 -// @name:zh-TW YouTube Shorts 轉普通視頻 -// @name:ja YouTube ショートから通常の動画へ -// @name:ko YouTube Shorts를 일반 비디오로 -// @name:es Cortos De YouTube A Video Normal -// @name:pt-BR YouTube Shorts Para Vídeo Normal -// @name:ru YouTube Shorts В Обычное Видео -// @name:id YouTube Shorts Ke Video Normal -// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक -// @name:nl YouTube Shorts Naar Normale Video -// @name:fr Short YouTube Vers Vidéo Normale -// @name:de YouTube-Shorts Zum Normalen Video -// @name:it Da YouTube Shorts A Video Normale -// @name:tr YouTube Shorts'tan Normal Videoya -// @name:th YouTube Shorts สู่วิดีโอปกติ -// @namespace https://github.com/tientq64/userscripts -// @version 1.0.2 +// @name YouTube Shorts To Normal Video +// @name:de YouTube-Shorts Zum Normalen Video +// @name:es Cortos De YouTube A Video Normal +// @name:fr Short YouTube Vers Vidéo Normale +// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक +// @name:id YouTube Shorts Ke Video Normal +// @name:it Da YouTube Shorts A Video Normale +// @name:ja YouTube ショートから通常の動画へ +// @name:ko YouTube Shorts를 일반 비디오로 +// @name:nl YouTube Shorts Naar Normale Video +// @name:pt-BR YouTube Shorts Para Vídeo Normal +// @name:ru YouTube Shorts В Обычное Видео +// @name:th YouTube Shorts สู่วิดีโอปกติ +// @name:tr YouTube Shorts'tan Normal Videoya +// @name:vi YouTube Shorts Thành Video Bình Thường +// @name:zh-CN YouTube Shorts 转普通视频 +// @name:zh-TW YouTube Shorts 轉普通視頻 +// @namespace https://github.com/tientq64/userscripts +// @version 1.1.0 // @description Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. -// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. -// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 -// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. +// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. +// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। +// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. +// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. // @description:ja YouTube Shorts 動画を通常の動画ビューに即座にリダイレクトし、保存、ダウンロード、品質の選択などを行うことができます。 // @description:ko YouTube Shorts 동영상을 일반 동영상 보기로 즉시 리디렉션하여 저장, 다운로드, 품질 선택 등이 가능합니다. -// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. // @description:pt-BR Redirecione instantaneamente os vídeos do YouTube Shorts para a visualização normal, permitindo que você salve, baixe, escolha a qualidade, etc. // @description:ru Мгновенно перенаправляет видеоролики YouTube Shorts в обычный режим просмотра, позволяя сохранять, загружать, выбирать качество и т. д. -// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. -// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। -// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. -// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. -// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. -// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. -// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. // @description:th เปลี่ยนเส้นทางวิดีโอ YouTube Shorts ไปยังมุมมองวิดีโอปกติทันที ช่วยให้คุณบันทึก ดาวน์โหลด เลือกคุณภาพ ฯลฯ ได้ -// @author tientq64 -// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png -// @match https://www.youtube.com/* -// @license MIT -// @grant none +// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. +// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 +// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png +// @match https://www.youtube.com/* +// @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* +// @license MIT +// @grant none // @noframes -// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/YouTube-Shorts-To-Normal-Video +// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/YouTube-Shorts-To-Normal-Video // ==/UserScript== function redirect() { diff --git a/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts b/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts index 5ffa3a5..b267168 100644 --- a/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts +++ b/scripts/YouTube-Shorts-To-Normal-Video/script.user.ts @@ -1,45 +1,47 @@ // ==UserScript== -// @name YouTube Shorts To Normal Video -// @name:vi YouTube Shorts Thành Video Bình Thường -// @name:zh-CN YouTube Shorts 转普通视频 -// @name:zh-TW YouTube Shorts 轉普通視頻 -// @name:ja YouTube ショートから通常の動画へ -// @name:ko YouTube Shorts를 일반 비디오로 -// @name:es Cortos De YouTube A Video Normal -// @name:pt-BR YouTube Shorts Para Vídeo Normal -// @name:ru YouTube Shorts В Обычное Видео -// @name:id YouTube Shorts Ke Video Normal -// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक -// @name:nl YouTube Shorts Naar Normale Video -// @name:fr Short YouTube Vers Vidéo Normale -// @name:de YouTube-Shorts Zum Normalen Video -// @name:it Da YouTube Shorts A Video Normale -// @name:tr YouTube Shorts'tan Normal Videoya -// @name:th YouTube Shorts สู่วิดีโอปกติ -// @namespace https://github.com/tientq64/userscripts -// @version 1.0.2 +// @name YouTube Shorts To Normal Video +// @name:de YouTube-Shorts Zum Normalen Video +// @name:es Cortos De YouTube A Video Normal +// @name:fr Short YouTube Vers Vidéo Normale +// @name:hi यूट्यूब शॉर्ट्स से सामान्य वीडियो तक +// @name:id YouTube Shorts Ke Video Normal +// @name:it Da YouTube Shorts A Video Normale +// @name:ja YouTube ショートから通常の動画へ +// @name:ko YouTube Shorts를 일반 비디오로 +// @name:nl YouTube Shorts Naar Normale Video +// @name:pt-BR YouTube Shorts Para Vídeo Normal +// @name:ru YouTube Shorts В Обычное Видео +// @name:th YouTube Shorts สู่วิดีโอปกติ +// @name:tr YouTube Shorts'tan Normal Videoya +// @name:vi YouTube Shorts Thành Video Bình Thường +// @name:zh-CN YouTube Shorts 转普通视频 +// @name:zh-TW YouTube Shorts 轉普通視頻 +// @namespace https://github.com/tientq64/userscripts +// @version 1.1.0 // @description Instantly redirect YouTube Shorts videos to normal video view, allowing you to save, download, choose quality, etc. -// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. -// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 -// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. +// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. +// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। +// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. +// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. // @description:ja YouTube Shorts 動画を通常の動画ビューに即座にリダイレクトし、保存、ダウンロード、品質の選択などを行うことができます。 // @description:ko YouTube Shorts 동영상을 일반 동영상 보기로 즉시 리디렉션하여 저장, 다운로드, 품질 선택 등이 가능합니다. -// @description:es Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. // @description:pt-BR Redirecione instantaneamente os vídeos do YouTube Shorts para a visualização normal, permitindo que você salve, baixe, escolha a qualidade, etc. // @description:ru Мгновенно перенаправляет видеоролики YouTube Shorts в обычный режим просмотра, позволяя сохранять, загружать, выбирать качество и т. д. -// @description:id Langsung mengalihkan video YouTube Shorts ke tampilan video normal, sehingga Anda dapat menyimpan, mengunduh, memilih kualitas, dan lain-lain. -// @description:hi YouTube शॉर्ट्स वीडियो को तुरंत सामान्य वीडियो दृश्य में पुनर्निर्देशित करें, जिससे आप सहेज सकें, डाउनलोड कर सकें, गुणवत्ता चुन सकें, आदि। -// @description:nl Stuur YouTube Shorts-video's direct door naar de normale videoweergave, zodat u ze kunt opslaan, downloaden, de kwaliteit kunt kiezen, enz. -// @description:fr Redirige instantanément les vidéos YouTube Shorts vers une vue vidéo normale, vous permettant d'enregistrer, de télécharger, de choisir la qualité, etc. -// @description:de Leiten Sie YouTube Shorts-Videos sofort zur normalen Videoansicht weiter, sodass Sie sie speichern, herunterladen, die Qualität auswählen usw. können. -// @description:it Reindirizza immediatamente i video di YouTube Shorts alla visualizzazione video normale, consentendoti di salvare, scaricare, scegliere la qualità, ecc. -// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. // @description:th เปลี่ยนเส้นทางวิดีโอ YouTube Shorts ไปยังมุมมองวิดีโอปกติทันที ช่วยให้คุณบันทึก ดาวน์โหลด เลือกคุณภาพ ฯลฯ ได้ -// @author tientq64 -// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png -// @match https://www.youtube.com/* -// @license MIT -// @grant none +// @description:tr Redirecciona instantáneamente los videos de YouTube Shorts a la vista de video normal, lo que te permite guardarlos, descargarlos, elegir la calidad, etc. +// @description:vi Chuyển hướng ngay lập tức video YouTube Shorts sang chế độ xem video thông thường, cho phép bạn lưu, tải xuống, chọn chất lượng, v.v. +// @description:zh-CN 立即将 YouTube Shorts 视频重定向至普通视频视图,让您可以保存、下载、选择质量等。 +// @description:zh-TW 立即將 YouTube Shorts 影片重定向到正常影片視圖,以便您儲存、下載、選擇品質等。 +// @author tientq64 +// @icon https://cdn-icons-png.flaticon.com/64/3670/3670147.png +// @match https://www.youtube.com/* +// @match https://m.youtube.com/* +// @exclude https://studio.youtube.com/* +// @license MIT +// @grant none // @noframes // ==/UserScript== diff --git a/tailwind.config.js b/tailwind.config.ts similarity index 50% rename from tailwind.config.js rename to tailwind.config.ts index e6d7780..2d8ca75 100644 --- a/tailwind.config.js +++ b/tailwind.config.ts @@ -1,5 +1,7 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { +/** + * @type {import('tailwindcss').Config} + */ +export default { content: ['**/*.tsx'], theme: { extend: {} diff --git a/watch.ts b/watch.ts index 0d9678e..1b98a81 100644 --- a/watch.ts +++ b/watch.ts @@ -9,28 +9,31 @@ function joinPath(...paths: string[]): string { return join(...paths).replace(/\\/g, '/') } -async function handleWatch(path: string, stat: Stats): Promise { +const endMetaTagRegex: RegExp = /^\/\/ ==\/UserScript==$/m +const tailwindcssMetaRegex: RegExp = /^\/\/ @resource {2,}TAILWINDCSS$/m +const watcherBlobs: string[] = ['scripts/*/script.user.{ts,tsx}', 'scripts/*/tracking.ts'] + +async function handleWatch(filePath: string, stat: Stats): Promise { if (!stat.isFile()) return - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dirPath: string = dirname(filePath).replace(/\\/g, '/') + const prodPath: string = joinPath(dirPath, 'script.user.js') + const devPath: string = joinPath(dirPath, 'dev.user.js') + const tsconfig: any = JSON.parse(readFileSync('tsconfig.json', 'utf-8')) - const dirPath: string = dirname(path).replace(/\\/g, '/') - const code: string = readFileSync(path, 'utf-8') + const userscriptCode: string = readFileSync(filePath, 'utf-8') - const matches = code.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/s) + const matches = userscriptCode.match(/\/\/ ==UserScript==\n.+?\n\/\/ ==\/UserScript==/su) if (matches === null) return const meta: string = matches[0] - const ts: string = code.replace(meta, '') + const ts: string = userscriptCode.replace(meta, '') - const prodPath: string = joinPath(dirPath, 'script.user.js') - const devPath: string = joinPath(dirPath, 'dev.user.js') - - const metaPadLength: number = meta.match(/^\/\/ @([a-z:]+ {2,})/m)?.[1].length || 13 + const alignmentSpaces: number = meta.match(/^\/\/ @([a-z:]+ {2,})/m)?.[1].length || 13 const bothMeta: string = meta.replace( endMetaTagRegex, - `// @${'homepage'.padEnd(metaPadLength)}https://github.com/tientq64/userscripts/tree/main/${encodeURI(dirPath)}\n$&` + `// @${'homepage'.padEnd(alignmentSpaces)}https://github.com/tientq64/userscripts/tree/main/${encodeURI(dirPath)}\n$&` ) const prettierConfig = await resolveConfig('.prettierrc') @@ -49,26 +52,22 @@ async function handleWatch(path: string, stat: Stats): Promise { useTabs: false, parser: 'typescript' }) - const prodCode: string = `${prodMeta}\n\n${prodJs}` - writeFileSync(prodPath, prodCode) + const prodUserscriptCode: string = `${prodMeta}\n\n${prodJs}` + writeFileSync(prodPath, prodUserscriptCode) const devMeta: string = bothMeta - .replace(/^\/\/ @name(:[a-zA-Z-]+)? .+(? { const paths: string[] = sync(watcherBlobs) for (const path of paths) {