diff --git a/.all-contributorsrc b/.all-contributorsrc index b3c71be9..a1167b73 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,137 +1,180 @@ { - "projectName": "extension", - "projectOwner": "YouTube-Enhancer", - "files": ["README.md"], - "commitType": "docs", - "commitConvention": "angular", - "contributorsPerLine": 7, - "contributors": [ - { - "login": "mist8kengas", - "name": "mist8kengas", - "avatar_url": "https://avatars.githubusercontent.com/u/46955469?v=4", - "profile": "https://hioctane.org", - "contributions": ["translation", "code", "bug"] - }, - { - "login": "VampireChicken12", - "name": "Nathan", - "avatar_url": "https://avatars.githubusercontent.com/u/45531575?v=4", - "profile": "https://hikari-bot.com", - "contributions": ["design", "code", "translation"] - }, - { - "login": "commonly-ts", - "name": "Commonly", - "avatar_url": "https://avatars.githubusercontent.com/u/51011212?v=4", - "profile": "https://github.com/commonly-ts", - "contributions": ["bug"] - }, - { - "login": "eduardozgz", - "name": "Eduardo Aznar", - "avatar_url": "https://avatars.githubusercontent.com/u/30407412?v=4", - "profile": "https://eduardozgz.com", - "contributions": ["translation"] - }, - { - "login": "lamyergeier", - "name": "Lamyergeier", - "avatar_url": "https://avatars.githubusercontent.com/u/42092626?v=4", - "profile": "https://github.com/lamyergeier", - "contributions": ["ideas"] - }, - { - "login": "SleepyPrince", - "name": "Jackal Chan", - "avatar_url": "https://avatars.githubusercontent.com/u/670216?v=4", - "profile": "https://github.com/SleepyPrince", - "contributions": ["translation"] - }, - { - "login": "livingflore", - "name": "livingflore", - "avatar_url": "https://avatars.githubusercontent.com/u/63370734?v=4", - "profile": "http://livingflore.me", - "contributions": ["translation", "code"] - }, - { - "login": "crvt7", - "name": "Patryk Popardowski", - "avatar_url": "https://avatars.githubusercontent.com/u/79649679?v=4", - "profile": "https://github.com/crvt7", - "contributions": ["translation"] - }, - { - "login": "luisaosan", - "name": "Luis Felipe", - "avatar_url": "https://avatars.githubusercontent.com/u/48157083?v=4", - "profile": "https://github.com/luisaosan", - "contributions": ["translation"] - }, - { - "login": "v1ctorio", - "name": "Vic", - "avatar_url": "https://avatars.githubusercontent.com/u/74506415?v=4", - "profile": "http://nosesisaid.com", - "contributions": ["translation"] - }, - { - "login": "rado84-github", - "name": "rado84", - "avatar_url": "https://avatars.githubusercontent.com/u/41172201?v=4", - "profile": "https://github.com/rado84-github", - "contributions": ["bug"] - }, - { - "login": "pulsar2105", - "name": "pulsar2105", - "avatar_url": "https://avatars.githubusercontent.com/u/54115653?v=4", - "profile": "https://github.com/pulsar2105", - "contributions": ["translation"] - }, - { - "login": "G-Ran-Berg", - "name": "Granberg", - "avatar_url": "https://avatars.githubusercontent.com/u/12037193?v=4", - "profile": "https://github.com/G-Ran-Berg", - "contributions": ["translation"] - }, - { - "login": "Secret-Peter", - "name": "Secret-Peter", - "avatar_url": "https://avatars.githubusercontent.com/u/166921574?v=4", - "profile": "https://github.com/Secret-Peter", - "contributions": ["translation"] - }, - { - "login": "szaumoor", - "name": "Marcos C.R.", - "avatar_url": "https://avatars.githubusercontent.com/u/78204388?v=4", - "profile": "https://github.com/szaumoor", - "contributions": ["ideas"] - }, - { - "login": "charlymoon741", - "name": "Carlos Ramos Luna", - "avatar_url": "https://avatars.githubusercontent.com/u/62484941?v=4", - "profile": "https://github.com/charlymoon741", - "contributions": ["ideas"] - }, - { - "login": "Angsimosaurus", - "name": "앙시모사우루스", - "avatar_url": "https://avatars.githubusercontent.com/u/79510039?v=4", - "profile": "https://github.com/Angsimosaurus", - "contributions": ["translation"] - }, - { - "login": "Mabra51", - "name": "Mabra51", - "avatar_url": "https://avatars.githubusercontent.com/u/12016338?v=4", - "profile": "https://github.com/Mabra51", - "contributions": ["ideas"] - }, + "projectName": "extension", + "projectOwner": "YouTube-Enhancer", + "files": [ + "README.md" + ], + "commitType": "docs", + "commitConvention": "angular", + "contributorsPerLine": 7, + "contributors": [ + { + "login": "mist8kengas", + "name": "mist8kengas", + "avatar_url": "https://avatars.githubusercontent.com/u/46955469?v=4", + "profile": "https://hioctane.org", + "contributions": [ + "translation", + "code", + "bug" + ] + }, + { + "login": "VampireChicken12", + "name": "Nathan", + "avatar_url": "https://avatars.githubusercontent.com/u/45531575?v=4", + "profile": "https://hikari-bot.com", + "contributions": [ + "design", + "code", + "translation" + ] + }, + { + "login": "commonly-ts", + "name": "Commonly", + "avatar_url": "https://avatars.githubusercontent.com/u/51011212?v=4", + "profile": "https://github.com/commonly-ts", + "contributions": [ + "bug" + ] + }, + { + "login": "eduardozgz", + "name": "Eduardo Aznar", + "avatar_url": "https://avatars.githubusercontent.com/u/30407412?v=4", + "profile": "https://eduardozgz.com", + "contributions": [ + "translation" + ] + }, + { + "login": "lamyergeier", + "name": "Lamyergeier", + "avatar_url": "https://avatars.githubusercontent.com/u/42092626?v=4", + "profile": "https://github.com/lamyergeier", + "contributions": [ + "ideas" + ] + }, + { + "login": "SleepyPrince", + "name": "Jackal Chan", + "avatar_url": "https://avatars.githubusercontent.com/u/670216?v=4", + "profile": "https://github.com/SleepyPrince", + "contributions": [ + "translation" + ] + }, + { + "login": "livingflore", + "name": "livingflore", + "avatar_url": "https://avatars.githubusercontent.com/u/63370734?v=4", + "profile": "http://livingflore.me", + "contributions": [ + "translation", + "code" + ] + }, + { + "login": "crvt7", + "name": "Patryk Popardowski", + "avatar_url": "https://avatars.githubusercontent.com/u/79649679?v=4", + "profile": "https://github.com/crvt7", + "contributions": [ + "translation" + ] + }, + { + "login": "luisaosan", + "name": "Luis Felipe", + "avatar_url": "https://avatars.githubusercontent.com/u/48157083?v=4", + "profile": "https://github.com/luisaosan", + "contributions": [ + "translation" + ] + }, + { + "login": "v1ctorio", + "name": "Vic", + "avatar_url": "https://avatars.githubusercontent.com/u/74506415?v=4", + "profile": "http://nosesisaid.com", + "contributions": [ + "translation" + ] + }, + { + "login": "rado84-github", + "name": "rado84", + "avatar_url": "https://avatars.githubusercontent.com/u/41172201?v=4", + "profile": "https://github.com/rado84-github", + "contributions": [ + "bug" + ] + }, + { + "login": "pulsar2105", + "name": "pulsar2105", + "avatar_url": "https://avatars.githubusercontent.com/u/54115653?v=4", + "profile": "https://github.com/pulsar2105", + "contributions": [ + "translation" + ] + }, + { + "login": "G-Ran-Berg", + "name": "Granberg", + "avatar_url": "https://avatars.githubusercontent.com/u/12037193?v=4", + "profile": "https://github.com/G-Ran-Berg", + "contributions": [ + "translation" + ] + }, + { + "login": "Secret-Peter", + "name": "Secret-Peter", + "avatar_url": "https://avatars.githubusercontent.com/u/166921574?v=4", + "profile": "https://github.com/Secret-Peter", + "contributions": [ + "translation" + ] + }, + { + "login": "szaumoor", + "name": "Marcos C.R.", + "avatar_url": "https://avatars.githubusercontent.com/u/78204388?v=4", + "profile": "https://github.com/szaumoor", + "contributions": [ + "ideas" + ] + }, + { + "login": "charlymoon741", + "name": "Carlos Ramos Luna", + "avatar_url": "https://avatars.githubusercontent.com/u/62484941?v=4", + "profile": "https://github.com/charlymoon741", + "contributions": [ + "ideas" + ] + }, + { + "login": "Angsimosaurus", + "name": "앙시모사우루스", + "avatar_url": "https://avatars.githubusercontent.com/u/79510039?v=4", + "profile": "https://github.com/Angsimosaurus", + "contributions": [ + "translation" + ] + }, + { + "login": "Mabra51", + "name": "Mabra51", + "avatar_url": "https://avatars.githubusercontent.com/u/12016338?v=4", + "profile": "https://github.com/Mabra51", + "contributions": [ + "ideas" + ] + }, { "login": "eduardozgz", "name": "Eduardo Aznar", @@ -141,6 +184,16 @@ "translation", "code" ] + }, + { + "login": "at-pyrix", + "name": "pyrix", + "avatar_url": "https://avatars.githubusercontent.com/u/90166733?v=4", + "profile": "https://notyasho.netlify.app/blogs", + "contributions": [ + "ideas", + "design" + ] } - ] + ] } diff --git a/.eslintrc b/.eslintrc index 5a90fe4f..08904f6b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,7 +30,7 @@ "plugins": ["react", "@typescript-eslint", "no-secrets"], "rules": { "react/react-in-jsx-scope": "off", - "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/restrict-template-expressions": "off", diff --git a/README.md b/README.md index bf5c7cbf..16fa12e9 100755 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ Contributions to the YouTube Enhancer Extension are welcome! If you'd like to co mist8kengas
mist8kengas

🌍 💻 🐛 Nathan
Nathan

🎨 💻 🌍 Commonly
Commonly

🐛 - Eduardo Aznar
Eduardo Aznar

🌍 💻 + Eduardo Aznar
Eduardo Aznar

🌍 Lamyergeier
Lamyergeier

🤔 Jackal Chan
Jackal Chan

🌍 livingflore
livingflore

🌍 💻 @@ -308,8 +308,10 @@ Contributions to the YouTube Enhancer Extension are welcome! If you'd like to co Marcos C.R.
Marcos C.R.

🤔 Carlos Ramos Luna
Carlos Ramos Luna

🤔 - Mabra51
Mabra51

🤔 앙시모사우루스
앙시모사우루스

🌍 + Mabra51
Mabra51

🤔 + Eduardo Aznar
Eduardo Aznar

🌍 💻 + pyrix
pyrix

🤔 🎨 diff --git a/assets/translation context screenshots/Button Placement Settings/Button_Name_Copy_Video_URL_With_Timestamp_Button.png b/assets/translation context screenshots/Button Placement Settings/Button_Name_Copy_Video_URL_With_Timestamp_Button.png new file mode 100644 index 00000000..ff19f14d Binary files /dev/null and b/assets/translation context screenshots/Button Placement Settings/Button_Name_Copy_Video_URL_With_Timestamp_Button.png differ diff --git a/assets/translation context screenshots/Miscellaneous Settings/Copy_Video_URL_With_Timestamp_Button.png b/assets/translation context screenshots/Miscellaneous Settings/Copy_Video_URL_With_Timestamp_Button.png new file mode 100644 index 00000000..719f74b0 Binary files /dev/null and b/assets/translation context screenshots/Miscellaneous Settings/Copy_Video_URL_With_Timestamp_Button.png differ diff --git a/assets/translation context screenshots/Miscellaneous Settings/Hide_Paid_Promotion_Banner.png b/assets/translation context screenshots/Miscellaneous Settings/Hide_Paid_Promotion_Banner.png new file mode 100644 index 00000000..9e481b8d Binary files /dev/null and b/assets/translation context screenshots/Miscellaneous Settings/Hide_Paid_Promotion_Banner.png differ diff --git a/assets/translation context screenshots/Playlist Length Settings/Display_Playlist_Length_Information.png b/assets/translation context screenshots/Playlist Length Settings/Display_Playlist_Length_Information.png new file mode 100644 index 00000000..2a04f348 Binary files /dev/null and b/assets/translation context screenshots/Playlist Length Settings/Display_Playlist_Length_Information.png differ diff --git a/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Playlist_Length.png b/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Playlist_Length.png new file mode 100644 index 00000000..a25e7e7f Binary files /dev/null and b/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Playlist_Length.png differ diff --git a/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Watched_Time.png b/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Watched_Time.png new file mode 100644 index 00000000..3857139f Binary files /dev/null and b/assets/translation context screenshots/Playlist Length Settings/Method_To_Get_Watched_Time.png differ diff --git a/assets/translation context screenshots/Playlist Length Settings/Title.png b/assets/translation context screenshots/Playlist Length Settings/Title.png new file mode 100644 index 00000000..26bbcdbe Binary files /dev/null and b/assets/translation context screenshots/Playlist Length Settings/Title.png differ diff --git a/assets/translation context screenshots/Setting Search Bar/Search_For_A_Setting.png b/assets/translation context screenshots/Setting Search Bar/Search_For_A_Setting.png new file mode 100644 index 00000000..21529f7c Binary files /dev/null and b/assets/translation context screenshots/Setting Search Bar/Search_For_A_Setting.png differ diff --git a/assets/translation context screenshots/YouTube API V3 Key/Api_Key.png b/assets/translation context screenshots/YouTube API V3 Key/Api_Key.png new file mode 100644 index 00000000..81bb05fa Binary files /dev/null and b/assets/translation context screenshots/YouTube API V3 Key/Api_Key.png differ diff --git a/assets/translation context screenshots/YouTube API V3 Key/Title.png b/assets/translation context screenshots/YouTube API V3 Key/Title.png new file mode 100644 index 00000000..2fb66286 Binary files /dev/null and b/assets/translation context screenshots/YouTube API V3 Key/Title.png differ diff --git a/assets/translation context screenshots/YouTube API V3 Key/getApiKeyLinkText.png b/assets/translation context screenshots/YouTube API V3 Key/getApiKeyLinkText.png new file mode 100644 index 00000000..50e0817a Binary files /dev/null and b/assets/translation context screenshots/YouTube API V3 Key/getApiKeyLinkText.png differ diff --git a/assets/translation context screenshots/YouTube Page/Buttons/Copy_Video_URL_With_Timestamp.png b/assets/translation context screenshots/YouTube Page/Buttons/Copy_Video_URL_With_Timestamp.png new file mode 100644 index 00000000..48087feb Binary files /dev/null and b/assets/translation context screenshots/YouTube Page/Buttons/Copy_Video_URL_With_Timestamp.png differ diff --git a/assets/translation context screenshots/YouTube Page/Buttons/Fast_Forward_By_Time.png b/assets/translation context screenshots/YouTube Page/Buttons/Fast_Forward_By_Time.png new file mode 100644 index 00000000..35c82780 Binary files /dev/null and b/assets/translation context screenshots/YouTube Page/Buttons/Fast_Forward_By_Time.png differ diff --git a/assets/translation context screenshots/YouTube Page/Buttons/Rewind_By_Time.png b/assets/translation context screenshots/YouTube Page/Buttons/Rewind_By_Time.png new file mode 100644 index 00000000..8d917723 Binary files /dev/null and b/assets/translation context screenshots/YouTube Page/Buttons/Rewind_By_Time.png differ diff --git a/assets/translation context screenshots/YouTube Page/Buttons/Video_URL_Copied.png b/assets/translation context screenshots/YouTube Page/Buttons/Video_URL_Copied.png new file mode 100644 index 00000000..556e573d Binary files /dev/null and b/assets/translation context screenshots/YouTube Page/Buttons/Video_URL_Copied.png differ diff --git a/assets/translation context screenshots/YouTube Page/Playlist Length/Total_Time_May_Not_Be_Accurate.png b/assets/translation context screenshots/YouTube Page/Playlist Length/Total_Time_May_Not_Be_Accurate.png new file mode 100644 index 00000000..cc4de2f3 Binary files /dev/null and b/assets/translation context screenshots/YouTube Page/Playlist Length/Total_Time_May_Not_Be_Accurate.png differ diff --git a/package-lock.json b/package-lock.json index 5f624e43..04a17073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "youtube-enhancer", - "version": "1.25.0", + "version": "1.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtube-enhancer", - "version": "1.25.0", + "version": "1.26.0", "license": "MIT", "dependencies": { "@formkit/auto-animate": "^0.8.1", @@ -14,7 +14,7 @@ "@tanstack/react-query": "^5.18.0", "dotenv": "^16.3.1", "i18next": "^23.7.3", - "monaco-editor": "^0.49.0", + "monaco-editor": "^0.51.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", @@ -23,36 +23,36 @@ "tailwindcss-multi": "^0.4.0", "use-debouncy": "^5.0.1", "vite-plugin-css-injected-by-js": "^3.3.0", - "webextension-polyfill": "^0.11.0" + "webextension-polyfill": "^0.12.0" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@thedutchcoder/postcss-rem-to-px": "^0.0.2", - "@total-typescript/ts-reset": "^0.5.1", + "@total-typescript/ts-reset": "^0.6.0", "@types/archiver": "^6.0.1", - "@types/chrome": "^0.0.268", - "@types/node": "^20.9.0", + "@types/chrome": "^0.0.273", + "@types/node": "^22.1.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "@types/webextension-polyfill": "^0.10.6", + "@types/webextension-polyfill": "^0.12.1", "@types/youtube-player": "^5.5.10", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.1", "@vitejs/plugin-react-swc": "^3.4.1", "archiver": "^7.0.1", "autoprefixer": "^10.4.16", "clsx": "^2.0.0", "concurrently": "^8.2.2", - "eslint": "^8.53.0", + "eslint": "^9.9.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-no-secrets": "^1.0.2", - "eslint-plugin-perfectionist": "^2.3.0", + "eslint-plugin-perfectionist": "^3.0.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-promise": "^7.0.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.13.0", @@ -66,8 +66,8 @@ "tailwindcss": "^3.3.6", "ts-json-as-const": "^1.0.7", "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "vite": "^5.0.12", + "typescript": "5.5", + "vite": "^5.4.8", "zod": "^3.22.4", "zod-error": "^1.5.0" }, @@ -254,14 +254,6 @@ "integrity": "sha512-QDlMJqwcE4eVCvxxQXp8nh7Nw9m5VQHPCAiyTD+W86Tl89VGhVJRb//RJRZKpn5A/Bq3EQNYDYlepurQ805MOQ==", "dev": true }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -623,24 +615,60 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -648,7 +676,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -677,55 +705,40 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@formkit/auto-animate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.2.tgz", - "integrity": "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "levn": "^0.4.1" }, "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formkit/auto-animate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.2.tgz", + "integrity": "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==" + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -739,11 +752,18 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -793,6 +813,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -806,6 +827,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -814,6 +836,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -822,6 +845,8 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -830,12 +855,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1108,9 +1135,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", - "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -1120,9 +1147,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", - "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -1132,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", - "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -1144,9 +1171,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", - "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -1156,9 +1183,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", - "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", "cpu": [ "arm" ], @@ -1168,9 +1195,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", - "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -1180,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", - "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -1192,9 +1219,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", - "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -1204,9 +1231,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", - "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "cpu": [ "ppc64" ], @@ -1216,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", - "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -1228,9 +1255,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", - "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "cpu": [ "s390x" ], @@ -1240,9 +1267,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -1252,9 +1279,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", - "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -1264,9 +1291,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", - "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -1276,9 +1303,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", - "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -1288,9 +1315,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", - "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -1299,6 +1326,12 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -1820,14 +1853,14 @@ } }, "node_modules/@swc/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.0.tgz", - "integrity": "sha512-fjADAC5gOOX54Rpcr1lF9DHLD+nPD5H/zXLtEgK2Ez3esmogT+LfHzCZtUxqetjvaMChKhQ0Pp0ZB6Hpz/tCbw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" }, "engines": { "node": ">=10" @@ -1837,19 +1870,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.5.0", - "@swc/core-darwin-x64": "1.5.0", - "@swc/core-linux-arm-gnueabihf": "1.5.0", - "@swc/core-linux-arm64-gnu": "1.5.0", - "@swc/core-linux-arm64-musl": "1.5.0", - "@swc/core-linux-x64-gnu": "1.5.0", - "@swc/core-linux-x64-musl": "1.5.0", - "@swc/core-win32-arm64-msvc": "1.5.0", - "@swc/core-win32-ia32-msvc": "1.5.0", - "@swc/core-win32-x64-msvc": "1.5.0" + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1857,10 +1890,154 @@ } } }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.0.tgz", - "integrity": "sha512-ZairtCwJsaxnUH85DcYCyGpNb9bUoIm9QXYW+VaEoXwbcB95dTIiJwN0aRxPT8B0B2MNw/CXLqjoPo6sDwz5iw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", "cpu": [ "x64" ], @@ -1880,29 +2057,29 @@ "dev": true }, "node_modules/@swc/types": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", - "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", "dev": true, "dependencies": { "@swc/counter": "^0.1.3" } }, "node_modules/@tanstack/query-core": { - "version": "5.34.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.34.2.tgz", - "integrity": "sha512-FM1UXldYcoMiWmVbpewV14EVpnr/ETBbdF84tmCktx29e2bXBFhGtAfyDHUD4X1FehaV5tC9GkXZUMZRQV3lcA==", + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", + "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.34.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.34.2.tgz", - "integrity": "sha512-5L9W9XQ/lRcyhlVN0xrOkPZE9PJxZWw7BdJR244j3G7sxMfSreZWvxx13DJt08M5DWn9B5VstpKgqpIIQOIJKA==", + "version": "5.51.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz", + "integrity": "sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A==", "dependencies": { - "@tanstack/query-core": "5.34.2" + "@tanstack/query-core": "5.51.21" }, "funding": { "type": "github", @@ -1925,9 +2102,9 @@ } }, "node_modules/@total-typescript/ts-reset": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz", - "integrity": "sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz", + "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==", "dev": true }, "node_modules/@tsconfig/node10": { @@ -1964,9 +2141,9 @@ } }, "node_modules/@types/chrome": { - "version": "0.0.268", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", - "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "version": "0.0.273", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.273.tgz", + "integrity": "sha512-6Wp4GO07GLvti13Rf/RpYG+0COSJDOLE4iq3g1+whn1SNGUVnv6vbXqSa/WFbuVpvN1lcBLiZ40+gSeWmKb+eA==", "dev": true, "dependencies": { "@types/filesystem": "*", @@ -1977,24 +2154,18 @@ "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/filesystem": { "version": "0.0.36", @@ -2020,7 +2191,10 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2029,11 +2203,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", - "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "devOptional": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -2049,9 +2224,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -2083,9 +2258,9 @@ "dev": true }, "node_modules/@types/webextension-polyfill": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.10.7.tgz", - "integrity": "sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.12.1.tgz", + "integrity": "sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w==", "dev": true }, "node_modules/@types/youtube-player": { @@ -2095,31 +2270,31 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz", - "integrity": "sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/type-utils": "7.13.0", - "@typescript-eslint/utils": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2128,16 +2303,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", - "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2145,12 +2320,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2158,16 +2333,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2175,26 +2350,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", - "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz", + "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.9.0", - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/typescript-estree": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2203,16 +2378,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", - "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", + "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0" + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2220,26 +2395,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz", - "integrity": "sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.13.0", - "@typescript-eslint/utils": "7.13.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -2247,12 +2419,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2260,22 +2432,22 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", - "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2288,16 +2460,16 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2305,12 +2477,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", - "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2318,22 +2490,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", - "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", - "@typescript-eslint/visitor-keys": "7.9.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2346,38 +2518,38 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz", - "integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.13.0", - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/typescript-estree": "7.13.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz", - "integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2385,12 +2557,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2398,22 +2570,22 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz", - "integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", - "@typescript-eslint/visitor-keys": "7.13.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2426,16 +2598,16 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz", - "integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.13.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2443,222 +2615,34 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", - "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/types": "8.7.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz", - "integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.1.tgz", + "integrity": "sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==", "dev": true, "dependencies": { - "@swc/core": "^1.3.107" + "@swc/core": "^1.7.26" }, "peerDependencies": { "vite": "^4 || ^5" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2678,9 +2662,10 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -2688,14 +2673,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2743,6 +2720,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2754,14 +2732,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-escapes": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", @@ -2872,12 +2842,12 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, "dependencies": { - "dequal": "^2.0.3" + "deep-equal": "^2.0.5" } }, "node_modules/array-buffer-byte-length": { @@ -2922,15 +2892,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -3007,18 +2968,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", @@ -3070,9 +3019,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -3089,11 +3038,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -3122,21 +3071,21 @@ } }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/b4a": { @@ -3224,9 +3173,10 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3242,10 +3192,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3290,7 +3240,9 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "optional": true, + "peer": true }, "node_modules/call-bind": { "version": "1.0.7", @@ -3330,9 +3282,10 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001612", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", - "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3421,14 +3374,6 @@ "node": ">= 6" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "engines": { - "node": ">=6.0" - } - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -3620,19 +3565,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3660,11 +3592,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3877,6 +3804,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4021,6 +3949,38 @@ } } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4070,15 +4030,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4112,18 +4063,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -4199,9 +4138,10 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.748", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.748.tgz", - "integrity": "sha512-VWqjOlPZn70UZ8FTKUOkUvBLeTQ0xpty66qV0yJcAGY2/CthI4xyW9aEozRVtuwv3Kpf5xTesmJUcPwuJmgP4A==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "dev": true }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -4215,18 +4155,6 @@ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "dev": true }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/env-ci": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", @@ -4383,17 +4311,6 @@ "node": ">=6" } }, - "node_modules/envinfo": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.12.0.tgz", - "integrity": "sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg==", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4484,6 +4401,26 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", @@ -4509,11 +4446,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" - }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -4607,6 +4539,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, "engines": { "node": ">=6" } @@ -4624,43 +4557,39 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", @@ -4672,10 +4601,18 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -4711,9 +4648,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -4737,34 +4674,36 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { @@ -4820,33 +4759,33 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", "dev": true, "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" }, "engines": { "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { @@ -4885,20 +4824,24 @@ } }, "node_modules/eslint-plugin-perfectionist": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.10.0.tgz", - "integrity": "sha512-P+tdrkHeMWBc55+DZsoDOAftV1WCsEoHaKm6JC7zajFus/syfT4vUPBFb3atGFSuyaVnGQGHlcKpP9X3Q0gH/w==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-3.6.0.tgz", + "integrity": "sha512-sA6ljy6dL/9cM5ruZ/pMqRVt0FQ4Z7mbQWlBYpyX9941LVfm65d2jl2k1ZbWD3ud9Wm+/NKgOvRnAatsKhMJbA==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^6.13.0 || ^7.0.0", - "minimatch": "^9.0.3", + "@typescript-eslint/types": "^8.5.0", + "@typescript-eslint/utils": "^8.5.0", + "minimatch": "^9.0.5", "natural-compare-lite": "^1.4.0" }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, "peerDependencies": { - "astro-eslint-parser": "^0.16.0", + "astro-eslint-parser": "^1.0.2", "eslint": ">=8.0.0", "svelte": ">=3.0.0", - "svelte-eslint-parser": "^0.33.0", + "svelte-eslint-parser": "^0.41.1", "vue-eslint-parser": ">=9.0.0" }, "peerDependenciesMeta": { @@ -4916,14 +4859,111 @@ } } }, + "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/scope-manager": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/types": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4947,12 +4987,12 @@ } }, "node_modules/eslint-plugin-promise": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.2.0.tgz", - "integrity": "sha512-QmAqwizauvnKOlifxyDj2ObfULpHQawlg/zQdgEixur9vl0CvZGv/LCJV2rtj3210QCoeGBzVMfMXqGAOr/4fA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.0.0.tgz", + "integrity": "sha512-wb1ECT+b90ndBdAujhIdAU8oQ3Vt5gKqP/t78KOmg0ifynrvc2jGR9f6ndbOVNFpKf6jLUBlBBDF3H3Wk0JICg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4962,35 +5002,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", - "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", "dev": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -5066,32 +5106,32 @@ } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.0.tgz", - "integrity": "sha512-Ofl7tNh57a3W8BKHstKZSkD2gSCEkw54ycwZ958IK9zUR8TiNYdp8b0WGoLWLeyOAbeF1VPVJFBnlkJeIM2kVg==", + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.4.tgz", + "integrity": "sha512-gJAEHmCq2XFfUP/+vwEfEJ9igrPeZFg+skeMtsxquSQdxba9XRk5bn0Bp9jxG1VV9/wwPKi1g3ZjItu6MIjhNg==", "dev": true, "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.12.0" }, "peerDependencies": { "tailwindcss": "^3.4.0" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5119,6 +5159,18 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5132,17 +5184,29 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5164,6 +5228,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -5175,6 +5240,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -5201,6 +5267,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, "engines": { "node": ">=0.8.x" } @@ -5231,7 +5298,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-diff": { "version": "1.3.0", @@ -5276,7 +5344,8 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -5284,14 +5353,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5317,15 +5378,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -5384,26 +5445,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { @@ -5522,12 +5574,6 @@ "node": ">=14.14" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5545,6 +5591,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5714,21 +5761,13 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5749,26 +5788,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5784,7 +5803,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -5826,6 +5846,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -5885,6 +5906,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -5961,9 +5983,9 @@ } }, "node_modules/i18next": { - "version": "23.11.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", - "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "version": "23.15.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.15.1.tgz", + "integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==", "funding": [ { "type": "individual", @@ -6046,24 +6068,6 @@ "node": ">=16.20" } }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -6104,16 +6108,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6140,14 +6134,6 @@ "node": ">= 0.4" } }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/into-stream": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", @@ -6164,6 +6150,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -6254,11 +6256,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6429,17 +6435,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6625,15 +6620,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/issue-parser": { "version": "7.0.0", @@ -6691,19 +6679,6 @@ "node": ">= 0.6.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -6745,12 +6720,14 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -6806,14 +6783,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -6936,14 +6905,6 @@ "node": ">=4" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7092,7 +7053,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7131,25 +7093,6 @@ "node": ">=16" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7160,9 +7103,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -7193,9 +7136,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", - "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==" + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz", + "integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==" }, "node_modules/ms": { "version": "2.1.2", @@ -7246,7 +7189,8 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true }, "node_modules/nerf-dart": { "version": "1.0.0", @@ -7270,14 +7214,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true }, "node_modules/nodemon": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", - "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -10245,6 +10190,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -10318,23 +10279,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -10352,15 +10296,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -10556,23 +10491,16 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -10580,7 +10508,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "1.10.2", @@ -10608,9 +10537,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -10713,73 +10642,6 @@ "node": ">=4" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -10790,9 +10652,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -10809,8 +10671,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -10947,9 +10809,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11030,6 +10892,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -11057,16 +10920,8 @@ "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true }, "node_modules/rc": { "version": "1.2.8", @@ -11125,9 +10980,9 @@ } }, "node_modules/react-icons": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", - "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", "peerDependencies": { "react": "*" } @@ -11312,17 +11167,6 @@ "node": ">=8.10.0" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -11392,6 +11236,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -11404,25 +11249,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -11442,69 +11268,12 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", - "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -11514,37 +11283,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.16.4", - "@rollup/rollup-android-arm64": "4.16.4", - "@rollup/rollup-darwin-arm64": "4.16.4", - "@rollup/rollup-darwin-x64": "4.16.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", - "@rollup/rollup-linux-arm-musleabihf": "4.16.4", - "@rollup/rollup-linux-arm64-gnu": "4.16.4", - "@rollup/rollup-linux-arm64-musl": "4.16.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", - "@rollup/rollup-linux-riscv64-gnu": "4.16.4", - "@rollup/rollup-linux-s390x-gnu": "4.16.4", - "@rollup/rollup-linux-x64-gnu": "4.16.4", - "@rollup/rollup-linux-x64-musl": "4.16.4", - "@rollup/rollup-win32-arm64-msvc": "4.16.4", - "@rollup/rollup-win32-ia32-msvc": "4.16.4", - "@rollup/rollup-win32-x64-msvc": "4.16.4", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", - "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11599,6 +11356,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -11644,27 +11402,10 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semantic-release": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.0.0.tgz", - "integrity": "sha512-v46CRPw+9eI3ZuYGF2oAjqPqsfbnfFTwLBgQsv/lch4goD09ytwOTESMN4QIrx/wPLxUGey60/NMx+ANQtWRsA==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.1.0.tgz", + "integrity": "sha512-FwaE2hKDHQn9G6GA7xmqsc9WnsjaFD/ppLM5PUg56Do9oKSCf+vH6cPeb3hEBV/m06n8Sh9vbVqPjHu/1onzQw==", "dev": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", @@ -11969,14 +11710,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12009,21 +11742,11 @@ "node": ">= 0.4" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -12035,6 +11758,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -12193,27 +11917,19 @@ "node": ">=8" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -12222,6 +11938,8 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "optional": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12276,6 +11994,18 @@ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-combiner2": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", @@ -12409,6 +12139,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/string.prototype.includes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", + "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -12435,6 +12175,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -12607,6 +12357,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12646,6 +12397,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -12654,9 +12406,9 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -12670,22 +12422,19 @@ } }, "node_modules/tailwind-merge": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", - "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz", + "integrity": "sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.24.1" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -12727,14 +12476,19 @@ "@types/node": "^20.4.1" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" + "node_modules/tailwindcss-multi/node_modules/@types/node": { + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "dependencies": { + "undici-types": "~5.26.4" } }, + "node_modules/tailwindcss-multi/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", @@ -12801,6 +12555,8 @@ "version": "5.30.4", "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz", "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -12814,43 +12570,12 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true, + "peer": true }, "node_modules/text-table": { "version": "0.2.0", @@ -13117,18 +12842,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -13223,10 +12936,11 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13270,9 +12984,10 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "devOptional": true }, "node_modules/unicode-emoji-modifier-base": { "version": "1.0.0", @@ -13326,9 +13041,10 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -13344,8 +13060,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -13358,6 +13074,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -13402,13 +13119,13 @@ } }, "node_modules/vite": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -13427,6 +13144,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -13444,6 +13162,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -13456,177 +13177,23 @@ } }, "node_modules/vite-plugin-css-injected-by-js": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.1.tgz", - "integrity": "sha512-9ioqwDuEBxW55gNoWFEDhfLTrVKXEEZgl5adhWmmqa88EQGKfTmexy4v1Rh0pAS6RhKQs2bUYQArprB32JpUZQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz", + "integrity": "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==", "peerDependencies": { "vite": ">2.0.0-0" } }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webextension-polyfill": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.11.0.tgz", - "integrity": "sha512-YUBSKQA0iCx2YtM75VFgvvcx1hLKaGGiph6a6UaUdSgk32VT9SzrcDAKBjeGHXoAZTnNBqS5skA4VfoKMXhEBA==", - "dependencies": { - "webpack": "^5.91.0", - "webpack-cli": "^5.1.4" - } - }, - "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz", + "integrity": "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -13716,11 +13283,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -13821,12 +13383,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 443c1326..77ce7a3e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@tanstack/react-query": "^5.18.0", "dotenv": "^16.3.1", "i18next": "^23.7.3", - "monaco-editor": "^0.49.0", + "monaco-editor": "^0.51.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", @@ -38,36 +38,36 @@ "tailwindcss-multi": "^0.4.0", "use-debouncy": "^5.0.1", "vite-plugin-css-injected-by-js": "^3.3.0", - "webextension-polyfill": "^0.11.0" + "webextension-polyfill": "^0.12.0" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@thedutchcoder/postcss-rem-to-px": "^0.0.2", - "@total-typescript/ts-reset": "^0.5.1", + "@total-typescript/ts-reset": "^0.6.0", "@types/archiver": "^6.0.1", - "@types/chrome": "^0.0.268", - "@types/node": "^20.9.0", + "@types/chrome": "^0.0.273", + "@types/node": "^22.1.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", - "@types/webextension-polyfill": "^0.10.6", + "@types/webextension-polyfill": "^0.12.1", "@types/youtube-player": "^5.5.10", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.7.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.1", "@vitejs/plugin-react-swc": "^3.4.1", "archiver": "^7.0.1", "autoprefixer": "^10.4.16", "clsx": "^2.0.0", "concurrently": "^8.2.2", - "eslint": "^8.53.0", + "eslint": "^9.9.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-no-secrets": "^1.0.2", - "eslint-plugin-perfectionist": "^2.3.0", + "eslint-plugin-perfectionist": "^3.0.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-promise": "^7.0.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.13.0", @@ -81,9 +81,9 @@ "tailwindcss": "^3.3.6", "ts-json-as-const": "^1.0.7", "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "vite": "^5.0.12", + "typescript": "5.5", + "vite": "^5.4.8", "zod": "^3.22.4", "zod-error": "^1.5.0" } -} \ No newline at end of file +} diff --git a/public/locales/ca-ES.json b/public/locales/ca-ES.json index 9e21dc4e..7de4ef07 100644 --- a/public/locales/ca-ES.json +++ b/public/locales/ca-ES.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/cs-CZ.json b/public/locales/cs-CZ.json index 6ca54a7e..6aea0bc5 100644 --- a/public/locales/cs-CZ.json +++ b/public/locales/cs-CZ.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/de-DE.json b/public/locales/de-DE.json index d181e03d..a878a6f2 100644 --- a/public/locales/de-DE.json +++ b/public/locales/de-DE.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatischer Theatermodus", "title": "Automatisch den Theatermodus aktivieren, wenn ein Video geladen wird" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Wiedergabegeschwindigkeit Einstellungen" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot-Taste", @@ -502,6 +547,14 @@ }, "title": "´Lautstärke-Boost Einstellungen" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/en-GB.json b/public/locales/en-GB.json index a4867adc..d3a27f02 100644 --- a/public/locales/en-GB.json +++ b/public/locales/en-GB.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theatre mode", "title": "Automatically enables theatre mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 62fe0511..f1274b2a 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/en-US.json.d.ts b/public/locales/en-US.json.d.ts index 24cf94ef..27e9c06f 100644 --- a/public/locales/en-US.json.d.ts +++ b/public/locales/en-US.json.d.ts @@ -8,6 +8,7 @@ interface EnUS { pages: { content: { features: { + copyTimestampUrlButton: { button: { copied: "Copied!"; label: "Copy video URL with timestamp" } }; featureMenu: { button: { label: "Feature menu" } }; forwardRewindButtons: { buttons: { @@ -32,6 +33,9 @@ interface EnUS { decreaseLimit: "Can't decrease further ({{SPEED}})"; increaseLimit: "Can't increase further ({{SPEED}})"; }; + playlistLength: { + title: "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length."; + }; screenshotButton: { button: { label: "Screenshot" }; copiedToClipboard: "Screenshot copied to clipboard"; @@ -98,6 +102,7 @@ interface EnUS { buttonPlacement: { select: { buttonNames: { + copyTimestampUrlButton: "Copy video URL with timestamp button"; decreasePlaybackSpeedButton: "Decrease Speed button"; forwardButton: "Fast Forward button"; hideEndScreenCardsButton: "Hide end screen cards button"; @@ -178,6 +183,14 @@ interface EnUS { label: "Automatic theater mode"; title: "Automatically enables theater mode when you load a video"; }; + automaticallyDisableClosedCaptions: { + label: "Automatically disable closed captions"; + title: "Automatically disables closed captions when you load a video"; + }; + copyTimestampUrlButton: { + label: "Copy video URL with timestamp button"; + title: "Copies video URL with timestamp (?t=123)"; + }; hideEndScreenCards: { label: "Hide end screen cards"; title: "Hides the cards at the end of the video"; @@ -187,6 +200,10 @@ interface EnUS { title: "Adds a button to show/hide the cards at the end of the video"; }; hideLiveStreamChat: { label: "Hide live stream chat"; title: "Hides the live stream chat" }; + hideOfficialArtistVideosFromHomePage: { + label: "Hide Official Artist Videos"; + title: "Hide Official Artist Videos from Home Page"; + }; hidePaidPromotionBanner: { label: "Hide paid promotion banner"; title: "Hides the banner that appears when you watch a video that has a paid promotion"; @@ -299,6 +316,26 @@ interface EnUS { select: { label: "Player speed"; title: "The speed to set the video to" }; title: "Playback speed settings"; }; + playlistLength: { + enable: { + label: "Display playlist length information"; + title: "Shows the total length of the playlist, how much has been watched, and how much remains."; + }; + title: "Playlist length settings"; + wayToGetLength: { + select: { + label: "Method to get playlist length"; + title: "The way to get playlist length information (API method will fallback to HTML if an error occurs)"; + }; + }; + wayToGetWatchTime: { + select: { + label: "Method to get watched time"; + options: { duration: "Video Length"; youtube: "Video Watch Time" }; + title: "The way to get the amount of time watched (Video Length type only used on watch page)"; + }; + }; + }; screenshotButton: { enable: { label: "Screenshot button"; @@ -371,6 +408,11 @@ interface EnUS { }; title: "Volume boost settings"; }; + youtubeDataApiV3Key: { + getApiKeyLinkText: "You can get one from here"; + input: { label: "API Key"; title: "Enter your Youtube Data API V3 key." }; + title: "YouTube API V3 Key"; + }; youtubeDeepDark: { author: "Author"; "co-authors": "Co-authors"; diff --git a/public/locales/es-ES.json b/public/locales/es-ES.json index fb8da3b2..d0ff9eca 100644 --- a/public/locales/es-ES.json +++ b/public/locales/es-ES.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Modo cine automático", "title": "Activar automáticamente el modo cine al cargar un vídeo" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Ajustes de velocidad de reproducción" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Botón de captura de pantalla", @@ -502,6 +547,14 @@ }, "title": "Ajustes de potenciado de volumen" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/fa-IR.json b/public/locales/fa-IR.json index c5f96127..45926df7 100644 --- a/public/locales/fa-IR.json +++ b/public/locales/fa-IR.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 2a82e879..88bf70f5 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Mode cinéma automatique", "title": "Activer automatiquement le mode cinéma lorsque vous chargez une vidéo" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Réglages de vitesse de lecture" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Bouton de capture d'écran", @@ -502,6 +547,14 @@ }, "title": "Réglages de du gain du volume" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/he-IL.json b/public/locales/he-IL.json index c55257dd..677d12c4 100644 --- a/public/locales/he-IL.json +++ b/public/locales/he-IL.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/hi-IN.json b/public/locales/hi-IN.json index 9efb0103..07ef1667 100644 --- a/public/locales/hi-IN.json +++ b/public/locales/hi-IN.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/it-IT.json b/public/locales/it-IT.json index 7a419a6d..5e2cdf27 100644 --- a/public/locales/it-IT.json +++ b/public/locales/it-IT.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copiato!", + "label": "Copia URL video con temporizzazione" + } + }, "featureMenu": { "button": { "label": "Menu' funzioni" @@ -67,6 +73,9 @@ "decreaseLimit": "Impossibile diminuire ulteriormente ({{SPEED}})", "increaseLimit": "Impossibile aumentare ulteriormente ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Schermata" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Pulsante \"Copia URL video con temporizzazione\"", "decreasePlaybackSpeedButton": "Pulsante Riduci velocità", "forwardButton": "Pulsante Avanti veloce", "hideEndScreenCardsButton": "Pulsante Nascondi schede finali", @@ -254,6 +264,14 @@ "label": "Modalità teatro automatica", "title": "Attiva automaticamente la modalità teatro quando carichi un video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Pulsante \"Copia URL video con temporizzazione\"", + "title": "Copia l'URL del video con la temporizzazione (?t=123)" + }, "hideEndScreenCards": { "label": "Nascondi le schede finali", "title": "Nasconde le schede alla fine del video" @@ -266,6 +284,10 @@ "label": "Nascondi chat live stream", "title": "Nasconde la chat live stream" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Nascondi banner promozioni a pagamento", "title": "Nasconde il banner con promozioni a pagamento che appare quando si guarda un video" @@ -398,6 +420,29 @@ }, "title": "Impostazioni velocità riproduzione" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Pulsante Schermata", @@ -502,6 +547,14 @@ }, "title": "Impostazioni aumento volume" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Autore", "co-authors": "Co-autori", diff --git a/public/locales/ja-JP.json b/public/locales/ja-JP.json index 0caded4b..11d7a147 100644 --- a/public/locales/ja-JP.json +++ b/public/locales/ja-JP.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "コピー完了!", + "label": "タイムスタンプ付きの動画URLをコピーする" + } + }, "featureMenu": { "button": { "label": "フィーチャー・メニュー" @@ -67,6 +73,9 @@ "decreaseLimit": "再生速度を下げるはできまっせん ({{SPEED}})", "increaseLimit": "再生速度を上げるはできまっせん ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "スクリーンショット" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "タイムスタンプ付きの動画URLをコピーのボタン", "decreasePlaybackSpeedButton": "速度を下げるボタン", "forwardButton": "早送ボタン", "hideEndScreenCardsButton": "終了画面のカードボタンを非表示にする", @@ -254,6 +264,14 @@ "label": "自動シアターモード", "title": "動画の読み込み時に自動的にシアターモードを有効にする" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "タイムスタンプ付きの動画URLをコピーのボタン", + "title": "タイムスタンプ付きの動画URLをコピーする (?t=123)" + }, "hideEndScreenCards": { "label": "終了画面のカードを非表示にする", "title": "動画の終了で画面カードを非表示にする" @@ -266,6 +284,10 @@ "label": "配信チャットを隠す", "title": "配信チャットを隠する" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "有料プロモーションバナーを隠す", "title": "有料プロモーションのある動画を視聴したときに表示されるバナーを隠すします" @@ -398,6 +420,29 @@ }, "title": "再生速度設定" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "スクリーンショットボタン", @@ -502,6 +547,14 @@ }, "title": "音量ブーストの設定" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "作家", "co-authors": "共同作家", diff --git a/public/locales/ko-KR.json b/public/locales/ko-KR.json index 18683be7..9f37c936 100644 --- a/public/locales/ko-KR.json +++ b/public/locales/ko-KR.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "복사되었습니다.", + "label": "현재 시간 주소 복사" + } + }, "featureMenu": { "button": { "label": "추가 기능" @@ -16,10 +22,10 @@ "forwardRewindButtons": { "buttons": { "forwardButton": { - "label": "Fast forward by {{TIME}}" + "label": "{{TIME}} 초 빨리 감기" }, "rewindButton": { - "label": "Rewind by {{TIME}}" + "label": "{{TIME}} 초 되감기" } } }, @@ -67,6 +73,9 @@ "decreaseLimit": "({{SPEED}}) 에서 더 줄일 수 없습니다.", "increaseLimit": "({{SPEED}}) 에서 더 늘릴 수 없습니다." }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "스크린샷" @@ -156,14 +165,15 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "현재 시간 주소 복사 버튼", "decreasePlaybackSpeedButton": "속도 조절 버튼", - "forwardButton": "Fast Forward button", + "forwardButton": "빨리 감기 버튼", "hideEndScreenCardsButton": "최종 화면 숨기기", "increasePlaybackSpeedButton": "속도 조절 버튼", "loopButton": "연속 재생 버튼", "maximizePlayerButton": "화면 크기에 맞추기 버튼", "openTranscriptButton": "스크립트 보기 버튼", - "rewindButton": "Rewind button", + "rewindButton": "되감기 버튼", "screenshotButton": "스크린샷 버튼", "volumeBoostButton": "볼륨 부스트 버튼" }, @@ -216,14 +226,14 @@ }, "forwardRewindButtons": { "enable": { - "label": "Enable forward/rewind buttons", - "title": "Adds forward and rewind buttons to the video player" + "label": "되감기/빨리 감기 버튼 사용", + "title": "플레이어에 되감기와 빨리 감기 버튼을 추가합니다." }, "time": { - "label": "Forward/rewind time", - "title": "The amount of time to forward/rewind the video by" + "label": "되감기/빨리 감기 시간", + "title": "되감기/빨리 감기 시간" }, - "title": "Forward/rewind button settings" + "title": "되감기/빨리 감기 버튼 설정" }, "importExportSettings": { "exportButton": { @@ -254,6 +264,14 @@ "label": "자동 극장 모드", "title": "동영상 재생 시 자동으로 극장 모드가 활성화 됩니다." }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "현재 시간 주소 복사 버튼", + "title": "현재 시간 주소 복사 (?t=123)" + }, "hideEndScreenCards": { "label": "최종 화면 숨기기", "title": "동영상 마지막에 등장하는 추천 동영상 카드들을 숨깁니다." @@ -266,9 +284,13 @@ "label": "실시간 채팅 숨기기", "title": "실시간 채팅 레이아웃을 숨깁니다." }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { - "label": "Hide paid promotion banner", - "title": "Hides the banner that appears when you watch a video that has a paid promotion" + "label": "유료 광고 배너 숨기기", + "title": "유료 광고가 있는 동영상을 볼 때 나타나는 배너를 숨깁니다." }, "hideScrollbar": { "label": "스크롤바 숨기기", @@ -398,6 +420,29 @@ }, "title": "재생 속도 설정" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "스크린샷 버튼", @@ -462,7 +507,7 @@ "title": "스크롤 휠로 볼륨 조절 설정" }, "settingSearch": { - "placeholder": "Search for a setting" + "placeholder": "설정 검색" }, "videoHistory": { "enable": { @@ -502,6 +547,14 @@ }, "title": "볼륨 부스트 설정" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "개발자", "co-authors": "도와주신분들", diff --git a/public/locales/pl-PL.json b/public/locales/pl-PL.json index 2b9cf421..0687e905 100644 --- a/public/locales/pl-PL.json +++ b/public/locales/pl-PL.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 3996d4d5..b5307f99 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Modo teatro automático", "title": "Ativar automaticamente o modo teatro quando você carregar um vídeo" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Configurações de velocidade de reprodução" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Botão de captura de tela", @@ -502,6 +547,14 @@ }, "title": "Configurações de impulso de volume" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/ru-RU.json b/public/locales/ru-RU.json index 178b30fd..4b3b85af 100644 --- a/public/locales/ru-RU.json +++ b/public/locales/ru-RU.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Скопировано!", + "label": "Копировать URL видео с таймкодом" + } + }, "featureMenu": { "button": { "label": "Меню функций" @@ -67,6 +73,9 @@ "decreaseLimit": "Невозможно уменьшить ({{SPEED}})", "increaseLimit": "Невозможно увеличить ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Скриншот" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Кнопка «Копировать URL видео с таймкодом»", "decreasePlaybackSpeedButton": "Кнопка уменьшения скорости видео", "forwardButton": "Кнопка перемотки вперёд", "hideEndScreenCardsButton": "Кнопка «Скрыть заставки следующих видео»", @@ -254,6 +264,14 @@ "label": "Включить автоматический режим кинотеатра", "title": "Автоматически включать режим кинотеатра при загрузке видео" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Кнопка «Копировать URL видео с таймкодом»", + "title": "Копирует ссылку видео с меткой времени (?t=123)" + }, "hideEndScreenCards": { "label": "Скрыть заставки следующих видео", "title": "Скрывает заставки следующих (предлагаемых) видео в конце ролика" @@ -266,6 +284,10 @@ "label": "Спрятать чат прямого эфира", "title": "Прячет чат прямого эфира (стрима)" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Скрыть \"Содержит прямую рекламу\"", "title": "Скрывает баннер, отображающийся при просмотре видео, которое имеет платную рекламу" @@ -398,6 +420,29 @@ }, "title": "Настройки скорости воспроизведения" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Включить кнопку «Скриншот»", @@ -502,6 +547,14 @@ }, "title": "Настройки усиления громкости" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Автор", "co-authors": "Соавторы", diff --git a/public/locales/sv-SE.json b/public/locales/sv-SE.json index 75ba0bcc..6a4132ce 100644 --- a/public/locales/sv-SE.json +++ b/public/locales/sv-SE.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Kopierad!", + "label": "Kopiera videons webbadress med tidsstämpel" + } + }, "featureMenu": { "button": { "label": "Funktionsmeny" @@ -16,10 +22,10 @@ "forwardRewindButtons": { "buttons": { "forwardButton": { - "label": "Fast forward by {{TIME}}" + "label": "Snabbspola med {{TIME}}" }, "rewindButton": { - "label": "Rewind by {{TIME}}" + "label": "Återspola med {{TIME}}" } } }, @@ -67,6 +73,9 @@ "decreaseLimit": "Det går inte att sänka mer ({{SPEED}})", "increaseLimit": "Det går inte att höja mer ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Skärmbild" @@ -156,14 +165,15 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Kopiera videons webbadress med tidsstämpelknappen", "decreasePlaybackSpeedButton": "Knapp för att mindska hastigheten", - "forwardButton": "Fast Forward button", + "forwardButton": "Snabbspolningsknapp", "hideEndScreenCardsButton": "Dölj knappen för slutskärmskort", "increasePlaybackSpeedButton": "Knapp för att öka hastigheten", "loopButton": "Slingknapp", "maximizePlayerButton": "Maximeraknapp", "openTranscriptButton": "Transkriptknapp", - "rewindButton": "Rewind button", + "rewindButton": "Återspolningsknapp", "screenshotButton": "Skärmbildsknapp", "volumeBoostButton": "Volymökningsknapp" }, @@ -216,14 +226,14 @@ }, "forwardRewindButtons": { "enable": { - "label": "Enable forward/rewind buttons", - "title": "Adds forward and rewind buttons to the video player" + "label": "Aktivera spola framåt-/återspolningsknappar", + "title": "Lägger till spola framåt- och återspolningsknappar till videospelaren" }, "time": { - "label": "Forward/rewind time", - "title": "The amount of time to forward/rewind the video by" + "label": "Spola framåt-/återspolningstid", + "title": "Tiden som videon ska spolas framåt/återspolas med" }, - "title": "Forward/rewind button settings" + "title": "Inställningar för knapparna spola framåt/återspola" }, "importExportSettings": { "exportButton": { @@ -254,6 +264,14 @@ "label": "Automatiskt teaterläge", "title": "Aktiverar automatiskt teaterläge när du startar en video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Kopiera videons webbadress med tidsstämpelknappen", + "title": "Kopiera videons webbadress med tidsstämpel (?t=123)" + }, "hideEndScreenCards": { "label": "Dölj slutskärmskort", "title": "Döljer korten i slutet av videon" @@ -266,9 +284,13 @@ "label": "Dölj direktströmschatt", "title": "Döljer direktströmschatten" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { - "label": "Hide paid promotion banner", - "title": "Hides the banner that appears when you watch a video that has a paid promotion" + "label": "Dölj betald marknadsföringsbanner", + "title": "Döljer bannern som visas när du tittar på en video som har betald marknadsföring" }, "hideScrollbar": { "label": "Dölj rullningslisten", @@ -398,6 +420,29 @@ }, "title": "Inställningar för uppspelningshastighet" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Skärmbildsknapp", @@ -462,7 +507,7 @@ "title": "Rullningshjulets volyminställningar" }, "settingSearch": { - "placeholder": "Search for a setting" + "placeholder": "Sök efter en inställning" }, "videoHistory": { "enable": { @@ -502,6 +547,14 @@ }, "title": "Inställningar för volymökning" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Skapare", "co-authors": "Medskapare", diff --git a/public/locales/tr-TR.json b/public/locales/tr-TR.json index c95471f7..54df26c2 100644 --- a/public/locales/tr-TR.json +++ b/public/locales/tr-TR.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Kopyalandı!", + "label": "Geçerli zamana ait video URL'sini kopyala" + } + }, "featureMenu": { "button": { "label": "Özellik menüsü" @@ -19,7 +25,7 @@ "label": "Fast forward by {{TIME}}" }, "rewindButton": { - "label": "Rewind by {{TIME}}" + "label": "{{TIME}} kadar geri sar" } } }, @@ -67,9 +73,12 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { - "label": "Screenshot" + "label": "Ekran görüntüsü" }, "copiedToClipboard": "Ekran görüntüsü panoya kopyalandı" }, @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Bitiş ekranı kartlarını gizleme düğmesi", @@ -254,6 +264,14 @@ "label": "Otomatik tiyatro modu", "title": "Video yüklediğinizde tiyatro modunu otomatik olarak etkinleştirir" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Bitiş ekranı kartlarını gizle", "title": "Video'nun sonundaki kartları gizler" @@ -266,6 +284,10 @@ "label": "Canlı yayın sohbetini gizle", "title": "Canlı yayın sohbetini gizler" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Oynatma hızı ayarları" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Ekran görüntüsü tuşunu etkinleştir", @@ -502,8 +547,16 @@ }, "title": "Ses güçlendirme ayarları" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { - "author": "Author", + "author": "Yazar", "co-authors": "Co-authors", "colors": { "colorShadow": { diff --git a/public/locales/uk-UA.json b/public/locales/uk-UA.json index d0d9fa15..e9a8f875 100644 --- a/public/locales/uk-UA.json +++ b/public/locales/uk-UA.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Меню функцій" @@ -67,6 +73,9 @@ "decreaseLimit": "Неможливо зменшити далі ({{SPEED}})", "increaseLimit": "Неможливо збільшити далі ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Знімок екрана" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Кнопка зменшення швидкості", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/vi-VN.json b/public/locales/vi-VN.json index 62fe0511..f1274b2a 100644 --- a/public/locales/vi-VN.json +++ b/public/locales/vi-VN.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "Copied!", + "label": "Copy video URL with timestamp" + } + }, "featureMenu": { "button": { "label": "Feature menu" @@ -67,6 +73,9 @@ "decreaseLimit": "Can't decrease further ({{SPEED}})", "increaseLimit": "Can't increase further ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "Screenshot" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "Copy video URL with timestamp button", "decreasePlaybackSpeedButton": "Decrease Speed button", "forwardButton": "Fast Forward button", "hideEndScreenCardsButton": "Hide end screen cards button", @@ -254,6 +264,14 @@ "label": "Automatic theater mode", "title": "Automatically enables theater mode when you load a video" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "Copy video URL with timestamp button", + "title": "Copies video URL with timestamp (?t=123)" + }, "hideEndScreenCards": { "label": "Hide end screen cards", "title": "Hides the cards at the end of the video" @@ -266,6 +284,10 @@ "label": "Hide live stream chat", "title": "Hides the live stream chat" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "Hide paid promotion banner", "title": "Hides the banner that appears when you watch a video that has a paid promotion" @@ -398,6 +420,29 @@ }, "title": "Playback speed settings" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "Screenshot button", @@ -502,6 +547,14 @@ }, "title": "Volume boost settings" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "Author", "co-authors": "Co-authors", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index b39b0b07..758489e3 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "复制成功!", + "label": "复制带有时间戳的视频 URL" + } + }, "featureMenu": { "button": { "label": "功能菜单" @@ -67,6 +73,9 @@ "decreaseLimit": "不能继续降低({{SPEED}})", "increaseLimit": "不能继续增加({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "屏幕截图" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "复制带有时间戳的视频 URL 按钮", "decreasePlaybackSpeedButton": "降低速度按钮", "forwardButton": "快进按钮", "hideEndScreenCardsButton": "隐藏结束界面卡片按钮", @@ -254,6 +264,14 @@ "label": "自动启用剧场模式", "title": "当视频打开时,自动启用剧场模式" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "复制带有时间戳的视频 URL 按钮", + "title": "复制带有时间戳的视频 URL (?t=123)" + }, "hideEndScreenCards": { "label": "隐藏结束界面卡片", "title": "隐藏视频末尾的卡片" @@ -266,6 +284,10 @@ "label": "隐藏直播聊天", "title": "隐藏直播聊天" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "隐藏付费推广横幅", "title": "隐藏当您观看带有付费促销的视频时出现的横幅广告" @@ -398,6 +420,29 @@ }, "title": "播放速度设置" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "屏幕截图按钮", @@ -502,6 +547,14 @@ }, "title": "音量增强设置" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "作者", "co-authors": "合作作者", diff --git a/public/locales/zh-TW.json b/public/locales/zh-TW.json index 920b35bd..005a7f10 100644 --- a/public/locales/zh-TW.json +++ b/public/locales/zh-TW.json @@ -8,6 +8,12 @@ "pages": { "content": { "features": { + "copyTimestampUrlButton": { + "button": { + "copied": "已複製!", + "label": "複製目前播放時間的影片網址" + } + }, "featureMenu": { "button": { "label": "功能選單" @@ -67,6 +73,9 @@ "decreaseLimit": "無法再減速 ({{SPEED}})", "increaseLimit": "無法再加速 ({{SPEED}})" }, + "playlistLength": { + "title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length." + }, "screenshotButton": { "button": { "label": "螢幕截圖" @@ -156,6 +165,7 @@ "buttonPlacement": { "select": { "buttonNames": { + "copyTimestampUrlButton": "複製目前播放時間的影片網址按鈕", "decreasePlaybackSpeedButton": "減慢速度按鈕", "forwardButton": "快轉按鈕", "hideEndScreenCardsButton": "隱藏片尾資訊卡按鈕", @@ -254,6 +264,14 @@ "label": "自動啟用劇院模式", "title": "載入影片時自動啟用劇院模式" }, + "automaticallyDisableClosedCaptions": { + "label": "Automatically disable closed captions", + "title": "Automatically disables closed captions when you load a video" + }, + "copyTimestampUrlButton": { + "label": "複製目前播放時間的影片網址按鈕", + "title": "複製目前播放時間的影片網址 (?t=123)" + }, "hideEndScreenCards": { "label": "隱藏片尾資訊卡", "title": "隱藏影片片尾的資訊卡" @@ -266,6 +284,10 @@ "label": "隱藏直播聊天室", "title": "隱藏直播的聊天室" }, + "hideOfficialArtistVideosFromHomePage": { + "label": "Hide Official Artist Videos", + "title": "Hide Official Artist Videos from Home Page" + }, "hidePaidPromotionBanner": { "label": "隱藏付費推廣橫幅", "title": "隱藏當影片帶有付費推廣時顯示的橫幅" @@ -398,6 +420,29 @@ }, "title": "播放速度設定" }, + "playlistLength": { + "enable": { + "label": "Display playlist length information", + "title": "Shows the total length of the playlist, how much has been watched, and how much remains." + }, + "title": "Playlist length settings", + "wayToGetLength": { + "select": { + "label": "Method to get playlist length", + "title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)" + } + }, + "wayToGetWatchTime": { + "select": { + "label": "Method to get watched time", + "options": { + "duration": "Video Length", + "youtube": "Video Watch Time" + }, + "title": "The way to get the amount of time watched (Video Length type only used on watch page)" + } + } + }, "screenshotButton": { "enable": { "label": "螢幕截圖按鈕", @@ -502,6 +547,14 @@ }, "title": "音量增強設定" }, + "youtubeDataApiV3Key": { + "getApiKeyLinkText": "You can get one from here", + "input": { + "label": "API Key", + "title": "Enter your Youtube Data API V3 key." + }, + "title": "YouTube API V3 Key" + }, "youtubeDeepDark": { "author": "作者", "co-authors": "共同作者", diff --git a/src/components/Inputs/TextInput/TextInput.tsx b/src/components/Inputs/TextInput/TextInput.tsx new file mode 100644 index 00000000..7e533a50 --- /dev/null +++ b/src/components/Inputs/TextInput/TextInput.tsx @@ -0,0 +1,59 @@ +import type { Nullable } from "@/src/types"; +import type { ChangeEvent } from "react"; + +import { cn, debounce } from "@/src/utils/utilities"; +import React, { useCallback, useRef, useState } from "react"; +import { IoMdEye, IoMdEyeOff } from "react-icons/io"; + +export type TextInputProps = { + className?: string; + id: string; + input_type: "password" | "text"; + label: string; + onChange: (event: ChangeEvent) => void; + title: string; + value: string; +}; + +const TextInput: React.FC = ({ className, id, input_type, label, onChange, title, value }) => { + const [showPassword, setShowPassword] = useState(false); + const debouncedOnChange = useCallback(debounce(onChange, 300), []); + const inputRef = useRef>(null); + const handleInputWrapperClick = () => { + inputRef.current?.focus(); + }; + // FIXME: cursor not being restored to position it was in when value is saved + return ( +
+ +
+ {input_type === "password" && ( + + )} + { + debouncedOnChange({ currentTarget: { value } }); + }} + ref={inputRef} + type={showPassword && input_type === "password" ? "text" : input_type} + value={value} + /> +
+
+ ); +}; + +export default TextInput; diff --git a/src/components/Inputs/TextInput/index.tsx b/src/components/Inputs/TextInput/index.tsx new file mode 100644 index 00000000..11da32c4 --- /dev/null +++ b/src/components/Inputs/TextInput/index.tsx @@ -0,0 +1,3 @@ +import TextInput from "./TextInput"; + +export { TextInput }; diff --git a/src/components/Inputs/index.tsx b/src/components/Inputs/index.tsx index 65be5a81..0a9090ab 100644 --- a/src/components/Inputs/index.tsx +++ b/src/components/Inputs/index.tsx @@ -6,4 +6,5 @@ import { ColorPicker } from "./ColorPicker"; import { NumberInput } from "./Number"; import { Select } from "./Select"; import { Slider } from "./Slider"; -export { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, type SelectOption, Slider }; +import { TextInput } from "./TextInput"; +export { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, type SelectOption, Slider, TextInput }; diff --git a/src/components/Settings/Settings.css b/src/components/Settings/Settings.css index a23089f3..38e84711 100644 --- a/src/components/Settings/Settings.css +++ b/src/components/Settings/Settings.css @@ -74,7 +74,7 @@ html { } /* https://stackoverflow.com/questions/11243337/a-taller-than-its-img-child */ -a>img { +a > img { display: block; } @@ -94,11 +94,11 @@ fieldset { padding-left: 0 !important; } -fieldset>p:first-of-type { +fieldset > p:first-of-type { margin-top: 0 !important; } -fieldset>p:last-of-type { +fieldset > p:last-of-type { margin-bottom: 0 !important; } @@ -185,7 +185,6 @@ hr { } /* inputs */ -input[type="text"], input[type="button"], button[id="openinnewtab_button"], select { @@ -194,7 +193,6 @@ select { font-size: 12px; } -input[type="text"], button[id="openinnewtab_button"], select { border: 0; @@ -359,7 +357,7 @@ button[id="openinnewtab_button"] { z-index: 100; } -#notifications>.notification { +#notifications > .notification { border-radius: var(--border-radius-md); padding: 10px 14px; -} \ No newline at end of file +} diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index e780dfae..ddffecf0 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -12,7 +12,7 @@ import SettingSearch from "@/src/components/Settings/components/SettingSearch"; import { deepDarkPreset } from "@/src/deepDarkPresets"; import { type i18nInstanceType, i18nService } from "@/src/i18n"; import { availableLocales, localeDirection, localePercentages } from "@/src/i18n/constants"; -import { buttonNames, youtubePlaybackSpeedButtonsRates, youtubePlayerSpeedRates } from "@/src/types"; +import { buttonNames, youtubePlayerMaxSpeed, youtubePlayerMinSpeed, youtubePlayerSpeedStep } from "@/src/types"; import { configurationImportSchema, defaultConfiguration as defaultSettings } from "@/src/utils/constants"; import { updateStoredSettings } from "@/src/utils/updateStoredSettings"; import { cn, deepMerge, formatDateForFileName, getPathValue, isButtonSelectDisabled, parseStoredValue } from "@/src/utils/utilities"; @@ -122,8 +122,8 @@ async function setSettings(settings: configuration) { localStorage.setItem(key, JSON.stringify(settings[key])); await chrome.storage.local.set({ [key]: JSON.stringify(settings[key]) }); } else { - localStorage.setItem(key, settings[key] as string); - await chrome.storage.local.set({ [key]: settings[key] as string }); + localStorage.setItem(key, settings[key]); + await chrome.storage.local.set({ [key]: settings[key] }); } } } @@ -204,8 +204,8 @@ export default function Settings() { localStorage.setItem(key, JSON.stringify(defaultSettings[key])); void chrome.storage.local.set({ [key]: JSON.stringify(defaultSettings[key]) }); } else { - localStorage.setItem(key, defaultSettings[key] as string); - void chrome.storage.local.set({ [key]: defaultSettings[key] as string }); + localStorage.setItem(key, defaultSettings[key]); + void chrome.storage.local.set({ [key]: defaultSettings[key] }); } } addNotification("success", "settings.clearData.allDataDeleted"); @@ -339,16 +339,6 @@ export default function Settings() { value: "lower" } ] as SelectOption<"player_quality_fallback_strategy">[]; - const YouTubePlayerSpeedOptions = youtubePlayerSpeedRates.map((rate) => ({ - label: rate?.toString(), - value: rate?.toString() - // This cast is here because I'm not sure what the proper type is - })) as SelectOption<"player_speed">[]; - const YouTubePlaybackSpeedButtonsOptions = youtubePlaybackSpeedButtonsRates.map((rate) => ({ - label: rate?.toString(), - value: rate?.toString() - // This cast is here because I'm not sure what the proper type is - })) as SelectOption<"playback_buttons_speed">[]; const ScreenshotFormatOptions: SelectOption<"screenshot_format">[] = [ { label: "PNG", value: "png" }, { label: "JPEG", value: "jpeg" }, @@ -369,6 +359,7 @@ export default function Settings() { } ]; const buttonPlacementOptions: SelectOption< + | "button_placements.copyTimestampUrlButton" | "button_placements.decreasePlaybackSpeedButton" | "button_placements.forwardButton" | "button_placements.hideEndScreenCardsButton" @@ -407,6 +398,26 @@ export default function Settings() { value }; }); + const playlistLengthGetMethodOptions: SelectOption<"playlist_length_get_method">[] = [ + { + label: "API", + value: "api" + }, + { + label: "HTML", + value: "html" + } + ]; + const playlistWatchTimeGetMethodOptions: SelectOption<"playlist_watch_time_get_method">[] = [ + { + label: t("settings.sections.playlistLength.wayToGetWatchTime.select.options.duration"), + value: "duration" + }, + { + label: t("settings.sections.playlistLength.wayToGetWatchTime.select.options.youtube"), + value: "youtube" + } + ]; const settingsImportChange: ChangeEventHandler = (event): void => { void (async () => { const { target } = event; @@ -434,8 +445,8 @@ export default function Settings() { localStorage.setItem(key, JSON.stringify(castSettings[key])); void chrome.storage.local.set({ [key]: JSON.stringify(castSettings[key]) }); } else { - localStorage.setItem(key, castSettings[key] as string); - void chrome.storage.local.set({ [key]: castSettings[key] as string }); + localStorage.setItem(key, castSettings[key]); + void chrome.storage.local.set({ [key]: castSettings[key] }); } } await updateStoredSettings(); @@ -620,6 +631,14 @@ export default function Settings() { title={t("settings.sections.miscellaneous.features.loopButton.title")} type="checkbox" /> + + + @@ -977,20 +1012,24 @@ export default function Settings() { id="player_speed" label={t("settings.sections.playbackSpeed.select.label")} onChange={setValueOption("player_speed")} - options={YouTubePlayerSpeedOptions} - selectedOption={getSelectedOption("player_speed")?.toString()} title={t("settings.sections.playbackSpeed.select.title")} - type="select" + value={settings.player_speed} + max={youtubePlayerMaxSpeed} + min={youtubePlayerSpeedStep} + step={youtubePlayerSpeedStep} + type="number" /> @@ -1191,6 +1230,55 @@ export default function Settings() { value={settings.custom_css_code} /> + + + + + + + + + +
+ + {t("settings.sections.youtubeDataApiV3Key.getApiKeyLinkText")} + +
+
+
= { id: ID; @@ -23,6 +24,7 @@ type SettingInputProps = { | ({ type: "number" } & NumberInputProps) | ({ type: "select" } & SelectProps) | ({ type: "slider" } & SliderProps) + | ({ type: "text-input" } & TextInputProps) ); function SettingInput(settingProps: SettingInputProps) { const { type } = settingProps; @@ -75,6 +77,10 @@ function SettingInput(settingProps: SettingInputProp const { className, disabled, id, label, onChange, title, value } = settingProps; return ; } + case "text-input": { + const { className, id, input_type, label, onChange, title, value } = settingProps; + return ; + } } } export default function Setting(settingProps: SettingInputProps) { @@ -86,11 +92,9 @@ export default function Setting(settingProps: Settin (settingProps.title !== undefined && settingProps.title.toLowerCase().includes(filter.toLowerCase())) || (settingProps.label !== undefined && settingProps.label.toLowerCase().includes(filter.toLowerCase())) ); - return ( - shouldSettingBeVisible && ( + return shouldSettingBeVisible ?
- ) - ); + : null; } diff --git a/src/components/Settings/components/SettingSearch.tsx b/src/components/Settings/components/SettingSearch.tsx index a4da0fd9..5a3fd9d9 100644 --- a/src/components/Settings/components/SettingSearch.tsx +++ b/src/components/Settings/components/SettingSearch.tsx @@ -13,7 +13,7 @@ export default function SettingSearch({ i18nInstance }: { i18nInstance: i18nInst return (
) => setFilter(e.target.value)} placeholder={t("settings.sections.settingSearch.placeholder")} ref={inputRef} diff --git a/src/components/Settings/components/SettingSection.tsx b/src/components/Settings/components/SettingSection.tsx index d0fd0daa..0458e29f 100644 --- a/src/components/Settings/components/SettingSection.tsx +++ b/src/components/Settings/components/SettingSection.tsx @@ -25,11 +25,9 @@ export default function SettingSection({ children, className = "", title: sectio (child.props.title !== undefined && child.props.title.toLowerCase().includes(filter.toLowerCase())) ); }).length > 0; - return ( - shouldSectionBeVisible && ( + return shouldSectionBeVisible ? {children} - ) - ); + : null; } diff --git a/src/components/Settings/components/SettingTitle.tsx b/src/components/Settings/components/SettingTitle.tsx index 58ac1d0c..77422c6f 100644 --- a/src/components/Settings/components/SettingTitle.tsx +++ b/src/components/Settings/components/SettingTitle.tsx @@ -5,5 +5,5 @@ export default function SettingTitle() { const { filter } = useSettingsFilter(); const { title } = useSectionTitle(); const shouldSettingTitleBeVisible = filter === "" ? true : title.toLowerCase().includes(filter.toLowerCase()); - return shouldSettingTitleBeVisible && {title}; + return shouldSettingTitleBeVisible ? {title} : null; } diff --git a/src/features/automaticTheaterMode/index.ts b/src/features/automaticTheaterMode/index.ts index 7efe2bcc..d110930a 100644 --- a/src/features/automaticTheaterMode/index.ts +++ b/src/features/automaticTheaterMode/index.ts @@ -1,20 +1,18 @@ import type { YouTubePlayerDiv } from "@/src/types"; -import { isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { isLivePage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; export async function enableAutomaticTheaterMode() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_automatic_theater_mode } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If automatic theater mode isn't enabled return if (!enable_automatic_theater_mode) return; - if (!isWatchPage()) return; // Get the player element - const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") : null; + const playerContainer = isWatchPage() || isLivePage() ? document.querySelector("div#movie_player") : null; // If player element is not available, return if (!playerContainer) return; const { width } = await playerContainer.getSize(); diff --git a/src/features/automaticallyDisableClosedCaptions/index.ts b/src/features/automaticallyDisableClosedCaptions/index.ts new file mode 100644 index 00000000..0559ae94 --- /dev/null +++ b/src/features/automaticallyDisableClosedCaptions/index.ts @@ -0,0 +1,31 @@ +import type { YouTubePlayerDiv } from "@/src/types"; +import { isLivePage, isWatchPage, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; +let captionsWhereEnabled = false; +export async function enableAutomaticallyDisableClosedCaptions() { + const { + data: { + options: { enable_automatically_disable_closed_captions } + } + } = await waitForSpecificMessage("options", "request_data", "content"); + if (!enable_automatically_disable_closed_captions) return; + await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); + // Get the player element + const playerContainer = isWatchPage() || isLivePage() ? document.querySelector("div#movie_player") : null; + const subtitlesButton = document.querySelector("button.ytp-subtitles-button"); + // If player element is not available, return + if (!playerContainer || !subtitlesButton) return; + captionsWhereEnabled = subtitlesButton.getAttribute("aria-pressed") === "true"; + // Disable captions + playerContainer.unloadModule("captions"); +} +export async function disableAutomaticallyDisableClosedCaptions() { + await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); + // Get the player element + const playerContainer = isWatchPage() || isLivePage() ? document.querySelector("div#movie_player") : null; + // If player element is not available, return + if (!playerContainer) return; + // If captions weren't enabled, return + if (!captionsWhereEnabled) return; + // Re-enable captions + playerContainer.loadModule("captions"); +} diff --git a/src/features/buttonPlacement/index.ts b/src/features/buttonPlacement/index.ts index 97627ba4..6b845b65 100644 --- a/src/features/buttonPlacement/index.ts +++ b/src/features/buttonPlacement/index.ts @@ -36,14 +36,13 @@ export async function removeFeatureButton(buttonNam const featureName = findKeyByValue(buttonName as MultiButtonNames) ?? (buttonName as SingleButtonFeatureNames); if (placement === undefined) { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); ({ data: { options: { button_placements: { [buttonName]: placement } } } - } = optionsData); + } = await waitForSpecificMessage("options", "request_data", "content")); } switch (placement) { case "feature_menu": { diff --git a/src/features/copyTimestampUrlButton/index.ts b/src/features/copyTimestampUrlButton/index.ts new file mode 100644 index 00000000..2e7b447b --- /dev/null +++ b/src/features/copyTimestampUrlButton/index.ts @@ -0,0 +1,54 @@ +import type { AddButtonFunction, RemoveButtonFunction } from "@/src/features"; + +import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; +import { getFeatureButton } from "@/src/features/buttonPlacement/utils"; +import { getFeatureIcon } from "@/src/icons"; +import eventManager from "@/src/utils/EventManager"; +import { createTooltip, waitForSpecificMessage } from "@/src/utils/utilities"; + +export const addCopyTimestampUrlButton: AddButtonFunction = async () => { + const { + data: { + options: { + button_placements: { copyTimestampUrlButton: copyTimestampUrlButtonPlacement }, + enable_copy_timestamp_url_button: enableCopyTimestampUrlButton + } + } + } = await waitForSpecificMessage("options", "request_data", "content"); + if (!enableCopyTimestampUrlButton) return; + function copyTimestampUrlButtonClickListener() { + const videoElement = document.querySelector("video"); + if (!videoElement) return; + const videoId = new URLSearchParams(window.location.search).get("v"); + const timestampUrl = `https://youtu.be/${videoId}?t=${videoElement.currentTime.toFixed()}`; + void navigator.clipboard.writeText(timestampUrl); + const button = getFeatureButton("copyTimestampUrlButton"); + if (!button) return; + const { remove, update } = createTooltip({ + direction: copyTimestampUrlButtonPlacement === "below_player" ? "down" : "up", + element: button, + featureName: "copyTimestampUrlButton", + id: "yte-feature-copyTimestampUrlButton-tooltip" + }); + button.dataset.title = window.i18nextInstance.t("pages.content.features.copyTimestampUrlButton.button.copied"); + update(); + setTimeout(() => { + remove(); + button.dataset.title = window.i18nextInstance.t("pages.content.features.copyTimestampUrlButton.button.label"); + update(); + }, 1000); + } + await addFeatureButton( + "copyTimestampUrlButton", + copyTimestampUrlButtonPlacement, + window.i18nextInstance.t("pages.content.features.copyTimestampUrlButton.button.label"), + getFeatureIcon("copyTimestampUrlButton", copyTimestampUrlButtonPlacement), + copyTimestampUrlButtonClickListener, + false + ); +}; + +export const removeCopyTimestampUrlButton: RemoveButtonFunction = async (placement) => { + await removeFeatureButton("copyTimestampUrlButton", placement); + eventManager.removeEventListeners("copyTimestampUrlButton"); +}; diff --git a/src/features/customCSS/index.ts b/src/features/customCSS/index.ts index c43f1eef..092d7ed3 100644 --- a/src/features/customCSS/index.ts +++ b/src/features/customCSS/index.ts @@ -4,12 +4,11 @@ import { createCustomCSSElement, customCSSExists, updateCustomCSS } from "./util export const customCssID = "yte-custom-css"; export async function enableCustomCSS() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { custom_css_code, enable_custom_css } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // Check if custom CSS is enabled if (!enable_custom_css) return; if (customCSSExists()) { diff --git a/src/features/deepDarkCSS/index.ts b/src/features/deepDarkCSS/index.ts index 118c5320..0980f72f 100644 --- a/src/features/deepDarkCSS/index.ts +++ b/src/features/deepDarkCSS/index.ts @@ -5,12 +5,11 @@ import { createDeepDarkCSSElement, deepDarkCSSExists, getDeepDarkCustomThemeStyl export const deepDarkCssID = "yte-deep-dark-css"; export async function enableDeepDarkCSS() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { deep_dark_custom_theme_colors, deep_dark_preset, enable_deep_dark_theme } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // Check if deep dark theme is enabled if (!enable_deep_dark_theme) return; if (deepDarkCSSExists()) { diff --git a/src/features/featureMenu/index.ts b/src/features/featureMenu/index.ts index fc3db3e1..054bac3e 100644 --- a/src/features/featureMenu/index.ts +++ b/src/features/featureMenu/index.ts @@ -66,12 +66,11 @@ async function createFeatureMenuButton() { settingsButton.insertAdjacentElement("beforebegin", featureMenuButton); playerContainer.insertAdjacentElement("afterbegin", featureMenu); // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { feature_menu_open_type: featureMenuOpenType } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); await waitForAllElements(["#yte-feature-menu", "#yte-feature-menu-button"]); setupFeatureMenuEventListeners(featureMenuOpenType); } @@ -100,13 +99,11 @@ export async function enableFeatureMenu() { } function adjustAdsContainerStyles(featureMenuOpen: boolean) { const adsContainer = document.querySelector("div.video-ads.ytp-ad-module"); - if (adsContainer) { - const adsSpan = adsContainer.querySelector("span.ytp-ad-preview-container"); - if (adsSpan) { - adsSpan.style.opacity = featureMenuOpen ? "0.4" : ""; - adsSpan.style.zIndex = featureMenuOpen ? "36" : ""; - } - } + if (!adsContainer) return; + const adsSpan = adsContainer.querySelector("span.ytp-ad-preview-container"); + if (!adsSpan) return; + adsSpan.style.opacity = featureMenuOpen ? "0.4" : ""; + adsSpan.style.zIndex = featureMenuOpen ? "36" : ""; } export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuOpenType) { eventManager.removeEventListeners("featureMenu"); @@ -152,9 +149,8 @@ export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuO if (!featureMenuButton) return; if (event.target === featureMenuButton) return; if (event.target === featureMenu) return; - if (!featureMenu.contains(event.target as Node)) { - hideFeatureMenu(); - } + if (featureMenu.contains(event.target as Node)) return; + hideFeatureMenu(); }; switch (featureMenuOpenType) { @@ -165,11 +161,8 @@ export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuO "click", () => { const featureMenuVisible = featureMenu.style.display === "block"; - if (featureMenuVisible) { - hideFeatureMenu(); - } else { - showFeatureMenu(); - } + if (featureMenuVisible) return hideFeatureMenu(); + showFeatureMenu(); }, "featureMenu" ); @@ -192,10 +185,9 @@ export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuO featureMenuButton, "mouseleave", (event) => { - if (![featureMenu, featureMenuButton].includes(event.target as HTMLButtonElement)) { - removeFeatureMenuTooltip(); - hideFeatureMenu(); - } + if ([featureMenu, featureMenuButton].includes(event.target as HTMLButtonElement)) return; + removeFeatureMenuTooltip(); + hideFeatureMenu(); }, "featureMenu" ); @@ -223,18 +215,15 @@ export function setupFeatureMenuEventListeners(featureMenuOpenType: FeatureMenuO } function handleMutation(mutations: MutationRecord[]) { mutations.forEach((mutation) => { - if (mutation.type === "childList") { - const addedNodes = Array.from(mutation.addedNodes); - const isAdsElementAdded = addedNodes.some( - (node) => (node as HTMLDivElement).classList?.contains("video-ads") && (node as HTMLDivElement).classList?.contains("ytp-ad-module") - ); - if (isAdsElementAdded) { - const featureMenu = document.querySelector("#yte-feature-menu"); - if (featureMenu) { - adjustAdsContainerStyles(featureMenu.style.display === "block"); - } - } - } + if (mutation.type !== "childList") return; + const addedNodes = Array.from(mutation.addedNodes); + const isAdsElementAdded = addedNodes.some( + (node) => (node as HTMLDivElement).classList?.contains("video-ads") && (node as HTMLDivElement).classList?.contains("ytp-ad-module") + ); + if (!isAdsElementAdded) return; + const featureMenu = document.querySelector("#yte-feature-menu"); + if (!featureMenu) return; + adjustAdsContainerStyles(featureMenu.style.display === "block"); }); } const observer = new MutationObserver(handleMutation); diff --git a/src/features/featureMenu/utils.ts b/src/features/featureMenu/utils.ts index f3cec0be..fe4a0e26 100644 --- a/src/features/featureMenu/utils.ts +++ b/src/features/featureMenu/utils.ts @@ -44,14 +44,11 @@ export async function addFeatureItemToMenu("#yte-feature-menu"); if (!featureMenu) return; - // Check if the feature item already exists in the menu const featureExistsInMenu = featureMenu.querySelector(`#${getFeatureIds(buttonName).featureMenuItemId}`); if (featureExistsInMenu) { @@ -61,26 +58,21 @@ export async function addFeatureItemToMenu featureMenuClickListener(menuItem, listener, isToggle), featureName); return; } - // Get the feature menu panel const featureMenuPanel = document.querySelector("#yte-panel-menu"); if (!featureMenuPanel) return; - // Get the IDs for the feature item const { featureMenuItemIconId, featureMenuItemId, featureMenuItemLabelId } = getFeatureIds(buttonName); - // Create a menu item element const menuItem = document.createElement("div"); menuItem.classList.add("ytp-menuitem"); menuItem.id = featureMenuItemId; - // Create the menu item icon element const menuItemIcon = document.createElement("div"); menuItemIcon.id = featureMenuItemIconId; menuItemIcon.classList.add("ytp-menuitem-icon"); menuItemIcon.appendChild(icon); menuItem.appendChild(menuItemIcon); - // Create the menu item label element const menuItemLabel = document.createElement("div"); menuItemLabel.classList.add("ytp-menuitem-label"); @@ -88,7 +80,6 @@ export async function addFeatureItemToMenu featureMenuClickListener(menuItem, listener, isToggle), featureName); menuItem.appendChild(menuItemLabel); - // If it's a toggle item, create the toggle elements if (isToggle) { const menuItemContent = document.createElement("div"); @@ -103,10 +94,8 @@ export async function addFeatureItemToMenu("#yte-panel-menu"); if (!featureMenuPanel) return; - // Find the specific feature menu item const featureMenuItem = featureMenuPanel.querySelector(`#${featureMenuItemId}`); if (!featureMenuItem) return; - // Remove the feature menu item featureMenuItem.remove(); - // Check if there are any items left in the menu if (featureMenuPanel.childElementCount === 0) { // If no items are left, hide the menu featureMenu.style.display = "none"; - // Find the feature menu button const featureMenuButton = document.querySelector("#yte-feature-menu-button"); if (!featureMenuButton) return; - // Hide the feature menu button since the menu is empty featureMenuButton.style.display = "none"; } - // Adjust the height and width of the feature menu panel featureMenu.style.height = `${40 * featureMenuPanel.childElementCount + 16}px`; } diff --git a/src/features/forwardRewindButtons/index.ts b/src/features/forwardRewindButtons/index.ts index 6a0089f7..894cb510 100644 --- a/src/features/forwardRewindButtons/index.ts +++ b/src/features/forwardRewindButtons/index.ts @@ -9,7 +9,7 @@ import { Measure, seconds } from "safe-units"; import type { AddButtonFunction, RemoveButtonFunction } from "../index"; const speedButtonListener = async (direction: "backward" | "forward", timeAdjustment: number) => { // Get the player element - const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") : null; + const playerContainer = document.querySelector("div#movie_player"); // If player element is not available, return if (!playerContainer) return; if (!playerContainer.seekTo) return; @@ -27,8 +27,9 @@ export const addForwardButton: AddButtonFunction = async () => { } } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_forward_rewind_buttons) return; + if (!isWatchPage()) return; // Get the player element - const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") : null; + const playerContainer = document.querySelector("div#movie_player"); // If player element is not available, return if (!playerContainer) return; const playerVideoData = await playerContainer.getVideoData(); @@ -56,8 +57,9 @@ export const addRewindButton: AddButtonFunction = async () => { } } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_forward_rewind_buttons) return; + if (!isWatchPage()) return; // Get the player element - const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") : null; + const playerContainer = document.querySelector("div#movie_player"); // If player element is not available, return if (!playerContainer) return; const playerVideoData = await playerContainer.getVideoData(); diff --git a/src/features/hideEndScreenCards/index.ts b/src/features/hideEndScreenCards/index.ts index 5cc9964f..560d0ae0 100644 --- a/src/features/hideEndScreenCards/index.ts +++ b/src/features/hideEndScreenCards/index.ts @@ -1,11 +1,11 @@ import type { AddButtonFunction, RemoveButtonFunction } from "@/src/features"; -import type { ButtonPlacement } from "@/src/types"; +import type { ButtonPlacement, YouTubePlayerDiv } from "@/src/types"; import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; import { updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; import { getFeatureIcon } from "@/src/icons"; import eventManager from "@/src/utils/EventManager"; -import { modifyElementsClassList, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; +import { isWatchPage, modifyElementsClassList, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; import "./index.css"; export async function enableHideEndScreenCards() { @@ -15,11 +15,13 @@ export async function enableHideEndScreenCards() { } } = await waitForSpecificMessage("options", "request_data", "content"); if (!enableHideEndScreenCards) return; + if (!isWatchPage()) return; await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); hideEndScreenCards(); } export async function disableHideEndScreenCards() { + if (!isWatchPage()) return; await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); showEndScreenCards(); } @@ -33,7 +35,13 @@ export const addHideEndScreenCardsButton: AddButtonFunction = async () => { } } = await waitForSpecificMessage("options", "request_data", "content"); if (!enableHideEndScreenCardsButton) return; + if (!isWatchPage()) return; await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); + // Get the player container element + const playerContainer = document.querySelector("div#movie_player"); + if (!playerContainer) return; + const videoData = await playerContainer.getVideoData(); + if (videoData.isLive) return; const endScreenCardsAreHidden = isEndScreenCardsHidden(); const handleButtonClick = (placement: ButtonPlacement, checked?: boolean) => { if (placement === "feature_menu") { @@ -63,6 +71,7 @@ export const addHideEndScreenCardsButton: AddButtonFunction = async () => { ); }; export const removeHideEndScreenCardsButton: RemoveButtonFunction = async (placement) => { + if (!isWatchPage()) return; await removeFeatureButton("hideEndScreenCardsButton", placement); eventManager.removeEventListeners("hideEndScreenCardsButton"); }; diff --git a/src/features/hideOfficialArtistVideosFromHomePage/index.css b/src/features/hideOfficialArtistVideosFromHomePage/index.css new file mode 100644 index 00000000..d48d9764 --- /dev/null +++ b/src/features/hideOfficialArtistVideosFromHomePage/index.css @@ -0,0 +1,3 @@ +.yte-hide-official-artist-videos-from-home-page { + display: none !important; +} diff --git a/src/features/hideOfficialArtistVideosFromHomePage/index.ts b/src/features/hideOfficialArtistVideosFromHomePage/index.ts new file mode 100644 index 00000000..5aa2973a --- /dev/null +++ b/src/features/hideOfficialArtistVideosFromHomePage/index.ts @@ -0,0 +1,29 @@ +import type { Nullable } from "@/src/types"; +import "./index.css"; +import { waitForSpecificMessage } from "@/src/utils/utilities"; +import { + hideOfficialArtistVideosFromHomePage, + observeOfficialArtistVideosFromHomePage, + showOfficialArtistVideosFromHomePage +} from "@/src/features/hideOfficialArtistVideosFromHomePage/utils"; +let officialArtistVideosObserver: Nullable = null; +export async function enableHideOfficialArtistVideosFromHomePage() { + // Wait for the "options" message from the content script + const { + data: { + options: { enable_hide_official_artist_videos_from_home_page: enableHideOfficialArtistVideosFromHomePage } + } + } = await waitForSpecificMessage("options", "request_data", "content"); + if (!enableHideOfficialArtistVideosFromHomePage) return; + hideOfficialArtistVideosFromHomePage(); + if (officialArtistVideosObserver) officialArtistVideosObserver.disconnect(); + officialArtistVideosObserver = observeOfficialArtistVideosFromHomePage(); +} + +export function disableHideOfficialArtistVideosFromHomePage() { + showOfficialArtistVideosFromHomePage(); + if (officialArtistVideosObserver) { + officialArtistVideosObserver.disconnect(); + officialArtistVideosObserver = null; + } +} diff --git a/src/features/hideOfficialArtistVideosFromHomePage/utils.ts b/src/features/hideOfficialArtistVideosFromHomePage/utils.ts new file mode 100644 index 00000000..1285962d --- /dev/null +++ b/src/features/hideOfficialArtistVideosFromHomePage/utils.ts @@ -0,0 +1,27 @@ +import { modifyElementsClassList } from "@/src/utils/utilities"; + +export function observeOfficialArtistVideosFromHomePage() { + const observer = new MutationObserver(() => { + hideOfficialArtistVideosFromHomePage(); + }); + observer.observe(document.body, { childList: true, subtree: true }); + return observer; +} +export function hideOfficialArtistVideosFromHomePage() { + const officialArtistVideos = document.querySelectorAll( + "ytd-rich-item-renderer:has(#byline-container #channel-name .badge-style-type-verified-artist)" + ); + modifyElementsClassList( + "add", + Array.from(officialArtistVideos).map((element) => ({ className: "yte-hide-official-artist-videos-from-home-page", element })) + ); +} +export function showOfficialArtistVideosFromHomePage() { + const officialArtistVideos = document.querySelectorAll( + "ytd-rich-item-renderer:has(#byline-container #channel-name .badge-style-type-verified-artist)" + ); + modifyElementsClassList( + "remove", + Array.from(officialArtistVideos).map((element) => ({ className: "yte-hide-official-artist-videos-from-home-page", element })) + ); +} diff --git a/src/features/hidePaidPromotionBanner/index.css b/src/features/hidePaidPromotionBanner/index.css index 4d2753fe..56881169 100644 --- a/src/features/hidePaidPromotionBanner/index.css +++ b/src/features/hidePaidPromotionBanner/index.css @@ -1,3 +1,3 @@ .yte-hide-paid-promotion-banner { display: none !important; -} \ No newline at end of file +} diff --git a/src/features/hideScrollBar/index.ts b/src/features/hideScrollBar/index.ts index eecb0aea..d0d18e20 100644 --- a/src/features/hideScrollBar/index.ts +++ b/src/features/hideScrollBar/index.ts @@ -4,12 +4,11 @@ import { hideScrollBar } from "./utils"; export async function enableHideScrollBar() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_hide_scrollbar } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the hide scroll bar option is disabled, return if (!enable_hide_scrollbar) return; hideScrollBar(); diff --git a/src/features/hideScrollBar/utils.ts b/src/features/hideScrollBar/utils.ts index fcc40d7f..eb71cea8 100644 --- a/src/features/hideScrollBar/utils.ts +++ b/src/features/hideScrollBar/utils.ts @@ -1,6 +1,6 @@ export function hideScrollBar() { const style = document.createElement("style"); - style.innerHTML = ` + style.textContent = ` ::-webkit-scrollbar { width: 0px; height: 0px; @@ -13,8 +13,9 @@ export function hideScrollBar() { document.head.appendChild(style); } export function showScrollBar() { - const style = document.getElementById("yte-hide-scroll-bar"); - if (style) { + let style = document.getElementById("yte-hide-scroll-bar"); + while (style) { style.remove(); + style = document.getElementById("yte-hide-scroll-bar"); } } diff --git a/src/features/hideShorts/index.ts b/src/features/hideShorts/index.ts index 49d6f035..c558c701 100644 --- a/src/features/hideShorts/index.ts +++ b/src/features/hideShorts/index.ts @@ -4,12 +4,11 @@ import { waitForSpecificMessage } from "@/src/utils/utilities"; let shortsObserver: Nullable = null; export async function enableHideShorts() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_hide_shorts } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the hide shorts option is disabled, return if (!enable_hide_shorts) return; hideShorts(); diff --git a/src/features/hideTranslateComment/utils.ts b/src/features/hideTranslateComment/utils.ts index c8b93640..40cb5745 100644 --- a/src/features/hideTranslateComment/utils.ts +++ b/src/features/hideTranslateComment/utils.ts @@ -11,10 +11,11 @@ export type EngagementPanelVisibility = (typeof engagementPanelVisibility)[numbe export function observeTranslateComment(): MutationObserver { const observer = new MutationObserver((mutationList) => { mutationList + .filter((mutation) => mutation.type === "childList") .filter( (mutation) => - mutation.type !== "childList" || - !mutation.addedNodes.length || + (mutation.target instanceof Element && + mutation.target.matches("ytd-comment-thread-renderer #replies ytd-comment-replies-renderer #expander #contents")) || Array.from(mutation.addedNodes).some( (addedNode) => addedNode instanceof Element && diff --git a/src/features/index.ts b/src/features/index.ts index 7e39448d..64a34664 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,5 +1,6 @@ import type { AllButtonNames, ButtonPlacement } from "@/src/types"; +import { addCopyTimestampUrlButton, removeCopyTimestampUrlButton } from "@/src/features/copyTimestampUrlButton"; import { addForwardButton, addRewindButton, removeForwardButton, removeRewindButton } from "@/src/features/forwardRewindButtons"; import { addHideEndScreenCardsButton, removeHideEndScreenCardsButton } from "@/src/features/hideEndScreenCards"; import { addLoopButton, removeLoopButton } from "@/src/features/loopButton"; @@ -20,6 +21,10 @@ export type FeatureFuncRecord = { }; export const featureButtonFunctions = { + copyTimestampUrlButton: { + add: addCopyTimestampUrlButton, + remove: removeCopyTimestampUrlButton + }, decreasePlaybackSpeedButton: { add: addDecreasePlaybackSpeedButton, remove: removeDecreasePlaybackSpeedButton diff --git a/src/features/loopButton/index.ts b/src/features/loopButton/index.ts index 05377dde..9a03efe3 100644 --- a/src/features/loopButton/index.ts +++ b/src/features/loopButton/index.ts @@ -13,7 +13,6 @@ import { loopButtonClickListener } from "./utils"; export const addLoopButton: AddButtonFunction = async () => { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { @@ -21,7 +20,7 @@ export const addLoopButton: AddButtonFunction = async () => { enable_loop_button } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the loop button option is disabled, return if (!enable_loop_button) return; // Get the volume control element @@ -30,7 +29,6 @@ export const addLoopButton: AddButtonFunction = async () => { if (!volumeControl) return; const videoElement = document.querySelector("video.html5-main-video"); if (!videoElement) return; - await addFeatureButton( "loopButton", loopButtonPlacement, diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts index e85cb33d..df1356a7 100644 --- a/src/features/maximizePlayerButton/index.ts +++ b/src/features/maximizePlayerButton/index.ts @@ -11,7 +11,6 @@ import { maximizePlayer, setupVideoPlayerTimeUpdate, updateProgressBarPositions // TODO: fix the "default/theatre" view button and pip button not making the player minimize to the previous state. export const addMaximizePlayerButton: AddButtonFunction = async () => { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { @@ -19,7 +18,7 @@ export const addMaximizePlayerButton: AddButtonFunction = async () => { enable_maximize_player_button: enableMaximizePlayerButton } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the maximize player button option is disabled, return if (!enableMaximizePlayerButton) return; // Add a click event listener to the maximize button diff --git a/src/features/openTranscriptButton/index.ts b/src/features/openTranscriptButton/index.ts index 8b5b0460..b38aaeb2 100644 --- a/src/features/openTranscriptButton/index.ts +++ b/src/features/openTranscriptButton/index.ts @@ -6,12 +6,11 @@ import { addOpenTranscriptButton } from "./utils"; export async function openTranscriptButton() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_open_transcript_button: enableOpenTranscriptButton } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the open transcript button option is disabled, return if (!enableOpenTranscriptButton) return; await waitForAllElements(["ytd-video-description-transcript-section-renderer button"]); diff --git a/src/features/openTranscriptButton/utils.ts b/src/features/openTranscriptButton/utils.ts index f4e52577..d4f39f1b 100644 --- a/src/features/openTranscriptButton/utils.ts +++ b/src/features/openTranscriptButton/utils.ts @@ -7,14 +7,13 @@ import { waitForSpecificMessage } from "@/src/utils/utilities"; export const addOpenTranscriptButton: AddButtonFunction = async () => { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { button_placements: { openTranscriptButton: openTranscriptButtonPlacement } } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); function transcriptButtonClickerListener() { const transcriptButton = document.querySelector("ytd-video-description-transcript-section-renderer button"); if (!transcriptButton) return; diff --git a/src/features/openYouTubeSettingsOnHover/index.ts b/src/features/openYouTubeSettingsOnHover/index.ts index c63e1c76..97001ac2 100644 --- a/src/features/openYouTubeSettingsOnHover/index.ts +++ b/src/features/openYouTubeSettingsOnHover/index.ts @@ -5,12 +5,11 @@ import { isNewYouTubeVideoLayout, isWatchPage, waitForSpecificMessage } from "@/ export async function enableOpenYouTubeSettingsOnHover() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_open_youtube_settings_on_hover: enableOpenYouTubeSettingsOnHover } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the open YouTube settings on hover option is disabled, return if (!enableOpenYouTubeSettingsOnHover) return; const settingsButton = document.querySelector(".ytp-button.ytp-settings-button"); diff --git a/src/features/pauseBackgroundPlayers/index.ts b/src/features/pauseBackgroundPlayers/index.ts index 874093b6..d2c9df06 100644 --- a/src/features/pauseBackgroundPlayers/index.ts +++ b/src/features/pauseBackgroundPlayers/index.ts @@ -7,17 +7,15 @@ const PauseBackgroundPlayers = () => { }; export async function enablePauseBackgroundPlayers() { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_pausing_background_players: pauseBackgroundPlayersEnabled } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!pauseBackgroundPlayersEnabled) return; // ignore home page and channel pages if (window.location.href.match(/^https?:\/\/(?:www\.)?youtube\.com(\/?|\/channel\/.+|\/\@.+)$/gm)) return; browserColorLog("Enabling pauseBackgroundPlayers", "FgMagenta"); - let videoPlayerContainer: HTMLVideoElement | null = null; if (!videoPlayerContainer) { videoPlayerContainer = document.querySelector(".html5-main-video"); @@ -27,7 +25,6 @@ export async function enablePauseBackgroundPlayers() { videoPlayerContainer.addEventListener("playing", PauseBackgroundPlayers); } } - let debounceTimeout: null | number = null; const observer = new MutationObserver((mutationsList: MutationRecord[]) => { if (debounceTimeout) clearTimeout(debounceTimeout); @@ -40,7 +37,6 @@ export async function enablePauseBackgroundPlayers() { } }, 100); }); - if (videoPlayerContainer) { observer.observe(videoPlayerContainer, { childList: true, subtree: true }); } diff --git a/src/features/playbackSpeedButtons/index.ts b/src/features/playbackSpeedButtons/index.ts index c45e9d42..9c6d74c1 100644 --- a/src/features/playbackSpeedButtons/index.ts +++ b/src/features/playbackSpeedButtons/index.ts @@ -1,32 +1,40 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import { youtubePlayerMinSpeed, type YouTubePlayerDiv } from "@/src/types"; import { addFeatureButton, removeFeatureButton } from "@/src/features/buttonPlacement"; -import { getFeatureButton } from "@/src/features/buttonPlacement/utils"; +import { checkIfFeatureButtonExists, getFeatureButton } from "@/src/features/buttonPlacement/utils"; import { setPlayerSpeed } from "@/src/features/playerSpeed"; import { getFeatureIcon } from "@/src/icons"; import eventManager from "@/src/utils/EventManager"; import OnScreenDisplayManager from "@/src/utils/OnScreenDisplayManager"; -import { createTooltip, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { createTooltip, isWatchPage, waitForSpecificMessage, round } from "@/src/utils/utilities"; import type { AddButtonFunction, RemoveButtonFunction } from "../index"; let currentPlaybackSpeed = 1; - -async function updateTooltip( +export function calculatePlaybackButtonSpeed(speed: number, playbackSpeedPerClick: number, direction: "decrease" | "increase") { + const calculatedSpeed = + speed == 16 && direction == "increase" ? 16 + : speed == youtubePlayerMinSpeed && direction == "decrease" ? youtubePlayerMinSpeed + : direction == "decrease" ? speed - playbackSpeedPerClick + : speed + playbackSpeedPerClick; + return round(calculatedSpeed, 2); +} +export async function updatePlaybackSpeedButtonTooltip( buttonName: ButtonName, speed: number ) { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { button_placements: { decreasePlaybackSpeedButton: decreasePlaybackSpeedButtonPlacement, increasePlaybackSpeedButton: increasePlaybackSpeedButtonPlacement - }, - playback_buttons_speed: playbackSpeedPerClick + } } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); + const videoElement = document.querySelector("video"); + if (!videoElement) return; + ({ playbackRate: currentPlaybackSpeed } = videoElement); const featureName = "playbackSpeedButtons"; const button = getFeatureButton(buttonName); if (!button) return; @@ -38,41 +46,34 @@ async function updateTooltip void { +function playbackSpeedButtonClickListener(speedPerClick: number, direction: "decrease" | "increase"): () => void { return () => { void (async () => { const videoElement = document.querySelector("video"); if (!videoElement) return; + const adjustmentAmount = direction === "increase" ? speedPerClick : -speedPerClick; try { - const { playbackRate: playbackRate } = videoElement; - currentPlaybackSpeed = playbackRate; - if (currentPlaybackSpeed + amount <= 0) return; - if (currentPlaybackSpeed + amount > 4) return; - const playerContainer = - isWatchPage() ? document.querySelector("div#movie_player") - : isShortsPage() ? document.querySelector("div#shorts-player") - : null; + ({ playbackRate: currentPlaybackSpeed } = videoElement); + if (currentPlaybackSpeed + adjustmentAmount > 16 || currentPlaybackSpeed + adjustmentAmount < youtubePlayerMinSpeed) return; + const playerContainer = document.querySelector("div#movie_player"); if (!playerContainer) return; - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { osd_display_color, osd_display_hide_time, osd_display_opacity, osd_display_padding, osd_display_position } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); new OnScreenDisplayManager( { displayColor: osd_display_color, @@ -85,15 +86,15 @@ function playbackSpeedButtonClickListener(amount: number): () => void { }, "yte-osd", { - max: 4, + max: 16, type: "speed", - value: currentPlaybackSpeed + amount + value: round(currentPlaybackSpeed + adjustmentAmount, 2) } ); - const speed = currentPlaybackSpeed + amount; + const speed = round(currentPlaybackSpeed + adjustmentAmount, 2); await setPlayerSpeed(speed); - await updateTooltip("increasePlaybackSpeedButton", speed); - await updateTooltip("decreasePlaybackSpeedButton", speed); + await updatePlaybackSpeedButtonTooltip("increasePlaybackSpeedButton", calculatePlaybackButtonSpeed(speed, speedPerClick, "increase")); + await updatePlaybackSpeedButtonTooltip("decreasePlaybackSpeedButton", calculatePlaybackButtonSpeed(speed, speedPerClick, "decrease")); } catch (error) { console.error(error); } @@ -102,7 +103,6 @@ function playbackSpeedButtonClickListener(amount: number): () => void { } export const addIncreasePlaybackSpeedButton: AddButtonFunction = async () => { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { @@ -111,22 +111,38 @@ export const addIncreasePlaybackSpeedButton: AddButtonFunction = async () => { playback_buttons_speed: playbackSpeedPerClick } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_playback_speed_buttons) return; + if (!isWatchPage()) return; + const videoElement = document.querySelector("video"); + if (!videoElement) return; + ({ playbackRate: currentPlaybackSpeed } = videoElement); + const playerContainer = document.querySelector("div#movie_player"); + if (!playerContainer) return; + const playerVideoData = await playerContainer.getVideoData(); + if (playerVideoData.isLive && checkIfFeatureButtonExists("increasePlaybackSpeedButton", increasePlaybackSpeedButtonPlacement)) { + await removeFeatureButton("increasePlaybackSpeedButton", increasePlaybackSpeedButtonPlacement); + eventManager.removeEventListeners("playbackSpeedButtons"); + } + if (playerVideoData.isLive) return; await addFeatureButton( "increasePlaybackSpeedButton", increasePlaybackSpeedButtonPlacement, - window.i18nextInstance.t("pages.content.features.playbackSpeedButtons.buttons.increasePlaybackSpeedButton.label", { - SPEED: currentPlaybackSpeed + playbackSpeedPerClick - }), + window.i18nextInstance.t( + currentPlaybackSpeed == 16 ? + `pages.content.features.playbackSpeedButtons.increaseLimit` + : "pages.content.features.playbackSpeedButtons.buttons.increasePlaybackSpeedButton.label", + { + SPEED: calculatePlaybackButtonSpeed(currentPlaybackSpeed, playbackSpeedPerClick, "increase") + } + ), getFeatureIcon("increasePlaybackSpeedButton", increasePlaybackSpeedButtonPlacement), - playbackSpeedButtonClickListener(playbackSpeedPerClick), + playbackSpeedButtonClickListener(playbackSpeedPerClick, "increase"), false ); }; export const addDecreasePlaybackSpeedButton: AddButtonFunction = async () => { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { @@ -135,16 +151,33 @@ export const addDecreasePlaybackSpeedButton: AddButtonFunction = async () => { playback_buttons_speed: playbackSpeedPerClick } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_playback_speed_buttons) return; + if (!isWatchPage()) return; + const videoElement = document.querySelector("video"); + if (!videoElement) return; + ({ playbackRate: currentPlaybackSpeed } = videoElement); + const playerContainer = document.querySelector("div#movie_player"); + if (!playerContainer) return; + const playerVideoData = await playerContainer.getVideoData(); + if (playerVideoData.isLive && checkIfFeatureButtonExists("decreasePlaybackSpeedButton", decreasePlaybackSpeedButtonPlacement)) { + await removeFeatureButton("decreasePlaybackSpeedButton", decreasePlaybackSpeedButtonPlacement); + eventManager.removeEventListeners("playbackSpeedButtons"); + } + if (playerVideoData.isLive) return; await addFeatureButton( "decreasePlaybackSpeedButton", decreasePlaybackSpeedButtonPlacement, - window.i18nextInstance.t("pages.content.features.playbackSpeedButtons.buttons.decreasePlaybackSpeedButton.label", { - SPEED: currentPlaybackSpeed - playbackSpeedPerClick - }), + window.i18nextInstance.t( + currentPlaybackSpeed == youtubePlayerMinSpeed ? + `pages.content.features.playbackSpeedButtons.decreaseLimit` + : "pages.content.features.playbackSpeedButtons.buttons.decreasePlaybackSpeedButton.label", + { + SPEED: calculatePlaybackButtonSpeed(currentPlaybackSpeed, playbackSpeedPerClick, "decrease") + } + ), getFeatureIcon("decreasePlaybackSpeedButton", decreasePlaybackSpeedButtonPlacement), - playbackSpeedButtonClickListener(-playbackSpeedPerClick), + playbackSpeedButtonClickListener(playbackSpeedPerClick, "decrease"), false ); }; diff --git a/src/features/playerQuality/index.ts b/src/features/playerQuality/index.ts index f6559dc9..486a3736 100644 --- a/src/features/playerQuality/index.ts +++ b/src/features/playerQuality/index.ts @@ -1,6 +1,6 @@ import type { YouTubePlayerDiv, YoutubePlayerQualityLevel } from "@/src/types"; -import { browserColorLog, chooseClosestQuality, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { browserColorLog, chooseClosestQuality, isLivePage, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; /** * Sets the player quality based on the options received from a specific message. @@ -10,41 +10,32 @@ import { browserColorLog, chooseClosestQuality, isShortsPage, isWatchPage, waitF */ export default async function setPlayerQuality(): Promise { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_automatically_set_quality, player_quality, player_quality_fallback_strategy } } - } = optionsData; - + } = await waitForSpecificMessage("options", "request_data", "content"); // If automatically set quality option is disabled, return if (!enable_automatically_set_quality) return; - // If player quality is not specified, return if (!player_quality) return; - // Get the player element const playerContainer = - isWatchPage() ? document.querySelector("div#movie_player") + isWatchPage() || isLivePage() ? document.querySelector("div#movie_player") : isShortsPage() ? document.querySelector("div#shorts-player") : null; - // If player element is not available, return if (!playerContainer) return; - // If setPlaybackQuality method is not available in the player, return if (!playerContainer.setPlaybackQuality) return; - // Get the available quality levels const availableQualityLevels = (await playerContainer.getAvailableQualityLevels()) as YoutubePlayerQualityLevel[]; - // Check if the specified player quality is available if (player_quality && player_quality !== "auto") { const closestQuality = chooseClosestQuality(player_quality, availableQualityLevels, player_quality_fallback_strategy); if (!closestQuality) return; // Log the message indicating the player quality being set browserColorLog(`Setting player quality to ${closestQuality}`, "FgMagenta"); - // Set the playback quality and update the default quality in the dataset void playerContainer.setPlaybackQualityRange(closestQuality); playerContainer.dataset.defaultQuality = closestQuality; diff --git a/src/features/playerSpeed/index.ts b/src/features/playerSpeed/index.ts index 0d684cfb..4cfe7748 100644 --- a/src/features/playerSpeed/index.ts +++ b/src/features/playerSpeed/index.ts @@ -24,12 +24,11 @@ export async function setPlayerSpeed(input?: number): Promise { // If the input is a number, set the player speed to the given number if (input === undefined) { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); ({ data: { options: { enable_forced_playback_speed: enablePlayerSpeed, player_speed: playerSpeed } } - } = optionsData); + } = await waitForSpecificMessage("options", "request_data", "content")); } else if (typeof input === "number") { playerSpeed = input; } @@ -45,7 +44,6 @@ export async function setPlayerSpeed(input?: number): Promise { // If player element is not available, return if (!playerContainer) return; const video = document.querySelector("video.html5-main-video"); - // If setPlaybackRate method is not available in the player, return if (!playerContainer.setPlaybackRate) return; const playerVideoData = await playerContainer.getVideoData(); @@ -53,7 +51,6 @@ export async function setPlayerSpeed(input?: number): Promise { if (playerVideoData.isLive) return; // Log the message indicating the player speed being set browserColorLog(`Setting player speed to ${playerSpeed}`, "FgMagenta"); - // Set the playback speed void playerContainer.setPlaybackRate(playerSpeed); // Set the video playback speed @@ -70,11 +67,11 @@ export function restorePlayerSpeed() { isWatchPage() ? document.querySelector("div#movie_player") : isShortsPage() ? document.querySelector("div#shorts-player") : null; - const video = document.querySelector("video.html5-main-video"); // If player element is not available, return if (!playerContainer) return; // If setPlaybackRate method is not available in the player, return if (!playerContainer.setPlaybackRate) return; + const video = document.querySelector("video.html5-main-video"); if (!video) return; // Log the message indicating the player speed being set browserColorLog(`Restoring player speed to ${playerSpeed}`, "FgMagenta"); @@ -101,12 +98,10 @@ export function setupPlaybackSpeedChangeListener() { } window.localStorage.setItem("playerSpeed", String(playerSpeed)); }; - // Create an observer instance const playerSpeedMenuObserver = new MutationObserver((mutationsList: MutationRecord[]) => { mutationsList.forEach((mutation) => { const { target: targetElement } = mutation as { target: HTMLDivElement } & MutationRecord; - // Check if the target element has the desired structure const panelHeader = targetElement.querySelector("div.ytp-panel > div.ytp-panel-header"); const panelMenu = targetElement.querySelector("div.ytp-panel > div.ytp-panel-menu"); @@ -129,7 +124,6 @@ export function setupPlaybackSpeedChangeListener() { }); }); const config: MutationObserverInit = { childList: true, subtree: true }; - if (settingsPanelMenu) { playerSpeedMenuObserver.observe(settingsPanelMenu, config); customSpeedSliderObserver.observe(settingsPanelMenu, { diff --git a/src/features/playlistLength/index.ts b/src/features/playlistLength/index.ts new file mode 100644 index 00000000..f36f8303 --- /dev/null +++ b/src/features/playlistLength/index.ts @@ -0,0 +1,49 @@ +import type { Nullable } from "@/src/types"; + +import eventManager from "@/src/utils/EventManager"; +import { YouTube_Enhancer_Public_Youtube_Data_API_V3_Key } from "@/src/utils/constants"; +import { isWatchPage, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; + +import { headerSelector, initializePlaylistLength, playlistItemsSelector } from "./utils"; +let documentObserver: Nullable = null; +export async function enablePlaylistLength() { + const IsWatchPage = isWatchPage(); + const { + data: { + options: { + enable_playlist_length, + playlist_length_get_method: playlistLengthGetMethod, + playlist_watch_time_get_method: playlistWatchTimeGetMethod, + youtube_data_api_v3_key + } + } + } = await waitForSpecificMessage("options", "request_data", "content"); + if (!enable_playlist_length) return; + const urlContainsListParameter = window.location.href.includes("list="); + if (!urlContainsListParameter) return; + await waitForAllElements([headerSelector(), playlistItemsSelector()]); + const apiKey = youtube_data_api_v3_key === "" ? YouTube_Enhancer_Public_Youtube_Data_API_V3_Key : youtube_data_api_v3_key; + const pageType = IsWatchPage ? "watch" : "playlist"; + try { + documentObserver = await initializePlaylistLength({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod + }); + } catch (error) { + documentObserver?.disconnect(); + documentObserver = null; + documentObserver = await initializePlaylistLength({ + apiKey, + pageType, + playlistLengthGetMethod: "html", + playlistWatchTimeGetMethod + }); + } +} +export function disablePlaylistLength() { + eventManager.removeEventListeners("playlistLength"); + if (documentObserver) documentObserver.disconnect(); + document.querySelector("#yte-playlist-length-ui")?.remove(); +} diff --git a/src/features/playlistLength/utils.ts b/src/features/playlistLength/utils.ts new file mode 100644 index 00000000..530a352f --- /dev/null +++ b/src/features/playlistLength/utils.ts @@ -0,0 +1,405 @@ +import type { Nullable, PlaylistLengthGetMethod, PlaylistWatchTimeGetMethod, VideoDetails, YouTubePlaylistItem } from "@/src/types"; + +import eventManager from "@/src/utils/EventManager"; +import { + conditionalStyles, + createStyledElement, + formatDuration, + isNewYouTubeVideoLayout, + isWatchPage, + parseISO8601Duration, + timeStringToSeconds, + waitForAllElements +} from "@/src/utils/utilities"; +import { z } from "zod"; + +export const headerSelector = () => + isWatchPage() ? + isNewYouTubeVideoLayout() ? "#page-manager > ytd-watch-grid #playlist #header-contents" + : "#page-manager > ytd-watch-flexy #playlist #header-contents" + : "ytd-playlist-header-renderer div.immersive-header-container div.immersive-header-content"; +export const playlistItemsSelector = () => + isWatchPage() ? "ytd-playlist-panel-renderer:not([hidden]) div#container div#items" : "ytd-playlist-video-list-renderer div#contents"; +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const youtubePlaylistResponseSchema = z.object({ + items: z.array(z.object({ contentDetails: z.object({ videoId: z.string() }) })), + nextPageToken: z.string().optional() +}); +const youtubeVideoDurationResponseSchema = z.object({ + items: z.array(z.object({ contentDetails: z.object({ duration: z.string() }) })) +}); +const youtubeDataAPIErrorSchema = z.object({ + error: z.object({ + code: z.number(), + errors: z.array( + z.object({ + reason: z.string() + }) + ), + message: z.string() + }) +}); +export async function fetchPlaylistVideos(playlistId: string, apiKey: string): Promise { + const playlistItemsUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails&maxResults=50&fields=items/contentDetails/videoId,nextPageToken&key=${apiKey}&playlistId=${playlistId}`; + let nextPageToken: string | undefined = undefined; + const allVideos: YouTubePlaylistItem[] = []; + try { + do { + const playlistResponse = await fetch(`${playlistItemsUrl}${nextPageToken ? `&pageToken=${nextPageToken}` : ""}`); + const data = await playlistResponse.json(); + const youtubeDataAPIParsed = youtubeDataAPIErrorSchema.safeParse(data); + if (youtubeDataAPIParsed.success && youtubeDataAPIParsed.data.error) { + switch (youtubeDataAPIParsed.data.error.code) { + case 403: + throw new Error("YouTube Enhancer (YouTube Data API V3): Quota exceeded. Please try again later."); + case 404: + throw new Error("YouTube Enhancer (YouTube Data API V3): Playlist not found. Please try again later."); + default: + throw new Error("YouTube Enhancer (YouTube Data API V3): Unknown error. Please try again later."); + } + } + const parsedData = youtubePlaylistResponseSchema.safeParse(data); + if (!parsedData.success) throw new Error("Failed to parse playlist response."); + if (parsedData.data.items && parsedData.data.items.length === 0) throw new Error("No items found in playlist."); + nextPageToken = parsedData.data.nextPageToken || ""; + allVideos.push(...parsedData.data.items); + await delay(40); // Adding delay between requests to avoid rate limiting + } while (nextPageToken); + return allVideos; + } catch (error) { + throw new Error(`Error fetching playlist videos: ${error}`); + } +} +export async function getPlaylistDuration(playlistVideos: YouTubePlaylistItem[], apiKey: string) { + const videoIds = playlistVideos.map((video) => video.contentDetails.videoId); + try { + const videoDurationPromises = videoIds.map(async (videoId) => { + await delay(1); // Adding delay between requests to avoid rate limiting + return getVideoDuration(videoId, apiKey); + }); + const videoDurations = await Promise.all(videoDurationPromises); + const totalDuration = videoDurations.reduce((acc, duration) => acc + duration, 0); + return totalDuration; + } catch (error) { + throw new Error(`Error fetching playlist duration: ${error}`); + } +} +export async function getVideoDuration(videoId: string, apiKey: string) { + const videoDetailsUrl = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&fields=items/contentDetails/duration&key=${apiKey}&id=${videoId}`; + try { + const videoResponse = await fetch(videoDetailsUrl); + const data = await videoResponse.json(); + const youtubeDataAPIParsed = youtubeDataAPIErrorSchema.safeParse(data); + const parsedData = youtubeVideoDurationResponseSchema.safeParse(data); + if (!parsedData.success) throw new Error("Failed to parse video duration response."); + if ( + youtubeDataAPIParsed.success && + youtubeDataAPIParsed.data.error && + youtubeDataAPIParsed.data.error.code === 403 && + youtubeDataAPIParsed.data.error.errors.find((e) => e.reason === "quotaExceeded") + ) { + throw new Error("Quota exceeded. Please try again later."); + } + const videoDuration = parsedData.data.items.at(0)?.contentDetails?.duration; + if (!videoDuration) throw new Error("Failed to get video duration."); + return parseISO8601Duration(videoDuration); + } catch (error) { + throw new Error(`Error getting video duration: ${error}`); + } +} +export function getPlaylistItemsFromWatchPage() { + const playlistItems = document.querySelector( + isNewYouTubeVideoLayout() ? "#page-manager > ytd-watch-grid #playlist #items" : "#page-manager > ytd-watch-flexy #playlist #items" + ); + if (!playlistItems) return []; + const { children: videos } = playlistItems; + return Array.from(videos) as HTMLElement[]; +} +export function getPlaylistItemsFromPlaylistPage() { + const playlistItems = document.querySelector("ytd-playlist-video-list-renderer div#contents"); + if (!playlistItems) return []; + const { children: videos } = playlistItems; + return Array.from(videos) as HTMLElement[]; +} +export function getPlaylistItemsWatchedProgress(playlistItems: HTMLElement[]): VideoDetails[] { + return playlistItems.map(getVideoDetails); +} +function getWatchedPercentage(videoElement: Element): number { + const progressBar = videoElement.querySelector(".ytd-thumbnail-overlay-resume-playback-renderer,#progress"); + if (!progressBar) return 0; + return parseFloat(progressBar.style.width) || 0; +} +function getVideoDurationInSeconds(videoElement: Element): number { + const durationElement = videoElement.querySelector("ytd-thumbnail-overlay-time-status-renderer > div#time-status"); + if (!durationElement || !durationElement.textContent?.trim()) return 0; + return timeStringToSeconds(durationElement.textContent.trim()); +} +function findIndexById(array: VideoDetails[], id: string): number { + return array.findIndex((video) => video.videoId === id); +} +export function sliceArrayById(array: VideoDetails[], id: string): VideoDetails[] { + const index = findIndexById(array, id); + if (index === -1) { + // If the videoId is not found, return an empty array or handle it as needed + return []; + } + // Slice the array from the start to the found index (inclusive) + return array.slice(0, index); +} +function getVideoId(videoElement: Element): null | string { + const videoIdElement = videoElement.querySelector("a#thumbnail"); + if (!videoIdElement) return null; + const url = new URL(`https://youtube.com${videoIdElement.href}`); + return url.searchParams.get("v"); +} +function getVideoDetails(videoElement: Element): VideoDetails { + const videoId = getVideoId(videoElement); + const duration = getVideoDurationInSeconds(videoElement); + const progress = Math.floor((getWatchedPercentage(videoElement) / 100) * duration); + return { + duration: duration, + progress: progress, + videoId: videoId + }; +} +type VideoTimeState = { totalTimeSeconds: number; watchedTimeSeconds: number }; +type PageType = "playlist" | "watch"; +type PlaylistLengthParameters = { + apiKey: string; + pageType: PageType; + playlistLengthGetMethod: PlaylistLengthGetMethod; + playlistWatchTimeGetMethod: PlaylistWatchTimeGetMethod; +}; +export function createPlaylistLengthUIElement( + initialState: VideoTimeState, + pageType: PageType +): { + element: HTMLDivElement; + update: (state: VideoTimeState) => void; +} { + const wrapper = createStyledElement({ + elementId: "yte-playlist-length-ui", + elementType: "div", + styles: { + backgroundColor: "rgb(43, 43, 43)", + border: "2px solid rgb(84, 84, 84)", + borderRadius: "10px", + height: "48px", + marginBottom: "10px", + overflow: "hidden", + position: "relative", + transform: "translate(-50%, 0%)", + ...conditionalStyles({ + condition: pageType === "watch", + left: "48.8%", + top: "50%", + width: "100%" + }), + ...conditionalStyles({ + condition: pageType === "playlist", + left: "50%", + marginTop: "10px", + width: "99%" + }) + } + }); + const watchedProgressBar = createStyledElement({ + elementId: "yte-playlist-length-ui-watchedProgressBar", + elementType: "div", + styles: { backgroundColor: "#522628", borderRadius: "8px", height: "100%" } + }); + const videoTimeDisplay = createStyledElement({ + elementId: "yte-playlist-length-ui-times", + elementType: "div", + styles: { + bottom: "15px", + color: "var(--yt-spec-text-primary)", + fontSize: "15px", + marginLeft: "19px", + position: "absolute" + } + }); + videoTimeDisplay.textContent = `${formatDuration(initialState.watchedTimeSeconds)} / ${formatDuration(initialState.totalTimeSeconds)} (- ${formatDuration(initialState.totalTimeSeconds - initialState.watchedTimeSeconds)})`; + const percentageWatched = createStyledElement({ + elementId: "yte-playlist-length-ui-percentageWatched", + elementType: "div", + styles: { + backgroundColor: "#3f3f3f", + borderRadius: "6px", + bottom: "0px", + color: "var(--yt-spec-text-primary)", + fontSize: "15px", + padding: "4px 8px", + position: "absolute", + right: "0px", + transform: "translateX(-24%) translateY(-11px)" + } + }); + percentageWatched.textContent = `0%`; + wrapper.append(watchedProgressBar, percentageWatched, videoTimeDisplay); + const updateElement = ({ totalTimeSeconds, watchedTimeSeconds }: VideoTimeState) => { + const watchedPercentage = + Number.isNaN(Math.floor((watchedTimeSeconds / totalTimeSeconds) * 100)) ? 0 + : Math.floor((watchedTimeSeconds / totalTimeSeconds) * 100) === Infinity ? 0 + : Math.floor((watchedTimeSeconds / totalTimeSeconds) * 100); + watchedProgressBar.style.width = `${watchedPercentage}%`; + videoTimeDisplay.textContent = `${formatDuration(watchedTimeSeconds)} / ${formatDuration(totalTimeSeconds)} (- ${formatDuration(totalTimeSeconds - watchedTimeSeconds)})`; + percentageWatched.textContent = `${watchedPercentage}%`; + }; + wrapper.title = window.i18nextInstance.t("pages.content.features.playlistLength.title"); + updateElement(initialState); + return { + element: wrapper, + update: updateElement + }; +} +export async function appendPlaylistLengthUIElement(playlistLengthUIElement: HTMLDivElement) { + await waitForAllElements([headerSelector()]); + const headerContents = document.querySelector(headerSelector()); + if (!headerContents) return null; + if (document.querySelector("#yte-playlist-length-ui") !== null) { + document.querySelector("#yte-playlist-length-ui")?.remove(); + } + headerContents.append(playlistLengthUIElement); +} +async function getDurationFromAPI(playlistId: string, apiKey: string): Promise { + const playlistVideos = await fetchPlaylistVideos(playlistId, apiKey); + return getPlaylistDuration(playlistVideos, apiKey); +} +type WatchTimeParameters = { + pageType: PageType; + playlistItemsVideoDetails: VideoDetails[]; + playlistWatchTimeGetMethod: PlaylistWatchTimeGetMethod; +}; + +function calculateWatchedTime({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod }: WatchTimeParameters): number { + if (pageType === "watch") { + const slicedItems = sliceArrayById(playlistItemsVideoDetails, getCurrentVideoId()!); + return ( + slicedItems.reduce((total, video) => total + (playlistWatchTimeGetMethod === "youtube" ? video.progress : video.duration), 0) + + getCurrentVideoTime() + ); + } else { + return playlistItemsVideoDetails.reduce((total, video) => total + video.progress, 0); + } +} +function getDurationAndWatchedTimeHTML({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod }: WatchTimeParameters): VideoTimeState { + const totalTimeSeconds = playlistItemsVideoDetails.reduce((total, video) => total + video.duration, 0); + const watchedTimeSeconds = calculateWatchedTime({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod }); + return { totalTimeSeconds, watchedTimeSeconds }; +} +export async function getDataForPlaylistLengthUIElement({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod +}: PlaylistLengthParameters): Promise { + const playlistId = getPlaylistId(); + if (!playlistId) return { totalTimeSeconds: 0, watchedTimeSeconds: 0 }; + // Check cache + if (playlistLengthGetMethod === "api" && window.cachedPlaylistDuration && window.cachedPlaylistDuration.playlistId === playlistId) { + const { + cachedPlaylistDuration: { totalTimeSeconds } + } = window; + const playlistItems = pageType === "watch" ? getPlaylistItemsFromWatchPage() : getPlaylistItemsFromPlaylistPage(); + const playlistItemsVideoDetails = getPlaylistItemsWatchedProgress(playlistItems); + const watchedTimeSeconds = calculateWatchedTime({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod }); + return { totalTimeSeconds, watchedTimeSeconds }; + } + const playlistItems = pageType === "watch" ? getPlaylistItemsFromWatchPage() : getPlaylistItemsFromPlaylistPage(); + const playlistItemsVideoDetails = getPlaylistItemsWatchedProgress(playlistItems); + let totalTimeSeconds: number; + if (playlistLengthGetMethod === "api") { + totalTimeSeconds = await getDurationFromAPI(playlistId, apiKey); + // Cache the duration + window.cachedPlaylistDuration = { playlistId, totalTimeSeconds }; + } else if (playlistLengthGetMethod === "html") { + ({ totalTimeSeconds } = getDurationAndWatchedTimeHTML({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod })); + } else { + return { totalTimeSeconds: 0, watchedTimeSeconds: 0 }; + } + const watchedTimeSeconds = calculateWatchedTime({ pageType, playlistItemsVideoDetails, playlistWatchTimeGetMethod }); + return { totalTimeSeconds, watchedTimeSeconds }; +} +function getPlaylistId() { + const playlistId = new URLSearchParams(window.location.search).get("list"); + return playlistId; +} +function getCurrentVideoId() { + const videoId = new URLSearchParams(window.location.search).get("v"); + return videoId; +} +function getCurrentVideoTime() { + const videoElement = document.querySelector("video") as HTMLVideoElement; + return videoElement.currentTime; +} +export async function initializePlaylistLength({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod +}: PlaylistLengthParameters): Promise> { + const playlistHeader = document.querySelector(headerSelector()); + if (!playlistHeader) return null; + let { totalTimeSeconds, watchedTimeSeconds } = await getDataForPlaylistLengthUIElement({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod + }); + if (playlistHeader) { + const videoElement = document.querySelector("video"); + const playlistItemsElement = document.querySelector(playlistItemsSelector()); + if (!playlistItemsElement) return null; + const { playbackRate: playerSpeed = 1 } = videoElement || {}; + const { element, update } = createPlaylistLengthUIElement( + { + totalTimeSeconds: Math.floor(totalTimeSeconds / playerSpeed), + watchedTimeSeconds: Math.floor(watchedTimeSeconds / playerSpeed) + }, + pageType + ); + await appendPlaylistLengthUIElement(element); + const documentObserver = new MutationObserver(() => { + void (async () => { + const videoElement = document.querySelector("video"); + const { playbackRate: playerSpeed = 1 } = videoElement || {}; + ({ totalTimeSeconds, watchedTimeSeconds } = await getDataForPlaylistLengthUIElement({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod + })); + update({ + totalTimeSeconds: Math.floor(totalTimeSeconds / playerSpeed), + watchedTimeSeconds: Math.floor(watchedTimeSeconds / playerSpeed) + }); + })(); + }); + documentObserver.observe(document.documentElement, { childList: true, subtree: true }); + if (videoElement) + eventManager.addEventListener( + videoElement, + "timeupdate", + () => { + void (async () => { + const videoElement = document.querySelector("video"); + const { playbackRate: playerSpeed = 1 } = videoElement || {}; + ({ totalTimeSeconds, watchedTimeSeconds } = await getDataForPlaylistLengthUIElement({ + apiKey, + pageType, + playlistLengthGetMethod, + playlistWatchTimeGetMethod + })); + update({ + totalTimeSeconds: Math.floor(totalTimeSeconds / playerSpeed), + watchedTimeSeconds: Math.floor(watchedTimeSeconds / playerSpeed) + }); + })(); + }, + "playlistLength" + ); + return documentObserver; + } + return null; +} diff --git a/src/features/remainingTime/index.ts b/src/features/remainingTime/index.ts index 1abdc1cc..d1b2c442 100644 --- a/src/features/remainingTime/index.ts +++ b/src/features/remainingTime/index.ts @@ -12,13 +12,10 @@ function playerTimeUpdateListener() { isWatchPage() ? document.querySelector("div#movie_player") : isShortsPage() ? document.querySelector("div#shorts-player") : null; - // If player element is not available, return if (!playerContainer) return; - // Get the video element const videoElement = playerContainer.querySelector("video"); - // If video element is not available, return if (!videoElement) return; // Get the remaining time element @@ -30,12 +27,11 @@ function playerTimeUpdateListener() { } export async function setupRemainingTime() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_remaining_time } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If remaining time option is disabled, return if (!enable_remaining_time) return; const timeDisplay = document.querySelector(".ytp-time-display > span:nth-of-type(2)"); @@ -55,7 +51,6 @@ export async function setupRemainingTime() { const remainingTime = await calculateRemainingTime({ playerContainer, videoElement }); const remainingTimeElementExists = document.querySelector("span#ytp-time-remaining") !== null; if (playerVideoData.isLive && !remainingTimeElementExists) return; - const remainingTimeElement = document.querySelector("span#ytp-time-remaining") ?? document.createElement("span"); // If the video is live return if (playerVideoData.isLive && remainingTimeElementExists) { diff --git a/src/features/remainingTime/utils.ts b/src/features/remainingTime/utils.ts index b255782b..99a26897 100644 --- a/src/features/remainingTime/utils.ts +++ b/src/features/remainingTime/utils.ts @@ -7,7 +7,6 @@ export function formatTime(timeInSeconds: number) { Math.floor((timeInSeconds % 3600) / 60), Math.floor(timeInSeconds % 60) ]; - const formattedUnits: string[] = units.reduce((acc: string[], unit) => { if (acc.length > 0) { acc.push(unit.toString().padStart(2, "0")); @@ -16,7 +15,6 @@ export function formatTime(timeInSeconds: number) { acc.push(unit.toString()); } } - return acc; }, []); return `${formattedUnits.length > 0 ? formattedUnits.join(":") : "0"}`; @@ -30,14 +28,11 @@ export async function calculateRemainingTime({ }) { // Get the player speed (playback rate) const { playbackRate } = videoElement; - // Get the current time and duration of the video const currentTime = await playerContainer.getCurrentTime(); const duration = await playerContainer.getDuration(); - // Calculate the remaining time in seconds const remainingTimeInSeconds = (duration - currentTime) / playbackRate; - // Format the remaining time return ` (-${formatTime(remainingTimeInSeconds)})`; } diff --git a/src/features/rememberVolume/index.ts b/src/features/rememberVolume/index.ts index bb29bf40..e66d024e 100644 --- a/src/features/rememberVolume/index.ts +++ b/src/features/rememberVolume/index.ts @@ -1,6 +1,6 @@ import type { YouTubePlayerDiv } from "@/src/types"; -import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { isLivePage, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; import { setRememberedVolume, setupVolumeChangeListener } from "./utils"; @@ -12,31 +12,29 @@ import { setRememberedVolume, setupVolumeChangeListener } from "./utils"; */ export default async function enableRememberVolume(): Promise { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_remember_last_volume: enableRememberVolume, remembered_volumes: rememberedVolumes } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the volume is not being remembered, return if (!enableRememberVolume) return; const IsWatchPage = isWatchPage(); + const IsLivePage = isLivePage(); const IsShortsPage = isShortsPage(); // Get the player container element const playerContainer = - IsWatchPage ? document.querySelector("div#movie_player") + IsWatchPage || IsLivePage ? document.querySelector("div#movie_player") : IsShortsPage ? document.querySelector("div#shorts-player") : null; - // If player container is not available, return if (!playerContainer) return; - // If setVolume method is not available in the player container, return if (!playerContainer.setVolume) return; void setRememberedVolume({ enableRememberVolume, isShortsPage: IsShortsPage, - isWatchPage: IsWatchPage, + isWatchPage: IsWatchPage || IsLivePage, playerContainer, rememberedVolumes }); diff --git a/src/features/rememberVolume/utils.ts b/src/features/rememberVolume/utils.ts index e8a10040..37ae7b30 100644 --- a/src/features/rememberVolume/utils.ts +++ b/src/features/rememberVolume/utils.ts @@ -1,22 +1,22 @@ import type { YouTubePlayerDiv, configuration } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { browserColorLog, isShortsPage, isWatchPage, sendContentOnlyMessage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { browserColorLog, isLivePage, isShortsPage, isWatchPage, sendContentOnlyMessage, waitForSpecificMessage } from "@/src/utils/utilities"; export async function setupVolumeChangeListener() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_remember_last_volume: enableRememberVolume } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the volume is not being remembered, return if (!enableRememberVolume) return; const IsWatchPage = isWatchPage(); + const IsLivePage = isLivePage(); const IsShortsPage = isShortsPage(); // Get the player container element const playerContainer = - IsWatchPage ? document.querySelector("div#movie_player") + IsWatchPage || IsLivePage ? document.querySelector("div#movie_player") : IsShortsPage ? document.querySelector("div#shorts-player") : null; if (!playerContainer) return; @@ -29,7 +29,7 @@ export async function setupVolumeChangeListener() { void (async () => { if (!currentTarget) return; const newVolume = await playerContainer.getVolume(); - if (IsWatchPage) { + if (IsWatchPage || IsLivePage) { // Send a "setVolume" message to the content script sendContentOnlyMessage("setRememberedVolume", { watchPageVolume: newVolume }); } else if (IsShortsPage) { diff --git a/src/features/removeRedirect/index.ts b/src/features/removeRedirect/index.ts index 0f17f6b2..693d162d 100644 --- a/src/features/removeRedirect/index.ts +++ b/src/features/removeRedirect/index.ts @@ -2,16 +2,14 @@ import { type Nullable } from "@/src/types"; import { browserColorLog, waitForSpecificMessage } from "@/src/utils/utilities"; export default async function enableRemoveRedirect() { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_redirect_remover: removeRedirectEnabled } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!removeRedirectEnabled) return; browserColorLog(`Enabling removeRedirect`, "FgMagenta"); const regex = /https\:\/\/www\.youtube\.com\/redirect\?.+/gm; - const links: NodeListOf = document.querySelectorAll( ".yt-core-attributed-string__link, .yt-simple-endpoint.style-scope.yt-formatted-string" ); @@ -22,7 +20,6 @@ export default async function enableRemoveRedirect() { link.setAttribute("href", urlParams.get("q") || ""); } }); - const callback: MutationCallback = (mutationsList: MutationRecord[]) => { for (const mutation of mutationsList) { if (mutation.type === "childList") { @@ -38,7 +35,6 @@ export default async function enableRemoveRedirect() { } } }; - const observer: MutationObserver = new MutationObserver(callback); observer.observe(document.body, { attributes: false, childList: true, subtree: true }); } diff --git a/src/features/screenshotButton/index.ts b/src/features/screenshotButton/index.ts index edb1da6a..4328c15c 100644 --- a/src/features/screenshotButton/index.ts +++ b/src/features/screenshotButton/index.ts @@ -12,26 +12,21 @@ async function takeScreenshot(videoElement: HTMLVideoElement) { // Create a canvas element and get its context const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); - // Set the dimensions of the canvas to the video's dimensions const { videoHeight, videoWidth } = videoElement; canvas.width = videoWidth; canvas.height = videoHeight; - // Draw the video element onto the canvas if (!context) return; context.drawImage(videoElement, 0, 0, canvas.width, canvas.height); - // Wait for the options message and get the format from it - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { screenshot_format, screenshot_save_as } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); const blob = await new Promise>((resolve) => canvas.toBlob(resolve, "image/png")); if (!blob) return; - switch (screenshot_save_as) { case "clipboard": { const screenshotButton = getFeatureButton("screenshotButton"); @@ -65,7 +60,6 @@ async function takeScreenshot(videoElement: HTMLVideoElement) { export const addScreenshotButton: AddButtonFunction = async () => { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { @@ -73,8 +67,7 @@ export const addScreenshotButton: AddButtonFunction = async () => { enable_screenshot_button: enableScreenshotButton } } - } = optionsData; - + } = await waitForSpecificMessage("options", "request_data", "content"); // If the screenshot button option is disabled, return if (!enableScreenshotButton) return; // Add a click event listener to the screenshot button diff --git a/src/features/scrollWheelSpeedControl/index.ts b/src/features/scrollWheelSpeedControl/index.ts index 3070b500..dc764c99 100644 --- a/src/features/scrollWheelSpeedControl/index.ts +++ b/src/features/scrollWheelSpeedControl/index.ts @@ -1,5 +1,6 @@ import type { YouTubePlayerDiv } from "@/src/types"; +import { calculatePlaybackButtonSpeed, updatePlaybackSpeedButtonTooltip } from "@/src/features/playbackSpeedButtons"; import { setupScrollListeners } from "@/src/features/scrollWheelVolumeControl/utils"; import OnScreenDisplayManager from "@/src/utils/OnScreenDisplayManager"; import { isShortsPage, isWatchPage, preventScroll, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; @@ -33,7 +34,6 @@ export default async function adjustSpeedOnScrollWheel() { const setOptionsData = async () => { return (optionsData = await waitForSpecificMessage("options", "request_data", "content")); }; - void (async () => { if (!optionsData) { return void (await setOptionsData()); @@ -48,6 +48,7 @@ export default async function adjustSpeedOnScrollWheel() { osd_display_padding, osd_display_position, osd_display_type, + playback_buttons_speed, scroll_wheel_speed_control_modifier_key, speed_adjustment_steps } @@ -56,14 +57,11 @@ export default async function adjustSpeedOnScrollWheel() { const wheelEvent = event as WheelEvent; // If the modifier key is required and not pressed, return if (enable_scroll_wheel_speed_control && !wheelEvent[scroll_wheel_speed_control_modifier_key]) return void (await setOptionsData()); - // Only prevent default scroll wheel behavior // if we are going to handle the event preventScroll(wheelEvent); - // Update the options data after preventScroll() await setOptionsData(); - // Get the player element const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") @@ -71,11 +69,18 @@ export default async function adjustSpeedOnScrollWheel() { : null; // If player element is not available, return if (!playerContainer) return; - // Adjust the speed based on the scroll direction const scrollDelta = wheelEvent.deltaY < 0 ? 1 : -1; // Adjust the speed based on the scroll direction and options - const { newSpeed } = await adjustSpeed(playerContainer, scrollDelta, speed_adjustment_steps); + const { newSpeed } = await adjustSpeed(scrollDelta, speed_adjustment_steps); + await updatePlaybackSpeedButtonTooltip( + "increasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(newSpeed, playback_buttons_speed, "increase") + ); + await updatePlaybackSpeedButtonTooltip( + "decreasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(newSpeed, playback_buttons_speed, "decrease") + ); new OnScreenDisplayManager( { displayColor: osd_display_color, @@ -88,14 +93,13 @@ export default async function adjustSpeedOnScrollWheel() { }, "yte-osd", { - max: 4, + max: 16, type: "speed", value: newSpeed } ); })(); }; - // Set up the scroll wheel event listeners on the specified container selectors for (const selector of containerSelectors) { setupScrollListeners(selector, handleWheel); diff --git a/src/features/scrollWheelSpeedControl/utils.ts b/src/features/scrollWheelSpeedControl/utils.ts index eeea217c..ba51d0cb 100644 --- a/src/features/scrollWheelSpeedControl/utils.ts +++ b/src/features/scrollWheelSpeedControl/utils.ts @@ -1,7 +1,8 @@ -import type { Selector, YouTubePlayerDiv } from "@/src/types"; +import { setPlayerSpeed } from "@/src/features/playerSpeed"; +import { youtubePlayerMinSpeed, type Selector } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { browserColorLog, clamp, round, toDivisible } from "@/src/utils/utilities"; +import { browserColorLog, round } from "@/src/utils/utilities"; /** * Adjust the speed based on the scroll direction. @@ -11,23 +12,17 @@ import { browserColorLog, clamp, round, toDivisible } from "@/src/utils/utilitie * @param speedStep - The speed adjustment steps. * @returns Promise that resolves with the new speed. */ -export function adjustSpeed( - playerContainer: YouTubePlayerDiv, - scrollDelta: number, - speedStep: number -): Promise<{ newSpeed: number; oldSpeed: number }> { +export function adjustSpeed(scrollDelta: number, speedStep: number): Promise<{ newSpeed: number; oldSpeed: number }> { return new Promise((resolve) => { void (async () => { - if (!playerContainer.getPlaybackRate) return; - if (!playerContainer.setPlaybackRate) return; - const video = playerContainer.querySelector("video"); - if (!video) return; - const { playbackRate: speed } = video; - const newSpeed = round(clamp(toDivisible(parseFloat((speed + scrollDelta * speedStep).toFixed(2)), speedStep), 0.25, 4), 2); - browserColorLog(`Adjusting speed by ${speedStep} to ${newSpeed}. Old speed was ${speed}`, "FgMagenta"); - await playerContainer.setPlaybackRate(newSpeed); - if (video) video.playbackRate = newSpeed; - resolve({ newSpeed, oldSpeed: speed }); + const videoElement = document.querySelector("video"); + if (!videoElement) return; + const { playbackRate: currentPlaybackSpeed } = videoElement; + const adjustmentAmount = speedStep * scrollDelta; + if (currentPlaybackSpeed + adjustmentAmount > 16 || currentPlaybackSpeed + adjustmentAmount < youtubePlayerMinSpeed) return; + const speed = round(currentPlaybackSpeed + adjustmentAmount, 2); + setPlayerSpeed(speed); + resolve({ newSpeed: speed, oldSpeed: currentPlaybackSpeed }); })(); }); } diff --git a/src/features/scrollWheelVolumeControl/index.ts b/src/features/scrollWheelVolumeControl/index.ts index d7087f02..2023f3a6 100644 --- a/src/features/scrollWheelVolumeControl/index.ts +++ b/src/features/scrollWheelVolumeControl/index.ts @@ -1,7 +1,7 @@ import type { YouTubePlayerDiv } from "@/src/types"; import OnScreenDisplayManager from "@/src/utils/OnScreenDisplayManager"; -import { isShortsPage, isWatchPage, preventScroll, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; +import { isLivePage, isShortsPage, isWatchPage, preventScroll, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; import { adjustVolume, setupScrollListeners } from "./utils"; @@ -34,7 +34,6 @@ export default async function adjustVolumeOnScrollWheel(): Promise { const setOptionsData = async () => { return (optionsData = await waitForSpecificMessage("options", "request_data", "content")); }; - void (async () => { if (!optionsData) { return void (await setOptionsData()); @@ -73,18 +72,15 @@ export default async function adjustVolumeOnScrollWheel(): Promise { // Only prevent default scroll wheel behavior // if we are going to handle the event preventScroll(wheelEvent); - // Update the options data after preventScroll() await setOptionsData(); - // Get the player element const playerContainer = - isWatchPage() ? document.querySelector("div#movie_player") + isWatchPage() || isLivePage() ? document.querySelector("div#movie_player") : isShortsPage() ? document.querySelector("div#shorts-player") : null; // If player element is not available, return if (!playerContainer) return; - // Adjust the volume based on the scroll direction const scrollDelta = wheelEvent.deltaY < 0 ? 1 : -1; // Adjust the volume based on the scroll direction and options @@ -108,7 +104,6 @@ export default async function adjustVolumeOnScrollWheel(): Promise { ); })(); }; - // Set up the scroll wheel event listeners on the specified container selectors for (const selector of containerSelectors) { setupScrollListeners(selector, handleWheel); diff --git a/src/features/shareShortener/index.ts b/src/features/shareShortener/index.ts index e744c038..33c71687 100644 --- a/src/features/shareShortener/index.ts +++ b/src/features/shareShortener/index.ts @@ -43,12 +43,11 @@ function handleInput(event: MouseEvent) { } export async function enableShareShortener() { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_share_shortener } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_share_shortener) return; cleanSearchPage(window.location.href); document.addEventListener("click", handleInput); diff --git a/src/features/shortsAutoScroll/index.ts b/src/features/shortsAutoScroll/index.ts index 8b0a9f67..155dc333 100644 --- a/src/features/shortsAutoScroll/index.ts +++ b/src/features/shortsAutoScroll/index.ts @@ -7,12 +7,11 @@ import { isShortsPage, waitForAllElements, waitForSpecificMessage } from "@/src/ export async function enableShortsAutoScroll() { if (!isShortsPage()) return; // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_shorts_auto_scroll } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); // If the shorts auto scroll option is disabled, return if (!enable_shorts_auto_scroll) return; await waitForAllElements(["#shorts-player"]); diff --git a/src/features/shortsAutoScroll/utils.ts b/src/features/shortsAutoScroll/utils.ts index 68003a8c..55b66c92 100644 --- a/src/features/shortsAutoScroll/utils.ts +++ b/src/features/shortsAutoScroll/utils.ts @@ -9,7 +9,6 @@ export const setupAutoScroll = (playerContainer: YouTubePlayerDiv, video: HTMLVi const progressState = playerContainer.getProgressState(); const currentTime = Math.floor(progressState.current); const duration = Math.floor(progressState.duration); - if (currentTime === duration) { eventManager.removeEventListener(video, "timeupdate", "shortsAutoScroll"); const nextButton = document.querySelector("#navigation-button-down > ytd-button-renderer > yt-button-shape > button"); diff --git a/src/features/skipContinueWatching/index.ts b/src/features/skipContinueWatching/index.ts index 13d04688..2d2c4526 100644 --- a/src/features/skipContinueWatching/index.ts +++ b/src/features/skipContinueWatching/index.ts @@ -5,12 +5,11 @@ interface YtdWatchElement extends Element { } export async function enableSkipContinueWatching() { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_skip_continue_watching } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_skip_continue_watching) return; browserColorLog("Enabling skipContinueWatching", "FgMagenta"); const ytdWatchElement = document.querySelector(isNewYouTubeVideoLayout() ? "ytd-watch-grid" : "ytd-watch-flexy"); diff --git a/src/features/videoHistory/index.ts b/src/features/videoHistory/index.ts index fd06fd91..371919c3 100644 --- a/src/features/videoHistory/index.ts +++ b/src/features/videoHistory/index.ts @@ -10,22 +10,20 @@ import { isWatchPage, round, sendContentMessage, + waitForAllElements, waitForSpecificMessage } from "@/utils/utilities"; export async function setupVideoHistory() { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_video_history: enableVideoHistory } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enableVideoHistory) return; + if (!isWatchPage()) return; // Get the player container element - const playerContainer = - isWatchPage() ? document.querySelector("div#movie_player") - : isShortsPage() ? null - : null; + const playerContainer = document.querySelector("div#movie_player"); // If player container is not available, return if (!playerContainer) return; const playerVideoData = await playerContainer.getVideoData(); @@ -35,7 +33,9 @@ export async function setupVideoHistory() { if (!videoId) return; const videoElement = playerContainer.querySelector("video.video-stream.html5-main-video"); if (!videoElement) return; - + await waitForAllElements(["#owner #upload-info #channel-name"]); + const isOfficialArtistChannel = document.querySelector("#owner #upload-info #channel-name .badge-style-type-verified-artist") !== null; + if (isOfficialArtistChannel) return; const videoPlayerTimeUpdateListener = () => { void (async () => { const currentTime = await playerContainer.getCurrentTime(); @@ -53,26 +53,24 @@ export async function setupVideoHistory() { } export async function promptUserToResumeVideo(cb: () => void) { // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); const { data: { options: { enable_video_history: enableVideoHistory, video_history_resume_type } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enableVideoHistory) return; - // Get the player container element const playerContainer = isWatchPage() ? document.querySelector("div#movie_player") : isShortsPage() ? null : null; - // If player container is not available, return if (!playerContainer) return; - + await waitForAllElements(["#owner #upload-info #channel-name"]); + const isOfficialArtistChannel = document.querySelector("#owner #upload-info #channel-name .badge-style-type-verified-artist") !== null; + if (isOfficialArtistChannel) return; const { video_id: videoId } = await playerContainer.getVideoData(); if (!videoId) return; - const videoHistoryOneData = await waitForSpecificMessage("videoHistoryOne", "request_data", "content", { id: videoId }); if (!videoHistoryOneData) { cb(); @@ -101,7 +99,6 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine const resumeButtonId = "resume-prompt-button"; const promptId = "resume-prompt"; const progressBarDuration = 15; - const prompt = createStyledElement({ elementId: promptId, elementType: "div", @@ -133,7 +130,6 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine zIndex: "1000" } }); - const closeButton = createStyledElement({ elementId: closeButtonId, elementType: "button", @@ -168,7 +164,6 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine } }); resumeButton.textContent = window.i18nextInstance.t("pages.content.features.videoHistory.resumeButton"); - function startCountdown() { if (prompt) prompt.style.display = "block"; if (animationFrameId) { @@ -188,24 +183,20 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine } animationFrameId = requestAnimationFrame(updateResumeProgress); } - function hidePrompt() { if (animationFrameId) cancelAnimationFrame(animationFrameId); prompt.style.display = "none"; cb(); } - function resumeButtonClickListener() { hidePrompt(); browserColorLog(window.i18nextInstance.t("messages.resumingVideo", { VIDEO_TIME: formatTime(videoHistoryEntry.timestamp) }), "FgGreen"); void playerContainer.seekTo(videoHistoryEntry.timestamp, true); cb(); } - if (!elementExists(progressBarId)) { prompt.appendChild(progressBar); } - if (!elementExists(closeButtonId)) { prompt.appendChild(closeButton); } @@ -217,9 +208,7 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine }); eventManager.removeEventListener(closeButton, "mouseover", "videoHistory"); eventManager.addEventListener(closeButton, "mouseover", resumePromptCloseButtonMouseOverListener, "videoHistory"); - startCountdown(); - const closeListener = () => { hidePrompt(); }; @@ -227,7 +216,6 @@ function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContaine eventManager.addEventListener(resumeButton, "click", resumeButtonClickListener, "videoHistory"); eventManager.removeEventListener(closeButton, "click", "videoHistory"); eventManager.addEventListener(closeButton, "click", closeListener, "videoHistory"); - // Display the prompt if (!elementExists(promptId)) { prompt.appendChild(resumeButton); diff --git a/src/features/volumeBoost/index.ts b/src/features/volumeBoost/index.ts index 80bd634b..31b72efc 100644 --- a/src/features/volumeBoost/index.ts +++ b/src/features/volumeBoost/index.ts @@ -7,16 +7,13 @@ import eventManager from "@/src/utils/EventManager"; import { browserColorLog, formatError, waitForSpecificMessage } from "@/src/utils/utilities"; export default async function volumeBoost() { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); - const { data: { options: { enable_volume_boost, volume_boost_amount, volume_boost_mode } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); if (!enable_volume_boost) return; setupVolumeBoost(); - if (volume_boost_mode === "per_video") { await addVolumeBoostButton(); } else if (volume_boost_mode === "global") { @@ -25,14 +22,11 @@ export default async function volumeBoost() { } export async function enableVolumeBoost() { setupVolumeBoost(); - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); - const { data: { options: { volume_boost_amount } } - } = optionsData; - + } = await waitForSpecificMessage("options", "request_data", "content"); applyVolumeBoost(volume_boost_amount); } function setupVolumeBoost() { @@ -64,15 +58,13 @@ export function applyVolumeBoost(volume_boost_amount: number) { window.gainNode.gain.value = Math.pow(10, volume_boost_amount / 20); } export const addVolumeBoostButton: AddButtonFunction = async () => { - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); - const { data: { options: { button_placements: { volumeBoostButton: volumeBoostButtonPlacement } } } - } = optionsData; + } = await waitForSpecificMessage("options", "request_data", "content"); await addFeatureButton( "volumeBoostButton", volumeBoostButtonPlacement, diff --git a/src/global.d.ts b/src/global.d.ts index 46b23fc7..3e76a27d 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -55,6 +55,8 @@ declare module "node_modules/@types/youtube-player/dist/types" { viewerLivestreamJoinMediaTime: number; } interface YouTubePlayer { + unloadModule(moduleName: string): void; + loadModule(moduleName: string): void; getProgressState(): ProgressState; getVideoBytesLoaded(): Promise; getVideoData(): Promise; @@ -68,6 +70,7 @@ declare global { } interface Window { audioCtx: AudioContext; + cachedPlaylistDuration: { playlistId: string; totalTimeSeconds: number } | null; gainNode: GainNode; i18nextInstance: i18nInstanceType; webkitAudioContext: AudioContext; diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts index 364f78c8..cc924bdb 100644 --- a/src/hooks/useClickOutside.ts +++ b/src/hooks/useClickOutside.ts @@ -6,43 +6,28 @@ import { type RefObject, useEffect } from "react"; const useClickOutside = ( ref: RefObject, - handler: (event: FocusEvent | MouseEvent | TouchEvent) => void ) => { useEffect(() => { let startedInside: Nullable | boolean> = false; - let startedWhenMounted: Nullable["current"] | boolean> = false; - const listener = (event: FocusEvent | MouseEvent | TouchEvent) => { // Do nothing if `mousedown` or `touchstart` started inside ref element - if (startedInside || !startedWhenMounted) return; - // Do nothing if clicking ref's element or descendent elements - if (!ref.current || ref.current.contains(event.target as Node)) return; - handler(event); }; - const validateEventStart = (event: FocusEvent | MouseEvent | TouchEvent) => { ({ current: startedWhenMounted } = ref); - startedInside = event.target && ref.current && ref.current.contains(event.target as Node); }; - document.addEventListener("mousedown", validateEventStart); - document.addEventListener("touchstart", validateEventStart); - document.addEventListener("click", listener); - return () => { document.removeEventListener("mousedown", validateEventStart); - document.removeEventListener("touchstart", validateEventStart); - document.removeEventListener("click", listener); }; }, [ref, handler]); diff --git a/src/hooks/useComponentVisible.ts b/src/hooks/useComponentVisible.ts index 263ad075..79c4c13c 100644 --- a/src/hooks/useComponentVisible.ts +++ b/src/hooks/useComponentVisible.ts @@ -5,7 +5,6 @@ export default function useComponentVisible { if (ref.current && !ref.current.contains(event.target as Node)) { @@ -14,13 +13,11 @@ export default function useComponentVisible { document.addEventListener("click", handleClickOutside, true); return () => { document.removeEventListener("click", handleClickOutside, true); }; }, [handleClickOutside]); - return { isComponentVisible, setIsComponentVisible }; } diff --git a/src/hooks/useNotifications/index.ts b/src/hooks/useNotifications/index.ts index 5728e3f0..ed205126 100644 --- a/src/hooks/useNotifications/index.ts +++ b/src/hooks/useNotifications/index.ts @@ -6,7 +6,6 @@ const useNotifications = () => { if (!context) { throw new Error("useNotifications must be used within a NotificationsProvider"); } - return context; }; export default useNotifications; diff --git a/src/hooks/useNotifications/provider.tsx b/src/hooks/useNotifications/provider.tsx index cfdeb0a0..d67fa48c 100644 --- a/src/hooks/useNotifications/provider.tsx +++ b/src/hooks/useNotifications/provider.tsx @@ -20,10 +20,8 @@ export const NotificationsProvider = ({ children }: NotificationProviderProps) = const createNotification: CreateNotification = (type, message, action) => { const removeNotificationAfterMs = action && action === "reset_settings" ? 15_000 : 5_000; const notification = { action, message, removeAfterMs: removeNotificationAfterMs, timestamp: +new Date(), type } satisfies Notification; - return notification; }; - const scheduleNotificationRemoval: ScheduleNotificationRemoval = (notification, removeAfterMs) => { if (removeAfterMs) { setTimeout(() => { @@ -35,7 +33,6 @@ export const NotificationsProvider = ({ children }: NotificationProviderProps) = const notification = createNotification(type, message, action); const existingNotification = notifications.find((n) => notificationIsEqual(n, notification)); if (existingNotification) return; - setNotifications((notifications) => [notification, ...notifications]); scheduleNotificationRemoval(notification, notification.removeAfterMs); }; diff --git a/src/hooks/useRunAfterUpdate.ts b/src/hooks/useRunAfterUpdate.ts index 27f35a19..1ba9383e 100644 --- a/src/hooks/useRunAfterUpdate.ts +++ b/src/hooks/useRunAfterUpdate.ts @@ -4,12 +4,10 @@ import type { AnyFunction } from "../types"; const useRunAfterUpdate = () => { const handlersRef = useRef([]); - useLayoutEffect(() => { handlersRef.current.forEach((handler) => handler()); handlersRef.current = []; }); - return (handler: AnyFunction) => { handlersRef.current.push(handler); }; diff --git a/src/hooks/useSectionTitle/index.ts b/src/hooks/useSectionTitle/index.ts index cf52d3c3..6415433b 100644 --- a/src/hooks/useSectionTitle/index.ts +++ b/src/hooks/useSectionTitle/index.ts @@ -4,11 +4,9 @@ import { SectionTitleContext } from "./context"; const useSectionTitle = () => { const context = useContext(SectionTitleContext); - if (context === undefined) { throw new Error("useSectionTitle must be used within a SectionTitleProvider"); } - return context; }; export default useSectionTitle; diff --git a/src/hooks/useSettingsFilter/index.ts b/src/hooks/useSettingsFilter/index.ts index 65a611cd..840a8115 100644 --- a/src/hooks/useSettingsFilter/index.ts +++ b/src/hooks/useSettingsFilter/index.ts @@ -4,11 +4,9 @@ import { SettingsFilterContext } from "./context"; const useSettingsFilter = () => { const context = useContext(SettingsFilterContext); - if (context === undefined) { throw new Error("useSettingsFilter must be used within a SettingsFilterProvider"); } - return context; }; diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts index 12a776b2..ae30a807 100644 --- a/src/hooks/useStorage.ts +++ b/src/hooks/useStorage.ts @@ -14,7 +14,6 @@ type SetValue = Dispatch>; */ export function useStorage(key: string, initialValue: T, area: StorageArea = "local"): [T, SetValue] { const [storedValue, setStoredValue] = useState(initialValue); - useEffect(() => { readStorage(key, area) .then((res) => { @@ -22,16 +21,13 @@ export function useStorage(key: string, initialValue: T, area: StorageArea = return; }) .catch((err) => console.error(err)); - chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === area && Object.hasOwnProperty.call(changes, key)) { if (changes[key].newValue) setStoredValue(changes[key].newValue as unknown as T); } }); }, [area, key]); - const setValueRef = useRef>(); - setValueRef.current = (value) => { // Allow value to be a function, so we have the same API as useState const newValue = value instanceof Function ? value(storedValue) : value; @@ -43,15 +39,12 @@ export function useStorage(key: string, initialValue: T, area: StorageArea = return; }) .catch((error) => console.error(error)); - return newValue; }); }; - // Return a wrapped version of useState's setter function that ... // ... persists the new value to storage. const setValue: SetValue = useCallback((value) => setValueRef.current?.(value), []); - return [storedValue, setValue]; } diff --git a/src/i18n/constants.ts b/src/i18n/constants.ts index 4ec7960c..b86a717d 100644 --- a/src/i18n/constants.ts +++ b/src/i18n/constants.ts @@ -1,50 +1,50 @@ export const availableLocales = [ - "ca-ES", - "cs-CZ", - "de-DE", - "en-GB", - "en-US", - "es-ES", - "fa-IR", - "fr-FR", - "he-IL", - "hi-IN", - "it-IT", - "ja-JP", - "ko-KR", - "pl-PL", - "pt-BR", - "ru-RU", - "sv-SE", - "tr-TR", - "uk-UA", - "vi-VN", - "zh-CN", - "zh-TW" + "ca-ES", + "cs-CZ", + "de-DE", + "en-GB", + "en-US", + "es-ES", + "fa-IR", + "fr-FR", + "he-IL", + "hi-IN", + "it-IT", + "ja-JP", + "ko-KR", + "pl-PL", + "pt-BR", + "ru-RU", + "sv-SE", + "tr-TR", + "uk-UA", + "vi-VN", + "zh-CN", + "zh-TW" ] as const; export const localePercentages: Record = { + "en-US": 100, "ca-ES": 0, "cs-CZ": 0, - "de-DE": 31, - "en-GB": 2, - "en-US": 100, - "es-ES": 54, + "de-DE": 27, + "en-GB": 1, + "es-ES": 48, "fa-IR": 0, - "fr-FR": 57, + "fr-FR": 50, "he-IL": 0, "hi-IN": 0, - "it-IT": 100, - "ja-JP": 100, - "ko-KR": 94, + "it-IT": 89, + "ja-JP": 89, + "ko-KR": 89, "pl-PL": 0, - "pt-BR": 63, - "ru-RU": 100, - "sv-SE": 94, - "tr-TR": 69, - "uk-UA": 21, + "pt-BR": 55, + "ru-RU": 89, + "sv-SE": 89, + "tr-TR": 62, + "uk-UA": 18, "vi-VN": 0, - "zh-CN": 100, - "zh-TW": 96 + "zh-CN": 89, + "zh-TW": 86 }; export const localeDirection: Record = { "ca-ES": "ltr", diff --git a/src/icons.ts b/src/icons.ts index 2cc7fec9..f714b68a 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -286,7 +286,23 @@ const rewindButtonSVG = createSVGElement( d: "M 11.685714,19.542856 V 4.4571438 L 1.000001,12 Z M 12.314285,12 22.999999,19.542856 V 4.4571438 Z" }) ); +const copyURLWithTimestampSVG = createSVGElement( + "svg", + { + fill: "white", + height: "23px", + viewBox: "0 0 21.443 23.000378", + width: "21.442648" + }, + createSVGElement("path", { + d: "m 15.231937,11.02203 c -3.168022,0 -5.7334298,2.571146 -5.7334298,5.739168 0,3.168023 2.5654078,5.739172 5.7334298,5.739172 3.17376,0 5.74491,-2.571149 5.74491,-5.739172 0,-3.168022 -2.57115,-5.739168 -5.74491,-5.739168 z m 0.0057,10.330505 c -2.536712,0 -4.591336,-2.054622 -4.591336,-4.591337 0,-2.536712 2.054624,-4.591335 4.591336,-4.591335 2.536713,0 4.591336,2.054623 4.591336,4.591335 0,2.536715 -2.054623,4.591337 -4.591336,4.591337 z m 0.286972,-7.460921 h -0.860876 v 3.443503 l 3.013064,1.807837 0.430438,-0.705918 -2.582626,-1.532357 z M 6.4145673,0.50020216 C 5.3380684,0.52772093 4.4660134,1.4182478 4.4660134,2.5010665 V 16.499645 c 0,1.100008 0.9008568,2.000863 2.0008639,2.000863 h 2.7892545 c -0.00367,-0.0126 -0.0076,-0.02471 -0.011238,-0.03738 -0.013947,-0.04919 -0.026468,-0.09981 -0.039218,-0.149459 -0.012751,-0.04966 -0.025827,-0.09933 -0.037381,-0.149458 -0.011555,-0.05014 -0.021442,-0.100746 -0.031723,-0.151322 -0.010367,-0.05062 -0.020801,-0.100308 -0.029895,-0.151323 -0.00909,-0.05102 -0.018332,-0.101758 -0.026143,-0.153195 -0.00786,-0.05142 -0.013947,-0.103218 -0.020562,-0.155063 -0.00658,-0.05182 -0.013392,-0.104699 -0.018648,-0.156925 -0.0053,-0.05221 -0.00909,-0.104327 -0.013076,-0.156928 -0.00402,-0.05261 -0.00858,-0.105839 -0.011238,-0.1588 -0.00265,-0.05292 -0.00428,-0.105496 -0.00564,-0.158798 -0.00137,-0.05333 -0.00188,-0.107037 -0.00188,-0.160659 0,-0.05364 5.187e-4,-0.107362 0.00188,-0.160665 8.547e-4,-0.03388 0.00231,-0.06711 0.00376,-0.100881 H 6.4668773 V 2.5010665 H 17.466961 v 8.4611715 c 0.01179,0.0045 0.02376,0.0084 0.03547,0.01308 0.04711,0.01858 0.09544,0.03826 0.14199,0.05796 0.04655,0.01969 0.0923,0.03905 0.13825,0.05978 0.04591,0.02073 0.09103,0.04168 0.136377,0.06353 0.04536,0.02185 0.09165,0.0444 0.136376,0.06727 0.04472,0.02288 0.08855,0.04703 0.132647,0.07102 0.04409,0.02391 0.08733,0.04791 0.130773,0.07286 0.04344,0.02495 0.08613,0.05063 0.128909,0.0766 0.0428,0.02599 0.08496,0.0534 0.127043,0.08034 0.04209,0.02694 0.08378,0.05428 0.12517,0.0822 0.04138,0.02798 0.08263,0.05515 0.123305,0.08407 0.04065,0.02894 0.0815,0.05796 0.121431,0.08781 0.03993,0.02981 0.08038,0.06074 0.119566,0.09154 0.03922,0.03077 0.0774,0.0617 0.115826,0.09341 0.03841,0.03173 0.07635,0.06456 0.113963,0.09715 0.03762,0.03261 0.07525,0.06552 0.112098,0.09901 0.02041,0.01849 0.03961,0.03723 0.05978,0.05603 V 2.5010665 c 2e-5,-1.1000068 -0.898969,-2.00086434 -1.998976,-2.00086434 H 6.4668773 c -0.017186,0 -0.035224,-4.3671e-4 -0.05231,0 z M 10.257499,20.499504 H 2.4651491 V 6.5009267 H 0.46615326 V 20.499504 c 0,1.100008 0.89898954,2.000866 1.99899584,2.000866 H 12.830037 c -0.0093,-0.0038 -0.01881,-0.0073 -0.02805,-0.01124 -0.04648,-0.01969 -0.09238,-0.03906 -0.138251,-0.05978 -0.04584,-0.02073 -0.09112,-0.04169 -0.136377,-0.06353 -0.04528,-0.02185 -0.08987,-0.0444 -0.134512,-0.06727 -0.04464,-0.02288 -0.08864,-0.04703 -0.132647,-0.07102 -0.04399,-0.02391 -0.08929,-0.04791 -0.132646,-0.07286 -0.04336,-0.02495 -0.08622,-0.05062 -0.128909,-0.0766 -0.04272,-0.02599 -0.08503,-0.0534 -0.127042,-0.08033 -0.042,-0.02695 -0.08386,-0.05428 -0.12517,-0.08219 -0.04128,-0.02798 -0.08084,-0.05515 -0.121432,-0.08407 -0.04057,-0.02894 -0.08158,-0.05796 -0.121431,-0.08781 -0.03986,-0.0298 -0.0786,-0.06074 -0.117701,-0.09154 -0.03914,-0.03076 -0.07748,-0.0617 -0.115827,-0.09341 -0.03834,-0.03164 -0.07636,-0.06456 -0.113963,-0.09715 -0.03754,-0.03261 -0.07533,-0.06552 -0.112089,-0.09902 -0.03675,-0.03348 -0.07238,-0.06656 -0.108359,-0.100882 -0.03596,-0.03436 -0.07326,-0.06942 -0.108359,-0.10462 -0.03515,-0.03515 -0.06848,-0.07047 -0.102755,-0.106486 -0.03428,-0.03603 -0.06935,-0.07341 -0.102755,-0.110225 -0.0334,-0.03683 -0.06648,-0.07262 -0.09902,-0.110223 -0.03252,-0.03762 -0.06361,-0.07556 -0.09528,-0.113963 -0.03164,-0.03843 -0.06265,-0.07852 -0.09341,-0.1177 -0.02583,-0.03285 -0.05149,-0.0656 -0.0766,-0.09902 z" + }) +); export const featureIcons = { + copyTimestampUrlButton: { + feature_menu: copyURLWithTimestampSVG, + shared_icon_position: copyURLWithTimestampSVG + }, decreasePlaybackSpeedButton: { feature_menu: decreasePlaybackSpeedButtonSVG, shared_icon_position: decreasePlaybackSpeedButtonSVG diff --git a/src/pages/content/index.ts b/src/pages/content/index.ts index 2f7d7776..17be107c 100644 --- a/src/pages/content/index.ts +++ b/src/pages/content/index.ts @@ -122,7 +122,6 @@ document.addEventListener("yte-message-from-youtube", () => { }); break; } - case "language": { const language = await new Promise((resolve) => { chrome.storage.local.get("language", (o) => { @@ -145,7 +144,6 @@ document.addEventListener("yte-message-from-youtube", () => { void chrome.storage.local.set({ remembered_volumes: JSON.stringify({ ...existingRememberedVolumes, ...message.data }) }); break; } - case "pageLoaded": { chrome.storage.onChanged.addListener(storageListeners); window.onunload = () => { @@ -196,36 +194,28 @@ const getStoredSettings = async (): Promise => { resolve(storedSettings as configuration); }); }); - return options; }; const deepEqual = (a: unknown, b: unknown): boolean => { if (a === b) return true; - if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { return false; } - const keysA = Object.keys(a); const keysB = Object.keys(b); - if (keysA.length !== keysB.length) return false; - for (const key of keysA) { if (!keysB.includes(key)) return false; if (!deepEqual((a as Record)[key], (b as Record)[key])) return false; } - return true; }; const isValidChange = (change?: { newValue?: unknown; oldValue?: unknown }) => { if (change?.newValue === undefined || change?.oldValue === undefined) { return false; } - const parsedOldValue = parseStoredValue(change.oldValue as string); const parsedNewValue = parseStoredValue(change.newValue as string); - return !deepEqual(parsedOldValue, parsedNewValue); }; const storageChangeHandler = async (changes: StorageChanges, areaName: string) => { @@ -281,6 +271,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = automaticTheaterModeEnabled: newValue }); }, + enable_copy_timestamp_url_button: (__oldValue, newValue) => { + sendExtensionOnlyMessage("copyTimestampUrlButtonChange", { + copyTimestampUrlButtonEnabled: newValue + }); + }, enable_custom_css: (__oldValue, newValue) => { sendExtensionOnlyMessage("customCSSChange", { customCSSCode: options.custom_css_code, customCSSEnabled: newValue }); }, @@ -348,6 +343,9 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = maximizePlayerButtonEnabled: newValue }); }, + enable_hide_official_artist_videos_from_home_page: (__oldValue, newValue) => { + sendExtensionOnlyMessage("hideOfficialArtistVideosFromHomePageChange", { hideOfficialArtistVideosFromHomePageEnabled: newValue }); + }, enable_open_transcript_button: (__oldValue, newValue) => { sendExtensionOnlyMessage("openTranscriptButtonChange", { openTranscriptButtonEnabled: newValue @@ -369,6 +367,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = playbackSpeedButtonsEnabled: newValue }); }, + enable_playlist_length: (__oldValue, newValue) => { + sendExtensionOnlyMessage("playlistLengthChange", { + playlistLengthEnabled: newValue + }); + }, enable_redirect_remover: (__oldValue, newValue) => { sendExtensionOnlyMessage("removeRedirectChange", { removeRedirectEnabled: newValue @@ -394,6 +397,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = scrollWheelSpeedControlEnabled: newValue }); }, + enable_automatically_disable_closed_captions: (__oldValue, newValue) => { + sendExtensionOnlyMessage("automaticallyDisableClosedCaptionsChange", { + automaticallyDisableClosedCaptionsEnabled: newValue + }); + }, enable_scroll_wheel_volume_control: (__oldValue, newValue) => { sendExtensionOnlyMessage("scrollWheelVolumeControlChange", { scrollWheelVolumeControlEnabled: newValue @@ -452,6 +460,12 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = playerSpeed: newValue }); }, + playlist_length_get_method: () => { + sendExtensionOnlyMessage("playlistLengthGetMethodChange", undefined); + }, + playlist_watch_time_get_method: () => { + sendExtensionOnlyMessage("playlistWatchTimeGetMethodChange", undefined); + }, volume_boost_amount: (newValue) => { sendExtensionOnlyMessage("volumeBoostAmountChange", { volumeBoostAmount: newValue, diff --git a/src/pages/embedded/index.ts b/src/pages/embedded/index.ts index 0b5b8642..ec28decd 100644 --- a/src/pages/embedded/index.ts +++ b/src/pages/embedded/index.ts @@ -1,9 +1,14 @@ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ import { deepDarkPresets } from "@/src/deepDarkPresets"; import { type FeatureFuncRecord, featureButtonFunctions } from "@/src/features"; +import { + disableAutomaticallyDisableClosedCaptions, + enableAutomaticallyDisableClosedCaptions +} from "@/src/features/automaticallyDisableClosedCaptions"; import { enableAutomaticTheaterMode } from "@/src/features/automaticTheaterMode"; import { featuresInControls } from "@/src/features/buttonPlacement"; import { getFeatureButton, updateFeatureButtonIcon, updateFeatureButtonTitle } from "@/src/features/buttonPlacement/utils"; +import { addCopyTimestampUrlButton, removeCopyTimestampUrlButton } from "@/src/features/copyTimestampUrlButton"; import { disableCustomCSS, enableCustomCSS } from "@/src/features/customCSS"; import { customCSSExists, updateCustomCSS } from "@/src/features/customCSS/utils"; import { disableDeepDarkCSS, enableDeepDarkCSS } from "@/src/features/deepDarkCSS"; @@ -19,6 +24,10 @@ import { removeHideEndScreenCardsButton } from "@/src/features/hideEndScreenCards"; import { disableHideLiveStreamChat, enableHideLiveStreamChat } from "@/src/features/hideLiveStreamChat"; +import { + disableHideOfficialArtistVideosFromHomePage, + enableHideOfficialArtistVideosFromHomePage +} from "@/src/features/hideOfficialArtistVideosFromHomePage"; import { disableHidePaidPromotionBanner, enableHidePaidPromotionBanner } from "@/src/features/hidePaidPromotionBanner"; import { enableHideScrollBar } from "@/src/features/hideScrollBar"; import { hideScrollBar, showScrollBar } from "@/src/features/hideScrollBar/utils"; @@ -34,11 +43,14 @@ import { disablePauseBackgroundPlayers, enablePauseBackgroundPlayers } from "@/s import { addDecreasePlaybackSpeedButton, addIncreasePlaybackSpeedButton, + calculatePlaybackButtonSpeed, removeDecreasePlaybackSpeedButton, - removeIncreasePlaybackSpeedButton + removeIncreasePlaybackSpeedButton, + updatePlaybackSpeedButtonTooltip } from "@/src/features/playbackSpeedButtons"; import setPlayerQuality from "@/src/features/playerQuality"; import { restorePlayerSpeed, setPlayerSpeed, setupPlaybackSpeedChangeListener } from "@/src/features/playerSpeed"; +import { disablePlaylistLength, enablePlaylistLength } from "@/src/features/playlistLength"; import { setupRemainingTime as enableRemainingTime, removeRemainingTimeDisplay } from "@/src/features/remainingTime"; import enableRememberVolume from "@/src/features/rememberVolume"; import enableRemoveRedirect from "@/src/features/removeRedirect"; @@ -78,7 +90,9 @@ import { findKeyByValue, formatError, groupButtonChanges, + isLivePage, isNewYouTubeVideoLayout, + isPlaylistPage, isShortsPage, isWatchPage, sendContentOnlyMessage, @@ -134,7 +148,9 @@ const alwaysShowProgressBar = async function () { progressPlay += progressWidth; progressLoad += progressWidth; }; - +function shouldEnableFeaturesFuncReturn() { + return !(isWatchPage() || isShortsPage() || isPlaylistPage() || isLivePage()); +} const enableFeatures = () => { browserColorLog(`Enabling features...`, "FgMagenta"); void (async () => { @@ -153,15 +169,13 @@ const enableFeatures = () => { enableShareShortener(), enableSkipContinueWatching(), enablePauseBackgroundPlayers(), - enableRememberVolume(), enableHideScrollBar(), enableCustomCSS(), - enableDeepDarkCSS() + enableDeepDarkCSS(), + enableHideOfficialArtistVideosFromHomePage() ]); - // Use a guard clause to reduce amount of times nesting code happens - if (!(isWatchPage() || isShortsPage())) return; - + if (shouldEnableFeaturesFuncReturn()) return; void Promise.all([ promptUserToResumeVideo(() => void setupVideoHistory()), setupPlaybackSpeedChangeListener(), @@ -171,13 +185,14 @@ const enableFeatures = () => { enableRememberVolume(), enableAutomaticTheaterMode(), enableRemainingTime(), - volumeBoost(), setPlayerQuality(), setPlayerSpeed(), adjustVolumeOnScrollWheel(), adjustSpeedOnScrollWheel(), enableHideTranslateComment(), - enableHideEndScreenCards() + enableHideEndScreenCards(), + enablePlaylistLength(), + enableAutomaticallyDisableClosedCaptions() ]); // Enable feature menu before calling button functions await enableFeatureMenu(); @@ -224,6 +239,8 @@ const enableFeatures = () => { await openTranscriptButton(); await addMaximizePlayerButton(); await addLoopButton(); + await addCopyTimestampUrlButton(); + await volumeBoost(); })(); }; const getFeatureFunctions = (featureName: AllButtonNames, oldPlacement: ButtonPlacement) => { @@ -232,17 +249,18 @@ const getFeatureFunctions = (featureName: AllButtonNames, oldPlacement: ButtonPl if (!featureFunctions) { throw new Error(`Feature '${featureName}' not found in featureButtonFunctions`); } - // Cast featureFunctions to FeatureFuncRecord const castFeatureFunctions = featureFunctions as unknown as FeatureFuncRecord; - return { add: () => castFeatureFunctions.add(), remove: () => castFeatureFunctions.remove(oldPlacement) }; }; function handleSoftNavigate() { - // Listen to YouTube's soft navigate event + // Remove existing listeners + document.removeEventListener("yt-navigate-finish", enableFeatures); + document.removeEventListener("yt-player-updated", enableFeatures); + // Add listeners document.addEventListener("yt-navigate-finish", enableFeatures); document.addEventListener("yt-player-updated", enableFeatures); } @@ -259,7 +277,7 @@ window.addEventListener("DOMContentLoaded", function () { enableFeatures(); handleSoftNavigate(); } else if (!isFirstLoad) { - handleSoftNavigate; + handleSoftNavigate(); } isFirstLoad = false; /** @@ -282,6 +300,34 @@ window.addEventListener("DOMContentLoaded", function () { } if (!message) return; switch (message.type) { + case "hideOfficialArtistVideosFromHomePageChange": { + const { + data: { hideOfficialArtistVideosFromHomePageEnabled } + } = message; + if (hideOfficialArtistVideosFromHomePageEnabled) { + await enableHideOfficialArtistVideosFromHomePage(); + } else { + disableHideOfficialArtistVideosFromHomePage(); + } + break; + } + case "playlistLengthChange": { + const { + data: { playlistLengthEnabled } + } = message; + if (playlistLengthEnabled) { + await enablePlaylistLength(); + } else { + disablePlaylistLength(); + } + break; + } + case "playlistLengthGetMethodChange": + case "playlistWatchTimeGetMethodChange": { + disablePlaylistLength(); + await enablePlaylistLength(); + break; + } case "volumeBoostChange": { const { data: { volumeBoostEnabled, volumeBoostMode } @@ -306,7 +352,6 @@ window.addEventListener("DOMContentLoaded", function () { const { data: { volumeBoostAmount, volumeBoostEnabled, volumeBoostMode } } = message; - switch (volumeBoostMode) { case "global": { if (!volumeBoostEnabled) return; @@ -326,10 +371,34 @@ window.addEventListener("DOMContentLoaded", function () { const { data: { enableForcedPlaybackSpeed, playerSpeed } } = message; + const { + data: { + options: { playback_buttons_speed: playbackSpeedPerClick } + } + } = await waitForSpecificMessage("options", "request_data", "content"); if (enableForcedPlaybackSpeed && playerSpeed) { + await updatePlaybackSpeedButtonTooltip( + "increasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(playerSpeed, playbackSpeedPerClick, "increase") + ); + await updatePlaybackSpeedButtonTooltip( + "decreasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(playerSpeed, playbackSpeedPerClick, "decrease") + ); await setPlayerSpeed(Number(playerSpeed)); } else if (!enableForcedPlaybackSpeed) { restorePlayerSpeed(); + const videoElement = document.querySelector("video"); + if (!videoElement) return; + const { playbackRate: currentSpeed } = videoElement; + await updatePlaybackSpeedButtonTooltip( + "increasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(currentSpeed, playbackSpeedPerClick, "increase") + ); + await updatePlaybackSpeedButtonTooltip( + "decreasePlaybackSpeedButton", + calculatePlaybackButtonSpeed(currentSpeed, playbackSpeedPerClick, "decrease") + ); } break; } @@ -431,6 +500,17 @@ window.addEventListener("DOMContentLoaded", function () { } break; } + case "copyTimestampUrlButtonChange": { + const { + data: { copyTimestampUrlButtonEnabled } + } = message; + if (copyTimestampUrlButtonEnabled) { + await addCopyTimestampUrlButton(); + } else { + await removeCopyTimestampUrlButton(); + } + break; + } case "forwardRewindButtonsChange": { const { data: { forwardRewindButtonsEnabled } @@ -573,6 +653,17 @@ window.addEventListener("DOMContentLoaded", function () { else await removeHideEndScreenCardsButton(); break; } + case "automaticallyDisableClosedCaptionsChange": { + const { + data: { automaticallyDisableClosedCaptionsEnabled } + } = message; + if (automaticallyDisableClosedCaptionsEnabled) { + await enableAutomaticallyDisableClosedCaptions(); + } else { + await disableAutomaticallyDisableClosedCaptions(); + } + break; + } case "hideLiveStreamChatChange": { const { data: { hideLiveStreamChatEnabled } @@ -678,7 +769,6 @@ window.addEventListener("DOMContentLoaded", function () { // If the size button is not available return if (!sizeButton) return; sizeButton.click(); - break; } case "featureMenuOpenTypeChange": { diff --git a/src/types/index.ts b/src/types/index.ts index d7762413..3fed8918 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -107,9 +107,9 @@ export const youtubePlayerQualityLevels = [ export type YoutubePlayerQualityLevel = (typeof youtubePlayerQualityLevels)[number]; export const PlayerQualityFallbackStrategy = ["higher", "lower"] as const; export type PlayerQualityFallbackStrategy = (typeof PlayerQualityFallbackStrategy)[number]; -export const youtubePlayerSpeedRatesExtended = [2.25, 2.5, 2.75, 3, 3.25, 3.75, 4] as const; -export const youtubePlayerSpeedRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, ...youtubePlayerSpeedRatesExtended] as const; -export const youtubePlaybackSpeedButtonsRates = [0.25, 0.5, 0.75, 1] as const; +export const youtubePlayerMaxSpeed = 16; +export const youtubePlayerMinSpeed = 0.07; +export const youtubePlayerSpeedStep = 0.01; export const screenshotTypes = ["file", "clipboard"] as const; export type ScreenshotType = (typeof screenshotTypes)[number]; export const screenshotFormats = ["png", "jpeg", "webp"] as const; @@ -122,6 +122,10 @@ export const videoHistoryResumeTypes = ["automatic", "prompt"] as const; export type VideoHistoryResumeType = (typeof videoHistoryResumeTypes)[number]; export const buttonPlacements = ["below_player", "feature_menu", "player_controls_left", "player_controls_right"] as const; export type ButtonPlacement = (typeof buttonPlacements)[number]; +export const playlistWatchTimeGetMethod = ["duration", "youtube"] as const; +export type PlaylistWatchTimeGetMethod = (typeof playlistWatchTimeGetMethod)[number]; +export const playlistLengthGetMethod = ["api", "html"] as const; +export type PlaylistLengthGetMethod = (typeof playlistLengthGetMethod)[number]; export const featureMenuOpenTypes = ["click", "hover"] as const; export type MultiButtonChange = { [K in MultiButtonFeatureNames]: { @@ -185,6 +189,7 @@ export type FeatureMenuItemIconId = `yte-${AllButtonNames}-icon`; export type FeatureMenuItemId = `yte-feature-${AllButtonNames}-menuitem`; export type FeatureMenuItemLabelId = `yte-${AllButtonNames}-label`; export const buttonNames = Object.keys({ + copyTimestampUrlButton: "", decreasePlaybackSpeedButton: "", forwardButton: "", hideEndScreenCardsButton: "", @@ -197,6 +202,7 @@ export const buttonNames = Object.keys({ volumeBoostButton: "" } satisfies Record); export const buttonNameToSettingName = { + copyTimestampUrlButton: "enable_copy_timestamp_url_button", decreasePlaybackSpeedButton: "enable_playback_speed_buttons", forwardButton: "enable_forward_rewind_buttons", hideEndScreenCardsButton: "enable_hide_end_screen_cards_button", @@ -232,6 +238,37 @@ export type Notification = { timestamp?: number; type: NotificationType; }; +export type YouTubePlaylistResponse = { + items: YouTubePlaylistItem[]; + nextPageToken?: string; +}; +export type YouTubePlaylistItem = { + contentDetails: { + videoId: string; + }; +}; + +export type YouTubeVideoResponse = { + items: YouTubeVideoItem[]; +}; + +export type YouTubeVideoItem = { + contentDetails: { + duration: string; // ISO 8601 duration format + }; +}; +export type YouTubeAPIQuotaError = { + error: { + code: number; + errors: { domain: string; message: string; reason: string }[]; + message: string; + }; +}; +export type VideoDetails = { + duration: number; + progress: number; + videoId: Nullable; +}; export type CrowdinLanguageProgressResponse = { data: { data: { @@ -318,6 +355,7 @@ export type ContentToBackgroundSendOnlyMessageMappings = { export type ExtensionSendOnlyMessageMappings = { automaticTheaterModeChange: DataResponseMessage<"automaticTheaterModeChange", { automaticTheaterModeEnabled: boolean }>; buttonPlacementChange: DataResponseMessage<"buttonPlacementChange", ButtonPlacementChange>; + copyTimestampUrlButtonChange: DataResponseMessage<"copyTimestampUrlButtonChange", { copyTimestampUrlButtonEnabled: boolean }>; customCSSChange: DataResponseMessage<"customCSSChange", { customCSSCode: string; customCSSEnabled: boolean }>; deepDarkThemeChange: DataResponseMessage< "deepDarkThemeChange", @@ -335,6 +373,10 @@ export type ExtensionSendOnlyMessageMappings = { hideScrollBarChange: DataResponseMessage<"hideScrollBarChange", { hideScrollBarEnabled: boolean }>; hideShortsChange: DataResponseMessage<"hideShortsChange", { hideShortsEnabled: boolean }>; hideTranslateCommentChange: DataResponseMessage<"hideTranslateCommentChange", { hideTranslateCommentEnabled: boolean }>; + hideOfficialArtistVideosFromHomePageChange: DataResponseMessage< + "hideOfficialArtistVideosFromHomePageChange", + { hideOfficialArtistVideosFromHomePageEnabled: boolean } + >; languageChange: DataResponseMessage<"languageChange", { language: AvailableLocales }>; loopButtonChange: DataResponseMessage<"loopButtonChange", { loopButtonEnabled: boolean }>; maximizeButtonChange: DataResponseMessage<"maximizeButtonChange", { maximizePlayerButtonEnabled: boolean }>; @@ -351,6 +393,9 @@ export type ExtensionSendOnlyMessageMappings = { { playbackButtonsSpeed: number; playbackSpeedButtonsEnabled: boolean } >; playerSpeedChange: DataResponseMessage<"playerSpeedChange", { enableForcedPlaybackSpeed: boolean; playerSpeed?: number }>; + playlistLengthChange: DataResponseMessage<"playlistLengthChange", { playlistLengthEnabled: boolean }>; + playlistLengthGetMethodChange: DataResponseMessage<"playlistLengthGetMethodChange", undefined>; + playlistWatchTimeGetMethodChange: DataResponseMessage<"playlistWatchTimeGetMethodChange", undefined>; remainingTimeChange: DataResponseMessage<"remainingTimeChange", { remainingTimeEnabled: boolean }>; rememberVolumeChange: DataResponseMessage<"rememberVolumeChange", { rememberVolumeEnabled: boolean }>; removeRedirectChange: DataResponseMessage<"removeRedirectChange", { removeRedirectEnabled: boolean }>; @@ -371,6 +416,10 @@ export type ExtensionSendOnlyMessageMappings = { { volumeBoostAmount: number; volumeBoostEnabled: boolean; volumeBoostMode: VolumeBoostMode } >; volumeBoostChange: DataResponseMessage<"volumeBoostChange", { volumeBoostEnabled: boolean; volumeBoostMode: VolumeBoostMode }>; + automaticallyDisableClosedCaptionsChange: DataResponseMessage< + "automaticallyDisableClosedCaptionsChange", + { automaticallyDisableClosedCaptionsEnabled: boolean } + >; }; export type FilterMessagesBySource = { [K in keyof T]: Extract; @@ -409,11 +458,14 @@ export type configuration = { deep_dark_preset: DeepDarkPreset; enable_automatic_theater_mode: boolean; enable_automatically_set_quality: boolean; + enable_copy_timestamp_url_button: boolean; + enable_automatically_disable_closed_captions: boolean; enable_custom_css: boolean; enable_deep_dark_theme: boolean; enable_forced_playback_speed: boolean; enable_forward_rewind_buttons: boolean; enable_hide_end_screen_cards: boolean; + enable_hide_official_artist_videos_from_home_page: boolean; enable_hide_end_screen_cards_button: boolean; enable_hide_live_stream_chat: boolean; enable_hide_paid_promotion_banner: boolean; @@ -426,6 +478,7 @@ export type configuration = { enable_open_youtube_settings_on_hover: boolean; enable_pausing_background_players: boolean; enable_playback_speed_buttons: boolean; + enable_playlist_length: boolean; enable_redirect_remover: boolean; enable_remaining_time: boolean; enable_remember_last_volume: boolean; @@ -452,6 +505,8 @@ export type configuration = { player_quality: YoutubePlayerQualityLevel; player_quality_fallback_strategy: PlayerQualityFallbackStrategy; player_speed: number; + playlist_length_get_method: PlaylistLengthGetMethod; + playlist_watch_time_get_method: PlaylistWatchTimeGetMethod; remembered_volumes: RememberedVolumes; screenshot_format: ScreenshotFormat; screenshot_save_as: ScreenshotType; @@ -462,6 +517,7 @@ export type configuration = { volume_adjustment_steps: number; volume_boost_amount: number; volume_boost_mode: VolumeBoostMode; + youtube_data_api_v3_key: string; }; export type configurationKeys = keyof configuration; export type configurationId = Path; diff --git a/src/utils/EventManager.ts b/src/utils/EventManager.ts index 96d47845..2091d255 100644 --- a/src/utils/EventManager.ts +++ b/src/utils/EventManager.ts @@ -1,5 +1,6 @@ export type FeatureName = | "automaticTheaterMode" + | "copyTimestampUrlButton" | "featureMenu" | "forwardRewindButtons" | "hideEndScreenCardsButton" @@ -12,6 +13,7 @@ export type FeatureName = | "playbackSpeedButtons" | "playerQuality" | "playerSpeed" + | "playlistLength" | "remainingTime" | "rememberVolume" | "removeRedirect" @@ -84,10 +86,8 @@ export const eventManager: EventManager = { target.addEventListener(eventName, callback, options); } }, - // event listener info objects - listeners: new Map(), - + listeners: new Map>(), // Removes all event listeners removeAllEventListeners: function (exclude) { // Iterate over all registered listeners diff --git a/src/utils/OnScreenDisplayManager.ts b/src/utils/OnScreenDisplayManager.ts index 0a691d0f..70bf700d 100644 --- a/src/utils/OnScreenDisplayManager.ts +++ b/src/utils/OnScreenDisplayManager.ts @@ -144,6 +144,11 @@ export default class OnScreenDisplayManager { this.canvas.width = width; this.canvas.height = fontSize; this.clearCanvas(); + // Add a shadow effect around the text. + this.context.shadowColor = "black"; + this.context.shadowBlur = 10; + this.context.shadowOffsetX = 0; + this.context.shadowOffsetY = 0; this.context.globalAlpha = displayOpacity / 100; this.context.fillStyle = displayColor; this.setFont(); @@ -156,11 +161,16 @@ export default class OnScreenDisplayManager { const lineHeight = 5; this.canvas.width = lineWidth; this.canvas.height = lineHeight; - this.clearCanvas(); this.context.globalAlpha = displayOpacity / 100; this.context.fillStyle = displayColor; const lineX = (this.canvas.width - lineWidth) / 2; const lineY = (this.canvas.height - lineHeight) / 2; + this.clearCanvas(); + // Add a shadow effect around the line. + this.context.shadowColor = "black"; + this.context.shadowBlur = 10; + this.context.shadowOffsetX = 0; + this.context.shadowOffsetY = 0; this.context.fillRect(lineX, lineY, lineWidth, lineHeight); break; } @@ -176,6 +186,11 @@ export default class OnScreenDisplayManager { const centerY = this.canvas.height / 2; const startAngle = Math.PI + Math.PI * round(value / max, 2); const endAngle = Math.PI - Math.PI * round(value / max, 2); + // Add a shadow effect around the circle. + this.context.shadowColor = "black"; + this.context.shadowBlur = 10; + this.context.shadowOffsetX = 0; + this.context.shadowOffsetY = 0; this.context.strokeStyle = displayColor; this.context.lineWidth = lineWidth; this.context.lineCap = "butt"; @@ -225,28 +240,23 @@ export default class OnScreenDisplayManager { // method to set up the canvas based on options. private setupCanvas(): void { if (!ensurePlayerContainerExists(this.options.playerContainer)) return; - const { options: { playerContainer: { clientHeight: height, clientWidth: width } } } = this; - // Adjust displayPadding if it exceeds max width or height. if (this.options.displayPadding > Math.max(width, height)) { this.options.displayPadding = clamp(this.options.displayPadding, 0, Math.max(width, height)); browserColorLog(`Clamped display padding to ${this.options.displayPadding}`, "FgRed"); } - // Calculate font size based on canvas dimensions. this.fontSize = clamp(Math.min(width, height) / 10, 48, 72); - // Find elements for positioning the canvas. const bottomElement: Nullable = document.querySelector( "ytd-reel-video-renderer[is-active] > div.overlay.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > div > ytd-reel-player-header-renderer" ) ?? document.querySelector(".ytp-chrome-bottom"); - const { top: topRectTop = 0 } = document.querySelector(".player-controls > ytd-shorts-player-controls")?.getBoundingClientRect() || {}; const { bottom: bottomRectBottom = 0, top: bottomRectTop = 0 } = bottomElement?.getBoundingClientRect() || {}; const heightExcludingMarginPadding = @@ -258,10 +268,8 @@ export default class OnScreenDisplayManager { parseInt(getComputedStyle(bottomElement).paddingBottom, 10)) + 10 : 0; - const paddingTop = isShortsPage() ? topRectTop / 2 : 0; const paddingBottom = isShortsPage() ? heightExcludingMarginPadding : Math.round(bottomRectBottom - bottomRectTop); - // Position the canvas based on options. Object.assign(this.canvas.style, { ...calculateCanvasPosition(this.options.displayPosition, this.options.displayPadding, paddingTop, paddingBottom) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 53a543fe..5b9c5a5e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -13,16 +13,21 @@ import { onScreenDisplayColors, onScreenDisplayPositions, onScreenDisplayTypes, + playlistLengthGetMethod, + playlistWatchTimeGetMethod, screenshotFormats, screenshotTypes, videoHistoryResumeTypes, volumeBoostModes, - youtubePlayerQualityLevels + youtubePlayerMinSpeed, + youtubePlayerQualityLevels, + youtubePlayerSpeedStep } from "../types"; export const outputFolderName = "dist"; export const defaultConfiguration = { button_placements: { + copyTimestampUrlButton: "player_controls_right", decreasePlaybackSpeedButton: "player_controls_left", forwardButton: "player_controls_right", hideEndScreenCardsButton: "player_controls_right", @@ -47,8 +52,10 @@ export const defaultConfiguration = { deep_dark_preset: "Deep-Dark", enable_automatic_theater_mode: false, enable_automatically_set_quality: false, + enable_copy_timestamp_url_button: false, enable_custom_css: false, enable_deep_dark_theme: false, + enable_hide_official_artist_videos_from_home_page: false, enable_forced_playback_speed: false, enable_forward_rewind_buttons: false, enable_hide_end_screen_cards: false, @@ -64,6 +71,7 @@ export const defaultConfiguration = { enable_open_youtube_settings_on_hover: false, enable_pausing_background_players: false, enable_playback_speed_buttons: false, + enable_playlist_length: false, enable_redirect_remover: false, enable_remaining_time: false, enable_remember_last_volume: false, @@ -90,6 +98,8 @@ export const defaultConfiguration = { player_quality: "auto", player_quality_fallback_strategy: "lower", player_speed: 1, + playlist_length_get_method: "api", + playlist_watch_time_get_method: "youtube", remembered_volumes: { shortsPageVolume: 100, watchPageVolume: 100 @@ -102,7 +112,9 @@ export const defaultConfiguration = { video_history_resume_type: "prompt", volume_adjustment_steps: 5, volume_boost_amount: 5, - volume_boost_mode: "global" + volume_boost_mode: "global", + youtube_data_api_v3_key: "", + enable_automatically_disable_closed_captions: false } satisfies configuration; export const configurationImportSchema: TypeToPartialZodSchema< configuration, @@ -135,6 +147,7 @@ export const configurationImportSchema: TypeToPartialZodSchema< deep_dark_preset: z.enum(deepDarkPreset).optional(), enable_automatic_theater_mode: z.boolean().optional(), enable_automatically_set_quality: z.boolean().optional(), + enable_copy_timestamp_url_button: z.boolean().optional(), enable_custom_css: z.boolean().optional(), enable_deep_dark_theme: z.boolean().optional(), enable_forced_playback_speed: z.boolean().optional(), @@ -142,6 +155,7 @@ export const configurationImportSchema: TypeToPartialZodSchema< enable_hide_end_screen_cards: z.boolean().optional(), enable_hide_end_screen_cards_button: z.boolean().optional(), enable_hide_live_stream_chat: z.boolean().optional(), + enable_hide_official_artist_videos_from_home_page: z.boolean().optional(), enable_hide_paid_promotion_banner: z.boolean().optional(), enable_hide_scrollbar: z.boolean().optional(), enable_hide_shorts: z.boolean().optional(), @@ -152,6 +166,7 @@ export const configurationImportSchema: TypeToPartialZodSchema< enable_open_youtube_settings_on_hover: z.boolean().optional(), enable_pausing_background_players: z.boolean().optional(), enable_playback_speed_buttons: z.boolean().optional(), + enable_playlist_length: z.boolean().optional(), enable_redirect_remover: z.boolean().optional(), enable_remaining_time: z.boolean().optional(), enable_remember_last_volume: z.boolean().optional(), @@ -174,10 +189,12 @@ export const configurationImportSchema: TypeToPartialZodSchema< osd_display_padding: z.number().optional(), osd_display_position: z.enum(onScreenDisplayPositions).optional(), osd_display_type: z.enum(onScreenDisplayTypes).optional(), - playback_buttons_speed: z.number().min(0.25).max(4.0).step(0.25).optional(), + playback_buttons_speed: z.number().min(youtubePlayerSpeedStep).max(1.0).step(youtubePlayerSpeedStep).optional(), player_quality: z.enum(youtubePlayerQualityLevels).optional(), player_quality_fallback_strategy: z.enum(PlayerQualityFallbackStrategy).optional(), - player_speed: z.number().min(0.25).max(4.0).step(0.25).optional(), + player_speed: z.number().min(youtubePlayerMinSpeed).max(16.0).step(youtubePlayerSpeedStep).optional(), + playlist_length_get_method: z.enum(playlistLengthGetMethod).optional(), + playlist_watch_time_get_method: z.enum(playlistWatchTimeGetMethod).optional(), remembered_volumes: z .object({ shortsPageVolume: z.number().min(0).max(100).optional(), @@ -192,7 +209,10 @@ export const configurationImportSchema: TypeToPartialZodSchema< video_history_resume_type: z.enum(videoHistoryResumeTypes).optional(), volume_adjustment_steps: z.number().min(1).max(100).optional(), volume_boost_amount: z.number().optional(), - volume_boost_mode: z.enum(volumeBoostModes).optional() + volume_boost_mode: z.enum(volumeBoostModes).optional(), + youtube_data_api_v3_key: z.string().optional(), + enable_automatically_disable_closed_captions: z.boolean().optional() }); export const DEV_MODE = process.env.__DEV__ === "true"; export const ENABLE_SOURCE_MAP = DEV_MODE === true ? "inline" : false; +export const YouTube_Enhancer_Public_Youtube_Data_API_V3_Key = "AIzaSyA_z2BR_HSfKsPvuttqjD_6AY60zgqbm5k"; diff --git a/src/utils/log.ts b/src/utils/log.ts index 3395d9a2..9059bb5f 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,3 +1,5 @@ +import { getFormattedTimestamp } from "./utilities"; + type ColorType = "error" | "info" | "success" | "warning" | keyof typeof TerminalColors; function getColor(type: ColorType) { switch (type) { @@ -18,7 +20,7 @@ export function colorizeTerminalLog(message: string, type: ColorType = "FgBlack" return `${color}${message}${TerminalColors.Reset}`; } export default function terminalColorLog(message: string, type?: ColorType) { - console.log(colorizeTerminalLog(`[YouTube Enhancer]`, "FgCyan"), colorizeTerminalLog(message, type)); + console.log(colorizeTerminalLog(`[${getFormattedTimestamp()}] [YouTube Enhancer]`, "FgCyan"), colorizeTerminalLog(message, type)); } const TerminalColors = { diff --git a/src/utils/updateAvailableLocales.ts b/src/utils/updateAvailableLocales.ts index 976e424b..90a055ae 100644 --- a/src/utils/updateAvailableLocales.ts +++ b/src/utils/updateAvailableLocales.ts @@ -3,7 +3,6 @@ import { readFileSync, readdirSync, writeFileSync } from "fs"; import { i18nDir, publicDir } from "./plugins/utils"; function updateAvailableLocalesArray(code: string, updatedArray: string[]) { const match = code.match(/export\s+const\s+availableLocales\s*=\s*\[([^\]]*)\]\s*as\s*const\s*;/); - if (match) { const [, oldArrayPart] = match; const newArrayPart = JSON.stringify(updatedArray, null, 2).replace(/^\[|\]$/g, ""); diff --git a/src/utils/updateLocalePercentages.ts b/src/utils/updateLocalePercentages.ts index 0fc77eec..f6f8c741 100644 --- a/src/utils/updateLocalePercentages.ts +++ b/src/utils/updateLocalePercentages.ts @@ -54,7 +54,6 @@ const crowdinLanguageProgressResponseSchema: TypeToZodSchema) { const match = code.match(/export\s+const\s+localePercentages\s*:\s*Record\s*=\s*({[^}]+});/); - if (match) { const [, oldObjectPart] = match; const newObjectPart = JSON.stringify(updatedObject, null, 2); diff --git a/src/utils/updateStoredSettings.ts b/src/utils/updateStoredSettings.ts index 98306528..eba99347 100644 --- a/src/utils/updateStoredSettings.ts +++ b/src/utils/updateStoredSettings.ts @@ -10,25 +10,20 @@ const changedKeys = Object.keys({ export async function updateStoredSettings() { try { const settings = await getStoredSettings(); - const removedKeys = Object.keys(settings).filter((key) => !Object.keys(defaultConfiguration).includes(key)); - for (const changedKey of changedKeys) { switch (changedKey) { case "osd_display_type": { if ((settings.osd_display_type as unknown as string) === "round") { settings.osd_display_type = "circle"; } - break; } } } - for (const key of removedKeys) { delete settings[key]; } - await setModifiedSettings(settings); } catch (error) { console.error("Failed to update stored settings:", error); @@ -37,11 +32,9 @@ export async function updateStoredSettings() { async function setModifiedSettings(settings: Partial) { const updates: Record = {}; - for (const [key, value] of Object.entries(settings)) { updates[key] = typeof value !== "string" ? JSON.stringify(value) : value; } - await chrome.storage.local.set(updates); } @@ -51,14 +44,10 @@ async function getStoredSettings(): Promise { try { const storedSettings: Partial = ( Object.keys(settings) - .filter((key) => typeof key === "string") - .filter((key) => Object.keys(defaultConfiguration).includes(key as unknown as string)) as configurationKeys[] ).reduce((acc, key) => Object.assign(acc, { [key]: parseStoredValue(settings[key] as string) }), {}); - const castedSettings = storedSettings as configuration; - resolve(castedSettings); } catch (error) { reject(error); diff --git a/src/utils/utilities.ts b/src/utils/utilities.ts index b0b70ab6..545a28e1 100644 --- a/src/utils/utilities.ts +++ b/src/utils/utilities.ts @@ -47,8 +47,7 @@ export const removeSpecialCharacters = (value: string) => { export const unique = (values: string[]) => [...new Set(values)]; export const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); - -export const round = (value: number, decimals = 0) => Number(`${Math.round(Number(`${value}e${decimals}`))}e-${decimals}`); +export const round = (value: number, decimals = 0) => Number(`${Math.round(Number(`${value + Number.EPSILON}e${decimals}`))}e-${decimals}`); export const toDivisible = (value: number, divider: number): number => Math.ceil(value / divider) * divider; @@ -178,7 +177,7 @@ function groupMessages(messages: { message: string; styling: string[] }[]): Arra * @returns The colorized log message. */ export function browserColorLog(message: string, type?: ColorType) { - const prependLog = colorizeLog(`[YouTube Enhancer]`, "FgCyan"); + const prependLog = colorizeLog(`[${getFormattedTimestamp()}] [YouTube Enhancer]`, "FgCyan"); const colorizedMessage = colorizeLog(message, type); console.log(...groupMessages([prependLog, colorizedMessage])); } @@ -365,6 +364,14 @@ export function isShortsPage() { const firstSection = extractFirstSectionFromYouTubeURL(window.location.href); return firstSection === "shorts"; } +export function isPlaylistPage() { + const firstSection = extractFirstSectionFromYouTubeURL(window.location.href); + return firstSection === "playlist"; +} +export function isLivePage() { + const firstSection = extractFirstSectionFromYouTubeURL(window.location.href); + return firstSection === "live"; +} export function formatError(error: unknown) { if (error instanceof Error) { return `${error.message}\n${error?.stack}`; @@ -728,7 +735,7 @@ export function groupButtonChanges(changes: ButtonPlacementChange): { .flat() .includes(buttonName) ) - // eslint-disable-next-line prefer-destructuring + // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-unnecessary-type-assertion return (singleButtonChanges[buttonName as SingleButtonFeatureNames] = changes.buttonPlacement[buttonName]); const multiButtonFeatureNames = findKeyByValue(buttonName as Exclude); if (multiButtonFeatureNames === undefined) return; @@ -766,3 +773,70 @@ export function isNewYouTubeVideoLayout(): boolean { return false; // It's the old layout } } +export function getFormattedTimestamp() { + const now = new Date(); + + const month = (now.getMonth() + 1).toString().padStart(2, "0"); + const day = now.getDate().toString().padStart(2, "0"); + const year = now.getFullYear().toString().substr(-2); + const hours = now.getHours(); + const minutes = now.getMinutes().toString().padStart(2, "0"); + const seconds = now.getSeconds().toString().padStart(2, "0"); + const milliseconds = now.getMilliseconds().toString().padStart(3, "0"); + + const period = hours >= 12 ? "PM" : "AM"; + const paddedHours = (hours % 12 || 12).toString().padStart(2, "0"); // Convert to 12-hour format and handle midnight (0 hours) + + return `${month}/${day}/${year} ${paddedHours}:${minutes}:${seconds}:${milliseconds} ${period}`; +} +/** + * Parses an ISO 8601 duration string and returns the total number of seconds. + * + * @param {string} duration - The ISO 8601 duration string to parse. + * @return {number} The total number of seconds represented by the duration string. + */ +export function parseISO8601Duration(duration: string): number { + // Regular expression to match ISO 8601 duration format + const regex = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/; + // Extract hours, minutes, and seconds from the duration string + const matches = regex.exec(duration); + // If the duration string does not match the expected format, return 0 + if (!matches) return 0; + + // Parse the hours, minutes, and seconds from the matches array + const hours = parseInt(matches[1] || "0", 10); + const minutes = parseInt(matches[2] || "0", 10); + const seconds = parseInt(matches[3] || "0", 10); + + // Calculate the total number of seconds by multiplying hours, minutes, and seconds + return hours * 3600 + minutes * 60 + seconds; +} + +/** + * Formats a duration in seconds into a string representation. + * + * @param {number} seconds - The duration in seconds. + * @return {string} The formatted duration string in the format "HHhMMmSSs". + */ +export function formatDuration(seconds: number): string { + // Calculate the hours, minutes, and seconds + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + // Format the hours, minutes, and seconds with leading zeros + const formattedHours = hours.toString(); + const formattedMinutes = minutes.toString().padStart(2, "0"); + const formattedSeconds = secs.toString().padStart(2, "0"); + + // Combine the formatted values into a single string + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; +} +export function timeStringToSeconds(timeString: string): number { + const parts = timeString.split(":").reverse(); + let seconds = 0; + for (let i = 0; i < parts.length; i++) { + seconds += parseInt(parts[i], 10) * Math.pow(60, i); + } + return seconds; +}