mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e33bc8f96 | |||
| 0d6da0681a | |||
| 1c76415846 | |||
| a3d620ba52 | |||
| 82bf407323 | |||
| d83556e9fa | |||
| f70ae4f7c4 | |||
| 268e7be15d | |||
| accd2bf350 | |||
| 58cf1a543d |
67
changelog.md
67
changelog.md
@ -2,8 +2,75 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v3.8.1](https://github.com/th-ch/youtube-music/compare/v3.8.0...v3.8.1)
|
||||
|
||||
- chore(deps): update dependency glob to v11.0.2 [`#3283`](https://github.com/th-ch/youtube-music/pull/3283)
|
||||
- fix(deps): update dependency i18next to v25.0.1 [`#3284`](https://github.com/th-ch/youtube-music/pull/3284)
|
||||
- chore(deps): update dependency typescript-eslint to v8.31.0 [`#3286`](https://github.com/th-ch/youtube-music/pull/3286)
|
||||
- chore(deps): update dependency discord-api-types to v0.38.1 [`#3285`](https://github.com/th-ch/youtube-music/pull/3285)
|
||||
- fix(deps): update dependency youtubei.js to v13.4.0 [`#3287`](https://github.com/th-ch/youtube-music/pull/3287)
|
||||
- fix(deps): update dependency zod to v3.24.3 [`#3250`](https://github.com/th-ch/youtube-music/pull/3250)
|
||||
- chore(deps): update dependency vite to v6.3.3 [`#3251`](https://github.com/th-ch/youtube-music/pull/3251)
|
||||
- fix(deps): update dependency @hono/zod-openapi to v0.19.5 [`#3258`](https://github.com/th-ch/youtube-music/pull/3258)
|
||||
- chore(deps): update dependency vite-plugin-inspect to v11.0.1 [`#3259`](https://github.com/th-ch/youtube-music/pull/3259)
|
||||
- chore(deps): update dependency esbuild to v0.25.3 [`#3282`](https://github.com/th-ch/youtube-music/pull/3282)
|
||||
- chore(deps): update eslint monorepo to v9.25.1 [`#3260`](https://github.com/th-ch/youtube-music/pull/3260)
|
||||
- chore(deps): update dependency electron to v35.2.1 [`#3281`](https://github.com/th-ch/youtube-music/pull/3281)
|
||||
- chore(deps): update playwright monorepo to v1.52.0 [`#3256`](https://github.com/th-ch/youtube-music/pull/3256)
|
||||
- chore(deps): update dependency eslint-import-resolver-typescript to v4.3.4 [`#3273`](https://github.com/th-ch/youtube-music/pull/3273)
|
||||
- fix: fix cuted "j" and glow in lyrics [`#3277`](https://github.com/th-ch/youtube-music/pull/3277)
|
||||
- chore(deps): update dependency electron to v35.2.0 [`#3263`](https://github.com/th-ch/youtube-music/pull/3263)
|
||||
- fix(unobtrusive-player): handle shuffle play [`#3247`](https://github.com/th-ch/youtube-music/pull/3247)
|
||||
- fix(deps): update dependency @ghostery/adblocker-electron to v2.5.1 [`#3238`](https://github.com/th-ch/youtube-music/pull/3238)
|
||||
- chore(deps): update dependency vite to v6.3.0 [`#3248`](https://github.com/th-ch/youtube-music/pull/3248)
|
||||
- chore(deps): update dependency typescript-eslint to v8.30.1 [`#3234`](https://github.com/th-ch/youtube-music/pull/3234)
|
||||
- fix(deps): update dependency @ghostery/adblocker-electron-preload to v2.5.1 [`#3239`](https://github.com/th-ch/youtube-music/pull/3239)
|
||||
- fix(deps): update dependency i18next to v25 [`#3243`](https://github.com/th-ch/youtube-music/pull/3243)
|
||||
- fix(deps): update dependency hono to v4.7.7 [`#3245`](https://github.com/th-ch/youtube-music/pull/3245)
|
||||
- chore(deps): update dependency vite to v6.2.6 [`#3189`](https://github.com/th-ch/youtube-music/pull/3189)
|
||||
- feat(Synced-Lyrics): Also search for lyrics with the original title language [`#3206`](https://github.com/th-ch/youtube-music/pull/3206)
|
||||
- chore(deps): update dependency eslint-config-prettier to v10.1.2 [`#3219`](https://github.com/th-ch/youtube-music/pull/3219)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.120 [`#3221`](https://github.com/th-ch/youtube-music/pull/3221)
|
||||
- fix(deps): update dependency @hono/node-server to v1.14.1 [`#3223`](https://github.com/th-ch/youtube-music/pull/3223)
|
||||
- chore(deps): update dependency vite to v6.2.6 [security] [`#3224`](https://github.com/th-ch/youtube-music/pull/3224)
|
||||
- chore(deps): update dependency rollup to v4.40.0 [`#3227`](https://github.com/th-ch/youtube-music/pull/3227)
|
||||
- fix(mpris): keep MPRIS volume in sync with the actual volume [`#3226`](https://github.com/th-ch/youtube-music/pull/3226)
|
||||
- fix(deps): update dependency @hono/zod-openapi to v0.19.4 [`#3215`](https://github.com/th-ch/youtube-music/pull/3215)
|
||||
- chore(deps): update dependency electron to v35.1.5 [`#3218`](https://github.com/th-ch/youtube-music/pull/3218)
|
||||
- fix(deps): update dependency hono to v4.7.6 [`#3217`](https://github.com/th-ch/youtube-music/pull/3217)
|
||||
- docs: add Portuguese README translation and update language shortcuts [`#3192`](https://github.com/th-ch/youtube-music/pull/3192)
|
||||
- fix: custom Video/Audio switcher in Plugins menu [`#3174`](https://github.com/th-ch/youtube-music/pull/3174)
|
||||
- chore(deps): update dependency typescript-eslint to v8.29.1 [`#3214`](https://github.com/th-ch/youtube-music/pull/3214)
|
||||
- chore(deps): update eslint monorepo to v9.24.0 [`#3195`](https://github.com/th-ch/youtube-music/pull/3195)
|
||||
- chore(deps): update dependency typescript to v5.8.3 [`#3196`](https://github.com/th-ch/youtube-music/pull/3196)
|
||||
- chore(deps): update dependency vite to v6.2.5 [security] [`#3194`](https://github.com/th-ch/youtube-music/pull/3194)
|
||||
- fix(deps): update dependency node-id3 to v0.2.9 [`#3191`](https://github.com/th-ch/youtube-music/pull/3191)
|
||||
- chore(deps): update dependency electron to v35.1.4 [`#3184`](https://github.com/th-ch/youtube-music/pull/3184)
|
||||
- chore(deps): update dependency eslint-plugin-prettier to v5.2.6 [`#3182`](https://github.com/th-ch/youtube-music/pull/3182)
|
||||
- chore(deps): update dependency eslint-import-resolver-typescript to v4.3.2 [`#3208`](https://github.com/th-ch/youtube-music/pull/3208)
|
||||
- docs: add Japanese README [`#3180`](https://github.com/th-ch/youtube-music/pull/3180)
|
||||
- chore(deps): update dependency node-gyp to v11.2.0 [`#3177`](https://github.com/th-ch/youtube-music/pull/3177)
|
||||
- chore(deps): update dependency rollup to v4.39.0 [`#3179`](https://github.com/th-ch/youtube-music/pull/3179)
|
||||
- chore(deps): update dependency typescript-eslint to v8.29.0 [`#3169`](https://github.com/th-ch/youtube-music/pull/3169)
|
||||
- fix(downloader): allow downloads for signed out users [`#3145`](https://github.com/th-ch/youtube-music/pull/3145)
|
||||
- fix(README): Fixed typos in some hyperlinks [`#3158`](https://github.com/th-ch/youtube-music/pull/3158)
|
||||
- chore(deps): update dependency vite to v6.2.4 [`#3124`](https://github.com/th-ch/youtube-music/pull/3124)
|
||||
- chore(deps): update dependency eslint-import-resolver-typescript to v4.3.1 [`#3151`](https://github.com/th-ch/youtube-music/pull/3151)
|
||||
- chore(deps): update dependency rollup to v4.38.0 [`#3154`](https://github.com/th-ch/youtube-music/pull/3154)
|
||||
- chore(deps): update dependency esbuild to v0.25.2 [`#3160`](https://github.com/th-ch/youtube-music/pull/3160)
|
||||
- chore(deps): update dependency electron to v35.1.2 [`#3147`](https://github.com/th-ch/youtube-music/pull/3147)
|
||||
- chore(deps): update dependency electron to v35.1.1 [`#3143`](https://github.com/th-ch/youtube-music/pull/3143)
|
||||
- chore(deps): update dependency eslint-import-resolver-typescript to v4.2.5 [`#3144`](https://github.com/th-ch/youtube-music/pull/3144)
|
||||
- chore(deps): update dependency @types/semver to v7.7.0 [`#3141`](https://github.com/th-ch/youtube-music/pull/3141)
|
||||
- fix(deps): update dependency electron-updater to v6.6.2 [`#3142`](https://github.com/th-ch/youtube-music/pull/3142)
|
||||
- chore(i18n): Translated using Weblate (Greek) [`8bb4f44`](https://github.com/th-ch/youtube-music/commit/8bb4f4426f6a82b1b5c13a385a6e2b94c25f88a2)
|
||||
- chore(i18n): Translated using Weblate (Bulgarian) [`89fe072`](https://github.com/th-ch/youtube-music/commit/89fe072c0e719026212bb506bb66baf37b31ceb4)
|
||||
- chore(i18n): Translated using Weblate (Greek) [`5a7daaf`](https://github.com/th-ch/youtube-music/commit/5a7daaf2f6d1211c4b9461facf4de1962714bacf)
|
||||
|
||||
#### [v3.8.0](https://github.com/th-ch/youtube-music/compare/v3.7.5...v3.8.0)
|
||||
|
||||
> 26 March 2025
|
||||
|
||||
- chore(deps): update dependency typescript-eslint to v8.28.0 [`#3128`](https://github.com/th-ch/youtube-music/pull/3128)
|
||||
- chore(deps): update dependency eslint-plugin-prettier to v5.2.5 [`#3123`](https://github.com/th-ch/youtube-music/pull/3123)
|
||||
- fix(deps): update dependency @hono/node-server to v1.14.0 [`#3131`](https://github.com/th-ch/youtube-music/pull/3131)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "youtube-music",
|
||||
"desktopName": "com.github.th_ch.youtube_music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.8.1",
|
||||
"version": "3.9.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
@ -263,6 +263,7 @@
|
||||
"conf": "13.1.0",
|
||||
"custom-electron-prompt": "1.5.8",
|
||||
"deepmerge-ts": "7.1.5",
|
||||
"delay": "6.0.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-is": "3.0.0",
|
||||
"electron-localshortcut": "3.2.1",
|
||||
@ -320,7 +321,7 @@
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "6.0.0",
|
||||
"discord-api-types": "0.38.1",
|
||||
"electron": "35.2.1",
|
||||
"electron": "34.5.3",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12",
|
||||
"electron-devtools-installer": "4.0.0",
|
||||
|
||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -32,7 +32,7 @@ importers:
|
||||
version: 1.0.1(@types/node@22.13.5)
|
||||
'@electron/remote':
|
||||
specifier: 2.1.2
|
||||
version: 2.1.2(electron@35.2.1)
|
||||
version: 2.1.2(electron@34.5.3)
|
||||
'@ffmpeg.wasm/core-mt':
|
||||
specifier: 0.12.0
|
||||
version: 0.12.0
|
||||
@ -47,10 +47,10 @@ importers:
|
||||
version: 2.0.5
|
||||
'@ghostery/adblocker-electron':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1(electron@35.2.1)
|
||||
version: 2.5.1(electron@34.5.3)
|
||||
'@ghostery/adblocker-electron-preload':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1(electron@35.2.1)
|
||||
version: 2.5.1(electron@34.5.3)
|
||||
'@hono/node-server':
|
||||
specifier: 1.14.1
|
||||
version: 1.14.1(hono@4.7.7)
|
||||
@ -101,10 +101,13 @@ importers:
|
||||
version: 13.1.0
|
||||
custom-electron-prompt:
|
||||
specifier: 1.5.8
|
||||
version: 1.5.8(electron@35.2.1)
|
||||
version: 1.5.8(electron@34.5.3)
|
||||
deepmerge-ts:
|
||||
specifier: 7.1.5
|
||||
version: 7.1.5
|
||||
delay:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
electron-debug:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
@ -272,8 +275,8 @@ importers:
|
||||
specifier: 0.38.1
|
||||
version: 0.38.1
|
||||
electron:
|
||||
specifier: 35.2.1
|
||||
version: 35.2.1
|
||||
specifier: 34.5.3
|
||||
version: 34.5.3
|
||||
electron-builder:
|
||||
specifier: 26.0.12
|
||||
version: 26.0.12(electron-builder-squirrel-windows@26.0.12)
|
||||
@ -1260,6 +1263,9 @@ packages:
|
||||
'@types/node@16.9.1':
|
||||
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
|
||||
|
||||
'@types/node@20.17.31':
|
||||
resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==}
|
||||
|
||||
'@types/node@22.13.5':
|
||||
resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
|
||||
|
||||
@ -2026,6 +2032,10 @@ packages:
|
||||
resolution: {integrity: sha512-R6ep6JJ+eOBZsBr9esiNN1gxFbZE4Q2cULkUSFumGYecAiS6qodDvcPx/sFuWHMNul7DWmrtoEOpYSm7o6tbSA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
delay@6.0.0:
|
||||
resolution: {integrity: sha512-2NJozoOHQ4NuZuVIr5CWd0iiLVIRSDepakaovIN+9eIDHEhdCAEvSy2cuf1DCrPPQLvHmbqTHODlhHg8UCy4zw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@ -2173,8 +2183,8 @@ packages:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
electron@35.2.1:
|
||||
resolution: {integrity: sha512-LO4xXLpzkPPUVLvAbdHMlW7N9Z+Qqz+7QsmSWXluTIQMeJk+v7o36nUTZghyA2I1s//tlMvuaCtle6Qo2W0Ktg==}
|
||||
electron@34.5.3:
|
||||
resolution: {integrity: sha512-3hGXL4Hzzt4tadPKaoHHDvOzNrUYqhKkLJpZE2PFoNdDL1HDZwL9sb+qldDoMSbgIVfJpo2/7i6PubK3jMIFLA==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
@ -4323,6 +4333,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
@ -4891,9 +4904,9 @@ snapshots:
|
||||
- bluebird
|
||||
- supports-color
|
||||
|
||||
'@electron/remote@2.1.2(electron@35.2.1)':
|
||||
'@electron/remote@2.1.2(electron@34.5.3)':
|
||||
dependencies:
|
||||
electron: 35.2.1
|
||||
electron: 34.5.3
|
||||
|
||||
'@electron/universal@2.0.2':
|
||||
dependencies:
|
||||
@ -5082,16 +5095,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@ghostery/adblocker-extended-selectors': 2.5.1
|
||||
|
||||
'@ghostery/adblocker-electron-preload@2.5.1(electron@35.2.1)':
|
||||
'@ghostery/adblocker-electron-preload@2.5.1(electron@34.5.3)':
|
||||
dependencies:
|
||||
'@ghostery/adblocker-content': 2.5.1
|
||||
electron: 35.2.1
|
||||
electron: 34.5.3
|
||||
|
||||
'@ghostery/adblocker-electron@2.5.1(electron@35.2.1)':
|
||||
'@ghostery/adblocker-electron@2.5.1(electron@34.5.3)':
|
||||
dependencies:
|
||||
'@ghostery/adblocker': 2.5.1
|
||||
'@ghostery/adblocker-electron-preload': 2.5.1(electron@35.2.1)
|
||||
electron: 35.2.1
|
||||
'@ghostery/adblocker-electron-preload': 2.5.1(electron@34.5.3)
|
||||
electron: 34.5.3
|
||||
tldts-experimental: 6.1.79
|
||||
|
||||
'@ghostery/adblocker-extended-selectors@2.5.1': {}
|
||||
@ -5616,7 +5629,7 @@ snapshots:
|
||||
|
||||
'@types/electron-localshortcut@3.1.3':
|
||||
dependencies:
|
||||
electron: 35.2.1
|
||||
electron: 34.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -5650,6 +5663,10 @@ snapshots:
|
||||
|
||||
'@types/node@16.9.1': {}
|
||||
|
||||
'@types/node@20.17.31':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/node@22.13.5':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
@ -6432,9 +6449,9 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
custom-electron-prompt@1.5.8(electron@35.2.1):
|
||||
custom-electron-prompt@1.5.8(electron@34.5.3):
|
||||
dependencies:
|
||||
electron: 35.2.1
|
||||
electron: 34.5.3
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
@ -6546,6 +6563,8 @@ snapshots:
|
||||
p-map: 7.0.3
|
||||
slash: 5.1.0
|
||||
|
||||
delay@6.0.0: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
@ -6766,10 +6785,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@35.2.1:
|
||||
electron@34.5.3:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 22.13.5
|
||||
'@types/node': 20.17.31
|
||||
extract-zip: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -9143,6 +9162,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
undici@5.28.5:
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
},
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"detail": "Meni je sakriven, koristite 'Alt' da ga prikazete (ili 'ESC' ako koristite meni u aplikaciji)",
|
||||
"message": "Sakrivanje menija je uključeno",
|
||||
"title": "Meni sakriven"
|
||||
},
|
||||
@ -52,7 +53,80 @@
|
||||
"later": "Kasnije",
|
||||
"restart-now": "Pokreni ponovo odmah"
|
||||
},
|
||||
"detail": "\"{{pluginName}}\" dodatak zahtjeva ponovno pokretanje kako bi se uključio"
|
||||
"detail": "\"{{pluginName}}\" dodatak zahtjeva ponovno pokretanje kako bi se uključio",
|
||||
"message": "\"{{pluginName}}\" potrebno je resetovat",
|
||||
"title": "Restart je potreban"
|
||||
},
|
||||
"unresponsive": {
|
||||
"buttons": {
|
||||
"quit": "Napusti",
|
||||
"relaunch": "Ponovo otvori",
|
||||
"wait": "Pricekajte"
|
||||
},
|
||||
"detail": "Izvinjavamo se zbog zabune! molimo vas da odaberete sta zelite uciniti",
|
||||
"message": "Aplikacija ne reagira",
|
||||
"title": "Prozor ne reagira"
|
||||
},
|
||||
"update-available": {
|
||||
"buttons": {
|
||||
"disable": "Ugasite Nadogradnje",
|
||||
"download": "Skinuti",
|
||||
"ok": "OK"
|
||||
},
|
||||
"detail": "Nova verzija je dostupna i može biti skinuta na {{downloadLink}}",
|
||||
"message": "Nova verzija je dostupna",
|
||||
"title": "Azuriranje dostupno"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "O nama",
|
||||
"navigation": {
|
||||
"label": "Plejer",
|
||||
"submenu": {
|
||||
"copy-current-url": "Kopirajte trenutni link",
|
||||
"go-back": "Idi Nazad",
|
||||
"go-forward": "Idi Naprijed",
|
||||
"quit": "Izadji",
|
||||
"restart": "Restartujte Aplikaciju"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"label": "Opcije",
|
||||
"submenu": {
|
||||
"advanced-options": {
|
||||
"label": "Napredne opcije",
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "Resetuje kes memoriju kad se aplikacija pokrene",
|
||||
"disable-hardware-acceleration": "Ugasite hardversko ubrzanje",
|
||||
"edit-config-json": "Uredite config.json",
|
||||
"override-user-agent": "Nadjacaj User-Agent",
|
||||
"restart-on-config-changes": "Ponovno pokretanje nakon promjena konfiguracije",
|
||||
"set-proxy": {
|
||||
"label": "Postavi proxy",
|
||||
"prompt": {
|
||||
"label": "Unesite adresu proxyja: (ostavite prazno za onemogućavanje)",
|
||||
"placeholder": "Primjer: SOCKS5://127.0.0.1:9999",
|
||||
"title": "Postavi proxy"
|
||||
}
|
||||
},
|
||||
"toggle-dev-tools": "Uključi/isključi DevTools"
|
||||
}
|
||||
},
|
||||
"always-on-top": "Uvijek na vrhu",
|
||||
"auto-update": "Automatski Update",
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "Meni će biti skriven pri sljedećem pokretanju, koristite [Alt] da ga prikažete (ili upotrijebite [`] ako koristite meni u aplikaciji)",
|
||||
"title": "Sakrij meni omogućen"
|
||||
},
|
||||
"label": "Sakrij meni"
|
||||
},
|
||||
"language": {
|
||||
"dialog": {
|
||||
"message": "Jezik će se promijeniti nakon ponovnog pokretanja"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -588,6 +588,10 @@
|
||||
},
|
||||
"name": "Notifications"
|
||||
},
|
||||
"performance-improvement": {
|
||||
"description": "Improve performance by enabling dangerous scripts",
|
||||
"name": "Performance improvement [Beta]"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Allows to switch the app to picture-in-picture mode",
|
||||
"menu": {
|
||||
|
||||
@ -600,6 +600,10 @@
|
||||
},
|
||||
"name": "알림"
|
||||
},
|
||||
"performance-improvement": {
|
||||
"description": "위험한 스크립트를 활성화하여 성능을 개선합니다",
|
||||
"name": "성능 개선 [베타]"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "앱을 PiP 모드로 전환할 수 있게 허용합니다",
|
||||
"menu": {
|
||||
|
||||
@ -14,7 +14,7 @@ export interface APIServerConfig {
|
||||
}
|
||||
|
||||
export const defaultAPIServerConfig: APIServerConfig = {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
hostname: '0.0.0.0',
|
||||
port: 26538,
|
||||
authStrategy: AuthStrategy.AUTH_AT_FIRST,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DataConnection, Peer } from 'peerjs';
|
||||
import { DataConnection, Peer, PeerErrorType } from 'peerjs';
|
||||
import delay from 'delay';
|
||||
|
||||
import type { Permission, Profile, VideoData } from './types';
|
||||
|
||||
@ -54,16 +55,16 @@ export class Connection {
|
||||
this._mode = 'host';
|
||||
this.waitOpen.resolve(id);
|
||||
});
|
||||
this.peer.on('connection', (conn) => {
|
||||
this.peer.on('connection', async (conn) => {
|
||||
this._mode = 'host';
|
||||
this.registerConnection(conn);
|
||||
await this.registerConnection(conn);
|
||||
});
|
||||
this.peer.on('error', (err) => {
|
||||
this._mode = 'disconnected';
|
||||
|
||||
this.waitOpen.reject(err);
|
||||
this.connectionListeners.forEach((listener) => listener());
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,7 +75,9 @@ export class Connection {
|
||||
|
||||
async connect(id: string) {
|
||||
this._mode = 'guest';
|
||||
const conn = this.peer.connect(id);
|
||||
const conn = this.peer.connect(id, {
|
||||
reliable: true,
|
||||
});
|
||||
await this.registerConnection(conn);
|
||||
return conn;
|
||||
}
|
||||
@ -120,7 +123,17 @@ export class Connection {
|
||||
/* privates */
|
||||
private async registerConnection(conn: DataConnection) {
|
||||
return new Promise<DataConnection>((resolve, reject) => {
|
||||
this.peer.once('error', (err) => {
|
||||
this.peer.once('error', async (err) => {
|
||||
if (err.type === PeerErrorType.Network) {
|
||||
// retrying after 10 seconds
|
||||
await delay(10000);
|
||||
try {
|
||||
this.peer.reconnect();
|
||||
return;
|
||||
} catch {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
this._mode = 'disconnected';
|
||||
|
||||
reject(err);
|
||||
|
||||
19
src/plugins/performance-improvement/index.ts
Normal file
19
src/plugins/performance-improvement/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { injectRm3 } from './scripts/rm3';
|
||||
import { injectCpuTamer } from './scripts/cpu-tamer';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.performance-improvement.name'),
|
||||
description: () => t('plugins.performance-improvement.description'),
|
||||
restartNeeded: true,
|
||||
addedVersion: '3.9.X',
|
||||
config: {
|
||||
enabled: true,
|
||||
},
|
||||
renderer() {
|
||||
injectRm3();
|
||||
injectCpuTamer();
|
||||
},
|
||||
});
|
||||
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts
vendored
Normal file
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export declare const injectCpuTamerByAnimationFrame: (
|
||||
__CONTEXT__: unknown,
|
||||
) => void;
|
||||
@ -0,0 +1,286 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2021-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const injectCpuTamerByAnimationFrame = ((__CONTEXT__) => {
|
||||
'use strict';
|
||||
|
||||
const win = this instanceof Window ? this : window;
|
||||
|
||||
// Create a unique key for the script and check if it is already running
|
||||
const hkey_script = 'nzsxclvflluv';
|
||||
if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
|
||||
win[hkey_script] = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
const PromiseExternal = ((resolve_, reject_) => {
|
||||
const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
|
||||
return class PromiseExternal extends Promise {
|
||||
constructor(cb = h) {
|
||||
super(cb);
|
||||
if (cb === h) {
|
||||
/** @type {(value: any) => void} */
|
||||
this.resolve = resolve_;
|
||||
/** @type {(reason?: any) => void} */
|
||||
this.reject = reject_;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const isGPUAccelerationAvailable = (() => {
|
||||
// https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!isGPUAccelerationAvailable) {
|
||||
throw new Error('Your browser does not support GPU Acceleration. YouTube CPU Tamer by AnimationFrame is skipped.');
|
||||
}
|
||||
|
||||
const timeupdateDT = (() => {
|
||||
|
||||
window.__j6YiAc__ = 1;
|
||||
|
||||
document.addEventListener('timeupdate', () => {
|
||||
window.__j6YiAc__ = Date.now();
|
||||
}, true);
|
||||
|
||||
let kz = -1;
|
||||
try {
|
||||
kz = top.__j6YiAc__;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return kz >= 1 ? () => top.__j6YiAc__ : () => window.__j6YiAc__;
|
||||
|
||||
})();
|
||||
|
||||
const cleanContext = async (win) => {
|
||||
const waitFn = requestAnimationFrame; // shall have been binded to window
|
||||
try {
|
||||
let mx = 16; // MAX TRIAL
|
||||
const frameId = 'vanillajs-iframe-v1'
|
||||
let frame = document.getElementById(frameId);
|
||||
let removeIframeFn = null;
|
||||
if (!frame) {
|
||||
frame = document.createElement('iframe');
|
||||
frame.id = frameId;
|
||||
const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
|
||||
frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
|
||||
let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
|
||||
n.appendChild(frame);
|
||||
while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
|
||||
const root = document.documentElement;
|
||||
root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
|
||||
if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
removeIframeFn = (setTimeout) => {
|
||||
const removeIframeOnDocumentReady = (e) => {
|
||||
e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
e = n;
|
||||
n = win = removeIframeFn = 0;
|
||||
setTimeout ? setTimeout(() => e.remove(), 200) : e.remove();
|
||||
}
|
||||
if (!setTimeout || document.readyState !== 'loading') {
|
||||
removeIframeOnDocumentReady();
|
||||
} else {
|
||||
win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
|
||||
const fc = frame.contentWindow;
|
||||
if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
|
||||
try {
|
||||
const { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout } = fc;
|
||||
const res = { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout };
|
||||
for (let k in res) res[k] = res[k].bind(win); // necessary
|
||||
if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (removeIframeFn) removeIframeFn();
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
cleanContext(win).then(__CONTEXT__ => {
|
||||
|
||||
if (!__CONTEXT__) return null;
|
||||
|
||||
const { requestAnimationFrame, setTimeout, setInterval, clearTimeout, clearInterval } = __CONTEXT__;
|
||||
|
||||
/** @type {Function|null} */
|
||||
let afInterupter = null;
|
||||
|
||||
const getRAFHelper = () => {
|
||||
const asc = document.createElement('a-f');
|
||||
if (!('onanimationiteration' in asc)) {
|
||||
return (resolve) => requestAnimationFrame(afInterupter = resolve);
|
||||
}
|
||||
asc.id = 'a-f';
|
||||
let qr = null;
|
||||
asc.onanimationiteration = function () {
|
||||
if (qr !== null) qr = (qr(), null);
|
||||
}
|
||||
if (!document.getElementById('afscript')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'afscript';
|
||||
style.textContent = `
|
||||
@keyFrames aF1 {
|
||||
0% {
|
||||
order: 0;
|
||||
}
|
||||
100% {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
#a-f[id] {
|
||||
visibility: collapse !important;
|
||||
position: fixed !important;
|
||||
display: block !important;
|
||||
top: -100px !important;
|
||||
left: -100px !important;
|
||||
margin:0 !important;
|
||||
padding:0 !important;
|
||||
outline:0 !important;
|
||||
border:0 !important;
|
||||
z-index:-1 !important;
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
contain: strict !important;
|
||||
pointer-events: none !important;
|
||||
animation: 1ms steps(2, jump-none) 0ms infinite alternate forwards running aF1 !important;
|
||||
}
|
||||
`;
|
||||
(document.head || document.documentElement).appendChild(style);
|
||||
}
|
||||
document.documentElement.insertBefore(asc, document.documentElement.firstChild);
|
||||
return (resolve) => (qr = afInterupter = resolve);
|
||||
};
|
||||
|
||||
/** @type {(resolve: () => void)} */
|
||||
const rafPN = getRAFHelper(); // rAF will not execute if document is hidden
|
||||
|
||||
(() => {
|
||||
let afPromiseP, afPromiseQ; // non-null
|
||||
afPromiseP = afPromiseQ = { resolved: true }; // initial state for !uP && !uQ
|
||||
let afix = 0;
|
||||
const afResolve = async (rX) => {
|
||||
await new Promise(rafPN);
|
||||
rX.resolved = true;
|
||||
const t = afix = (afix & 1073741823) + 1;
|
||||
return rX.resolve(t), t;
|
||||
};
|
||||
const eFunc = async () => {
|
||||
const uP = !afPromiseP.resolved ? afPromiseP : null;
|
||||
const uQ = !afPromiseQ.resolved ? afPromiseQ : null;
|
||||
let t = 0;
|
||||
if (uP && uQ) {
|
||||
const t1 = await uP;
|
||||
const t2 = await uQ;
|
||||
t = ((t1 - t2) & 536870912) === 0 ? t1 : t2; // = 0 for t1 - t2 = [0, 536870911], [–1073741824, -536870913]
|
||||
} else {
|
||||
const vP = !uP ? (afPromiseP = new PromiseExternal()) : null;
|
||||
const vQ = !uQ ? (afPromiseQ = new PromiseExternal()) : null;
|
||||
if (uQ) await uQ; else if (uP) await uP;
|
||||
if (vP) t = await afResolve(vP);
|
||||
if (vQ) t = await afResolve(vQ);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
const inExec = new Set();
|
||||
const wFunc = async (handler, wStore) => {
|
||||
try {
|
||||
const ct = Date.now();
|
||||
if (ct - timeupdateDT() < 800 && ct - wStore.dt < 800) {
|
||||
const cid = wStore.cid;
|
||||
inExec.add(cid);
|
||||
const t = await eFunc();
|
||||
const didNotRemove = inExec.delete(cid); // true for valid key
|
||||
if (!didNotRemove || t === wStore.lastExecution) return;
|
||||
wStore.lastExecution = t;
|
||||
}
|
||||
wStore.dt = ct;
|
||||
handler();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const sFunc = (propFunc) => {
|
||||
return (func, ms = 0, ...args) => {
|
||||
if (typeof func === 'function') { // ignore all non-function parameter (e.g. string)
|
||||
const wStore = { dt: Date.now() };
|
||||
return (wStore.cid = propFunc(wFunc, ms, (args.length > 0 ? func.bind(null, ...args) : func), wStore));
|
||||
} else {
|
||||
return propFunc(func, ms, ...args);
|
||||
}
|
||||
};
|
||||
};
|
||||
win.setTimeout = sFunc(setTimeout);
|
||||
win.setInterval = sFunc(setInterval);
|
||||
|
||||
const dFunc = (propFunc) => {
|
||||
return (cid) => {
|
||||
if (cid) inExec.delete(cid) || propFunc(cid);
|
||||
};
|
||||
};
|
||||
|
||||
win.clearTimeout = dFunc(clearTimeout);
|
||||
win.clearInterval = dFunc(clearInterval);
|
||||
|
||||
try {
|
||||
win.setTimeout.toString = setTimeout.toString.bind(setTimeout);
|
||||
win.setInterval.toString = setInterval.toString.bind(setInterval);
|
||||
win.clearTimeout.toString = clearTimeout.toString.bind(clearTimeout);
|
||||
win.clearInterval.toString = clearInterval.toString.bind(clearInterval);
|
||||
} catch (e) { console.warn(e) }
|
||||
|
||||
})();
|
||||
|
||||
let mInterupter = null;
|
||||
setInterval(() => {
|
||||
if (mInterupter === afInterupter) {
|
||||
if (mInterupter !== null) afInterupter = mInterupter = (mInterupter(), null);
|
||||
} else {
|
||||
mInterupter = afInterupter;
|
||||
}
|
||||
}, 125);
|
||||
});
|
||||
|
||||
});
|
||||
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts
vendored
Normal file
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export declare const injectCpuTamerByDomMutation: (
|
||||
__CONTEXT__: unknown,
|
||||
) => void;
|
||||
@ -0,0 +1,281 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2024-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const injectCpuTamerByDomMutation = ((__CONTEXT__) => {
|
||||
'use strict';
|
||||
|
||||
const win = this instanceof Window ? this : window;
|
||||
|
||||
// Create a unique key for the script and check if it is already running
|
||||
const hkey_script = 'nzsxclvflluv';
|
||||
if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
|
||||
win[hkey_script] = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
const PromiseExternal = ((resolve_, reject_) => {
|
||||
const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
|
||||
return class PromiseExternal extends Promise {
|
||||
constructor(cb = h) {
|
||||
super(cb);
|
||||
if (cb === h) {
|
||||
/** @type {(value: any) => void} */
|
||||
this.resolve = resolve_;
|
||||
/** @type {(reason?: any) => void} */
|
||||
this.reject = reject_;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// for future use
|
||||
/*
|
||||
const timeupdateDT = (() => {
|
||||
|
||||
window.__j6YiAc__ = 1;
|
||||
|
||||
document.addEventListener('timeupdate', () => {
|
||||
window.__j6YiAc__ = Date.now();
|
||||
}, true);
|
||||
|
||||
let kz = -1;
|
||||
try {
|
||||
kz = top.__j6YiAc__;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return kz >= 1 ? () => top.__j6YiAc__ : () => window.__j6YiAc__;
|
||||
|
||||
})();
|
||||
*/
|
||||
|
||||
const cleanContext = async (win) => {
|
||||
const waitFn = requestAnimationFrame; // shall have been binded to window
|
||||
try {
|
||||
let mx = 16; // MAX TRIAL
|
||||
const frameId = 'vanillajs-iframe-v1'
|
||||
let frame = document.getElementById(frameId);
|
||||
let removeIframeFn = null;
|
||||
if (!frame) {
|
||||
frame = document.createElement('iframe');
|
||||
frame.id = frameId;
|
||||
const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
|
||||
frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
|
||||
let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
|
||||
n.appendChild(frame);
|
||||
while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
|
||||
const root = document.documentElement;
|
||||
root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
|
||||
if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
removeIframeFn = (setTimeout) => {
|
||||
const removeIframeOnDocumentReady = (e) => {
|
||||
e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
e = n;
|
||||
n = win = removeIframeFn = 0;
|
||||
setTimeout ? setTimeout(() => e.remove(), 200) : e.remove();
|
||||
}
|
||||
if (!setTimeout || document.readyState !== 'loading') {
|
||||
removeIframeOnDocumentReady();
|
||||
} else {
|
||||
win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
|
||||
const fc = frame.contentWindow;
|
||||
if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
|
||||
try {
|
||||
const { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout } = fc;
|
||||
const res = { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout };
|
||||
for (let k in res) res[k] = res[k].bind(win); // necessary
|
||||
if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (removeIframeFn) removeIframeFn();
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const { _setAttribute, _insertBefore, _hasAttribute } = (() => {
|
||||
let _setAttribute = Element.prototype.setAttribute;
|
||||
try {
|
||||
_setAttribute = ShadyDOM.nativeMethods.setAttribute || _setAttribute;
|
||||
} catch (e) { }
|
||||
let _hasAttribute = Element.prototype.hasAttribute;
|
||||
try {
|
||||
_hasAttribute = ShadyDOM.nativeMethods.hasAttribute || _hasAttribute;
|
||||
} catch (e) { }
|
||||
let _insertBefore = Node.prototype.insertBefore;
|
||||
try {
|
||||
_insertBefore = ShadyDOM.nativeMethods.insertBefore || _insertBefore;
|
||||
} catch (e) { }
|
||||
return { _setAttribute, _insertBefore, _hasAttribute};
|
||||
})();
|
||||
|
||||
cleanContext(win).then(__CONTEXT__ => {
|
||||
|
||||
if (!__CONTEXT__) return null;
|
||||
|
||||
const { setTimeout, setInterval, clearTimeout, clearInterval } = __CONTEXT__;
|
||||
|
||||
/*
|
||||
/-** @type {Function|null} *-/
|
||||
// let afInterupter = null;
|
||||
*/
|
||||
|
||||
const getDMHelper = () => {
|
||||
let _dm = document.getElementById('d-m');
|
||||
if (!_dm) {
|
||||
_dm = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
_dm.id = 'd-m';
|
||||
_insertBefore.call(document.documentElement, _dm, document.documentElement.firstChild);
|
||||
}
|
||||
const dm = _dm;
|
||||
dm._setAttribute = _setAttribute;
|
||||
dm._hasAttribute = _hasAttribute;
|
||||
let j = 0;
|
||||
let attributeName_;
|
||||
while (dm._hasAttribute(attributeName_ = `dm-${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`)) {
|
||||
// none
|
||||
}
|
||||
const attributeName = attributeName_;
|
||||
let sr = null;
|
||||
const mo = new MutationObserver(() => {
|
||||
const sr_ = sr;
|
||||
if (sr_ !== null) {
|
||||
sr = null;
|
||||
if (j > 8) j = 0;
|
||||
sr_.resolve();
|
||||
}
|
||||
});
|
||||
mo.observe(document, { childList: true, subtree: true, attributes: true });
|
||||
return () => {
|
||||
return sr || (sr = (dm._setAttribute(attributeName, ++j), (new PromiseExternal()))); // mutationcallback in next macrotask
|
||||
};
|
||||
};
|
||||
|
||||
/** @type {(resolve: () => void)} */
|
||||
const dmSN = getDMHelper(); // dm will execute even if document is hidden
|
||||
|
||||
(() => {
|
||||
let dmPromiseP, dmPromiseQ; // non-null
|
||||
dmPromiseP = dmPromiseQ = { resolved: true }; // initial state for !uP && !uQ
|
||||
let dmix = 0;
|
||||
const dmResolve = async (rX) => {
|
||||
await dmSN();
|
||||
rX.resolved = true;
|
||||
const t = dmix = (dmix & 1073741823) + 1;
|
||||
return rX.resolve(t), t;
|
||||
};
|
||||
const eFunc = async () => {
|
||||
const uP = !dmPromiseP.resolved ? dmPromiseP : null;
|
||||
const uQ = !dmPromiseQ.resolved ? dmPromiseQ : null;
|
||||
let t = 0;
|
||||
if (uP && uQ) {
|
||||
const t1 = await uP;
|
||||
const t2 = await uQ;
|
||||
t = ((t1 - t2) & 536870912) === 0 ? t1 : t2; // = 0 for t1 - t2 = [0, 536870911], [–1073741824, -536870913]
|
||||
} else {
|
||||
const vP = !uP ? (dmPromiseP = new PromiseExternal()) : null;
|
||||
const vQ = !uQ ? (dmPromiseQ = new PromiseExternal()) : null;
|
||||
if (uQ) await uQ; else if (uP) await uP;
|
||||
if (vP) t = await dmResolve(vP);
|
||||
if (vQ) t = await dmResolve(vQ);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
const inExec = new Set();
|
||||
const wFunc = async (handler, wStore) => {
|
||||
try {
|
||||
const ct = Date.now();
|
||||
if (ct - wStore.dt < 800) {
|
||||
const cid = wStore.cid;
|
||||
inExec.add(cid);
|
||||
const t = await eFunc();
|
||||
const didNotRemove = inExec.delete(cid); // true for valid key
|
||||
if (!didNotRemove || t === wStore.lastExecution) return;
|
||||
wStore.lastExecution = t;
|
||||
}
|
||||
wStore.dt = ct;
|
||||
handler();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const sFunc = (propFunc) => {
|
||||
return (func, ms = 0, ...args) => {
|
||||
if (typeof func === 'function') { // ignore all non-function parameter (e.g. string)
|
||||
const wStore = { dt: Date.now() };
|
||||
return (wStore.cid = propFunc(wFunc, ms, (args.length > 0 ? func.bind(null, ...args) : func), wStore));
|
||||
} else {
|
||||
return propFunc(func, ms, ...args);
|
||||
}
|
||||
};
|
||||
};
|
||||
win.setTimeout = sFunc(setTimeout);
|
||||
win.setInterval = sFunc(setInterval);
|
||||
|
||||
const dFunc = (propFunc) => {
|
||||
return (cid) => {
|
||||
if (cid) inExec.delete(cid) || propFunc(cid);
|
||||
};
|
||||
};
|
||||
|
||||
win.clearTimeout = dFunc(clearTimeout);
|
||||
win.clearInterval = dFunc(clearInterval);
|
||||
|
||||
try {
|
||||
win.setTimeout.toString = setTimeout.toString.bind(setTimeout);
|
||||
win.setInterval.toString = setInterval.toString.bind(setInterval);
|
||||
win.clearTimeout.toString = clearTimeout.toString.bind(clearTimeout);
|
||||
win.clearInterval.toString = clearInterval.toString.bind(clearInterval);
|
||||
} catch (e) { console.warn(e) }
|
||||
|
||||
})();
|
||||
|
||||
/*
|
||||
let mInterupter = null;
|
||||
setInterval(() => {
|
||||
if (mInterupter === afInterupter) {
|
||||
if (mInterupter !== null) afInterupter = mInterupter = (mInterupter(), null);
|
||||
} else {
|
||||
mInterupter = afInterupter;
|
||||
}
|
||||
}, 125);
|
||||
*/
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { injectCpuTamerByAnimationFrame } from './cpu-tamer-by-animationframe';
|
||||
import { injectCpuTamerByDomMutation } from './cpu-tamer-by-dom-mutation';
|
||||
|
||||
const isGPUAccelerationAvailable = () => {
|
||||
// https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!(
|
||||
canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const injectCpuTamer = () => {
|
||||
if (isGPUAccelerationAvailable()) {
|
||||
injectCpuTamerByAnimationFrame(null);
|
||||
} else {
|
||||
injectCpuTamerByDomMutation(null);
|
||||
}
|
||||
};
|
||||
1
src/plugins/performance-improvement/scripts/rm3/index.ts
Normal file
1
src/plugins/performance-improvement/scripts/rm3/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './rm3';
|
||||
121
src/plugins/performance-improvement/scripts/rm3/rm3.d.ts
vendored
Normal file
121
src/plugins/performance-improvement/scripts/rm3/rm3.d.ts
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
declare class Rm3LinkedArrayNode<T> {
|
||||
value: T;
|
||||
next: Rm3LinkedArrayNode<T> | null;
|
||||
prev: Rm3LinkedArrayNode<T> | null;
|
||||
constructor(value: T);
|
||||
}
|
||||
|
||||
declare class Rm3LinkedArray<T> {
|
||||
head: Rm3LinkedArrayNode<T> | null;
|
||||
tail: Rm3LinkedArrayNode<T> | null;
|
||||
length: number;
|
||||
|
||||
constructor();
|
||||
|
||||
push(value: T): number;
|
||||
pop(): T | undefined;
|
||||
unshift(value: T): number;
|
||||
shift(): T | undefined;
|
||||
size(): number;
|
||||
getNode(index: number): Rm3LinkedArrayNode<T> | null;
|
||||
get(index: number): T | undefined;
|
||||
findNode(value: T): { node: Rm3LinkedArrayNode<T> | null; index: number };
|
||||
toArray(): T[];
|
||||
insertBeforeNode(node: Rm3LinkedArrayNode<T> | null, newValue: T): boolean;
|
||||
insertAfterNode(node: Rm3LinkedArrayNode<T> | null, newValue: T): boolean;
|
||||
insertBefore(existingValue: T, newValue: T): boolean;
|
||||
insertAfter(existingValue: T, newValue: T): boolean;
|
||||
deleteNode(node: Rm3LinkedArrayNode<T>): boolean; // Note: Original JS allowed deleting null, but TS implies non-null here
|
||||
|
||||
static Node: typeof Rm3LinkedArrayNode;
|
||||
}
|
||||
|
||||
// Define the structure of the internal LimitedSizeSet class
|
||||
declare class Rm3LimitedSizeSet<T> extends Set<T> {
|
||||
limit: number;
|
||||
constructor(n: number);
|
||||
add(key: T): this;
|
||||
removeAdd(key: T): void;
|
||||
}
|
||||
|
||||
// Define the structure of the entryRecord tuple used internally
|
||||
// [ WeakRef<HTMLElement>, attached time, detached time, time of change, inside availablePool, reuse count ]
|
||||
type Rm3EntryRecord = [
|
||||
WeakRef<HTMLElement>,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
boolean,
|
||||
number,
|
||||
];
|
||||
|
||||
// Define the interface for the exported rm3 object
|
||||
export interface Rm3 {
|
||||
/**
|
||||
* Removes duplicate values from an array.
|
||||
* @param array The input array.
|
||||
* @returns A new array with unique values.
|
||||
*/
|
||||
uniq: <T>(array: T[]) => T[];
|
||||
|
||||
/**
|
||||
* [Debug only] The current page URL. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
location?: string;
|
||||
|
||||
/**
|
||||
* [Debug only] Inspects the document for elements with a polymerController and returns their unique node names.
|
||||
* @returns An array of unique node names.
|
||||
*/
|
||||
inspect: () => string[];
|
||||
|
||||
/**
|
||||
* A Set containing records of element operations (attach/detach).
|
||||
* Each record tracks an element's lifecycle state.
|
||||
*/
|
||||
operations: Set<Rm3EntryRecord>;
|
||||
|
||||
/**
|
||||
* A Map where keys are component identifiers (e.g., "creatorTag.componentTag")
|
||||
* and values are LinkedArrays of potentially reusable EntryRecords for detached elements.
|
||||
*/
|
||||
availablePools: Map<string, Rm3LinkedArray<Rm3EntryRecord>>;
|
||||
|
||||
/**
|
||||
* Checks the parent status of elements tracked in the operations set.
|
||||
* Primarily for elements that have been detached (detached time > 0).
|
||||
* @returns An array of tuples: [elementExists: boolean, nodeName: string | undefined, isParentNull: boolean]
|
||||
*/
|
||||
checkWhetherUnderParent: () => [boolean, string | undefined, boolean][];
|
||||
|
||||
/**
|
||||
* Gets a list of unique element tag names (from `element.is`) that have been tracked.
|
||||
* @returns An array of unique tag names.
|
||||
*/
|
||||
hookTags: () => string[];
|
||||
|
||||
/**
|
||||
* [Debug only] A Set containing tags that have had their methods hooked. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
hookTos?: Set<string>;
|
||||
|
||||
/**
|
||||
* [Debug only] A function that returns an array representation of the reuse record log. Only available if DEBUG_OPT was true.
|
||||
* @returns An array of tuples: [timestamp, tagName, entryRecord]
|
||||
*/
|
||||
reuseRecord?: () => [number, string, Rm3EntryRecord][];
|
||||
|
||||
/**
|
||||
* [Debug only] A Map tracking the reuse count per component tag name. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
reuseCount_?: Map<string, number>;
|
||||
|
||||
/**
|
||||
* A counter for the total number of times elements have been reused.
|
||||
*/
|
||||
reuseCount: number;
|
||||
}
|
||||
|
||||
export const rm3: Rm3;
|
||||
|
||||
export function injectRm3(): void;
|
||||
828
src/plugins/performance-improvement/scripts/rm3/rm3.js
Normal file
828
src/plugins/performance-improvement/scripts/rm3/rm3.js
Normal file
@ -0,0 +1,828 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2024-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const rm3 = {};
|
||||
|
||||
export const injectRm3 = () => {
|
||||
const DEBUG_OPT = false;
|
||||
const CONFIRM_TIME = 4000;
|
||||
const CHECK_INTERVAL = 400;
|
||||
const DEBUG_dataChangeReflection = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => {})().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
|
||||
// https://qiita.com/piroor/items/02885998c9f76f45bfa0
|
||||
// https://gist.github.com/piroor/829ecb32a52c2a42e5393bbeebe5e63f
|
||||
function uniq(array) {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
rm3.uniq = uniq; // [[debug]]
|
||||
DEBUG_OPT && (rm3.location = location.href);
|
||||
|
||||
rm3.inspect = () => {
|
||||
return uniq(
|
||||
[...document.getElementsByTagName('*')]
|
||||
.filter((e) => e?.polymerController?.createComponent_)
|
||||
.map((e) => e.nodeName),
|
||||
); // [[debug]]
|
||||
};
|
||||
|
||||
const insp = (o) => o ? o.polymerController || o.inst || o || 0 : o || 0;
|
||||
const indr = (o) => insp(o).$ || o.$ || 0;
|
||||
|
||||
const getProto = (element) => {
|
||||
if (element) {
|
||||
const cnt = insp(element);
|
||||
return cnt.constructor.prototype || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const LinkedArray = (() => {
|
||||
class Node {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
this.next = null;
|
||||
this.prev = null;
|
||||
}
|
||||
}
|
||||
|
||||
class LinkedArray {
|
||||
constructor() {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
push(value) {
|
||||
const newNode = new Node(value);
|
||||
if (this.length === 0) {
|
||||
this.head = newNode;
|
||||
this.tail = newNode;
|
||||
} else {
|
||||
this.tail.next = newNode;
|
||||
newNode.prev = this.tail;
|
||||
this.tail = newNode;
|
||||
}
|
||||
this.length++;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
pop() {
|
||||
if (this.length === 0) return undefined;
|
||||
const removedNode = this.tail;
|
||||
if (this.length === 1) {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else {
|
||||
this.tail = removedNode.prev;
|
||||
this.tail.next = null;
|
||||
removedNode.prev = null;
|
||||
}
|
||||
this.length--;
|
||||
return removedNode.value;
|
||||
}
|
||||
|
||||
unshift(value) {
|
||||
const newNode = new Node(value);
|
||||
if (this.length === 0) {
|
||||
this.head = newNode;
|
||||
this.tail = newNode;
|
||||
} else {
|
||||
newNode.next = this.head;
|
||||
this.head.prev = newNode;
|
||||
this.head = newNode;
|
||||
}
|
||||
this.length++;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
shift() {
|
||||
if (this.length === 0) return undefined;
|
||||
const removedNode = this.head;
|
||||
if (this.length === 1) {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else {
|
||||
this.head = removedNode.next;
|
||||
this.head.prev = null;
|
||||
removedNode.next = null;
|
||||
}
|
||||
this.length--;
|
||||
return removedNode.value;
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
// Get a node by index (0-based)
|
||||
getNode(index) {
|
||||
if (index < 0 || index >= this.length) return null;
|
||||
|
||||
let current;
|
||||
let counter;
|
||||
|
||||
// Optimization: start from closest end
|
||||
if (index < this.length / 2) {
|
||||
current = this.head;
|
||||
counter = 0;
|
||||
while (counter !== index) {
|
||||
current = current.next;
|
||||
counter++;
|
||||
}
|
||||
} else {
|
||||
current = this.tail;
|
||||
counter = this.length - 1;
|
||||
while (counter !== index) {
|
||||
current = current.prev;
|
||||
counter--;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// Get value by index
|
||||
get(index) {
|
||||
const node = this.getNode(index);
|
||||
return node ? node.value : undefined;
|
||||
}
|
||||
|
||||
// Find the first node with the given value and return both node and index
|
||||
findNode(value) {
|
||||
let current = this.head;
|
||||
let idx = 0;
|
||||
while (current) {
|
||||
if (current.value === value) {
|
||||
return { node: current, index: idx };
|
||||
}
|
||||
current = current.next;
|
||||
idx++;
|
||||
}
|
||||
return { node: null, index: -1 };
|
||||
}
|
||||
|
||||
toArray() {
|
||||
const arr = [];
|
||||
let current = this.head;
|
||||
while (current) {
|
||||
arr.push(current.value);
|
||||
current = current.next;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Insert a new value before a given node (provided you already have the node reference)
|
||||
insertBeforeNode(node, newValue) {
|
||||
if (!node) {
|
||||
this.unshift(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node === this.head) {
|
||||
// If the target is the head, just unshift
|
||||
this.unshift(newValue);
|
||||
} else {
|
||||
const newNode = new Node(newValue);
|
||||
const prevNode = node.prev;
|
||||
|
||||
prevNode.next = newNode;
|
||||
newNode.prev = prevNode;
|
||||
newNode.next = node;
|
||||
node.prev = newNode;
|
||||
|
||||
this.length++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert a new value after a given node (provided you already have the node reference)
|
||||
insertAfterNode(node, newValue) {
|
||||
if (!node) {
|
||||
this.push(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node === this.tail) {
|
||||
// If the target is the tail, just push
|
||||
this.push(newValue);
|
||||
} else {
|
||||
const newNode = new Node(newValue);
|
||||
const nextNode = node.next;
|
||||
|
||||
node.next = newNode;
|
||||
newNode.prev = node;
|
||||
newNode.next = nextNode;
|
||||
nextNode.prev = newNode;
|
||||
|
||||
this.length++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert a new value before the first occurrence of an existing value (search by value)
|
||||
insertBefore(existingValue, newValue) {
|
||||
const { node } = this.findNode(existingValue);
|
||||
if (!node) return false; // Not found
|
||||
return this.insertBeforeNode(node, newValue);
|
||||
}
|
||||
|
||||
// Insert a new value after the first occurrence of an existing value (search by value)
|
||||
insertAfter(existingValue, newValue) {
|
||||
const { node } = this.findNode(existingValue);
|
||||
if (!node) return false; // Not found
|
||||
return this.insertAfterNode(node, newValue);
|
||||
}
|
||||
|
||||
// Delete a given node from the list
|
||||
deleteNode(node) {
|
||||
if (!node) return false;
|
||||
|
||||
if (this.length === 1 && node === this.head && node === this.tail) {
|
||||
// Only one element in the list
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else if (node === this.head) {
|
||||
// Node is the head
|
||||
this.head = node.next;
|
||||
this.head.prev = null;
|
||||
node.next = null;
|
||||
} else if (node === this.tail) {
|
||||
// Node is the tail
|
||||
this.tail = node.prev;
|
||||
this.tail.next = null;
|
||||
node.prev = null;
|
||||
} else {
|
||||
// Node is in the middle
|
||||
const prevNode = node.prev;
|
||||
const nextNode = node.next;
|
||||
prevNode.next = nextNode;
|
||||
nextNode.prev = prevNode;
|
||||
node.prev = null;
|
||||
node.next = null;
|
||||
}
|
||||
|
||||
this.length--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LinkedArray.Node = Node;
|
||||
return LinkedArray;
|
||||
})();
|
||||
|
||||
class LimitedSizeSet extends Set {
|
||||
constructor(n) {
|
||||
super();
|
||||
this.limit = n;
|
||||
}
|
||||
|
||||
add(key) {
|
||||
if (!super.has(key)) {
|
||||
super.add(key);
|
||||
let n = super.size - this.limit;
|
||||
if (n > 0) {
|
||||
const iterator = super.values();
|
||||
do {
|
||||
const firstKey = iterator.next().value; // Get the first (oldest) key
|
||||
super.delete(firstKey); // Delete the oldest key
|
||||
} while (--n > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAdd(key) {
|
||||
super.delete(key);
|
||||
this.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!document.createElement9512 &&
|
||||
typeof document.createElement === 'function' &&
|
||||
document.createElement.length === 1
|
||||
) {
|
||||
// sizing of Map / Set. Shall limit ?
|
||||
|
||||
const hookTos = new Set(); // [[debug]]
|
||||
DEBUG_OPT && (rm3.hookTos = hookTos);
|
||||
|
||||
// const reusePool = new Map(); // xx858
|
||||
const entryRecords = new WeakMap(); // a weak link between element and record
|
||||
|
||||
// rm3.list = [];
|
||||
|
||||
const operations = rm3.operations = new Set(); // to find out the "oldest elements"
|
||||
|
||||
const availablePools = rm3.availablePools = new Map(); // those "old elements" can be used
|
||||
let lastTimeCheck = 0;
|
||||
|
||||
const reuseRecord_ = new LimitedSizeSet(256); // [[debug]]
|
||||
const reuseCount_ = new Map();
|
||||
|
||||
let noTimeCheck = false;
|
||||
|
||||
// const defaultValues = new Map();
|
||||
// const noValues = new Map();
|
||||
|
||||
const timeCheck = () => {
|
||||
// regularly check elements are old enough to put into the available pools
|
||||
// note: the characterists of YouTube components are non-volatile. So don't need to waste time to check weakRef.deref() is null or not for removing in operations.
|
||||
|
||||
const ct = Date.now();
|
||||
if (ct - lastTimeCheck < CHECK_INTERVAL || noTimeCheck) return;
|
||||
lastTimeCheck = ct;
|
||||
noTimeCheck = true;
|
||||
|
||||
// 16,777,216
|
||||
if (hookTos.size > 777216) hookTos.clear(); // just debug usage, dont concern
|
||||
if (operations.size > 7777216) {
|
||||
// extremely old elements in operations mean they have no attach/detach action. so no reuse as well. they are just trash in memory.
|
||||
// as no checking of the weakRef.deref() being null or not, those trash could be already cleaned. However we don't concern this.
|
||||
// (not to count whether they are actual memory trash or not)
|
||||
const half = operations.size >>> 1;
|
||||
let i = 0;
|
||||
for (const value of operations) {
|
||||
if (i++ > half) break;
|
||||
operations.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// // smallest to largest
|
||||
// // past to recent
|
||||
|
||||
// const iterator = operations[Symbol.iterator]();
|
||||
// console.log(1831, '------------------------')
|
||||
// while (true) {
|
||||
// const iteratorResult = iterator.next(); // 順番に値を取りだす
|
||||
// if (iteratorResult.done) break; // 取り出し終えたなら、break
|
||||
// console.log(1835, iteratorResult.value[3])
|
||||
// }
|
||||
|
||||
// console.log(1839, '------------------------')
|
||||
// }
|
||||
|
||||
// Set iterator
|
||||
// s.add(2) s.add(6) s.add(1) s.add(3)
|
||||
// next: 2 -> 6 -> 1 -> 3
|
||||
// op1 (oldest) -> op2 -> op3 -> op4 (latest)
|
||||
const iterator = operations[Symbol.iterator]();
|
||||
|
||||
const targetTime = ct - CONFIRM_TIME;
|
||||
|
||||
const pivotNodes = new WeakMap();
|
||||
|
||||
while (true) {
|
||||
const iteratorResult = iterator.next(); // 順番に値を取りだす
|
||||
if (iteratorResult.done) break; // 取り出し終えたなら、break
|
||||
const entryRecord = iteratorResult.value;
|
||||
if (entryRecord[3] > targetTime) break;
|
||||
|
||||
if (!entryRecord[4] && entryRecord[1] < 0 && entryRecord[2] > 0) {
|
||||
const element = entryRecord[0].deref();
|
||||
const eKey = (element || 0).__rm3Tag003__;
|
||||
if (!eKey) {
|
||||
operations.delete(entryRecord);
|
||||
} else if (
|
||||
element.isConnected === false &&
|
||||
insp(element).isAttached === false
|
||||
) {
|
||||
entryRecord[4] = true;
|
||||
|
||||
let availablePool = availablePools.get(eKey);
|
||||
if (!availablePool)
|
||||
availablePools.set(eKey, availablePool = new LinkedArray());
|
||||
if (!(availablePool instanceof LinkedArray)) throw new Error();
|
||||
DEBUG_OPT &&
|
||||
console.log(3885, 'add key', eKey, availablePools.size);
|
||||
// rm3.showSize = ()=>availablePools.size
|
||||
// setTimeout(()=>{
|
||||
// // window?.euu1 = availablePools
|
||||
// // window?.euu2 = availablePools.size
|
||||
// console.log(availablePools.size)
|
||||
// }, 8000)
|
||||
let pivotNode = pivotNodes.get(availablePool);
|
||||
if (!pivotNode)
|
||||
pivotNodes.set(availablePool, pivotNode = availablePool.head); // cached the previous newest node (head) as pivotNode
|
||||
|
||||
availablePool.insertBeforeNode(pivotNode, entryRecord); // head = newest, tail = oldest
|
||||
}
|
||||
}
|
||||
}
|
||||
noTimeCheck = false;
|
||||
};
|
||||
|
||||
const attachedDefine = function () {
|
||||
Promise.resolve().then(timeCheck);
|
||||
try {
|
||||
const hostElement = this?.hostElement;
|
||||
if (hostElement instanceof HTMLElement) {
|
||||
const entryRecord = entryRecords.get(hostElement);
|
||||
if (
|
||||
entryRecord &&
|
||||
entryRecord[0].deref() === hostElement &&
|
||||
hostElement.isConnected === true &&
|
||||
this?.isAttached === true
|
||||
) {
|
||||
noTimeCheck = true;
|
||||
const ct = Date.now();
|
||||
entryRecord[1] = ct;
|
||||
entryRecord[2] = -1;
|
||||
entryRecord[3] = ct;
|
||||
operations.delete(entryRecord);
|
||||
operations.add(entryRecord);
|
||||
noTimeCheck = false;
|
||||
// note: because of performance prespective, deletion for availablePools[eKey]'s linked element would not be done here.
|
||||
// entryRecord[4] is not required to be updated here.
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return this.attached9512();
|
||||
};
|
||||
const detachedDefine = function () {
|
||||
Promise.resolve().then(timeCheck);
|
||||
try {
|
||||
const hostElement = this?.hostElement;
|
||||
if (hostElement instanceof HTMLElement) {
|
||||
const entryRecord = entryRecords.get(hostElement);
|
||||
if (
|
||||
entryRecord &&
|
||||
entryRecord[0].deref() === hostElement &&
|
||||
hostElement.isConnected === false &&
|
||||
this?.isAttached === false
|
||||
) {
|
||||
noTimeCheck = true;
|
||||
const ct = Date.now();
|
||||
entryRecord[2] = ct;
|
||||
entryRecord[1] = -1;
|
||||
entryRecord[3] = ct;
|
||||
operations.delete(entryRecord);
|
||||
operations.add(entryRecord);
|
||||
noTimeCheck = false;
|
||||
// note: because of performance prespective, deletion for availablePools[eKey]'s linked element would not be done here.
|
||||
// entryRecord[4] is not required to be updated here.
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return this.detached9512();
|
||||
};
|
||||
|
||||
// function cpy(x) {
|
||||
// if (!x) return x;
|
||||
// try {
|
||||
// if (typeof x === 'object' && typeof x.length ==='number' && typeof x.slice === 'function') {
|
||||
// x = x.slice(0)
|
||||
// } else if (typeof x === 'object' && !x.length) {
|
||||
// x = JSON.parse(JSON.stringify(x));
|
||||
// } else {
|
||||
// return Object.assign({}, x);
|
||||
// }
|
||||
// } catch (e) { }
|
||||
// return x;
|
||||
// }
|
||||
|
||||
async function digestMessage(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message); // (utf-8 の) Uint8Array にエンコードする
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // メッセージをハッシュする
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // バッファーをバイト列に変換する
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(''); // バイト列を 16 進文字列に変換する
|
||||
return hashHex.toUpperCase();
|
||||
}
|
||||
|
||||
let onPageContainer = null;
|
||||
|
||||
const createComponentDefine_ = function (a, b, c) {
|
||||
Promise.resolve().then(timeCheck);
|
||||
|
||||
const creatorTag = this?.is || this?.nodeName?.toLowerCase() || '';
|
||||
|
||||
const componentTag = typeof a === 'string' ? a : (a || 0).component || '';
|
||||
|
||||
const eKey =
|
||||
creatorTag && componentTag ? `${creatorTag}.${componentTag}` : '*'; // '*' for play-safe
|
||||
const availablePool = availablePools.get(eKey);
|
||||
|
||||
try {
|
||||
if (availablePool instanceof LinkedArray) {
|
||||
noTimeCheck = true;
|
||||
|
||||
let node = availablePool.tail; // oldest
|
||||
|
||||
while (node instanceof LinkedArray.Node) {
|
||||
const entryRecord = node.value;
|
||||
const prevNode = node.prev;
|
||||
|
||||
let ok = false;
|
||||
let elm = null;
|
||||
if (entryRecord[1] < 0 && entryRecord[2] > 0 && entryRecord[4]) {
|
||||
elm = entryRecord[0].deref();
|
||||
// elm && console.log(3882, (elm.__shady_native_textContent || elm.textContent))
|
||||
if (
|
||||
elm &&
|
||||
elm instanceof HTMLElement &&
|
||||
elm.isConnected === false &&
|
||||
insp(elm).isAttached === false &&
|
||||
elm.parentNode === null
|
||||
) {
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
// useEntryRecord = entryRecord;
|
||||
entryRecord[4] = false;
|
||||
// console.log('nodeDeleted', 1, entryRecord[0].deref().nodeName)
|
||||
availablePool.deleteNode(node);
|
||||
// break;
|
||||
|
||||
if (!onPageContainer) {
|
||||
let p = document.createElement('noscript');
|
||||
document.body.prepend(p);
|
||||
onPageContainer = p;
|
||||
}
|
||||
|
||||
onPageContainer.appendChild(elm); // to fix some issues for the rendered elements
|
||||
|
||||
const cnt = insp(elm);
|
||||
|
||||
cnt.__dataInvalid = false;
|
||||
// cnt._initializeProtoProperties(cnt.data)
|
||||
|
||||
// window.meaa = cnt.$.container;
|
||||
if (typeof (cnt.__data || 0) === 'object') {
|
||||
cnt.__data = Object.assign({}, cnt.__data);
|
||||
}
|
||||
cnt.__dataPending = {};
|
||||
cnt.__dataOld = {};
|
||||
|
||||
try {
|
||||
cnt.markDirty();
|
||||
} catch (e) {}
|
||||
try {
|
||||
cnt.markDirtyVisibilityObserver();
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
cnt.wasPrescan = cnt.wasVisible = !1;
|
||||
} catch (e) {}
|
||||
|
||||
// try{
|
||||
// cnt._setPendingProperty('data', Object.assign({}, cntData), !0);
|
||||
// }catch(e){}
|
||||
// try {
|
||||
// cnt._flushProperties();
|
||||
// } catch (e) { }
|
||||
|
||||
if (DEBUG_OPT && DEBUG_dataChangeReflection) {
|
||||
let jC1 = null;
|
||||
let jC2 = null;
|
||||
const jKey = `${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`;
|
||||
try {
|
||||
jC1 =
|
||||
cnt.hostElement.__shady_native_textContent ||
|
||||
cnt.hostElement.textContent;
|
||||
// console.log(83802, jKey, (cnt.hostElement.__shady_native_textContent || cnt.hostElement.textContent))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
jC2 =
|
||||
cnt.hostElement.__shady_native_textContent ||
|
||||
cnt.hostElement.textContent;
|
||||
// console.log(83804, jKey, (cnt.hostElement.__shady_native_textContent || cnt.hostElement.textContent))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
jC1 = await digestMessage(jC1);
|
||||
jC2 = await digestMessage(jC2);
|
||||
|
||||
console.log(
|
||||
83804,
|
||||
jKey,
|
||||
jC1.substring(0, 7),
|
||||
jC2.substring(0, 7),
|
||||
);
|
||||
})();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (entryRecord[5] < 1e9) entryRecord[5] += 1;
|
||||
DEBUG_OPT &&
|
||||
Promise.resolve().then(() =>
|
||||
console.log(`${eKey} reuse`, entryRecord),
|
||||
); // give some time for attach process
|
||||
DEBUG_OPT && reuseRecord_.add([Date.now(), cnt.is, entryRecord]);
|
||||
DEBUG_OPT &&
|
||||
reuseCount_.set(cnt.is, (reuseCount_.get(cnt.is) || 0) + 1);
|
||||
if (rm3.reuseCount < 1e9) rm3.reuseCount++;
|
||||
|
||||
return elm;
|
||||
}
|
||||
|
||||
// console.log('condi88', entryRecord[1] < 0 , entryRecord[2] > 0 , !!entryRecord[4], !!entryRecord[0].deref())
|
||||
|
||||
entryRecord[4] = false;
|
||||
|
||||
// console.log(entryRecord);
|
||||
// console.log('nodeDeleted',2, entryRecord[0]?.deref()?.nodeName)
|
||||
availablePool.deleteNode(node);
|
||||
node = prevNode;
|
||||
}
|
||||
// for(const ) availablePool
|
||||
// noTimeCheck = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
noTimeCheck = false;
|
||||
|
||||
// console.log('createComponentDefine_', a, b, c)
|
||||
|
||||
// if (!reusePool.has(componentTag)) reusePool.set(componentTag, new LinkedArray()); // xx858
|
||||
|
||||
// const pool = reusePool.get(componentTag); // xx858
|
||||
// if (!(pool instanceof LinkedArray)) throw new Error(); // xx858
|
||||
|
||||
const newElement = this.createComponent9512_(a, b, c);
|
||||
// if(componentTag.indexOf( 'ticker')>=0)console.log(1883, a,newElement)
|
||||
|
||||
try {
|
||||
const cntE = insp(newElement);
|
||||
if (!cntE.attached9512 && cntE.attached) {
|
||||
const cProtoE = getProto(newElement);
|
||||
|
||||
if (cProtoE.attached === cntE.attached) {
|
||||
if (
|
||||
!cProtoE.attached9512 &&
|
||||
typeof cProtoE.attached === 'function' &&
|
||||
cProtoE.attached.length === 0
|
||||
) {
|
||||
cProtoE.attached9512 = cProtoE.attached;
|
||||
|
||||
cProtoE.attached = attachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cntE.attached === 'function' &&
|
||||
cntE.attached.length === 3
|
||||
) {
|
||||
cntE.attached9512 = cntE.attached;
|
||||
|
||||
cntE.attached = attachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cntE.detached9512 && cntE.detached) {
|
||||
const cProtoE = getProto(newElement);
|
||||
|
||||
if (cProtoE.detached === cntE.detached) {
|
||||
if (
|
||||
!cProtoE.detached9512 &&
|
||||
typeof cProtoE.detached === 'function' &&
|
||||
cProtoE.detached.length === 0
|
||||
) {
|
||||
cProtoE.detached9512 = cProtoE.detached;
|
||||
|
||||
cProtoE.detached = detachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cntE.detached === 'function' &&
|
||||
cntE.detached.length === 3
|
||||
) {
|
||||
cntE.detached9512 = cntE.detached;
|
||||
|
||||
cntE.detached = detachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acceptance = true;
|
||||
// const acceptance = !cntE.__dataReady && cntE.__dataInvalid !== false; // we might need to change the acceptance condition along with YouTube Coding updates.
|
||||
if (acceptance) {
|
||||
// [[ weak ElementNode, attached time, detached time, time of change, inside availablePool, reuse count ]]
|
||||
const entryRecord = [new WeakRef(newElement), -1, -1, -1, false, 0];
|
||||
|
||||
newElement.__rm3Tag003__ = eKey;
|
||||
entryRecords.set(newElement, entryRecord);
|
||||
} else {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
return newElement;
|
||||
};
|
||||
|
||||
document.createElement9512 = document.createElement;
|
||||
document.createElement = function (a) {
|
||||
const r = document.createElement9512(a);
|
||||
try {
|
||||
const cnt = insp(r);
|
||||
if (cnt.createComponent_ && !cnt.createComponent9512_) {
|
||||
const cProto = getProto(r);
|
||||
if (cProto.createComponent_ === cnt.createComponent_) {
|
||||
if (
|
||||
!cProto.createComponent9512_ &&
|
||||
typeof cProto.createComponent_ === 'function' &&
|
||||
cProto.createComponent_.length === 3
|
||||
) {
|
||||
cProto.createComponent9512_ = cProto.createComponent_;
|
||||
|
||||
cProto.createComponent_ = createComponentDefine_;
|
||||
DEBUG_OPT && hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cnt.createComponent_ === 'function' &&
|
||||
cnt.createComponent_.length === 3
|
||||
) {
|
||||
cnt.createComponent9512_ = cnt.createComponent_;
|
||||
|
||||
cnt.createComponent_ = createComponentDefine_;
|
||||
DEBUG_OPT && hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
rm3.checkWhetherUnderParent = () => {
|
||||
const r = [];
|
||||
for (const operation of operations) {
|
||||
const elm = operation[0].deref();
|
||||
if (operation[2] > 0) {
|
||||
r.push([
|
||||
!!elm,
|
||||
elm?.nodeName.toLowerCase(),
|
||||
elm?.parentNode === null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
rm3.hookTags = () => {
|
||||
const r = new Set();
|
||||
for (const operation of operations) {
|
||||
const elm = operation[0].deref();
|
||||
if (elm && elm.is) {
|
||||
r.add(elm.is);
|
||||
}
|
||||
}
|
||||
return [...r];
|
||||
};
|
||||
|
||||
DEBUG_OPT &&
|
||||
(rm3.reuseRecord = () => {
|
||||
return [...reuseRecord_]; // [[debug]]
|
||||
});
|
||||
|
||||
DEBUG_OPT && (rm3.reuseCount_ = reuseCount_);
|
||||
}
|
||||
|
||||
rm3.reuseCount = 0;
|
||||
};
|
||||
@ -174,7 +174,7 @@ export const romanizeJapanese = async (line: string) =>
|
||||
(await kuroshiro.get()).convert(line, {
|
||||
to: 'romaji',
|
||||
mode: 'spaced',
|
||||
});
|
||||
}) ?? '';
|
||||
|
||||
export const romanizeHangul = (line: string) =>
|
||||
esHangulRomanize(hanja.translate(line, 'SUBSTITUTION'));
|
||||
|
||||
@ -16,7 +16,9 @@ interface Data {
|
||||
progress: number;
|
||||
status: string;
|
||||
title: string;
|
||||
alternativeTitle: string;
|
||||
url: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export default createPlugin({
|
||||
@ -86,10 +88,12 @@ export default createPlugin({
|
||||
cover_url: songInfo.imageSrc ?? '',
|
||||
album_url: songInfo.imageSrc ?? '',
|
||||
title: songInfo.title,
|
||||
alternativeTitle: songInfo.alternativeTitle ?? '',
|
||||
artists: [songInfo.artist],
|
||||
status: songInfo.isPaused ? 'stopped' : 'playing',
|
||||
album: songInfo.album,
|
||||
url: songInfo.url ?? '',
|
||||
tags: songInfo.tags ?? [],
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user