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/.prettierrc b/.prettierrc
index 6b8baa4..ff90788 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,5 @@
{
+ "endOfLine": "lf",
"printWidth": 100,
"semi": false,
"singleQuote": true,
@@ -7,5 +8,6 @@
"useTabs": true,
"plugins": ["prettier-plugin-jsdoc"],
"jsdocCommentLineStrategy": "multiline",
- "jsdocPrintWidth": 90
+ "jsdocPrintWidth": 90,
+ "jsdocDescriptionWithDot": true
}
diff --git a/.resources/global.d.ts b/.resources/global.d.ts
index 7cf59e2..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
@@ -49,6 +51,27 @@ declare const _: Lodash
interface UnsafeWindow extends Window {}
declare const unsafeWindow: UnsafeWindow
-interface HTMLYtdPlayerElement extends HTMLElement {
+interface YtdPlayerElement extends HTMLElement {
loadVideoWithPlayerVars(options: { videoId: string; start?: number }): void
+ getPlayer: () => YouTubePlayer
+}
+
+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.
+
+
+
+
+ Name
+ Description
+ Daily installs
+ Total 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/eslint.config.mjs b/eslint.config.mjs
index fe5f1d8..d7801f0 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -24,6 +24,7 @@ export default [
rules: {
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-unused-vars': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
'react/react-in-jsx-scope': 'off',
'prefer-const': 'warn'
}
diff --git a/package.json b/package.json
index d0ea677..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",
diff --git a/scripts/Auto-Skip-YouTube-Ads/README.md b/scripts/Auto-Skip-YouTube-Ads/README.md
index 76ab1b2..6bfe5bf 100644
--- a/scripts/Auto-Skip-YouTube-Ads/README.md
+++ b/scripts/Auto-Skip-YouTube-Ads/README.md
@@ -1,175 +1,211 @@
-## 📰 Introduction
-
-Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up.
-
-几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。
-
-Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo.
-
-## 📑 Changelog
-
-### 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 195c116..49c85d0 100644
--- a/scripts/Auto-Skip-YouTube-Ads/script.user.js
+++ b/scripts/Auto-Skip-YouTube-Ads/script.user.js
@@ -1,33 +1,40 @@
// ==UserScript==
// @name Auto Skip YouTube Ads
-// @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube
-// @name:zh-CN 自动跳过 YouTube 广告
-// @name:zh-TW 自動跳過 YouTube 廣告
+// @name:ar تخطي إعلانات YouTube تلقائيًا
+// @name:es Saltar Automáticamente Anuncios De YouTube
+// @name:fr Ignorer Automatiquement Les Publicités YouTube
+// @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें
+// @name:id Lewati Otomatis Iklan YouTube
// @name:ja YouTube 広告を自動スキップ
// @name:ko YouTube 광고 자동 건너뛰기
-// @name:es Saltar Automáticamente Anuncios De YouTube
+// @name:nl YouTube-Advertenties Automatisch Overslaan
// @name:pt-BR Pular Automaticamente Anúncios Do YouTube
// @name:ru Автоматический Пропуск Рекламы На YouTube
-// @name:id Lewati Otomatis Iklan YouTube
-// @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें
+// @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube
+// @name:zh-CN 自动跳过 YouTube 广告
+// @name:zh-TW 自動跳過 YouTube 廣告
// @namespace https://github.com/tientq64/userscripts
-// @version 5.3.0
-// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up.
-// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo.
-// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。
-// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。
-// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。
-// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다.
-// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios.
-// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios.
-// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы.
-// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan.
-// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ।
+// @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.
+// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube.
+// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया।
+// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube.
+// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。
+// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다.
+// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen.
+// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube.
+// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube.
+// @description:vi 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.
+// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。
+// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。
// @author tientq64
// @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
@@ -39,331 +46,204 @@
// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Auto-Skip-YouTube-Ads
// ==/UserScript==
-/**
- * Skip ads. Remove ad blocker warning.
- */
function skipAd() {
- hideAdElements()
-
- video = null
- fineScrubber = document.querySelector('.ytp-fine-scrubbing')
-
- // Check if the current URL is a YouTube Shorts URL and exit the function if true.
- if (window.location.pathname.startsWith('/shorts/')) return
-
- const moviePlayer = document.querySelector('#movie_player')
-
- if (moviePlayer) {
- hasAd = moviePlayer.classList.contains('ad-showing')
- video = moviePlayer.querySelector('video.html5-main-video')
- }
-
- if (hasAd) {
- const skipButton = document.querySelector(`
- .ytp-skip-ad-button,
- .ytp-ad-skip-button,
- .ytp-ad-skip-button-modern,
- .ytp-ad-survey-answer-button
- `)
- // Click the skip ad button if available.
- if (skipButton) {
- skipButton.click()
- skipButton.remove()
- }
- // Otherwise, fast forward to the end of the ad video.
- // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number.
- else if (video && video.src) {
- video.currentTime = 9999
- }
- }
+ if (checkIsYouTubeShorts()) return
- if (video) {
- video.addEventListener('pause', handleVideoPause)
- video.addEventListener('pointerup', allowPauseVideo)
- video.addEventListener('timeupdate', handleVideoTimeUpdate)
- }
+ // This element appears when a video ad appears.
+ const adShowing = document.querySelector('.ad-showing')
// Timed pie countdown ad.
const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container')
- if (pieCountdown) {
- pieCountdown.remove()
- replaceCurrentVideo()
- }
- // Handle when ad blocker warning appears inside video player.
- const adBlockerWarningInner = document.querySelector(
- '.yt-playability-error-supported-renderers'
- )
- if (adBlockerWarningInner) {
- adBlockerWarningInner.remove()
- document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish)
- replaceCurrentVideo()
- }
+ // Survey questions in video player.
+ const surveyQuestions = document.querySelector('.ytp-ad-survey-questions')
- // Video play/pause button.
- const playButton = document.querySelector(
- 'button.ytp-play-button, button.player-control-play-pause-icon'
- )
- if (playButton) {
- playButton.addEventListener('click', allowPauseVideo)
- }
-}
+ if (adShowing === null && pieCountdown === null && surveyQuestions === null) return
-function queryHasSelector(selector, hasSelector, element = document) {
- const el = element.querySelector(selector)
- if (el === null) return null
- const hasEl = el.querySelector(hasSelector)
- if (hasEl === null) return null
- return el
-}
+ const moviePlayerEl = document.querySelector('#movie_player')
+ let playerEl
+ let player
-function queryHasSelectorAll(selector, hasSelector, element = document) {
- const els = element.querySelectorAll(selector)
- const result = []
- for (const el of els) {
- const hasEl = el.querySelector(hasSelector)
- if (hasEl === null) continue
- result.push(el)
+ if (isYouTubeMobile || isYouTubeMusic) {
+ playerEl = moviePlayerEl
+ player = playerEl
+ } else {
+ playerEl = document.querySelector('#ytd-player')
+ player = playerEl && playerEl.getPlayer()
}
- return result
-}
-
-function getCurrentVideoId() {
- const params = new URLSearchParams(location.search)
- const videoId = params.get('v')
- return videoId
-}
-/**
- * Check if the user is focused on the input.
- */
-function checkEnteringInput() {
- if (document.activeElement === null) {
- return false
+ if (playerEl === null || player === null) {
+ console.log({
+ message: 'Player not found',
+ timeStamp: getCurrentTimeString()
+ })
+ return
}
- return document.activeElement.matches('input, textarea, select')
-}
-/**
- * Temporarily allows the video to be paused, for a short period of time.
- */
-function allowPauseVideo() {
- pausedByUser = true
- window.clearTimeout(allowPauseVideoTimeoutId)
- allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500)
-}
+ // ad.classList.remove('ad-showing')
-/**
- * Pausing the video is not allowed. The purpose is to prevent video from being paused, against the
- * behavior of pausing video when YouTube ad blocking warning dialog appears. Unless certain
- * conditions, such as pausing by user, etc.
- */
-function disallowPauseVideo() {
- pausedByUser = false
- window.clearTimeout(allowPauseVideoTimeoutId)
-}
+ let adVideo = null
-function handleWindowBlur() {
- isTabBlurred = true
-}
+ if (pieCountdown === null && surveyQuestions === null) {
+ adVideo = document.querySelector(
+ '#ytd-player video.html5-main-video, #song-video video.html5-main-video'
+ )
-function handleWindowFocus() {
- isTabBlurred = false
-}
+ console.table({
+ message: 'Ad video',
+ video: adVideo !== null,
+ src: adVideo?.src,
+ paused: adVideo?.paused,
+ currentTime: adVideo?.currentTime,
+ duration: adVideo?.duration,
+ timeStamp: getCurrentTimeString()
+ })
-/**
- * Handle when video is paused. If certain conditions are not met, it will continue playing.
- * Returning early in this function means the video should be paused as it should be.
- */
-function handleVideoPause() {
- if (isYouTubeMusic) return
+ if (adVideo !== null) {
+ adVideo.muted = true
+ }
+ if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) {
+ return
+ }
- // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function.
- if (pausedByUser) {
- disallowPauseVideo()
- return
+ console.log({
+ message: 'Ad video has finished loading',
+ timeStamp: getCurrentTimeString()
+ })
}
- // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc.
- // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off.
- if (document.hidden) return
- if (isTabBlurred) return
-
- if (fineScrubber && fineScrubber.style.display !== 'none') return
- if (video === null) return
- if (video.duration - video.currentTime < 0.1) return
-
- // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal.
- video.play()
-}
+ if (isYouTubeMusic && adVideo !== null) {
+ adVideo.currentTime = adVideo.duration
-function handleVideoTimeUpdate() {
- if (hasAd || video === null) return
- currentVideoTime = video.currentTime
-}
+ 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())
-/**
- * Handle both keyboard press or release events.
- */
-function handleWindowKeyDownAndKeyUp(event) {
- if (isYouTubeMusic) return
- if (checkEnteringInput()) return
- const code = event.code
- if (event.type === 'keydown') {
- if (code === 'KeyK' || code === 'MediaPlayPause') {
- allowPauseVideo()
+ if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) {
+ window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000)
}
- } else {
- if (code === 'Space') {
- allowPauseVideo()
+
+ if ('loadVideoWithPlayerVars' in playerEl) {
+ playerEl.loadVideoWithPlayerVars({ videoId, start })
+ } else {
+ playerEl.loadVideoByPlayerVars({ videoId, start })
}
- }
-}
-function handleYouTubeNavigateFinish() {
- currentVideoTime = 0
- replaceCurrentVideo()
+ console.table({
+ message: 'Ad skipped',
+ videoId,
+ start,
+ title: videoData.title,
+ timeStamp: getCurrentTimeString(),
+ adShowing: adShowing !== null,
+ pieCountdown: pieCountdown !== null,
+ surveyQuestions: surveyQuestions !== null
+ })
+ }
}
-async function replaceCurrentVideo() {
- const start = Math.floor(currentVideoTime)
- for (let i = 0; i < 16; i++) {
- await waitFor(500)
- const videoId = getCurrentVideoId()
- const player = document.querySelector('#ytd-player')
- if (video && !video.src && videoId && player) {
- player.loadVideoWithPlayerVars({ videoId, start })
- }
- }
+function checkIsYouTubeShorts() {
+ return location.pathname.startsWith('/shorts/')
}
-function waitFor(millis) {
- return new Promise((resolve) => {
- window.setTimeout(resolve, millis)
- })
+function getCurrentTimeString() {
+ return new Date().toTimeString().split(' ', 1)[0]
}
-/**
- * Add CSS hides some ad elements on the page.
- */
function addCss() {
- const hideCssSelector = [
+ 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',
-
- // Ad blocker warning inside the player.
- 'yt-playability-error-supported-renderers#error-screen',
+ // Sponsored ad video items on home page.
+ // 'ytd-ad-slot-renderer',
- '.ytp-suggested-action',
+ // '.ytp-suggested-action',
'.yt-mealbar-promo-renderer',
+ // Featured product ad banner at the bottom left of the video.
+ '.ytp-featured-product',
+
+ // Products shelf ad banner below the video description.
+ 'ytd-merch-shelf-renderer',
+
// YouTube Music Premium trial promotion dialog, bottom left corner.
'ytmusic-mealbar-promo-renderer',
// YouTube Music Premium trial promotion banner on home page.
'ytmusic-statement-banner-renderer'
- ].join(',')
- const css = `
- #ytd-player { visibility: visible !important }
- ${hideCssSelector} { display: none !important }
- `
+ ]
+ const adsSelector = adsSelectors.join(',')
+ const css = `${adsSelector} { display: none !important; }`
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
}
/**
- * Remove ad elements using javascript because these selectors require the use of the CSS `:has`
- * selector which is not supported in older browser versions.
+ * Remove ad elements using JavaScript because these selectors require the use of the CSS
+ * `:has` selector which is not supported in older browser versions.
*/
-function hideAdElements() {
+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"]'],
-
- // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124).
- // ['#panels', 'ytd-ads-engagement-panel-content-renderer'],
-
// 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 adEls = queryHasSelectorAll(adSelector[0], adSelector[1])
- for (const adEl of adEls) {
- adEl.remove()
- }
+ const adEl = document.querySelector(adSelector[0])
+ if (adEl === null) continue
+ const neededEl = adEl.querySelector(adSelector[1])
+ if (neededEl === null) continue
+ adEl.remove()
}
}
-/**
- * Is it YouTube mobile version.
- */
const isYouTubeMobile = location.hostname === 'm.youtube.com'
+const isYouTubeDesktop = !isYouTubeMobile
-/**
- * Is the current page YouTube Music.
- */
const isYouTubeMusic = location.hostname === 'music.youtube.com'
+const isYouTubeVideo = !isYouTubeMusic
-/**
- * Current video element.
- */
-let video = null
-
-let fineScrubber = null
-let hasAd = false
-let currentVideoTime = 0
-
-/**
- * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog.
- */
-let pausedByUser = false
+addCss()
-/**
- * Is the current tab blurred.
- */
-let isTabBlurred = false
-
-let allowPauseVideoTimeoutId = 0
-
-// Observe DOM changes to detect ads.
-if (window.MutationObserver) {
- const observer = new MutationObserver(skipAd)
- observer.observe(document.body, {
- attributes: true,
- attributeFilter: ['class', 'src'],
- childList: true,
- subtree: true
- })
-}
-// If DOM observation is not supported. Detect ads every 500ms (2 times per second).
-else {
- window.setInterval(skipAd, 500)
+if (isYouTubeVideo) {
+ window.setInterval(removeAdElements, 1000)
+ removeAdElements()
}
-window.addEventListener('blur', handleWindowBlur)
-window.addEventListener('focus', handleWindowFocus)
-window.addEventListener('keydown', handleWindowKeyDownAndKeyUp)
-window.addEventListener('keyup', handleWindowKeyDownAndKeyUp)
-
-addCss()
+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 eff0999..a88d35c 100644
--- a/scripts/Auto-Skip-YouTube-Ads/script.user.ts
+++ b/scripts/Auto-Skip-YouTube-Ads/script.user.ts
@@ -1,33 +1,40 @@
// ==UserScript==
// @name Auto Skip YouTube Ads
-// @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube
-// @name:zh-CN 自动跳过 YouTube 广告
-// @name:zh-TW 自動跳過 YouTube 廣告
+// @name:ar تخطي إعلانات YouTube تلقائيًا
+// @name:es Saltar Automáticamente Anuncios De YouTube
+// @name:fr Ignorer Automatiquement Les Publicités YouTube
+// @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें
+// @name:id Lewati Otomatis Iklan YouTube
// @name:ja YouTube 広告を自動スキップ
// @name:ko YouTube 광고 자동 건너뛰기
-// @name:es Saltar Automáticamente Anuncios De YouTube
+// @name:nl YouTube-Advertenties Automatisch Overslaan
// @name:pt-BR Pular Automaticamente Anúncios Do YouTube
// @name:ru Автоматический Пропуск Рекламы На YouTube
-// @name:id Lewati Otomatis Iklan YouTube
-// @name:hi YouTube विज्ञापन स्वचालित रूप से छोड़ें
+// @name:vi Tự Động Bỏ Qua Quảng Cáo YouTube
+// @name:zh-CN 自动跳过 YouTube 广告
+// @name:zh-TW 自動跳過 YouTube 廣告
// @namespace https://github.com/tientq64/userscripts
-// @version 5.3.0
-// @description Automatically skip YouTube ads almost instantly. Remove the ad blocker warning pop-up.
-// @description:vi Tự động bỏ qua quảng cáo YouTube gần như ngay lập tức. Loại bỏ cửa sổ bật lên cảnh báo trình chặn quảng cáo.
-// @description:zh-CN 几乎立即自动跳过 YouTube 广告。删除广告拦截器警告弹出窗口。
-// @description:zh-TW 幾乎立即自動跳過 YouTube 廣告。刪除廣告攔截器警告彈出視窗。
-// @description:ja YouTube 広告をほぼ瞬時に自動的にスキップします。広告ブロッカーの警告ポップアップを削除します。
-// @description:ko YouTube 광고를 거의 즉시 자동으로 건너뜁니다. 광고 차단 경고 팝업을 제거합니다.
-// @description:es Omite automáticamente los anuncios de YouTube casi al instante. Elimina la ventana emergente de advertencia del bloqueador de anuncios.
-// @description:pt-BR Pule automaticamente os anúncios do YouTube quase instantaneamente. Remova o pop-up de aviso do bloqueador de anúncios.
-// @description:ru Автоматически пропускайте рекламу YouTube почти мгновенно. Уберите всплывающее предупреждение о блокировщике рекламы.
-// @description:id Lewati iklan YouTube secara otomatis hampir seketika. Hapus pop-up peringatan pemblokir iklan.
-// @description:hi YouTube विज्ञापनों को लगभग तुरंत ही स्वचालित रूप से छोड़ दें। विज्ञापन अवरोधक चेतावनी पॉप-अप हटाएँ।
+// @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.
+// @description:fr Ignorez automatiquement et instantanément les publicités YouTube. Non détecté par les avertissements du bloqueur de publicités YouTube.
+// @description:hi YouTube विज्ञापनों को स्वचालित रूप से तुरंत छोड़ दें। YouTube विज्ञापन अवरोधक चेतावनियों द्वारा पता नहीं लगाया गया।
+// @description:id Lewati iklan YouTube secara otomatis secara instan. Tidak terdeteksi oleh peringatan pemblokir iklan YouTube.
+// @description:ja YouTube 広告を即座に自動的にスキップします。YouTube 広告ブロッカーの警告には検出されません。
+// @description:ko YouTube 광고를 즉시 자동으로 건너뜁니다. YouTube 광고 차단 경고에 감지되지 않습니다.
+// @description:nl Sla YouTube-advertenties direct automatisch over. Ongemerkt door YouTube-adblockerwaarschuwingen.
+// @description:pt-BR Pule anúncios do YouTube instantaneamente. Não detectado pelos avisos do bloqueador de anúncios do YouTube.
+// @description:ru Автоматически пропускать рекламу YouTube мгновенно. Не обнаруживается предупреждениями блокировщиков рекламы YouTube.
+// @description:vi 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.
+// @description:zh-CN 立即自动跳过 YouTube 广告。不会被 YouTube 广告拦截器警告检测到。
+// @description:zh-TW 立即自動跳過 YouTube 廣告。 YouTube 廣告攔截器警告未被偵測到。
// @author tientq64
// @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
@@ -38,342 +45,206 @@
// @noframes
// ==/UserScript==
-/**
- * Skip ads. Remove ad blocker warning.
- */
function skipAd(): void {
- hideAdElements()
-
- video = null
- fineScrubber = document.querySelector('.ytp-fine-scrubbing')
-
- // Check if the current URL is a YouTube Shorts URL and exit the function if true.
- if (window.location.pathname.startsWith('/shorts/')) return
-
- const moviePlayer = document.querySelector('#movie_player')
-
- if (moviePlayer) {
- hasAd = moviePlayer.classList.contains('ad-showing')
- video = moviePlayer.querySelector('video.html5-main-video')
- }
-
- if (hasAd) {
- const skipButton = document.querySelector(`
- .ytp-skip-ad-button,
- .ytp-ad-skip-button,
- .ytp-ad-skip-button-modern,
- .ytp-ad-survey-answer-button
- `)
- // Click the skip ad button if available.
- if (skipButton) {
- skipButton.click()
- skipButton.remove()
- }
- // Otherwise, fast forward to the end of the ad video.
- // Use `9999` instead of `video.duration` to avoid errors when `duration` is not a number.
- else if (video && video.src) {
- video.currentTime = 9999
- }
- }
+ if (checkIsYouTubeShorts()) return
- if (video) {
- video.addEventListener('pause', handleVideoPause)
- video.addEventListener('pointerup', allowPauseVideo)
- video.addEventListener('timeupdate', handleVideoTimeUpdate)
- }
+ // This element appears when a video ad appears.
+ const adShowing = document.querySelector('.ad-showing')
// Timed pie countdown ad.
const pieCountdown = document.querySelector(
'.ytp-ad-timed-pie-countdown-container'
)
- if (pieCountdown) {
- pieCountdown.remove()
- replaceCurrentVideo()
- }
- // Handle when ad blocker warning appears inside video player.
- const adBlockerWarningInner = document.querySelector(
- '.yt-playability-error-supported-renderers'
- )
- if (adBlockerWarningInner) {
- adBlockerWarningInner.remove()
- document.addEventListener('yt-navigate-finish', handleYouTubeNavigateFinish)
- replaceCurrentVideo()
- }
+ // Survey questions in video player.
+ const surveyQuestions = document.querySelector('.ytp-ad-survey-questions')
- // Video play/pause button.
- const playButton = document.querySelector(
- 'button.ytp-play-button, button.player-control-play-pause-icon'
- )
- if (playButton) {
- playButton.addEventListener('click', allowPauseVideo)
- }
-}
+ if (adShowing === null && pieCountdown === null && surveyQuestions === null) return
-function queryHasSelector(
- selector: string,
- hasSelector: string,
- element: Document | Element = document
-): T | null {
- const el = element.querySelector(selector)
- if (el === null) return null
- const hasEl = el.querySelector(hasSelector)
- if (hasEl === null) return null
- return el
-}
+ const moviePlayerEl = document.querySelector('#movie_player')
+ let playerEl: YtdPlayerElement | YouTubeMoviePlayerElement | null
+ let player: YouTubePlayer | YouTubeMoviePlayerElement | null
-function queryHasSelectorAll(
- selector: string,
- hasSelector: string,
- element: Document | Element = document
-): T[] {
- const els = element.querySelectorAll(selector)
- const result: T[] = []
- for (const el of els) {
- const hasEl = el.querySelector(hasSelector)
- if (hasEl === null) continue
- result.push(el)
+ if (isYouTubeMobile || isYouTubeMusic) {
+ playerEl = moviePlayerEl
+ player = playerEl
+ } else {
+ playerEl = document.querySelector('#ytd-player')
+ player = playerEl && playerEl.getPlayer()
}
- return result
-}
-function getCurrentVideoId(): string | null {
- const params = new URLSearchParams(location.search)
- const videoId: string | null = params.get('v')
- return videoId
-}
-
-/**
- * Check if the user is focused on the input.
- */
-function checkEnteringInput(): boolean {
- if (document.activeElement === null) {
- return false
+ if (playerEl === null || player === null) {
+ console.log({
+ message: 'Player not found',
+ timeStamp: getCurrentTimeString()
+ })
+ return
}
- return document.activeElement.matches('input, textarea, select')
-}
-/**
- * Temporarily allows the video to be paused, for a short period of time.
- */
-function allowPauseVideo(): void {
- pausedByUser = true
- window.clearTimeout(allowPauseVideoTimeoutId)
- allowPauseVideoTimeoutId = window.setTimeout(disallowPauseVideo, 500)
-}
+ // ad.classList.remove('ad-showing')
-/**
- * Pausing the video is not allowed. The purpose is to prevent video from being paused,
- * against the behavior of pausing video when YouTube ad blocking warning dialog appears.
- * Unless certain conditions, such as pausing by user, etc.
- */
-function disallowPauseVideo(): void {
- pausedByUser = false
- window.clearTimeout(allowPauseVideoTimeoutId)
-}
+ let adVideo: HTMLVideoElement | null = null
-function handleWindowBlur(): void {
- isTabBlurred = true
-}
+ if (pieCountdown === null && surveyQuestions === null) {
+ adVideo = document.querySelector(
+ '#ytd-player video.html5-main-video, #song-video video.html5-main-video'
+ )
-function handleWindowFocus(): void {
- isTabBlurred = false
-}
+ console.table({
+ message: 'Ad video',
+ video: adVideo !== null,
+ src: adVideo?.src,
+ paused: adVideo?.paused,
+ currentTime: adVideo?.currentTime,
+ duration: adVideo?.duration,
+ timeStamp: getCurrentTimeString()
+ })
-/**
- * Handle when video is paused. If certain conditions are not met, it will continue
- * playing. Returning early in this function means the video should be paused as it should
- * be.
- */
-function handleVideoPause(): void {
- if (isYouTubeMusic) return
+ if (adVideo !== null) {
+ adVideo.muted = true
+ }
+ if (adVideo === null || !adVideo.src || adVideo.paused || isNaN(adVideo.duration)) {
+ return
+ }
- // If it was stopped by the user, it's ok, let the video pause as it should, and exit the function.
- if (pausedByUser) {
- disallowPauseVideo()
- return
+ console.log({
+ message: 'Ad video has finished loading',
+ timeStamp: getCurrentTimeString()
+ })
}
- // The video will pause normally if the tab is not focused. This is to allow for pausing the video via the media controller (of the browser or operating system), etc.
- // Note: While this also gives YouTube the opportunity to pause videos to annoy users, it's an acceptable trade-off.
- if (document.hidden) return
- if (isTabBlurred) return
-
- if (fineScrubber && fineScrubber.style.display !== 'none') return
- if (video === null) return
- if (video.duration - video.currentTime < 0.1) return
+ if (isYouTubeMusic && adVideo !== null) {
+ adVideo.currentTime = adVideo.duration
- // This is YouTube's disruptive behavior towards users, so the video should continue to play as normal.
- video.play()
-}
-
-function handleVideoTimeUpdate(): void {
- if (hasAd || video === null) return
- currentVideoTime = video.currentTime
-}
+ 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())
-/**
- * Handle both keyboard press or release events.
- */
-function handleWindowKeyDownAndKeyUp(event: KeyboardEvent): void {
- if (isYouTubeMusic) return
- if (checkEnteringInput()) return
- const code: string = event.code
- if (event.type === 'keydown') {
- if (code === 'KeyK' || code === 'MediaPlayPause') {
- allowPauseVideo()
+ if (moviePlayerEl !== null && moviePlayerEl.isSubtitlesOn()) {
+ window.setTimeout(moviePlayerEl.toggleSubtitlesOn, 1000)
}
- } else {
- if (code === 'Space') {
- allowPauseVideo()
+
+ if ('loadVideoWithPlayerVars' in playerEl) {
+ playerEl.loadVideoWithPlayerVars({ videoId, start })
+ } else {
+ playerEl.loadVideoByPlayerVars({ videoId, start })
}
- }
-}
-function handleYouTubeNavigateFinish(): void {
- currentVideoTime = 0
- replaceCurrentVideo()
+ console.table({
+ message: 'Ad skipped',
+ videoId,
+ start,
+ title: videoData.title,
+ timeStamp: getCurrentTimeString(),
+ adShowing: adShowing !== null,
+ pieCountdown: pieCountdown !== null,
+ surveyQuestions: surveyQuestions !== null
+ })
+ }
}
-async function replaceCurrentVideo(): Promise {
- const start: number = Math.floor(currentVideoTime)
- for (let i = 0; i < 16; i++) {
- await waitFor(500)
- const videoId: string | null = getCurrentVideoId()
- const player = document.querySelector('#ytd-player')
- if (video && !video.src && videoId && player) {
- player.loadVideoWithPlayerVars({ videoId, start })
- }
- }
+function checkIsYouTubeShorts(): boolean {
+ return location.pathname.startsWith('/shorts/')
}
-function waitFor(millis: number): Promise {
- return new Promise((resolve) => {
- window.setTimeout(resolve, millis)
- })
+function getCurrentTimeString(): string {
+ return new Date().toTimeString().split(' ', 1)[0]
}
-/**
- * Add CSS hides some ad elements on the page.
- */
function addCss(): void {
- const hideCssSelector: string = [
+ 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',
-
- // Ad blocker warning inside the player.
- 'yt-playability-error-supported-renderers#error-screen',
+ // Sponsored ad video items on home page.
+ // 'ytd-ad-slot-renderer',
- '.ytp-suggested-action',
+ // '.ytp-suggested-action',
'.yt-mealbar-promo-renderer',
+ // Featured product ad banner at the bottom left of the video.
+ '.ytp-featured-product',
+
+ // Products shelf ad banner below the video description.
+ 'ytd-merch-shelf-renderer',
+
// YouTube Music Premium trial promotion dialog, bottom left corner.
'ytmusic-mealbar-promo-renderer',
// YouTube Music Premium trial promotion banner on home page.
'ytmusic-statement-banner-renderer'
- ].join(',')
- const css: string = `
- #ytd-player { visibility: visible !important }
- ${hideCssSelector} { display: none !important }
- `
- const style: HTMLStyleElement = document.createElement('style')
+ ]
+ const adsSelector: string = adsSelectors.join(',')
+ const css: string = `${adsSelector} { display: none !important; }`
+ const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
}
/**
- * Remove ad elements using javascript because these selectors require the use of the CSS
+ * Remove ad elements using JavaScript because these selectors require the use of the CSS
* `:has` selector which is not supported in older browser versions.
*/
-function hideAdElements(): 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"]'],
-
- // Temporarily comment out this selector to fix issue [#265124](https://greasyfork.org/en/scripts/498197-auto-skip-youtube-ads/discussions/265124).
- // ['#panels', 'ytd-ads-engagement-panel-content-renderer'],
-
// 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 adEls = queryHasSelectorAll(adSelector[0], adSelector[1])
- for (const adEl of adEls) {
- adEl.remove()
- }
+ const adEl = document.querySelector(adSelector[0])
+ if (adEl === null) continue
+ const neededEl = adEl.querySelector(adSelector[1])
+ if (neededEl === null) continue
+ adEl.remove()
}
}
-/**
- * Is it YouTube mobile version.
- */
const isYouTubeMobile: boolean = location.hostname === 'm.youtube.com'
+const isYouTubeDesktop: boolean = !isYouTubeMobile
-/**
- * Is the current page YouTube Music.
- */
const isYouTubeMusic: boolean = location.hostname === 'music.youtube.com'
+const isYouTubeVideo: boolean = !isYouTubeMusic
-/**
- * Current video element.
- */
-let video: HTMLVideoElement | null = null
-
-let fineScrubber: HTMLDivElement | null = null
-let hasAd: boolean = false
-let currentVideoTime: number = 0
-
-/**
- * Is the video paused by the user, not paused by YouTube's ad blocker warning dialog.
- */
-let pausedByUser: boolean = false
+addCss()
-/**
- * Is the current tab blurred.
- */
-let isTabBlurred: boolean = false
-
-let allowPauseVideoTimeoutId: number = 0
-
-// Observe DOM changes to detect ads.
-if (window.MutationObserver) {
- const observer: MutationObserver = new MutationObserver(skipAd)
- observer.observe(document.body, {
- attributes: true,
- attributeFilter: ['class', 'src'],
- childList: true,
- subtree: true
- })
-}
-// If DOM observation is not supported. Detect ads every 500ms (2 times per second).
-else {
- window.setInterval(skipAd, 500)
+if (isYouTubeVideo) {
+ window.setInterval(removeAdElements, 1000)
+ removeAdElements()
}
-window.addEventListener('blur', handleWindowBlur)
-window.addEventListener('focus', handleWindowFocus)
-window.addEventListener('keydown', handleWindowKeyDownAndKeyUp)
-window.addEventListener('keyup', handleWindowKeyDownAndKeyUp)
-
-addCss()
+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) => (
- handlePresetClick(preset)}
- >
- {preset.text}
-
- ))}
-
-
-
- )}
- {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 d90a331..bed56f8 100644
--- a/scripts/Mien-AI-Music/script.user.js
+++ b/scripts/Mien-AI-Music/script.user.js
@@ -14,14 +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 () {
+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) => {
@@ -55,29 +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(/, */)
- }
-
- 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])
@@ -85,126 +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 (location.href === 'https://suno.com/') {
- location.pathname = '/me'
- }
- //
- else if (location.href === 'https://suno.com/me') {
- await GM_deleteValue('accountEmail')
- await wait(2000)
- clickIfContentIncludes('button', 'Liked')
- document.activeElement.blur()
- 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}`
+ 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)
}
- el.addEventListener('click', () => {
- switchAccount(accountEmail)
- })
- siblingEl.after(el)
- }
- }
- //
- else if (location.href.startsWith('https://accounts.suno.com/sign-in')) {
- await wait(500)
- click('.cl-socialButtonsIconButton__google')
- }
- //
- else if (location.href.startsWith('https://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 (location.href.startsWith('https://www.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 075f964..444622f 100644
--- a/scripts/Mien-AI-Music/script.user.ts
+++ b/scripts/Mien-AI-Music/script.user.ts
@@ -14,13 +14,20 @@
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
+// @grant window.onurlchange
// @license MIT
// @noframes
// ==/UserScript==
-;(async function () {
+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) => {
@@ -62,29 +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(/, */)
- }
-
- 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])
@@ -92,128 +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 (location.href === 'https://suno.com/') {
- location.pathname = '/me'
- }
- //
- else if (location.href === 'https://suno.com/me') {
- await GM_deleteValue('accountEmail')
- await wait(2000)
- clickIfContentIncludes('button', 'Liked')
- ;(document.activeElement as HTMLElement).blur()
- 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}`
+ 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)
}
- el.addEventListener('click', (): void => {
- switchAccount(accountEmail)
- })
- siblingEl.after(el)
- }
- }
- //
- else if (location.href.startsWith('https://accounts.suno.com/sign-in')) {
- await wait(500)
- click('.cl-socialButtonsIconButton__google')
- }
- //
- else if (location.href.startsWith('https://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 (location.href.startsWith('https://www.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 4f6cbec..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
@@ -13,29 +13,64 @@
// @homepage https://github.com/tientq64/userscripts/tree/main/scripts/Tetr-io-Improvements
// ==/UserScript==
-const adElsSelectors = ['#ceriad-auth-return-lb', '#ceriad-menus-persistent-mpu', 'iframe']
-const adElsSelector = adElsSelectors.join(',')
+var TetrIOImprovements
+;(function (TetrIOImprovements) {
+ const adElsSelectors = ['#ceriad-auth-return-lb', '#ceriad-menus-persistent-mpu', 'iframe']
+ const adElsSelector = adElsSelectors.join(',')
-function removeAds() {
- const adEls = document.querySelectorAll(adElsSelector)
- for (const adEl of adEls) {
- adEl.remove()
+ function removeAds() {
+ const adEls = document.querySelectorAll(adElsSelector)
+ for (const adEl of adEls) {
+ adEl.remove()
+ }
}
-}
-function handleWindowKeyDown(event) {
- if (event.code === 'Escape') {
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur()
+ function handleWindowKeyDown(event) {
+ switch (event.code) {
+ case 'Escape':
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur()
+ }
+ break
+
+ case 'Home':
+ {
+ const joinBtn = document.querySelector('#return_button')
+ if (joinBtn?.checkVisibility()) {
+ joinBtn.click()
+ }
+ const playMultiBtn = document.querySelector('#play_multi')
+ if (playMultiBtn?.checkVisibility()) {
+ playMultiBtn.click()
+ }
+ const multiLeagueBtn = document.querySelector('#multi_league')
+ if (multiLeagueBtn?.checkVisibility()) {
+ multiLeagueBtn.click()
+ }
+ const enterMatchMakingBtn = document.querySelector('#enter_matchmaking')
+ if (enterMatchMakingBtn?.checkVisibility()) {
+ enterMatchMakingBtn.click()
+ }
+ }
+ break
+
+ case 'End':
+ {
+ const backToLeagueBtn = document.querySelector('#backtoleague')
+ if (backToLeagueBtn?.checkVisibility()) {
+ backToLeagueBtn.click()
+ }
+ }
+ break
}
}
-}
-window.setInterval(removeAds, 5000)
-window.addEventListener('keydown', handleWindowKeyDown)
+ window.setInterval(removeAds, 5000)
+ window.addEventListener('keydown', handleWindowKeyDown)
-GM_addStyle(`
- * {
- transition: none !important;
- }
-`)
+ GM_addStyle(`
+ * {
+ transition: none !important;
+ }
+ `)
+})(TetrIOImprovements || (TetrIOImprovements = {}))
diff --git a/scripts/Tetr-io-Improvements/script.user.ts b/scripts/Tetr-io-Improvements/script.user.ts
index d6897d6..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
@@ -12,33 +12,68 @@
// @noframes
// ==/UserScript==
-const adElsSelectors: string[] = [
- '#ceriad-auth-return-lb',
- '#ceriad-menus-persistent-mpu',
- 'iframe'
-]
-const adElsSelector: string = adElsSelectors.join(',')
+namespace TetrIOImprovements {
+ const adElsSelectors: string[] = [
+ '#ceriad-auth-return-lb',
+ '#ceriad-menus-persistent-mpu',
+ 'iframe'
+ ]
+ const adElsSelector: string = adElsSelectors.join(',')
-function removeAds(): void {
- const adEls = document.querySelectorAll(adElsSelector)
- for (const adEl of adEls) {
- adEl.remove()
+ function removeAds(): void {
+ const adEls = document.querySelectorAll(adElsSelector)
+ for (const adEl of adEls) {
+ adEl.remove()
+ }
}
-}
-function handleWindowKeyDown(event: KeyboardEvent): void {
- if (event.code === 'Escape') {
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur()
+ function handleWindowKeyDown(event: KeyboardEvent): void {
+ switch (event.code) {
+ case 'Escape':
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur()
+ }
+ break
+
+ case 'Home':
+ {
+ const joinBtn = document.querySelector('#return_button')
+ if (joinBtn?.checkVisibility()) {
+ joinBtn.click()
+ }
+ const playMultiBtn = document.querySelector('#play_multi')
+ if (playMultiBtn?.checkVisibility()) {
+ playMultiBtn.click()
+ }
+ const multiLeagueBtn = document.querySelector('#multi_league')
+ if (multiLeagueBtn?.checkVisibility()) {
+ multiLeagueBtn.click()
+ }
+ const enterMatchMakingBtn =
+ document.querySelector('#enter_matchmaking')
+ if (enterMatchMakingBtn?.checkVisibility()) {
+ enterMatchMakingBtn.click()
+ }
+ }
+ break
+
+ case 'End':
+ {
+ const backToLeagueBtn = document.querySelector('#backtoleague')
+ if (backToLeagueBtn?.checkVisibility()) {
+ backToLeagueBtn.click()
+ }
+ }
+ break
}
}
-}
-window.setInterval(removeAds, 5000)
-window.addEventListener('keydown', handleWindowKeyDown)
+ window.setInterval(removeAds, 5000)
+ window.addEventListener('keydown', handleWindowKeyDown)
-GM_addStyle(`
- * {
- transition: none !important;
- }
-`)
+ GM_addStyle(`
+ * {
+ 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) {