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