diff --git a/.eslintrc b/.eslintrc index 0d6c6eb3..dbb5326d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -57,7 +57,7 @@ "error", { "arrowParens": "always", - "endOfLine": "lf", + "endOfLine": "crlf", "printWidth": 150, "semi": true, "singleQuote": false, diff --git a/CHANGELOG.md b/CHANGELOG.md index 02228277..526489c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,33 @@ ## [1.11.8](https://github.com/VampireChicken12/youtube-enhancer/compare/v1.11.7...v1.11.8) (2023-11-16) - ### Bug Fixes -* Fix on screen display color visibility in settings ([95ad5e9](https://github.com/VampireChicken12/youtube-enhancer/commit/95ad5e9d4ced61c2b8301ba10f14c2d5401cc2d0)) -* Fix selected option text being white on light mode ([2781492](https://github.com/VampireChicken12/youtube-enhancer/commit/278149232e9fc34ef46bc9e9f56aa11fb24e9214)) - - - - +- Fix on screen display color visibility in settings ([95ad5e9](https://github.com/VampireChicken12/youtube-enhancer/commit/95ad5e9d4ced61c2b8301ba10f14c2d5401cc2d0)) +- Fix selected option text being white on light mode ([2781492](https://github.com/VampireChicken12/youtube-enhancer/commit/278149232e9fc34ef46bc9e9f56aa11fb24e9214)) ## Release Artifacts -| File Name | SHA-256 Hash | -| :--- | :---: | -| youtube-enhancer-v1.11.8-Chrome.zip | 87600763560a4935b8369974d9757c3350f572019ada31d8ec5a79fcdfdcf7a6 | + +| File Name | SHA-256 Hash | +| :------------------------------------ | :--------------------------------------------------------------: | +| youtube-enhancer-v1.11.8-Chrome.zip | 87600763560a4935b8369974d9757c3350f572019ada31d8ec5a79fcdfdcf7a6 | | youtube-enhancer-v1.11.8-Chromium.zip | 87600763560a4935b8369974d9757c3350f572019ada31d8ec5a79fcdfdcf7a6 | -| youtube-enhancer-v1.11.8-Edge.zip | 87600763560a4935b8369974d9757c3350f572019ada31d8ec5a79fcdfdcf7a6 | -| youtube-enhancer-v1.11.8-Firefox.zip | 10acae4d7e87dc4b55258a83d6f63c5c7ca8a25eadeae7e822ab78647f3940f6 | +| youtube-enhancer-v1.11.8-Edge.zip | 87600763560a4935b8369974d9757c3350f572019ada31d8ec5a79fcdfdcf7a6 | +| youtube-enhancer-v1.11.8-Firefox.zip | 10acae4d7e87dc4b55258a83d6f63c5c7ca8a25eadeae7e822ab78647f3940f6 | ## [1.11.7](https://github.com/VampireChicken12/youtube-enhancer/compare/v1.11.6...v1.11.7) (2023-11-15) - ### Bug Fixes -* fix bug caused by previous bug fix ([68225b6](https://github.com/VampireChicken12/youtube-enhancer/commit/68225b685a4900200c0f3d045972e1b31ee80955)) - - - - +- fix bug caused by previous bug fix ([68225b6](https://github.com/VampireChicken12/youtube-enhancer/commit/68225b685a4900200c0f3d045972e1b31ee80955)) ## Release Artifacts -| File Name | SHA-256 Hash | -| :--- | :---: | -| youtube-enhancer-v1.11.7-Chrome.zip | 81372739f88cd91097ee422a48a32af264d3df510e2fa0d9c7a53133bdcb9184 | + +| File Name | SHA-256 Hash | +| :------------------------------------ | :--------------------------------------------------------------: | +| youtube-enhancer-v1.11.7-Chrome.zip | 81372739f88cd91097ee422a48a32af264d3df510e2fa0d9c7a53133bdcb9184 | | youtube-enhancer-v1.11.7-Chromium.zip | 81372739f88cd91097ee422a48a32af264d3df510e2fa0d9c7a53133bdcb9184 | -| youtube-enhancer-v1.11.7-Edge.zip | 81372739f88cd91097ee422a48a32af264d3df510e2fa0d9c7a53133bdcb9184 | -| youtube-enhancer-v1.11.7-Firefox.zip | 23b446cb33e9cbe052a7f823f3e32a3f0522d17c63fea9814fe3bd237a9ef49e | +| youtube-enhancer-v1.11.7-Edge.zip | 81372739f88cd91097ee422a48a32af264d3df510e2fa0d9c7a53133bdcb9184 | +| youtube-enhancer-v1.11.7-Firefox.zip | 23b446cb33e9cbe052a7f823f3e32a3f0522d17c63fea9814fe3bd237a9ef49e | ## [1.11.6](https://github.com/VampireChicken12/youtube-enhancer/compare/v1.11.5...v1.11.6) (2023-11-15) diff --git a/README.md b/README.md index d341b715..ecd61886 100755 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ The YouTube Enhancer Extension is a powerful browser extension designed to enhan ## Table of Contents -- [Introduction](#🌟-introduction) -- [Features](#🎛️-features) -- [Building the Extension](#🛠️-building-the-extension) -- [Configuration](#⚙-configuration) -- [Usage](#🔧-usage) -- [Contributing](#📝-contributing) -- [License](#📜-license) +- [Introduction](#-introduction) +- [Features](#%EF%B8%8F-features) +- [Building the Extension](#%EF%B8%8F-building-the-extension) +- [Configuration](#-configuration) +- [Usage](#-usage) +- [Contributing](#-contributing) +- [Internationalization (i18n)](#-internationalization-i18n) +- [License](#-license) ## 🌟 Introduction @@ -40,10 +41,14 @@ YouTube Enhancer is a browser extension that aims to improve your YouTube experi - **Enable Hide Scrollbar:** Hides the pages scroll bar +- **Enable Automatic Theater Mode:** Automatically enables theater mode when you load a video + ### 2. Scroll Wheel Volume Control Settings - **Enable Scroll Wheel Volume Control:** Control video volume with your mouse's scroll wheel for quick and easy adjustments. +- **Scroll Wheel Volume Control Modifier Key**: Optionally, enable a modifier key to adjust the volume only when the specified key is held down during scroll wheel actions. + - **OSD Color:** Choose the color of the On-Screen Display (OSD) for volume control. - **OSD Type:** Define the type of OSD, including text, line, round, or no display. @@ -170,7 +175,7 @@ Using the YouTube Enhancer Extension is straightforward: Contributions to the YouTube Enhancer Extension are welcome! If you'd like to contribute to the development of this extension or report issues, please refer to the project's GitHub repository. -## Internationalization (i18n) +## 🌐 Internationalization (i18n) ### Crowdin Translation Project diff --git a/package-lock.json b/package-lock.json index 7b85d819..48df8f68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "youtube-enhancer", - "version": "1.11.5", + "version": "1.11.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtube-enhancer", - "version": "1.11.5", + "version": "1.11.8", "license": "MIT", "dependencies": { "@formkit/auto-animate": "^0.8.1", @@ -689,9 +689,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1826,9 +1826,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", + "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", "devOptional": true, "dependencies": { "undici-types": "~5.26.4" @@ -2083,23 +2083,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", - "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", @@ -2184,46 +2167,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/types": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", - "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", - "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/utils": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", @@ -2323,23 +2266,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", - "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.10.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.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", @@ -2347,15 +2273,15 @@ "dev": true }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.4.1.tgz", - "integrity": "sha512-7YQOQcVV5x1luD8nkbCDdyYygFvn1hjqJk68UvNAzY2QG4o4N5EwAhLLFNOcd1HrdMwDl0VElP8VutoWf9IvJg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.5.0.tgz", + "integrity": "sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==", "dev": true, "dependencies": { - "@swc/core": "^1.3.95" + "@swc/core": "^1.3.96" }, "peerDependencies": { - "vite": "^4" + "vite": "^4 || ^5" } }, "node_modules/abbrev": { @@ -4153,15 +4079,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4370,9 +4296,9 @@ } }, "node_modules/eslint-plugin-perfectionist": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.3.0.tgz", - "integrity": "sha512-T/1HOysrsyExPr/N5apy3XFhejYqIturtejlSbTGy0WCw5dt72FDT92NOvRRKJvx8lftZDJ8AEIs5nHk9Pfa9Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.4.0.tgz", + "integrity": "sha512-til+vyf56wAUgFv5guBA1Zo5lTw9xj2kCeK/g+9NBtsRy1rkGrlqnvxYNuFExcK3VsPhUUtx5UdScEDz9ahQ5Q==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^6.10.0", @@ -5386,11 +5312,12 @@ } }, "node_modules/import-from-esm": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.2.1.tgz", - "integrity": "sha512-Nly5Ab75rWZmOwtMa0B0NQNnHGcHOQ2zkU/bVENwK2lbPq+kamPDqNKNJ0hF7w7lR/ETD5nGgJq0XbofsZpYCA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.3.tgz", + "integrity": "sha512-U3Qt/CyfFpTUv6LOP2jRTLYjphH6zg3okMfHbyqRa/W2w6hr8OsJWVggNlR4jxuojQy81TgTJTxgSkyoteRGMQ==", "dev": true, "dependencies": { + "debug": "^4.3.4", "import-meta-resolve": "^4.0.0" }, "engines": { @@ -11026,9 +10953,9 @@ } }, "node_modules/semantic-release": { - "version": "22.0.7", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.7.tgz", - "integrity": "sha512-Stx23Hjn7iU8GOAlhG3pHlR7AoNEahj9q7lKBP0rdK2BasGtJ4AWYh3zm1u3SCMuFiA8y4CE/Gu4RGKau1WiaQ==", + "version": "22.0.8", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.8.tgz", + "integrity": "sha512-55rb31jygqIYsGU/rY+gXXm2fnxBIWo9azOjxbqKsPnq7p70zwZ5v+xnD7TxJC+zvS3sy1eHLGXYWCaX3WI76A==", "dev": true, "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", @@ -11047,6 +10974,7 @@ "git-log-parser": "^1.2.0", "hook-std": "^3.0.0", "hosted-git-info": "^7.0.0", + "import-from-esm": "^1.3.1", "lodash-es": "^4.17.21", "marked": "^9.0.0", "marked-terminal": "^6.0.0", diff --git a/package.json b/package.json index 7ea1f200..461f70a1 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,75 @@ { - "name": "youtube-enhancer", - "author": { - "name": "VampireChicken12" - }, - "displayName": "YouTube Enhancer", - "version": "1.11.8", - "description": "YouTube Enhancer is a simple extension that adds some useful features to YouTube.", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/VampireChicken12/youtube-enhancer.git" - }, - "scripts": { - "build": "vite build", - "dev": "nodemon", - "format": "prettier --write .", - "lint": "eslint --fix" - }, - "type": "module", - "dependencies": { - "@formkit/auto-animate": "^0.8.1", - "i18next": "^23.7.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "vite-plugin-css-injected-by-js": "^3.3.0", - "webextension-polyfill": "^0.10.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", - "@types/archiver": "^6.0.1", - "@types/chrome": "^0.0.251", - "@types/node": "^20.9.0", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@types/webextension-polyfill": "^0.10.6", - "@types/youtube-player": "^5.5.10", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", - "@vitejs/plugin-react-swc": "^3.4.1", - "archiver": "^6.0.1", - "autoprefixer": "^10.4.16", - "clsx": "^2.0.0", - "concurrently": "^8.2.2", - "eslint": "^8.53.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-no-secrets": "^0.8.9", - "eslint-plugin-perfectionist": "^2.3.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-tailwindcss": "^3.13.0", - "fs-extra": "^11.1.1", - "get-installed-browsers": "^0.1.7", - "nodemon": "^3.0.1", - "postcss": "^8.4.31", - "prettier": "^3.0.3", - "semantic-release": "^22.0.7", - "tailwind-merge": "^2.0.0", - "tailwindcss": "^3.3.5", - "ts-json-as-const": "^1.0.7", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "vite": "^4.5.0", - "zod": "^3.22.4", - "zod-error": "^1.5.0" - } -} \ No newline at end of file + "name": "youtube-enhancer", + "author": { + "name": "VampireChicken12" + }, + "displayName": "YouTube Enhancer", + "version": "1.11.8", + "description": "YouTube Enhancer is a simple extension that adds some useful features to YouTube.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/VampireChicken12/youtube-enhancer.git" + }, + "scripts": { + "build": "vite build", + "dev": "nodemon", + "format": "prettier --write .", + "lint": "eslint --fix" + }, + "type": "module", + "dependencies": { + "@formkit/auto-animate": "^0.8.1", + "i18next": "^23.7.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite-plugin-css-injected-by-js": "^3.3.0", + "webextension-polyfill": "^0.10.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", + "@types/archiver": "^6.0.1", + "@types/chrome": "^0.0.251", + "@types/node": "^20.9.0", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@types/webextension-polyfill": "^0.10.6", + "@types/youtube-player": "^5.5.10", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@vitejs/plugin-react-swc": "^3.4.1", + "archiver": "^6.0.1", + "autoprefixer": "^10.4.16", + "clsx": "^2.0.0", + "concurrently": "^8.2.2", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-no-secrets": "^0.8.9", + "eslint-plugin-perfectionist": "^2.3.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tailwindcss": "^3.13.0", + "fs-extra": "^11.1.1", + "get-installed-browsers": "^0.1.7", + "nodemon": "^3.0.1", + "postcss": "^8.4.31", + "prettier": "^3.0.3", + "semantic-release": "^22.0.7", + "tailwind-merge": "^2.0.0", + "tailwindcss": "^3.3.5", + "ts-json-as-const": "^1.0.7", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "vite": "^4.5.0", + "zod": "^3.22.4", + "zod-error": "^1.5.0" + } +} diff --git a/prettier.config.cjs b/prettier.config.cjs index 2c965d42..3b870a9c 100644 --- a/prettier.config.cjs +++ b/prettier.config.cjs @@ -1,7 +1,7 @@ /** @type {import("prettier").Config} */ module.exports = { arrowParens: "always", - endOfLine: "lf", + endOfLine: "crlf", printWidth: 150, semi: true, singleQuote: false, diff --git a/public/locales/en-US.json b/public/locales/en-US.json index cfa87117..443dc2cd 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -59,7 +59,7 @@ "title": "Miscellaneous settings", "features": { "rememberLastVolume": { - "title": "Remembers the volume you were watching at and sets it as the volume when you open a new video", + "title": "Remembers the volume of the last video you were watching and sets it when you open a new video", "label": "Remember last volume" }, "maximizePlayerButton": { @@ -81,6 +81,10 @@ "hideScrollbar": { "title": "Hides the pages scrollbar", "label": "Enable hide scrollbar" + }, + "automaticTheaterMode": { + "title": "Automatically enables theater mode when you load a video", + "label": "Enable automatic theater mode" } } }, @@ -90,16 +94,16 @@ "title": "Lets you use the scroll wheel to control the volume of the video you're watching", "label": "Enable scroll wheel volume control" }, - "osdColor": { "title": "The color of the On Screen Display", "label": "OSD color" }, - "osdType": { "title": "The type of On Screen Display", "label": "OSD type" }, - "osdPosition": { "title": "The position of the On Screen Display", "label": "OSD position" }, - "osdOpacity": { - "title": "The opacity of the On Screen Display", - "label": "OSD opacity" + "osdColor": { "title": "Select the color for the On-Screen Display", "label": "OSD Color" }, + "osdType": { "title": "Select the style of On-Screen Display", "label": "OSD Type" }, + "osdPosition": { "title": "Select the position of the On-Screen Display", "label": "OSD Position" }, + "osdOpacity": { "title": "Adjust the transparency of the On-Screen Display", "label": "OSD Opacity" }, + "osdVolumeAdjustmentSteps": { "title": "Adjust the volume change per scroll", "label": "Volume Change Per Scroll" }, + "osdHide": { "title": "Specify the time, in milliseconds, before automatically hiding the OSD", "label": "Hide Delay" }, + "osdPadding": { + "title": "Adjust the spacing around the on-screen display (OSD) in pixels. This applies specifically to corner OSD.", + "label": "Padding" }, - "osdVolumeAdjustmentSteps": { "title": "The amount to adjust volume per scroll", "label": "Amount to adjust" }, - "osdHide": { "title": "The amount of milliseconds to wait before hiding the OSD", "label": "Time to hide" }, - "osdPadding": { "title": "The amount of padding to add to the OSD (in pixels, only applies to corner OSD)", "label": "Padding" }, "onScreenDisplay": { "colors": { "red": "Red", @@ -124,6 +128,22 @@ "line": "Line", "round": "Round" } + }, + "modifierKey": { + "title": "Scroll Wheel Modifier Key", + "enable": { + "title": "Press a modifier key to enable volume adjustment with the scroll wheel.", + "label": "Enable modifier key" + }, + "options": { + "altKey": "{{KEY}} key", + "ctrlKey": "{{KEY}} key", + "shiftKey": "{{KEY}} key" + }, + "select": { + "label": "Modifier key", + "title": "The modifier key to use" + } } }, "automaticQuality": { @@ -173,11 +193,6 @@ "label": "Screenshot format", "title": "The format to save the screenshot in" }, - "format": { - "png": "PNG", - "jpeg": "JPEG", - "webp": "WebP" - }, "saveAs": { "file": "File", "clipboard": "Clipboard" diff --git a/public/locales/en-US.json.d.ts b/public/locales/en-US.json.d.ts index 05957ae0..c3d133b8 100644 --- a/public/locales/en-US.json.d.ts +++ b/public/locales/en-US.json.d.ts @@ -74,6 +74,10 @@ interface EnUS { }; miscellaneous: { features: { + automaticTheaterMode: { + label: "Enable automatic theater mode"; + title: "Automatically enables theater mode when you load a video"; + }; hideScrollbar: { label: "Enable hide scrollbar"; title: "Hides the pages scrollbar" }; loopButton: { label: "Enable loop button"; @@ -89,7 +93,7 @@ interface EnUS { }; rememberLastVolume: { label: "Remember last volume"; - title: "Remembers the volume you were watching at and sets it as the volume when you open a new video"; + title: "Remembers the volume of the last video you were watching and sets it when you open a new video"; }; videoHistory: { label: "Enable video history"; @@ -111,7 +115,6 @@ interface EnUS { label: "Enable screenshot button"; title: "Adds a button to the player to take a screenshot of the video"; }; - format: { jpeg: "JPEG"; png: "PNG"; webp: "WebP" }; saveAs: { clipboard: "Clipboard"; file: "File" }; selectFormat: { label: "Screenshot format"; title: "The format to save the screenshot in" }; selectSaveAs: { label: "Screenshot save type"; title: "The screenshot save type" }; @@ -122,6 +125,15 @@ interface EnUS { label: "Enable scroll wheel volume control"; title: "Lets you use the scroll wheel to control the volume of the video you're watching"; }; + modifierKey: { + enable: { + label: "Enable modifier key"; + title: "Press a modifier key to enable volume adjustment with the scroll wheel."; + }; + options: { altKey: "{{KEY}} key"; ctrlKey: "{{KEY}} key"; shiftKey: "{{KEY}} key" }; + select: { label: "Modifier key"; title: "The modifier key to use" }; + title: "Scroll Wheel Modifier Key"; + }; onScreenDisplay: { colors: { blue: "Blue"; @@ -142,19 +154,25 @@ interface EnUS { }; type: { line: "Line"; no_display: "No display"; round: "Round"; text: "Text" }; }; - osdColor: { label: "OSD color"; title: "The color of the On Screen Display" }; + osdColor: { label: "OSD Color"; title: "Select the color for the On-Screen Display" }; osdHide: { - label: "Time to hide"; - title: "The amount of milliseconds to wait before hiding the OSD"; + label: "Hide Delay"; + title: "Specify the time, in milliseconds, before automatically hiding the OSD"; + }; + osdOpacity: { + label: "OSD Opacity"; + title: "Adjust the transparency of the On-Screen Display"; }; - osdOpacity: { label: "OSD opacity"; title: "The opacity of the On Screen Display" }; osdPadding: { label: "Padding"; - title: "The amount of padding to add to the OSD (in pixels, only applies to corner OSD)"; + title: "Adjust the spacing around the on-screen display (OSD) in pixels. This applies specifically to corner OSD."; + }; + osdPosition: { label: "OSD Position"; title: "Select the position of the On-Screen Display" }; + osdType: { label: "OSD Type"; title: "Select the style of On-Screen Display" }; + osdVolumeAdjustmentSteps: { + label: "Volume Change Per Scroll"; + title: "Adjust the volume change per scroll"; }; - osdPosition: { label: "OSD position"; title: "The position of the On Screen Display" }; - osdType: { label: "OSD type"; title: "The type of On Screen Display" }; - osdVolumeAdjustmentSteps: { label: "Amount to adjust"; title: "The amount to adjust volume per scroll" }; title: "Scroll wheel volume control settings"; }; volumeBoost: { diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 43794a73..d9836e3c 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,12 +1,12 @@ -import type { configuration, configurationKeys } from "@/src/@types"; +import type { ModifierKey, configuration, configurationKeys } from "@/src/types"; import type EnUS from "public/locales/en-US.json"; import type { ChangeEvent, Dispatch, SetStateAction } from "react"; import "@/assets/styles/tailwind.css"; import "@/components/Settings/Settings.css"; import { useNotifications } from "@/hooks"; -import { youtubePlayerSpeedRate } from "@/src/@types"; import { availableLocales, type i18nInstanceType } from "@/src/i18n"; +import { youtubePlayerSpeedRate } from "@/src/types"; import { configurationImportSchema } from "@/src/utils/constants"; import { cn, settingsAreDefault } from "@/src/utils/utilities"; import React, { Suspense, useEffect, useState } from "react"; @@ -52,7 +52,7 @@ function LanguageOptions({ >; setSelectedDisplayType: Dispatch>; setSelectedLanguage: Dispatch>; + setSelectedModifierKey: Dispatch>; setSelectedPlayerQuality: Dispatch>; setSelectedPlayerSpeed: Dispatch>; setSelectedScreenshotFormat: Dispatch>; @@ -163,12 +167,31 @@ export default function Settings({ returnObjects: true }); const { - format: { jpeg, png, webp }, saveAs: { clipboard, file } } = t("settings.sections.screenshotButton", { defaultValue: {}, returnObjects: true }); + const scrollWheelVolumeControlModifierKeyOptions = [ + { + label: t("settings.sections.scrollWheelVolumeControl.modifierKey.options.altKey", { + KEY: "Alt" + }), + value: "altKey" + }, + { + label: t("settings.sections.scrollWheelVolumeControl.modifierKey.options.ctrlKey", { + KEY: "Ctrl" + }), + value: "ctrlKey" + }, + { + label: t("settings.sections.scrollWheelVolumeControl.modifierKey.options.shiftKey", { + KEY: "Shift" + }), + value: "shiftKey" + } + ] as { label: string; value: ModifierKey }[] as SelectOption[]; const colorOptions: SelectOption[] = [ { element:
, @@ -266,9 +289,9 @@ export default function Settings({ ].reverse(); const YouTubePlayerSpeedOptions: SelectOption[] = youtubePlayerSpeedRate.map((rate) => ({ label: rate.toString(), value: rate.toString() })); const ScreenshotFormatOptions: SelectOption[] = [ - { label: png, value: "png" }, - { label: jpeg, value: "jpeg" }, - { label: webp, value: "webp" } + { label: "PNG", value: "png" }, + { label: "JPEG", value: "jpeg" }, + { label: "WebP", value: "webp" } ]; const ScreenshotSaveAsOptions: SelectOption[] = [ { label: file, value: "file" }, @@ -406,6 +429,14 @@ export default function Settings({ title={t("settings.sections.miscellaneous.features.hideScrollbar.title")} type="checkbox" /> + @@ -417,9 +448,28 @@ export default function Settings({ title={t("settings.sections.scrollWheelVolumeControl.enable.title")} type="checkbox" /> + + (); const [selectedScreenshotFormat, setSelectedScreenshotFormat] = useState(); const [selectedLanguage, setSelectedLanguage] = useState(); + const [selectedModifierKey, setSelectedModifierKey] = useState(); const [i18nInstance, setI18nInstance] = useState(null); useEffect(() => { const fetchSettings = () => { @@ -25,15 +26,17 @@ export default function SettingsWrapper(): JSX.Element { for (const [key, value] of Object.entries(settings)) { settings[key] = parseStoredValue(value); } - setSettings({ ...settings } as configuration); - setSelectedColor(settings.osd_display_color); - setSelectedDisplayType(settings.osd_display_type); - setSelectedDisplayPosition(settings.osd_display_position); - setSelectedPlayerQuality(settings.player_quality); - setSelectedPlayerSpeed(settings.player_speed); - setSelectedScreenshotSaveAs(settings.screenshot_save_as); - setSelectedScreenshotFormat(settings.screenshot_format); - setSelectedLanguage(settings.language); + const castedSettings = settings as configuration; + setSettings({ ...castedSettings }); + setSelectedColor(castedSettings.osd_display_color); + setSelectedDisplayType(castedSettings.osd_display_type); + setSelectedDisplayPosition(castedSettings.osd_display_position); + setSelectedPlayerQuality(castedSettings.player_quality); + setSelectedPlayerSpeed(castedSettings.player_speed.toString()); + setSelectedScreenshotSaveAs(castedSettings.screenshot_save_as); + setSelectedScreenshotFormat(castedSettings.screenshot_format); + setSelectedLanguage(castedSettings.language); + setSelectedModifierKey(castedSettings.scroll_wheel_volume_control_modifier_key); }); }; @@ -116,6 +119,7 @@ export default function SettingsWrapper(): JSX.Element { selectedDisplayPosition={selectedDisplayPosition} selectedDisplayType={selectedDisplayType} selectedLanguage={selectedLanguage} + selectedModifierKey={selectedModifierKey} selectedPlayerQuality={selectedPlayerQuality} selectedPlayerSpeed={selectedPlayerSpeed} selectedScreenshotFormat={selectedScreenshotFormat} @@ -124,6 +128,7 @@ export default function SettingsWrapper(): JSX.Element { setSelectedDisplayPosition={setSelectedDisplayPosition} setSelectedDisplayType={setSelectedDisplayType} setSelectedLanguage={setSelectedLanguage} + setSelectedModifierKey={setSelectedModifierKey} setSelectedPlayerQuality={setSelectedPlayerQuality} setSelectedPlayerSpeed={setSelectedPlayerSpeed} setSelectedScreenshotFormat={setSelectedScreenshotFormat} diff --git a/src/features/automaticTheaterMode/index.ts b/src/features/automaticTheaterMode/index.ts new file mode 100644 index 00000000..e6696125 --- /dev/null +++ b/src/features/automaticTheaterMode/index.ts @@ -0,0 +1,33 @@ +import type { YouTubePlayerDiv } from "@/src/types"; + +import { isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; + +export async function automaticTheaterMode() { + // Wait for the "options" message from the content script + const optionsData = await waitForSpecificMessage("options", "request_data", "content"); + if (!optionsData) return; + const { + data: { options } + } = optionsData; + // Extract the necessary properties from the options object + const { enable_automatic_theater_mode } = options; + // 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") as YouTubePlayerDiv | null) : null; + // If player element is not available, return + if (!playerContainer) return; + const { width } = await playerContainer.getSize(); + const { + body: { clientWidth } + } = document; + const isTheaterMode = width === clientWidth; + // Get the size button + const sizeButton = document.querySelector("button.ytp-size-button") as HTMLButtonElement | null; + // If the size button is not available return + if (!sizeButton) return; + if (!isTheaterMode) { + sizeButton.click(); + } +} diff --git a/src/features/featureMenu/index.ts b/src/features/featureMenu/index.ts index 80294a71..86529ea8 100644 --- a/src/features/featureMenu/index.ts +++ b/src/features/featureMenu/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { createTooltip, isShortsPage, isWatchPage } from "@/src/utils/utilities"; diff --git a/src/features/featureMenu/utils.ts b/src/features/featureMenu/utils.ts index e528e766..f4a20bf7 100644 --- a/src/features/featureMenu/utils.ts +++ b/src/features/featureMenu/utils.ts @@ -1,4 +1,4 @@ -import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, WithId } from "@/src/@types"; +import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, WithId } from "@/src/types"; import eventManager, { type FeatureName } from "@/src/utils/EventManager"; import { waitForAllElements } from "@/src/utils/utilities"; diff --git a/src/features/loopButton/index.ts b/src/features/loopButton/index.ts index 390381ac..d8e8093f 100644 --- a/src/features/loopButton/index.ts +++ b/src/features/loopButton/index.ts @@ -26,7 +26,7 @@ export async function addLoopButton() { featureName: "loopButton", icon: loopSVG, isToggle: true, - label: `Loop`, + label: window.i18nextInstance.t("pages.content.features.loopButton.label"), listener: loopButtonClickListener }); const loopChangedHandler = (mutationList: MutationRecord[]) => { diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts index 51d7d492..4003bb3a 100644 --- a/src/features/maximizePlayerButton/index.ts +++ b/src/features/maximizePlayerButton/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { waitForSpecificMessage } from "@/src/utils/utilities"; @@ -46,7 +46,7 @@ export async function addMaximizePlayerButton(): Promise { featureName: "maximizePlayerButton", icon: maximizeSVG, isToggle: true, - label: "Maximize Player", + label: window.i18nextInstance.t("pages.content.features.maximizePlayerButton.label"), listener: maximizePlayerButtonClickListener }); function ytpLeftButtonMouseEnterListener(event: MouseEvent) { diff --git a/src/features/maximizePlayerButton/utils.ts b/src/features/maximizePlayerButton/utils.ts index 03890118..497e5199 100644 --- a/src/features/maximizePlayerButton/utils.ts +++ b/src/features/maximizePlayerButton/utils.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; let wasInTheatreMode = false; diff --git a/src/features/playerQuality/index.ts b/src/features/playerQuality/index.ts index c774fc79..b6a7bf5f 100644 --- a/src/features/playerQuality/index.ts +++ b/src/features/playerQuality/index.ts @@ -1,7 +1,6 @@ -import type { YouTubePlayerDiv, YoutubePlayerQualityLabel, YoutubePlayerQualityLevel } from "@/src/@types"; +import type { YouTubePlayerDiv, YoutubePlayerQualityLevel } from "@/src/types"; -import { youtubePlayerQualityLabel, youtubePlayerQualityLevel } from "@/src/@types"; -import { browserColorLog, chooseClosetQuality, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { browserColorLog, chooseClosestQuality, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; /** * Sets the player quality based on the options received from a specific message. @@ -25,9 +24,6 @@ export default async function setPlayerQuality(): Promise { // If player quality is not specified, return if (!player_quality) return; - // Initialize the playerQuality variable - let playerQuality: YoutubePlayerQualityLabel | YoutubePlayerQualityLevel = player_quality; - // Get the player element const playerContainer = isWatchPage() ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) @@ -45,31 +41,14 @@ export default async function setPlayerQuality(): Promise { const availableQualityLevels = (await playerContainer.getAvailableQualityLevels()) as YoutubePlayerQualityLevel[]; // Check if the specified player quality is available - if (playerQuality && playerQuality !== "auto") { - if (!availableQualityLevels.includes(playerQuality)) { - // Convert the available quality levels to their corresponding labels - const availableResolutions = availableQualityLevels.reduce(function (array, elem) { - if (youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(elem)]) { - array.push(youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(elem)]); - } - return array; - }, [] as YoutubePlayerQualityLabel[]); - - // Choose the closest quality level based on the available resolutions - playerQuality = chooseClosetQuality(youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(playerQuality)], availableResolutions); - - // If the chosen quality level is not available, return - if (!youtubePlayerQualityLevel.at(youtubePlayerQualityLabel.indexOf(playerQuality))) return; - - // Update the playerQuality variable - playerQuality = youtubePlayerQualityLevel.at(youtubePlayerQualityLabel.indexOf(playerQuality)) as YoutubePlayerQualityLevel; - } - + if (player_quality && player_quality !== "auto") { + const closestQuality = chooseClosestQuality(player_quality, availableQualityLevels); + if (!closestQuality) return; // Log the message indicating the player quality being set - browserColorLog(`Setting player quality to ${playerQuality}`, "FgMagenta"); + browserColorLog(`Setting player quality to ${closestQuality}`, "FgMagenta"); // Set the playback quality and update the default quality in the dataset - playerContainer.setPlaybackQualityRange(playerQuality); - playerContainer.dataset.defaultQuality = playerQuality; + playerContainer.setPlaybackQualityRange(closestQuality); + playerContainer.dataset.defaultQuality = closestQuality; } } diff --git a/src/features/playerSpeed/index.ts b/src/features/playerSpeed/index.ts index 06917dd4..ff2d1e10 100644 --- a/src/features/playerSpeed/index.ts +++ b/src/features/playerSpeed/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { browserColorLog, isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; @@ -86,42 +86,75 @@ export function restorePlayerSpeed() { video.playbackRate = Number(playerSpeed); } export function setupPlaybackSpeedChangeListener() { - const settingsMenu = document.querySelector("div.ytp-settings-menu:not(#yte-feature-menu)"); - - // Function to handle the playback speed click event - function handlePlaybackSpeedClick(node: HTMLDivElement): void { - // Extract the playback speed value - const speedValue = node.textContent?.trim(); + const settingsPanelMenu = document.querySelector("div.ytp-settings-menu:not(#yte-feature-menu)"); + const speedMenuItemClickListener = (event: Event) => { + const { target: speedMenuItem } = event as Event & { target: HTMLDivElement }; + if (!speedMenuItem) return; + const { textContent: speedValue } = speedMenuItem; // If the playback speed is not available, return if (!speedValue) return; - const playerSpeed = speedValue === "Normal" ? 1 : Number(speedValue); + const speedValueRegex = /(? { - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - const titleElement: HTMLSpanElement | null = document.querySelector("div.ytp-panel > div.ytp-panel-header > span.ytp-panel-title"); - // TODO: fix this it relies on the language being English - if (titleElement && titleElement.textContent && titleElement.textContent.includes("Playback speed")) { - const menuItems: NodeListOf = document.querySelectorAll("div.ytp-panel-menu div.ytp-menuitem"); - menuItems.forEach((node: HTMLDivElement) => { - eventManager.addEventListener(node, "click", () => handlePlaybackSpeedClick(node), "playerSpeed"); - }); - } else { - const menuItems: NodeListOf = document.querySelectorAll("div.ytp-panel-menu div.ytp-menuitem"); - menuItems.forEach((node: HTMLDivElement) => { - eventManager.removeEventListener(node, "click", "playerSpeed"); - }); - } + const playerSpeedMenuObserver = new MutationObserver((mutationsList: MutationRecord[]) => { + mutationsList.forEach((mutation) => { + const { target: targetElement } = mutation as MutationRecord & { target: HTMLDivElement }; + + // Check if the target element has the desired structure + const panelHeader: HTMLDivElement | null = targetElement.querySelector("div.ytp-panel > div.ytp-panel-header"); + const panelMenu: HTMLDivElement | null = targetElement.querySelector("div.ytp-panel > div.ytp-panel-menu"); + const menuItems = panelMenu?.querySelectorAll("div.ytp-menuitem"); + if (panelHeader && panelMenu && menuItems && menuItems.length === 8) { + menuItems.forEach((menuItem) => { + eventManager.addEventListener(menuItem as HTMLDivElement, "click", speedMenuItemClickListener, "playerSpeed"); + }); } - } + }); + }); + const customSpeedSliderObserver = new MutationObserver((mutationsList: MutationRecord[]) => { + mutationsList.forEach((mutation) => { + const { target: targetElement } = mutation as MutationRecord & { target: HTMLDivElement }; + const speedValue = targetElement.getAttribute("aria-valuenow"); + // If the playback speed is not available, return + if (!speedValue) return; + const playerSpeed = parseFloat(speedValue); + window.localStorage.setItem("playerSpeed", String(playerSpeed)); + }); }); - const config: MutationObserverInit = { childList: true, subtree: true }; - if (settingsMenu) { - observer.observe(settingsMenu, config); + if (settingsPanelMenu) { + playerSpeedMenuObserver.observe(settingsPanelMenu, config); + customSpeedSliderObserver.observe(settingsPanelMenu, { + attributeFilter: ["aria-valuenow"], + attributes: true, + childList: true, + subtree: true + }); + const menuItems = [...document.querySelectorAll('div.ytp-settings-menu:not(#yte-feature-menu) > .ytp-panel > .ytp-panel-menu [role="menuitem"]')]; + const speedMenuItem = menuItems.find( + (el) => + el.children[0].innerHTML === + `` + ) as HTMLDivElement | undefined; + if (!speedMenuItem) return; + const { + children: [, , speedMenuItemContent] + } = speedMenuItem; + if (!speedMenuItemContent) return; + const { textContent: speedValue } = speedMenuItem; + // If the playback speed is not available, return + if (!speedValue) return; + const playerSpeed = isNaN(Number(speedValue)) ? 1 : Number(speedValue); + window.localStorage.setItem("playerSpeed", String(playerSpeed)); } } diff --git a/src/features/remainingTime/index.ts b/src/features/remainingTime/index.ts index 10819a47..c397ffd5 100644 --- a/src/features/remainingTime/index.ts +++ b/src/features/remainingTime/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; diff --git a/src/features/remainingTime/utils.ts b/src/features/remainingTime/utils.ts index a692ad05..b255782b 100644 --- a/src/features/remainingTime/utils.ts +++ b/src/features/remainingTime/utils.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; export function formatTime(timeInSeconds: number) { timeInSeconds = Math.round(timeInSeconds); const units: number[] = [ diff --git a/src/features/rememberVolume/index.ts b/src/features/rememberVolume/index.ts index eff36d67..f4cbe82d 100644 --- a/src/features/rememberVolume/index.ts +++ b/src/features/rememberVolume/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; diff --git a/src/features/rememberVolume/utils.ts b/src/features/rememberVolume/utils.ts index 67cb4178..510ce446 100644 --- a/src/features/rememberVolume/utils.ts +++ b/src/features/rememberVolume/utils.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv, configuration } from "@/src/@types"; +import type { YouTubePlayerDiv, configuration } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; import { browserColorLog, isShortsPage, isWatchPage, sendContentOnlyMessage, waitForSpecificMessage } from "@/src/utils/utilities"; diff --git a/src/features/screenshotButton/index.ts b/src/features/screenshotButton/index.ts index ee4ff1db..cc1b1272 100644 --- a/src/features/screenshotButton/index.ts +++ b/src/features/screenshotButton/index.ts @@ -1,7 +1,7 @@ import eventManager from "@/src/utils/EventManager"; import { waitForSpecificMessage } from "@/src/utils/utilities"; -import { addFeatureItemToMenu, removeFeatureItemFromMenu } from "../featureMenu/utils"; +import { addFeatureItemToMenu, getFeatureMenuItem, removeFeatureItemFromMenu } from "../featureMenu/utils"; async function takeScreenshot(videoElement: HTMLVideoElement) { try { @@ -35,13 +35,26 @@ async function takeScreenshot(videoElement: HTMLVideoElement) { switch (screenshot_save_as) { case "clipboard": { - const screenshotTooltip = document.querySelector("div#yte-screenshot-tooltip"); - if (screenshotTooltip) { - const clipboardImage = new ClipboardItem({ "image/png": blob }); - navigator.clipboard.write([clipboardImage]); - navigator.clipboard.writeText(dataUrl); - screenshotTooltip.textContent = window.i18nextInstance.t("pages.content.features.screenshotButton.copiedToClipboard"); - } + const tooltip = document.createElement("div"); + const screenshotMenuItem = getFeatureMenuItem("screenshotButton"); + if (!screenshotMenuItem) return; + const rect = screenshotMenuItem.getBoundingClientRect(); + tooltip.classList.add("yte-button-tooltip"); + tooltip.classList.add("ytp-tooltip"); + tooltip.classList.add("ytp-rounded-tooltip"); + tooltip.classList.add("ytp-bottom"); + tooltip.id = "yte-screenshot-tooltip"; + tooltip.style.left = `${rect.left + rect.width / 2}px`; + tooltip.style.top = `${rect.top - 2}px`; + tooltip.style.zIndex = "99999"; + tooltip.textContent = window.i18nextInstance.t("pages.content.features.screenshotButton.copiedToClipboard"); + document.body.appendChild(tooltip); + const clipboardImage = new ClipboardItem({ "image/png": blob }); + navigator.clipboard.write([clipboardImage]); + navigator.clipboard.writeText(dataUrl); + setTimeout(() => { + tooltip.remove(); + }, 1200); break; } case "file": { diff --git a/src/features/scrollWheelVolumeControl/index.ts b/src/features/scrollWheelVolumeControl/index.ts index 364c1f71..e30c03e6 100644 --- a/src/features/scrollWheelVolumeControl/index.ts +++ b/src/features/scrollWheelVolumeControl/index.ts @@ -1,9 +1,14 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import { isShortsPage, isWatchPage, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities"; -import { adjustVolume, drawVolumeDisplay, getScrollDirection, setupScrollListeners } from "./utils"; +import { adjustVolume, drawVolumeDisplay, setupScrollListeners } from "./utils"; +function preventScroll(event: Event) { + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); +} /** * Adjusts the volume on scroll wheel events. * It listens for scroll wheel events on specified container selectors, @@ -28,10 +33,26 @@ export default async function adjustVolumeOnScrollWheel(): Promise { // Define the event handler for the scroll wheel events const handleWheel = async (event: Event) => { - const wheelEvent = event as WheelEvent | undefined; - - // If it's not a wheel event, return - if (!wheelEvent) return; + preventScroll(event); + // Wait for the "options" message from the content script + const optionsData = await waitForSpecificMessage("options", "request_data", "content"); + if (!optionsData) return; + const { + data: { options } + } = optionsData; + // Extract the necessary properties from the options object + const { enable_scroll_wheel_volume_control, enable_scroll_wheel_volume_control_modifier_key, scroll_wheel_volume_control_modifier_key } = options; + const wheelEvent = event as WheelEvent; + if ( + !( + (enable_scroll_wheel_volume_control && + enable_scroll_wheel_volume_control_modifier_key && + scroll_wheel_volume_control_modifier_key && + wheelEvent[scroll_wheel_volume_control_modifier_key]) || + (enable_scroll_wheel_volume_control && !enable_scroll_wheel_volume_control_modifier_key) + ) + ) + return; // Get the player element const playerContainer = isWatchPage() ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) @@ -42,15 +63,9 @@ export default async function adjustVolumeOnScrollWheel(): Promise { if (!playerContainer) return; // Adjust the volume based on the scroll direction - const scrollDelta = getScrollDirection(wheelEvent.deltaY); - // Wait for the "options" message from the content script - const optionsData = await waitForSpecificMessage("options", "request_data", "content"); - if (!optionsData) return; - const { - data: { options } - } = optionsData; + const scrollDelta = wheelEvent.deltaY < 0 ? 1 : -1; // Adjust the volume based on the scroll direction and options - const { newVolume } = await adjustVolume(scrollDelta, options.volume_adjustment_steps); + const { newVolume } = await adjustVolume(playerContainer, scrollDelta, options.volume_adjustment_steps); // Update the volume display drawVolumeDisplay({ @@ -60,7 +75,7 @@ export default async function adjustVolumeOnScrollWheel(): Promise { displayPadding: options.osd_display_padding, displayPosition: options.osd_display_position, displayType: options.osd_display_type, - playerContainer: playerContainer || null, + playerContainer: playerContainer, volume: newVolume }); }; diff --git a/src/features/scrollWheelVolumeControl/utils.ts b/src/features/scrollWheelVolumeControl/utils.ts index 8d66d197..bab674f2 100644 --- a/src/features/scrollWheelVolumeControl/utils.ts +++ b/src/features/scrollWheelVolumeControl/utils.ts @@ -1,33 +1,23 @@ -import type { OnScreenDisplayColor, OnScreenDisplayPosition, OnScreenDisplayType, Selector, YouTubePlayerDiv } from "@/src/@types"; +import type { OnScreenDisplayColor, OnScreenDisplayPosition, OnScreenDisplayType, Selector, YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/src/utils/EventManager"; -import { browserColorLog, clamp, isShortsPage, isWatchPage, round, toDivisible } from "@/src/utils/utilities"; +import { browserColorLog, clamp, createStyledElement, isShortsPage, round, toDivisible } from "@/src/utils/utilities"; -/** - * Get the scroll direction based on the deltaY value. - * - * @param deltaY - The deltaY value from the scroll event. - * @returns The scroll direction: 1 for scrolling up, -1 for scrolling down. - */ -export function getScrollDirection(deltaY: number): number { - return deltaY < 0 ? 1 : -1; -} /** * Adjust the volume based on the scroll direction. * + * @param playerContainer - The player container element. * @param scrollDelta - The scroll direction. * @param adjustmentSteps - The volume adjustment steps. * @returns Promise that resolves with the new volume. */ -export function adjustVolume(scrollDelta: number, volumeStep: number): Promise<{ newVolume: number; oldVolume: number }> { +export function adjustVolume( + playerContainer: YouTubePlayerDiv, + scrollDelta: number, + volumeStep: number +): Promise<{ newVolume: number; oldVolume: number }> { return new Promise((resolve) => { (async () => { - const playerContainer = isWatchPage() - ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) - : isShortsPage() - ? (document.querySelector("div#shorts-player") as YouTubePlayerDiv | null) - : null; - if (!playerContainer) return; if (!playerContainer.getVolume) return; if (!playerContainer.setVolume) return; if (!playerContainer.isMuted) return; @@ -53,16 +43,35 @@ export function setupScrollListeners(selector: Selector, handleWheel: (event: Ev const elements: NodeListOf = document.querySelectorAll(selector); if (!elements.length) return browserColorLog(`No elements found with selector ${selector}`, "FgRed"); for (const element of elements) { - const mouseWheelListener = (e: Event) => { - if (element.clientHeight === 0) return; - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - handleWheel(e); - }; - eventManager.addEventListener(element, "wheel", mouseWheelListener, "scrollWheelVolumeControl"); + eventManager.addEventListener(element, "wheel", handleWheel, "scrollWheelVolumeControl", { passive: false }); } } +function calculateCanvasPosition(displayPosition: OnScreenDisplayPosition, displayPadding: number, paddingTop: number, paddingBottom: number) { + let styles: Partial = {}; + + switch (displayPosition) { + case "top_left": + styles = { left: `${displayPadding}px`, top: `${displayPadding + paddingTop}px` }; + break; + case "top_right": + styles = { right: `${displayPadding}px`, top: `${displayPadding + paddingTop}px` }; + break; + case "bottom_left": + styles = { bottom: `${displayPadding + paddingBottom}px`, left: `${displayPadding}px` }; + break; + case "bottom_right": + styles = { bottom: `${displayPadding + paddingBottom}px`, right: `${displayPadding}px` }; + break; + case "center": + styles = { left: "50%", top: "50%", transform: "translate(-50%, -50%)" }; + break; + default: + console.error("Invalid display position"); + break; + } + + return styles; +} /** * Draw the volume display over the player. * @@ -88,15 +97,6 @@ export function drawVolumeDisplay({ volume: number; }) { volume = clamp(volume, 0, 100); - const canvas = document.createElement("canvas"); - canvas.id = "volume-display"; - const context = canvas.getContext("2d"); - - if (!context) { - browserColorLog("Canvas not supported", "FgRed"); - return; - } - // Set canvas dimensions based on player/container dimensions if (!playerContainer) { browserColorLog("Player container not found", "FgRed"); @@ -129,100 +129,77 @@ export function drawVolumeDisplay({ browserColorLog(`Clamped display padding to ${displayPadding}`, "FgRed"); } - // Set canvas styles for positioning - canvas.style.position = "absolute"; + const bottomElement: HTMLDivElement | null = + 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 = bottomElement + ? bottomElement.offsetHeight - + (parseInt(getComputedStyle(bottomElement).marginTop, 10) + + parseInt(getComputedStyle(bottomElement).marginBottom, 10) + + parseInt(getComputedStyle(bottomElement).paddingTop, 10) + + parseInt(getComputedStyle(bottomElement).paddingBottom, 10)) + + 10 + : 0; + const paddingTop = isShortsPage() ? topRectTop / 2 : 0; + const paddingBottom = isShortsPage() ? heightExcludingMarginPadding : Math.round(bottomRectBottom - bottomRectTop); - switch (displayPosition) { - case "top_left": - canvas.style.top = `${displayPadding}px`; - canvas.style.left = `${displayPadding}px`; - break; - case "top_right": - canvas.style.top = `${displayPadding}px`; - canvas.style.right = `${displayPadding}px`; - break; - case "bottom_left": - canvas.style.bottom = `${displayPadding}px`; - canvas.style.left = `${displayPadding}px`; - break; - case "bottom_right": - canvas.style.bottom = `${displayPadding}px`; - canvas.style.right = `${displayPadding}px`; - break; - case "center": - canvas.style.top = "50%"; - canvas.style.left = "50%"; - canvas.style.transform = "translate(-50%, -50%)"; - break; - default: - console.error("Invalid display position"); - return; - } - switch (displayType) { - case "text": { - const fontSize = Math.min(originalWidth, originalHeight) / 10; - context.font = `${clamp(fontSize, 48, 72)}px bold Arial`; - const { width: textWidth } = context.measureText(`${round(volume)}`); - width = textWidth + 4; - height = clamp(fontSize, 48, 72) + 4; - break; - } - case "line": { - const maxLineWidth = 100; // Maximum width of the volume line - const lineHeight = 5; // Height of the volume line - const lineWidth = Math.round(round(volume / 100, 2) * maxLineWidth); - width = lineWidth; - height = lineHeight; - break; - } - case "round": { - const lineWidth = 5; - const radius = Math.min(width, height, 75) / 2 - lineWidth; // Maximum radius based on canvas dimensions - const circleWidth = radius * 2 + lineWidth * 2; - width = circleWidth; - height = circleWidth; - break; - } - default: - console.error("Invalid display type"); - return; - } + const canvas = createStyledElement("volume-display", "canvas", { + pointerEvents: "none", + position: "absolute", + zIndex: "2021", + ...calculateCanvasPosition(displayPosition, displayPadding, paddingTop, paddingBottom) + }); + const context = canvas.getContext("2d"); - // Apply content dimensions to the canvas - canvas.width = width; - canvas.height = height; - canvas.style.zIndex = "2021"; - canvas.style.pointerEvents = "none"; + if (!context) { + browserColorLog("Canvas not supported", "FgRed"); + return; + } - // Clear canvas - context.clearRect(0, 0, width, height); - context.fillStyle = displayColor; - context.globalAlpha = displayOpacity / 100; - // Draw volume representation based on display type switch (displayType) { case "text": { - const fontSize = Math.min(originalWidth, originalHeight) / 10; - context.font = `${clamp(fontSize, 48, 72)}px bold Arial`; + const fontSize = clamp(Math.min(originalWidth, originalHeight) / 10, 48, 72); + canvas.width = fontSize + 4; + canvas.height = fontSize + 4; + // Clear canvas + context.clearRect(0, 0, canvas.width, canvas.height); context.textAlign = "center"; context.textBaseline = "middle"; - context.fillText(`${round(volume)}`, width / 2, height / 2); + context.fillStyle = displayColor; + context.globalAlpha = displayOpacity / 100; + context.font = `${fontSize}px bold Arial`; + context.fillText(`${round(volume)}`, canvas.width / 2, canvas.height / 2); break; } case "line": { const maxLineWidth = 100; // Maximum width of the volume line const lineHeight = 5; // Height of the volume line const lineWidth = Math.round(round(volume / 100, 2) * maxLineWidth); - const lineX = (width - lineWidth) / 2; - const lineY = (height - lineHeight) / 2; + canvas.width = lineWidth; + canvas.height = lineHeight; + // Clear canvas + context.clearRect(0, 0, canvas.width, canvas.height); + const lineX = (canvas.width - lineWidth) / 2; + const lineY = (canvas.height - lineHeight) / 2; + context.fillStyle = displayColor; + context.globalAlpha = displayOpacity / 100; context.fillRect(lineX, lineY, lineWidth, lineHeight); break; } case "round": { const lineWidth = 5; - const centerX = width / 2; - const centerY = height / 2; const radius = Math.min(width, height, 75) / 2 - lineWidth; // Maximum radius based on canvas dimensions + const circleWidth = radius * 2 + lineWidth * 2; + canvas.width = circleWidth; + canvas.height = circleWidth; + // Clear canvas + context.clearRect(0, 0, canvas.width, canvas.height); + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; const startAngle = Math.PI + Math.PI * round(volume / 100, 2); // Start angle based on volume const endAngle = Math.PI - Math.PI * round(volume / 100, 2); // End angle based on volume // Draw the volume arc as a circle at 100% volume @@ -236,7 +213,7 @@ export function drawVolumeDisplay({ } default: console.error("Invalid display type"); - break; + return; } // Append canvas to player container if it doesn't already exist @@ -250,33 +227,4 @@ export function drawVolumeDisplay({ setTimeout(() => { canvas.remove(); }, displayHideTime); - const topElement = document.querySelector(".player-controls > ytd-shorts-player-controls"); - const bottomElement: HTMLDivElement | null = - 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 topRect = topElement?.getBoundingClientRect(); - const bottomRect = bottomElement?.getBoundingClientRect(); - const heightExcludingMarginPadding = bottomElement - ? bottomElement.offsetHeight - - (parseInt(getComputedStyle(bottomElement).marginTop, 10) + - parseInt(getComputedStyle(bottomElement).marginBottom, 10) + - parseInt(getComputedStyle(bottomElement).paddingTop, 10) + - parseInt(getComputedStyle(bottomElement).paddingBottom, 10)) + - 10 - : 0; - const paddingTop = topRect ? (isShortsPage() ? topRect.top / 2 : 0) : 0; - const paddingBottom = bottomRect ? (isShortsPage() ? heightExcludingMarginPadding : Math.round(bottomRect.bottom - bottomRect.top)) : 0; - switch (displayPosition) { - case "top_left": - case "top_right": - canvas.style.top = `${displayPadding + paddingTop}px`; - break; - case "bottom_left": - case "bottom_right": - canvas.style.bottom = `${displayPadding + paddingBottom}px`; - break; - default: - return; - } } diff --git a/src/features/videoHistory/index.ts b/src/features/videoHistory/index.ts index a515dc46..11e697c0 100644 --- a/src/features/videoHistory/index.ts +++ b/src/features/videoHistory/index.ts @@ -1,10 +1,17 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { VideoHistoryEntry, YouTubePlayerDiv } from "@/src/types"; import eventManager from "@/utils/EventManager"; -import { browserColorLog, createTooltip, isShortsPage, isWatchPage, sendContentMessage, waitForSpecificMessage } from "@/utils/utilities"; +import { + browserColorLog, + createStyledElement, + createTooltip, + isShortsPage, + isWatchPage, + sendContentMessage, + waitForSpecificMessage +} from "@/utils/utilities"; import { formatTime } from "../remainingTime/utils"; - export async function setupVideoHistory() { // Wait for the "options" message from the content script const optionsData = await waitForSpecificMessage("options", "request_data", "content"); @@ -23,7 +30,7 @@ export async function setupVideoHistory() { if (playerVideoData.isLive) return; const { video_id: videoId } = await playerContainer.getVideoData(); if (!videoId) return; - const videoElement = document.querySelector("video.video-stream.html5-main-video") as HTMLVideoElement | null; + const videoElement = playerContainer.querySelector("video.video-stream.html5-main-video") as HTMLVideoElement | null; if (!videoElement) return; const videoPlayerTimeUpdateListener = async () => { @@ -63,126 +70,146 @@ export async function promptUserToResumeVideo() { data: { video_history_entry } } = videoHistoryOneData; if (video_history_entry && video_history_entry.status === "watching" && video_history_entry.timestamp > 0) { - // Check if the prompt element already exists - const prompt = document.getElementById("resume-prompt") ?? document.createElement("div"); - // Check if the prompt progress bar already exists - const progressBar = document.getElementById("resume-prompt-progress-bar") ?? document.createElement("div"); - const progressBarDuration = 15; - // Create a countdown timer - let countdown = 15; // Countdown in seconds - const countdownInterval = setInterval(() => { + createResumePrompt(video_history_entry, playerContainer); + } +} +// Utility function to check if an element exists +const elementExists = (elementId: string) => !!document.getElementById(elementId); +function createResumePrompt(videoHistoryEntry: VideoHistoryEntry, playerContainer: YouTubePlayerDiv) { + const progressBarId = "resume-prompt-progress-bar"; + const overlayId = "resume-prompt-overlay"; + const closeButtonId = "resume-prompt-close-button"; + const resumeButtonId = "resume-prompt-button"; + const promptId = "resume-prompt"; + const progressBarDuration = 15; + let countdownInterval: NodeJS.Timeout | undefined; + + const prompt = createStyledElement(promptId, "div", { + backgroundColor: "#181a1b", + borderRadius: "5px", + bottom: "10px", + boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.2)", + left: "10px", + padding: "12px", + paddingBottom: "17px", + position: "fixed", + transition: "all 0.5s ease-in-out", + zIndex: "25000" + }); + const progressBar = createStyledElement(progressBarId, "div", { + backgroundColor: "#007acc", + borderBottomLeftRadius: "5px", + borderBottomRightRadius: "5px", + bottom: "0", + height: "5px", + left: "0", + position: "absolute", + transition: "all 0.5s ease-in-out", + width: "100%", + zIndex: "1000" + }); + + const overlay = createStyledElement(overlayId, "div", { + backgroundColor: "rgba(0, 0, 0, 0.75)", + cursor: "pointer", + height: "100%", + left: "0", + position: "fixed", + top: "0", + width: "100%", + zIndex: "2500" + }); + + const closeButton = createStyledElement(closeButtonId, "button", { + backgroundColor: "transparent", + border: "0", + color: "#fff", + cursor: "pointer", + fontSize: "16px", + lineHeight: "1px", + padding: "5px", + position: "absolute", + right: "-2px", + top: "2px" + }); + closeButton.textContent = "ₓ"; + + const resumeButton = createStyledElement(resumeButtonId, "button", { + backgroundColor: "hsl(213, 80%, 50%)", + border: "transparent", + borderRadius: "5px", + boxShadow: "0px 0px 5px rgba(0, 0, 0, 0.2)", + color: "white", + cursor: "pointer", + padding: "5px", + textAlign: "center", + transition: "all 0.5s ease-in-out", + verticalAlign: "middle" + }); + resumeButton.textContent = window.i18nextInstance.t("pages.content.features.videoHistory.resumeButton"); + + function startCountdown() { + if (prompt) prompt.style.display = "block"; + if (overlay) overlay.style.display = "block"; + let countdown = progressBarDuration; + countdownInterval = setInterval(() => { countdown--; - progressBar.style.width = `${(countdown / progressBarDuration) * 100}%`; // Update the progress bar + progressBar.style.width = `${(countdown / progressBarDuration) * 100}%`; if (countdown <= 0) { - // Automatically hide the prompt when the countdown reaches 0 - clearInterval(countdownInterval); - prompt.style.display = "none"; - overlay.style.display = "none"; + hidePrompt(); } }, 1000); - if (!document.getElementById("resume-prompt-progress-bar")) { - progressBar.id = "resume-prompt-progress-bar"; - progressBar.style.width = "100%"; - progressBar.style.height = "5px"; // Height of the progress bar - progressBar.style.backgroundColor = "#007acc"; // Progress bar color - progressBar.style.position = "absolute"; - progressBar.style.zIndex = "1000"; - progressBar.style.left = "0"; // Place at the left of the prompt - progressBar.style.bottom = "0"; // Place at the bottom of the prompt - progressBar.style.transition = "all 0.5s ease-in-out"; - progressBar.style.borderBottomRightRadius = "5px"; - progressBar.style.borderBottomLeftRadius = "5px"; - prompt.appendChild(progressBar); - } - const resumeButtonClickListener = () => { - // Hide the prompt and clear the countdown timer - clearInterval(countdownInterval); - prompt.style.display = "none"; - overlay.style.display = "none"; - browserColorLog(window.i18nextInstance.t("messages.resumingVideo", { VIDEO_TIME: formatTime(video_history_entry.timestamp) }), "FgGreen"); - playerContainer.seekTo(video_history_entry.timestamp, true); - }; - const overlay = document.getElementById("resume-prompt-overlay") ?? document.createElement("div"); - const resumeButton = document.getElementById("resume-prompt-button") ?? document.createElement("button"); - const closeButton = document.getElementById("resume-prompt-close-button") ?? document.createElement("button"); - - // Create the overlay if it doesn't exist - if (!document.getElementById("resume-prompt-overlay")) { - overlay.style.position = "fixed"; - overlay.style.top = "0"; - overlay.style.left = "0"; - overlay.style.width = "100%"; - overlay.style.height = "100%"; - overlay.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; - overlay.style.zIndex = "2500"; - overlay.style.cursor = "pointer"; - document.body.appendChild(overlay); - } - // Create the close button if it doesn't exist - if (!document.getElementById("resume-prompt-close-button")) { - closeButton.id = "resume-prompt-close-button"; - closeButton.textContent = "ₓ"; - closeButton.style.fontSize = "16px"; - closeButton.style.position = "absolute"; - closeButton.style.top = "2px"; - closeButton.style.right = "-2px"; - closeButton.style.backgroundColor = "transparent"; - closeButton.style.color = "#fff"; - closeButton.style.border = "0"; - closeButton.style.padding = "5px"; - closeButton.style.cursor = "pointer"; - closeButton.style.lineHeight = "1px"; - closeButton.dataset.title = window.i18nextInstance.t("pages.content.features.videoHistory.resumePrompt.close"); - const { listener: resumePromptCloseButtonMouseOverListener } = createTooltip({ - element: closeButton, - featureName: "videoHistory", - id: "yte-resume-prompt-close-button-tooltip" - }); - eventManager.addEventListener(closeButton, "mouseover", resumePromptCloseButtonMouseOverListener, "videoHistory"); - prompt.appendChild(closeButton); - } - // Create the prompt element if it doesn't exist - if (!document.getElementById("resume-prompt")) { - prompt.id = "resume-prompt"; - prompt.style.position = "fixed"; - prompt.style.bottom = "10px"; - prompt.style.left = "10px"; - prompt.style.backgroundColor = "#181a1b"; - prompt.style.padding = "12px"; - prompt.style.paddingBottom = "17px"; - prompt.style.transition = "all 0.5s ease-in-out"; - prompt.style.borderRadius = "5px"; - prompt.style.boxShadow = "0px 0px 10px rgba(0, 0, 0, 0.2)"; - prompt.style.zIndex = "25000"; - resumeButton.id = "resume-prompt-button"; - resumeButton.textContent = window.i18nextInstance.t("pages.content.features.videoHistory.resumeButton"); - resumeButton.style.backgroundColor = "hsl(213, 80%, 50%)"; - resumeButton.style.border = "transparent"; - resumeButton.style.color = "white"; - resumeButton.style.padding = "5px"; - resumeButton.style.borderRadius = "5px"; - resumeButton.style.boxShadow = "0px 0px 5px rgba(0, 0, 0, 0.2)"; - resumeButton.style.cursor = "pointer"; - resumeButton.style.textAlign = "center"; - resumeButton.style.verticalAlign = "middle"; - resumeButton.style.transition = "all 0.5s ease-in-out"; - - prompt.appendChild(resumeButton); - document.body.appendChild(prompt); - } - if (document.getElementById("resume-prompt-button")) { - eventManager.removeEventListener(resumeButton, "click", "videoHistory"); - } - const closeListener = () => { - clearInterval(countdownInterval); - prompt.style.display = "none"; - overlay.style.display = "none"; - }; - eventManager.addEventListener(resumeButton, "click", resumeButtonClickListener, "videoHistory"); - eventManager.addEventListener(overlay, "click", closeListener, "videoHistory"); - eventManager.addEventListener(closeButton, "click", closeListener, "videoHistory"); - // Display the prompt - prompt.style.display = "block"; + } + + function hidePrompt() { + clearInterval(countdownInterval); + prompt.style.display = "none"; + overlay.style.display = "none"; + } + + function resumeButtonClickListener() { + hidePrompt(); + browserColorLog(window.i18nextInstance.t("messages.resumingVideo", { VIDEO_TIME: formatTime(videoHistoryEntry.timestamp) }), "FgGreen"); + playerContainer.seekTo(videoHistoryEntry.timestamp, true); + } + + if (!elementExists(progressBarId)) { + prompt.appendChild(progressBar); + } + + if (!elementExists(overlayId)) { + document.body.appendChild(overlay); + } + + if (!elementExists(closeButtonId)) { + const { listener: resumePromptCloseButtonMouseOverListener } = createTooltip({ + element: closeButton, + featureName: "videoHistory", + id: "yte-resume-prompt-close-button-tooltip", + text: window.i18nextInstance.t("pages.content.features.videoHistory.resumePrompt.close") + }); + eventManager.addEventListener(closeButton, "mouseover", resumePromptCloseButtonMouseOverListener, "videoHistory"); + prompt.appendChild(closeButton); + } + + startCountdown(); + + if (elementExists(resumeButtonId)) { + eventManager.removeEventListener(resumeButton, "click", "videoHistory"); + } + + const closeListener = () => { + hidePrompt(); + }; + + eventManager.addEventListener(resumeButton, "click", resumeButtonClickListener, "videoHistory"); + eventManager.addEventListener(overlay, "click", closeListener, "videoHistory"); + eventManager.addEventListener(closeButton, "click", closeListener, "videoHistory"); + + // Display the prompt + if (!elementExists(promptId)) { + document.body.appendChild(prompt); + prompt.appendChild(resumeButton); } } diff --git a/src/features/videoHistory/utils.ts b/src/features/videoHistory/utils.ts index b36b93ca..5e54a760 100644 --- a/src/features/videoHistory/utils.ts +++ b/src/features/videoHistory/utils.ts @@ -1,4 +1,4 @@ -import type { VideoHistoryStatus, VideoHistoryStorage } from "@/src/@types"; +import type { VideoHistoryStatus, VideoHistoryStorage } from "@/src/types"; export function getVideoHistory() { return JSON.parse(window.localStorage.getItem("videoHistory") ?? "{}") as VideoHistoryStorage; } diff --git a/src/features/volumeBoost/index.ts b/src/features/volumeBoost/index.ts index 1bac393f..d632d8fd 100644 --- a/src/features/volumeBoost/index.ts +++ b/src/features/volumeBoost/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/@types"; +import type { YouTubePlayerDiv } from "@/src/types"; import { browserColorLog, formatError, waitForSpecificMessage } from "@/src/utils/utilities"; diff --git a/src/hooks/useNotifications/context.ts b/src/hooks/useNotifications/context.ts index a999ce9a..5661122a 100644 --- a/src/hooks/useNotifications/context.ts +++ b/src/hooks/useNotifications/context.ts @@ -1,4 +1,4 @@ -import type { Notification, NotificationAction, NotificationType } from "@/src/@types"; +import type { Notification, NotificationAction, NotificationType } from "@/src/types"; import { createContext } from "react"; diff --git a/src/hooks/useNotifications/provider.tsx b/src/hooks/useNotifications/provider.tsx index a8fb22f7..178c26be 100644 --- a/src/hooks/useNotifications/provider.tsx +++ b/src/hooks/useNotifications/provider.tsx @@ -1,4 +1,4 @@ -import type { Notification, NotificationAction, NotificationType } from "@/src/@types"; +import type { Notification, NotificationAction, NotificationType } from "@/src/types"; import { isNotStrictEqual } from "@/src/utils/utilities"; import React, { type ReactElement, useEffect, useState } from "react"; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 11b90a43..5b470198 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -18,8 +18,10 @@ export async function i18nService(locale: AvailableLocales) { extensionURL = chrome.runtime.getURL(""); } if (!availableLocales.includes(locale)) throw new Error(`The locale '${locale}' is not available`); - const response = await fetch(`${extensionURL}locales/${locale}.json`).catch((err) => console.error(err)); - const translations = (await response?.json()) as typeof import("../../public/locales/en-US.json"); + const response = await fetch(`${extensionURL}locales/${locale}.json`).catch((err) => { + throw new Error(err); + }); + const translations = (await response.json()) as typeof import("../../public/locales/en-US.json"); const i18nextInstance = await new Promise((resolve, reject) => { const resources: { [k in AvailableLocales]?: { diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 9fb629cc..8fbb7e1c 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -1,5 +1,6 @@ -import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/@types"; +import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/types"; +import { automaticTheaterMode } from "@/src/features/automaticTheaterMode"; import { enableFeatureMenu } from "@/src/features/featureMenu"; import { updateFeatureMenuItemLabel, updateFeatureMenuTitle } from "@/src/features/featureMenu/utils"; import { enableHideScrollBar } from "@/src/features/hideScrollBar"; @@ -17,9 +18,26 @@ import { promptUserToResumeVideo, setupVideoHistory } from "@/src/features/video import volumeBoost from "@/src/features/volumeBoost"; import { i18nService } from "@/src/i18n"; import eventManager from "@/utils/EventManager"; -import { browserColorLog, formatError, sendContentOnlyMessage, waitForSpecificMessage } from "@/utils/utilities"; +import { + browserColorLog, + formatError, + isShortsPage, + isWatchPage, + sendContentOnlyMessage, + waitForAllElements, + waitForSpecificMessage +} from "@/utils/utilities"; // TODO: Add always show progressbar feature +/** + * Creates a hidden div element with a specific ID that can be used to receive messages from YouTube. + * The element is appended to the document's root element. + */ +const element = document.createElement("div"); +element.style.display = "none"; +element.id = "yte-message-from-youtube"; +document.documentElement.appendChild(element); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const alwaysShowProgressBar = async function () { const player = document.querySelector("#movie_player") as YouTubePlayerDiv | null; @@ -59,20 +77,13 @@ const alwaysShowProgressBar = async function () { progressLoad += progressWidth; }; -/** - * Creates a hidden div element with a specific ID that can be used to receive messages from YouTube. - * The element is appended to the document's root element. - */ -const element = document.createElement("div"); -element.style.display = "none"; -element.id = "yte-message-from-youtube"; -document.documentElement.appendChild(element); - -window.onload = async function () { +window.addEventListener("DOMContentLoaded", async function () { enableRememberVolume(); enableHideScrollBar(); - const enableFeatures = () => { + const enableFeatures = async () => { + // Wait for the specified container selectors to be available on the page + await waitForAllElements(["div#player", "div#player-wide-container", "div#video-container", "div#player-container"]); eventManager.removeAllEventListeners(["featureMenu"]); enableFeatureMenu(); addLoopButton(); @@ -87,6 +98,7 @@ window.onload = async function () { setupVideoHistory(); promptUserToResumeVideo(); setupRemainingTime(); + automaticTheaterMode(); }; const response = await waitForSpecificMessage("language", "request_data", "content"); if (!response) return; @@ -95,6 +107,7 @@ window.onload = async function () { } = response; const i18nextInstance = await i18nService(language); window.i18nextInstance = i18nextInstance; + if (isWatchPage() || isShortsPage()) document.addEventListener("yt-navigate-finish", enableFeatures); document.addEventListener("yt-player-updated", enableFeatures); /** * Listens for the "yte-message-from-youtube" event and handles incoming messages from the YouTube page. @@ -270,13 +283,30 @@ window.onload = async function () { updateFeatureMenuItemLabel("loopButton", window.i18nextInstance.t("pages.content.features.loopButton.label")); break; } + case "automaticTheaterModeChange": { + // Get the player element + const playerContainer = isWatchPage() + ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) + : isShortsPage() + ? (document.querySelector("div#shorts-player") as YouTubePlayerDiv | null) + : null; + // If player element is not available, return + if (!playerContainer) return; + // Get the size button + const sizeButton = document.querySelector("button.ytp-size-button") as HTMLButtonElement | null; + // If the size button is not available return + if (!sizeButton) return; + sizeButton.click(); + + break; + } default: { return; } } }); sendContentOnlyMessage("pageLoaded", undefined); -}; +}); window.onbeforeunload = function () { eventManager.removeAllEventListeners(); element.remove(); diff --git a/src/pages/inject/index.tsx b/src/pages/inject/index.tsx index 4ca428f0..cefd3eb0 100644 --- a/src/pages/inject/index.tsx +++ b/src/pages/inject/index.tsx @@ -1,5 +1,5 @@ -import type { ContentSendOnlyMessageMappings, Messages, StorageChanges, configuration } from "@/src/@types"; import type { AvailableLocales } from "@/src/i18n"; +import type { ContentSendOnlyMessageMappings, Messages, StorageChanges, configuration } from "@/src/types"; import { getVideoHistory, setVideoHistory } from "@/src/features/videoHistory/utils"; import { parseReviver, parseStoredValue, sendExtensionMessage, sendExtensionOnlyMessage } from "@/src/utils/utilities"; @@ -158,6 +158,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = const keyActions: { [K in keyof configuration]?: () => void; } = { + enable_automatic_theater_mode: () => { + sendExtensionOnlyMessage("automaticTheaterModeChange", { + automaticTheaterModeEnabled: castedChanges.enable_automatic_theater_mode.newValue + }); + }, enable_forced_playback_speed: () => { sendExtensionOnlyMessage("playerSpeedChange", { enableForcedPlaybackSpeed: castedChanges.enable_forced_playback_speed.newValue, diff --git a/src/@types/index.ts b/src/types/index.ts similarity index 93% rename from src/@types/index.ts rename to src/types/index.ts index bf41536c..15a9fd5e 100644 --- a/src/@types/index.ts +++ b/src/types/index.ts @@ -1,11 +1,9 @@ -import type { YouTubePlayer } from "node_modules/@types/youtube-player/dist/types"; +import type { YouTubePlayer } from "youtube-player/dist/types"; import z from "zod"; import type { AvailableLocales } from "../i18n"; import type { FeatureName } from "../utils/EventManager"; - -/* eslint-disable no-mixed-spaces-and-tabs */ export type Writeable = { -readonly [P in keyof T]: T[P] }; export type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; export const onScreenDisplayColor = ["red", "green", "blue", "yellow", "orange", "purple", "pink", "white"] as const; @@ -38,8 +36,11 @@ export type ScreenshotType = (typeof screenshotType)[number]; export const screenshotFormat = ["png", "jpg", "webp"] as const; export type ScreenshotFormat = (typeof screenshotFormat)[number]; +export const modifierKey = ["altKey", "ctrlKey", "shiftKey"] as const; +export type ModifierKey = (typeof modifierKey)[number]; export type configuration = { + enable_automatic_theater_mode: boolean; enable_automatically_set_quality: boolean; enable_forced_playback_speed: boolean; enable_hide_scrollbar: boolean; @@ -49,6 +50,7 @@ export type configuration = { enable_remember_last_volume: boolean; enable_screenshot_button: boolean; enable_scroll_wheel_volume_control: boolean; + enable_scroll_wheel_volume_control_modifier_key: boolean; enable_video_history: boolean; enable_volume_boost: boolean; language: AvailableLocales; @@ -66,10 +68,12 @@ export type configuration = { }; screenshot_format: ScreenshotFormat; screenshot_save_as: ScreenshotType; + scroll_wheel_volume_control_modifier_key: ModifierKey; volume_adjustment_steps: number; volume_boost_amount: number; }; export type configurationKeys = keyof configuration; +export type configurationId = configurationKeys; export type VideoHistoryStatus = "watched" | "watching"; export type VideoHistoryEntry = { id: string; @@ -108,6 +112,7 @@ export type ContentSendOnlyMessageMappings = { setRememberedVolume: SendDataMessage<"send_data", "content", "setRememberedVolume", { shortsPageVolume?: number; watchPageVolume?: number }>; }; export type ExtensionSendOnlyMessageMappings = { + automaticTheaterModeChange: DataResponseMessage<"automaticTheaterModeChange", { automaticTheaterModeEnabled: boolean }>; hideScrollBarChange: DataResponseMessage<"hideScrollBarChange", { hideScrollBarEnabled: boolean }>; languageChange: DataResponseMessage<"languageChange", { language: AvailableLocales }>; loopButtonChange: DataResponseMessage<"loopButtonChange", { loopButtonEnabled: boolean }>; diff --git a/src/utils/EventManager.ts b/src/utils/EventManager.ts index a24e26fb..79c5a147 100644 --- a/src/utils/EventManager.ts +++ b/src/utils/EventManager.ts @@ -1,4 +1,5 @@ export type FeatureName = + | "automaticTheaterMode" | "featureMenu" | "hideScrollBar" | "loopButton" @@ -26,7 +27,8 @@ export type EventManager = { target: HTMLElementTagNameMap[keyof HTMLElementTagNameMap], eventName: T, callback: EventCallback, - featureName: FeatureName + featureName: FeatureName, + options?: AddEventListenerOptions | boolean ) => void; listeners: Map>; @@ -45,7 +47,7 @@ export type EventManager = { export const eventManager: EventManager = { // Map of feature names to a map of targets to // Adds an event listener for the given target, eventName, and featureName - addEventListener: function (target, eventName, callback, featureName) { + addEventListener: function (target, eventName, callback, featureName, options) { // Get the map of target listeners for the given featureName const targetListeners = this.listeners.get(featureName) || new Map(); // Store the event listener info object in the map @@ -53,7 +55,7 @@ export const eventManager: EventManager = { // Store the map of target listeners for the given featureName this.listeners.set(featureName, targetListeners); // Add the event listener to the target - target.addEventListener(eventName, callback); + target.addEventListener(eventName, callback, options); }, // event listener info objects diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 33751f6e..232defba 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,19 +1,20 @@ import z from "zod"; -import type { TypeToPartialZodSchema, configuration } from "../@types"; +import type { TypeToPartialZodSchema, configuration } from "../types"; +import { availableLocales } from "../i18n/index"; import { + modifierKey, onScreenDisplayColor, onScreenDisplayPosition, onScreenDisplayType, screenshotFormat, screenshotType, youtubePlayerQualityLevel -} from "../@types"; -import { availableLocales } from "../i18n/index"; +} from "../types"; export const outputFolderName = "dist"; export const defaultConfiguration = { - // Options + enable_automatic_theater_mode: false, enable_automatically_set_quality: false, enable_forced_playback_speed: false, enable_hide_scrollbar: false, @@ -22,12 +23,11 @@ export const defaultConfiguration = { enable_remaining_time: false, enable_remember_last_volume: false, enable_screenshot_button: false, - // General enable_scroll_wheel_volume_control: false, + enable_scroll_wheel_volume_control_modifier_key: false, enable_video_history: false, enable_volume_boost: false, language: "en-US", - // Images osd_display_color: "white", osd_display_hide_time: 750, osd_display_opacity: 75, @@ -38,11 +38,13 @@ export const defaultConfiguration = { player_speed: 1, screenshot_format: "png", screenshot_save_as: "file", + scroll_wheel_volume_control_modifier_key: "ctrlKey", volume_adjustment_steps: 5, volume_boost_amount: 1 } satisfies configuration; export const configurationImportSchema: TypeToPartialZodSchema = z.object({ + enable_automatic_theater_mode: z.boolean().optional(), enable_automatically_set_quality: z.boolean().optional(), enable_forced_playback_speed: z.boolean().optional(), enable_hide_scrollbar: z.boolean().optional(), @@ -52,6 +54,7 @@ export const configurationImportSchema: TypeToPartialZodSchema = enable_remember_last_volume: z.boolean().optional(), enable_screenshot_button: z.boolean().optional(), enable_scroll_wheel_volume_control: z.boolean().optional(), + enable_scroll_wheel_volume_control_modifier_key: z.boolean().optional(), enable_video_history: z.boolean().optional(), enable_volume_boost: z.boolean().optional(), language: z.enum(availableLocales).optional(), @@ -66,6 +69,7 @@ export const configurationImportSchema: TypeToPartialZodSchema = remembered_volume: z.number().optional(), screenshot_format: z.enum(screenshotFormat).optional(), screenshot_save_as: z.enum(screenshotType).optional(), + scroll_wheel_volume_control_modifier_key: z.enum(modifierKey).optional(), volume_adjustment_steps: z.number().min(1).max(100).optional(), volume_boost_amount: z.number().optional() }); diff --git a/src/utils/utilities.ts b/src/utils/utilities.ts index bda3985a..e537026d 100644 --- a/src/utils/utilities.ts +++ b/src/utils/utilities.ts @@ -1,3 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + import type { ContentSendOnlyMessageMappings, ExtensionSendOnlyMessageMappings, @@ -6,13 +9,11 @@ import type { Messages, Selector, SendDataMessage, - YoutubePlayerQualityLabel, + YoutubePlayerQualityLevel, configuration -} from "@/src/@types"; - -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; +} from "../types"; +import { youtubePlayerQualityLevel } from "../types"; import { type FeatureName, eventManager } from "./EventManager"; export const isStrictEqual = (value1: unknown) => (value2: unknown) => value1 === value2; @@ -34,25 +35,41 @@ export const round = (value: number, decimals = 0) => Number(`${Math.round(Numbe export const toDivisible = (value: number, divider: number): number => Math.ceil(value / divider) * divider; -export const chooseClosetQuality = (num: YoutubePlayerQualityLabel, arr: YoutubePlayerQualityLabel[]): YoutubePlayerQualityLabel => { - const parsedNum = parseInt(num, 10); - let [curr] = arr; - let currDiff = Math.abs(parsedNum - parseInt(curr)); +export function chooseClosestQuality( + selectedQuality: YoutubePlayerQualityLevel, + availableQualities: YoutubePlayerQualityLevel[] +): YoutubePlayerQualityLevel | null { + // If there are no available qualities, return null + if (availableQualities.length === 0) { + return null; + } - for (let i = 1; i < arr.length; i++) { - const label = arr.at(i); - if (!label) continue; - const parsedLabel = parseInt(label); - const diff = Math.abs(parsedNum - parsedLabel); + // Find the index of the selected quality in the array + const selectedIndex = youtubePlayerQualityLevel.indexOf(selectedQuality); - if (diff < currDiff) { - curr = label; - currDiff = diff; - } + // If the selected quality is not in the array, return null + if (selectedIndex === -1) { + return null; } - return curr; -}; + // Find the available quality levels that are closest to the selected quality level + const closestQualities = availableQualities.reduce( + (acc, quality) => { + const qualityIndex = youtubePlayerQualityLevel.indexOf(quality); + if (qualityIndex !== -1) { + acc.push({ difference: Math.abs(selectedIndex - qualityIndex), quality }); + } + return acc; + }, + [] as { difference: number; quality: YoutubePlayerQualityLevel }[] + ); + + // Sort the closest qualities by difference in ascending order + closestQualities.sort((a, b) => a.difference - b.difference); + + // Return the quality level with the minimum difference + return closestQualities[0].quality; +} const BrowserColors = { BgBlack: "background-color: black; color: white;", BgBlue: "background-color: blue; color: white;", @@ -487,3 +504,15 @@ export function createTooltip({ element, featureName, id, text }: { element: HTM export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +// Utility function to create and style an element +export function createStyledElement( + elementId: ID, + elementType: K, + styles: Partial +): HTMLElementTagNameMap[K] { + const elementExists = document.getElementById(elementId) !== null; + const element = (elementExists ? document.getElementById(elementId) : document.createElement(elementType)) as HTMLElementTagNameMap[K]; + if (!element.id) element.id = elementId; + Object.assign(element.style, styles); + return element; +} diff --git a/yarn.lock b/yarn.lock index 34d73b23..400442d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -195,10 +195,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.53.0": - version "8.53.0" - resolved "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz" - integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== +"@eslint/js@8.54.0": + version "8.54.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.54.0.tgz#4fab9a2ff7860082c304f750e94acd644cf984cf" + integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ== "@formkit/auto-animate@^0.8.1": version "0.8.1" @@ -805,9 +805,9 @@ resolved "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.96.tgz" integrity sha512-4VbSAniIu0ikLf5mBX81FsljnfqjoVGleEkCQv4+zRlyZtO3FHoDPkeLVoy6WRlj7tyrRcfUJ4mDdPkbfTO14g== -"@swc/core@^1.3.95": +"@swc/core@^1.3.96": version "1.3.96" - resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.96.tgz" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.96.tgz#f04d58b227ceed2fee6617ce2cdddf21d0803f96" integrity sha512-zwE3TLgoZwJfQygdv2SdCK9mRLYluwDOM53I+dT6Z5ZvrgVENmY3txvWDvduzkV+/8IuvrRbVezMpxcojadRdQ== dependencies: "@swc/counter" "^0.1.1" @@ -920,9 +920,9 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/node@*", "@types/node@^20.9.0": - version "20.9.0" - resolved "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz" - integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw== + version "20.9.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.2.tgz#002815c8e87fe0c9369121c78b52e800fadc0ac6" + integrity sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg== dependencies: undici-types "~5.26.4" @@ -1007,14 +1007,6 @@ "@typescript-eslint/visitor-keys" "6.11.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz" - integrity sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg== - dependencies: - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" - "@typescript-eslint/scope-manager@6.11.0": version "6.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz#621f603537c89f4d105733d949aa4d55eee5cea8" @@ -1023,14 +1015,6 @@ "@typescript-eslint/types" "6.11.0" "@typescript-eslint/visitor-keys" "6.11.0" -"@typescript-eslint/type-utils@6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz" - integrity sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg== - dependencies: - "@typescript-eslint/types" "6.11.0" - "@typescript-eslint/visitor-keys" "6.11.0" - "@typescript-eslint/type-utils@6.11.0": version "6.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz#d0b8b1ab6c26b974dbf91de1ebc5b11fea24e0d1" @@ -1041,29 +1025,11 @@ debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz" - integrity sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg== - "@typescript-eslint/types@6.11.0": version "6.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.11.0.tgz#8ad3aa000cbf4bdc4dcceed96e9b577f15e0bf53" integrity sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA== -"@typescript-eslint/typescript-estree@6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz" - integrity sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg== - dependencies: - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/typescript-estree@6.11.0": version "6.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz#7b52c12a623bf7f8ec7f8a79901b9f98eb5c7990" @@ -1077,10 +1043,10 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.10.0", "@typescript-eslint/utils@^6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz" - integrity sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg== +"@typescript-eslint/utils@6.11.0", "@typescript-eslint/utils@^6.10.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.11.0.tgz#11374f59ef4cea50857b1303477c08aafa2ca604" + integrity sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" @@ -1090,14 +1056,6 @@ "@typescript-eslint/typescript-estree" "6.11.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.10.0": - version "6.10.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz" - integrity sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg== - dependencies: - "@typescript-eslint/types" "6.10.0" - eslint-visitor-keys "^3.4.1" - "@typescript-eslint/visitor-keys@6.11.0": version "6.11.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz#d991538788923f92ec40d44389e7075b359f3458" @@ -1112,11 +1070,11 @@ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vitejs/plugin-react-swc@^3.4.1": - version "3.4.1" - resolved "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.4.1.tgz" - integrity sha512-7YQOQcVV5x1luD8nkbCDdyYygFvn1hjqJk68UvNAzY2QG4o4N5EwAhLLFNOcd1HrdMwDl0VElP8VutoWf9IvJg== + version "3.5.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.5.0.tgz#1fadff5148003e8091168c431e44c850f9a39e74" + integrity sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig== dependencies: - "@swc/core" "^1.3.95" + "@swc/core" "^1.3.96" JSONStream@^1.3.5: version "1.3.5" @@ -2345,9 +2303,9 @@ eslint-plugin-no-secrets@^0.8.9: integrity sha512-CqaBxXrImABCtxMWspAnm8d5UKkpNylC7zqVveb+fJHEvsSiNGJlSWzdSIvBUnW1XhJXkzifNIZQC08rEII5Ng== eslint-plugin-perfectionist@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.3.0.tgz" - integrity sha512-T/1HOysrsyExPr/N5apy3XFhejYqIturtejlSbTGy0WCw5dt72FDT92NOvRRKJvx8lftZDJ8AEIs5nHk9Pfa9Q== + version "2.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-2.4.0.tgz#9a261c2b35c79fd581261888c64d711f96de7395" + integrity sha512-til+vyf56wAUgFv5guBA1Zo5lTw9xj2kCeK/g+9NBtsRy1rkGrlqnvxYNuFExcK3VsPhUUtx5UdScEDz9ahQ5Q== dependencies: "@typescript-eslint/utils" "^6.10.0" minimatch "^9.0.3" @@ -2415,14 +2373,14 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.53.0: - version "8.53.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz" - integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag== + version "8.54.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.54.0.tgz#588e0dd4388af91a2e8fa37ea64924074c783537" + integrity sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.3" - "@eslint/js" "8.53.0" + "@eslint/js" "8.54.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -3116,11 +3074,12 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-from-esm@^1.0.3: - version "1.2.1" - resolved "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.2.1.tgz" - integrity sha512-Nly5Ab75rWZmOwtMa0B0NQNnHGcHOQ2zkU/bVENwK2lbPq+kamPDqNKNJ0hF7w7lR/ETD5nGgJq0XbofsZpYCA== +import-from-esm@^1.0.3, import-from-esm@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/import-from-esm/-/import-from-esm-1.3.3.tgz#eea1c4ad86a54bf425b3b71fca56d50215ccc6b7" + integrity sha512-U3Qt/CyfFpTUv6LOP2jRTLYjphH6zg3okMfHbyqRa/W2w6hr8OsJWVggNlR4jxuojQy81TgTJTxgSkyoteRGMQ== dependencies: + debug "^4.3.4" import-meta-resolve "^4.0.0" import-meta-resolve@^4.0.0: @@ -5151,9 +5110,9 @@ scheduler@^0.23.0: loose-envify "^1.1.0" semantic-release@^22.0.7: - version "22.0.7" - resolved "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.7.tgz" - integrity sha512-Stx23Hjn7iU8GOAlhG3pHlR7AoNEahj9q7lKBP0rdK2BasGtJ4AWYh3zm1u3SCMuFiA8y4CE/Gu4RGKau1WiaQ== + version "22.0.8" + resolved "https://registry.yarnpkg.com/semantic-release/-/semantic-release-22.0.8.tgz#13470a2af04e42fd767da278bcdddb0e91c8fb3f" + integrity sha512-55rb31jygqIYsGU/rY+gXXm2fnxBIWo9azOjxbqKsPnq7p70zwZ5v+xnD7TxJC+zvS3sy1eHLGXYWCaX3WI76A== dependencies: "@semantic-release/commit-analyzer" "^11.0.0" "@semantic-release/error" "^4.0.0" @@ -5171,6 +5130,7 @@ semantic-release@^22.0.7: git-log-parser "^1.2.0" hook-std "^3.0.0" hosted-git-info "^7.0.0" + import-from-esm "^1.3.1" lodash-es "^4.17.21" marked "^9.0.0" marked-terminal "^6.0.0"