diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..3eef13a --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,40 @@ +name: Build Extension with Node.js + +on: + push: + branches: [ "master", "v3" ] + pull_request: + branches: [ "master", "v3" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build Chrome extension + run: npm run build + + - name: Upload Chrome extension + uses: actions/upload-artifact@v3 + with: + name: chrome-extension-${{ github.sha }} + path: dist + + - name: Build Firefox extension + run: npm run build:firefox -- --skip-build + + - name: Upload Firefox extension + uses: actions/upload-artifact@v3 + with: + name: firefox-extension-${{ github.sha }} + path: dist diff --git a/.gitignore b/.gitignore index 6f66c74..6b576c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,32 @@ -*.zip \ No newline at end of file +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# extension packing files +dist.crx +dist.pem + +# scss generated files +*.css +*.css.map diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 0211d26..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Ng Young Shung - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 773f261..d790252 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-![BYS Icon](https://raw.githubusercontent.com/ynshung/better-yt-shorts/master/icons/byts128.png) +![BYS Icon](./src/assets/icons/bys-128.png) # Better YouTube Shorts @@ -11,50 +11,38 @@ ![License: MIT](https://img.shields.io/github/license/ynshung/better-yt-shorts)
-Control your YouTube Shorts just like a normal YouTube video! Features include progress bar, seeking, playback speed, auto skip and more. You can also customize the keybinds to your liking! - -## 🫵 Help us test Version 3! -Help us test out the latest version at the [v3 branch](https://github.com/ynshung/better-yt-shorts/tree/v3) with multiple features and improved functionalities. Simply clone the repository, install the dependencies, start development mode and load the `dist` folder unpackaged (full guide [here](https://github.com/ynshung/better-yt-shorts/tree/v3#development-guide)). - -We encourage you to test out the version extensively, report any bugs and leave your suggestion in the issue page. Your help is greatly appreciated! Do note that it only works on Chrome currently and you need to manually check for update from time to time. - -Special thanks to [Adam Suth](https://github.com/adsuth) for the development of the project! +Control your YouTube shorts just like a normal YouTube video! Features include progress bar, seeking, playback speed, auto skip and more. You can also customize the keybinds to your liking! ## Installation -* ⚠️ **[NEW LINK]** Chrome Extension: https://chrome.google.com/webstore/detail/better-youtube-shorts/pehohlhkhbcfdneocgnfbnilppmfncdg -* Firefox Add-on: https://addons.mozilla.org/en-US/firefox/addon/better-youtube-shorts - -### Notice: Chrome Web Store Takedown +* Chrome Extension: https://chrome.google.com/webstore/detail/better-youtube-shorts/pehohlhkhbcfdneocgnfbnilppmfncdg +* Firefox Add-ons: https://addons.mozilla.org/en-US/firefox/addon/better-youtube-shorts -We have reuploaded the extension under a new link above as the previous one was taken down due to trademark issue. The original extension had a total of 25K installs, 16K active users and 4.59★ rating. We hope the original link will be restored soon, if possible. We apologize for any inconvenience caused. +## Features +- **Progress bar** at the bottom with time and duration +- **Seeking** 5 seconds backward and forward with arrow keys (adjustable time) +- Mini **timestamp** and speed above the like button (can be scrolled on!) +- Decrease and increase **playback speed** with keys U and O +- Toggle to auto skip short when current one ends +- Control volume with the **volume slider** or with - and =, mute audio with M +- **Customizable** keybinds -### Guide to load the extension manually (unpacked) +Extra features: +- Start short from beginning with J +- Auto skip short with likes below custom threshold (e.g. 500 likes) +- Auto open comment section on each short +- Hide overlay on shorts (title, channel, etc.) +- Revert to normal speed with I or by clicking the speed button +- Navigate to previous or next short without animation with W and S +- Go to the next frame or previous frame with . and , while paused -1. Click the "<> Code" green button and click "Download ZIP" -2. Extract the zip file -3. Go to the "Manage extensions" page by navigating to `chrome://extensions` -4. Enable developer mode by clicking the button at the top right corner -5. Drag the unzipped folder (make sure the contents are the file and not another folder) into the page -6. **OR** Click load unpacked and select the `manifest.json` file in the unzipped folder +### Screenshots -Note: The extension will not automatically update. You must check for updates manually in this repository for new eversions. +![image](https://github.com/ynshung/better-yt-shorts/assets/61302840/448f4050-cc7f-4676-b072-8bf2771d4b59) -## Features -* **Progress bar** at the bottom with time and duration -* **Seeking** 5 seconds backward and forward with arrow keys -* **Auto skip** short when current one ends -* Auto skip short with likes below custom threshold (e.g. 500 likes) -* Auto open comment section on each short -* Decrease and increase **playback speed** with keys U and O -* Revert to normal speed with I or by clicking the speed button -* Control **volume** with the volume slider or with - and =, mute audio with M -* Mini timestamp and speed above the like button (can be scrolled on!) -* Navigate to next or previous short **without animation** with W and S -* Go to the next **frame** or previous frame with . and , while paused -* **Customizable** keybinds ### Default Keybinds + | Action | Shortcut | |----------------------|------------| | Seek Backward (+5s) | ArrowLeft | @@ -65,22 +53,51 @@ Note: The extension will not automatically update. You must check for updates ma | Decrease Volume | Minus | | Increase Volume | Equal | | Toggle Mute | KeyM | -| Next Frame | Comma | -| Previous Frame | Period | -| Next Short | KeyS | -| Previous Short | KeyW | +| Restart Short | KeyJ | +| Next Frame | | +| Previous Frame | | +| Next Short | | +| Previous Short | | -## Screenshots +Some keybinds are disabled by default. You can enable them by setting its keybinds. -![image](https://github.com/ynshung/better-yt-shorts/assets/61302840/6d7ac315-7c16-4490-a1fe-683a3aa5538d) -![image](https://user-images.githubusercontent.com/80070435/219866370-d1acbd50-049b-47ef-9688-19d1dc4efe91.png) -![image](https://user-images.githubusercontent.com/80070435/219866388-13770811-674d-4681-be32-c7d27f35c000.png) +## Contributing +All type of contributions are welcome. You may contribute by reporting bugs, suggesting new features, translating the extension or even by submitting a pull request. -## Issues / Suggestion -If you faced any issue with the extension or any suggestion that can help to improve the extension, you may create an issue [here](https://github.com/ynshung/better-yt-shorts/issues) or if you know how to code, fork the repo, make the necessary changes and create a pull request. +### Translation +Know multiple languages? Help translate the extension so we can have a reach worldwide! See the list of supported locales [here](https://developer.chrome.com/docs/webstore/i18n/#choosing-locales-to-support). + +We are currently using POEditor to facilitate the localization process for new users. You can join the project using this [invite link](https://poeditor.com/join/project/QwlUFSANOG). Note that you can choose to translate or copy the `description` as it is just for reference. To test your translation in the browser, export the file as _Key-Value JSON_, rename the file to `messages.json` and put it in the `_locales/[LANG]` folder where _LANG_ is the code of the language. Make sure the [locale of your browser](https://developer.chrome.com/docs/extensions/reference/i18n/#how-to-set-browsers-locale) is set properly. See the development guide below to build your extension in real-time. Please note that this method haven't been fully tested yet, so please let us know of any issues you faced in the issue page. + +Alternatively, you can start by forking the repo, copying the `_locales/en/messages.json` file and paste it to your locale code directory. Then, you can start translating the messages in the `messages.json` file. The `description` are just for reference and will not be visible to the user so you may translate it or leave it as it-is. + +You can also add help translate the **store listing description** which is under the `store-desc/` directory. Create a file based on the original English language and translate it. Once you are done, you may create a pull request. + +If you need any help in translating, you may create an issue or contact us using the Google Form below. + +### Issues / Suggestion +If you have faced any issue with the extension or any suggestion that can help to improve the extension, you may create an issue [here](https://github.com/ynshung/better-yt-shorts/issues) or if you know how to code, fork the repo, make the necessary changes and create a pull request. You may leave your feedback in this [Google Form](https://forms.gle/pvSiMwDeQVfwyALfA). +### Development Guide +1. Fork the project on Github +2. Clone your fork in your local machine +3. Open the working directory in the terminal +4. Run `npm i` to install all dependencies (ensure that [node and npm are installed](https://nodejs.org/en)) +5. For **Chrome development** + 1. Run `npm run dev` to start development + 2. Open Chrome and navigate to `chrome://extensions` + 3. Toggle `Developer Mode` with the switch at the top-right of that page + 4. Drag and drop the `dist` directory into that page to load the unpacked extension + 5. **OR** click load unpacked and select the `manifest.json` file in the directory + 6. Changing a file should automatically update and refresh the extension +8. For **Firefox development** + 1. Run `npm run dev:firefox` to start development + 2. Open Firefox and navigate to `about:debugging#/runtime/this-firefox` + 3. Click `Load Temporary Add-on...` and select the `package.json` in the `dist` directory + 4. Everytime a file is changed, make sure to reload the extension after the message of `Firefox manifest created successfully.` is shown. + ## License MIT License diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..819c5a6 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,55 @@ +
+ +![BYS Icon](./src/assets/icons/bys-128.png) + +# Better YouTube Shorts v3 - Roadmap +
+ +## Current Changes and Improvements from v2 +- Options and Keybinds now take effect immediately +- Added "tabbed" page view for the popup to split up keybinds, options, and anything else in the future +- Fixed issue where volume would ignore slider sporadically +- Added "disable key bind" option in the edit keybind modal +- You can now reset options to defaults (options = extraoptions) +- Added "settings" object to storage, to contain excess content-script settings like volume and autoplay +- Added "features" object to storage, you can now enable and disable certain content script UI elements (autoplay, volume, etc) + + +## Codebase Format +- the `lib` directory contains exports for both the content and popup scripts. I recommend containing each bit of unrelated functionality in their own files. + - This excludes the `declarations.ts`, which should contain global variables used throughout the program for easy access + - All global types (including interfaces and enums) are defined in `definitions.ts` + - `utils.ts` is a file for utility functions that are generic. Basically think anything that could be transferred to a different project + - Finally, theres `getters.ts` which is for selector functions (eg *getVideo()*) +- the `components` directory is for react components. Try splitting up your TSX into reusable components if possible (its not too big of a deal if you cant mind you) +- Using an npm module for the icons (react-icons). Ideally, they should be imported from there (also stick to the material design ones for consistency) + +--- +## General +- Test with Firefox (I can't seem to get it to load at the moment, but the [compatibility checker](https://www.extensiontest.com/) agrees it is a compatible extension) +- Update README content and screenshots + +## Content Script +- ~~Separate Popup and Content CSS into their own files (prevent weird side effects)~~ +- ~~Implement the seek bar~~ +- ~~Implement the volume slider~~ + - ~~Save setting to storage~~ + - Slider should automatically toggle youtube's built in mute button when on 0 or >0 respectively (just do a synthetic click on the btn) +- Clean up code; move each element to their own script +- ~~⚠️ **error from recent chrome update may be unfixed**~~ +- Fix styling for autoplay (when comments are open, doesnt follow the transparent effect of other buttons) + +## Popup +- ~~Add missing functionality from more recent main branch patches (copy from the main branch):~~ + - ~~Option to **auto open comments**, and appropriate logic~~ + - ~~Option to **change the seek amount**~~ +- ~~Add proper icons for the tabs see [this icon pack](https://fonts.google.com/icons)~~ +- ~~Tweak styling for the indicators (padding and margins look off)~~ +- ⚠️ **Update logo when a new logo is decided if needed** +- Remove console logs **that aren't prefaced with "[BYS] :: "** +- ~~Fix issue with number and text inputs on the option page losing focus on input (on change changes the state, perhaps we need to instead update on loss of focus, not change)~~ + +## Language Support +Not a priority of mine, but I do have an idea of how to accomodate other languages, but its a bit finnicky. +It'd also require help from native localisers ideally. +Essentially, we could have an object of "terms" (could this be JSON?), with the key being the language code from `navigator.language` diff --git a/UNINSTALL.md b/UNINSTALL.md index 195c77c..97d30fa 100644 --- a/UNINSTALL.md +++ b/UNINSTALL.md @@ -2,10 +2,6 @@ We're sad to see you go! It would be great if you could let us know why you're uninstalling the extension by leaving feedback below. -### Notice: Chrome Web Store Takedown - -We have reuploaded the extension under a [new link](https://chrome.google.com/webstore/detail/better-youtube-shorts/pehohlhkhbcfdneocgnfbnilppmfncdg) as the previous one was taken down due to trademark issue. The original extension had a total of 25K installs, 16K active users and 4.59★ rating. We hope the original link will be restored soon, if possible. We apologize for any inconvenience caused. - ## Help us improve! If there's anything we can do to improve your experience, please let us know via the [issue tracker](https://github.com/ynshung/better-yt-shorts/issues) in GitHub. @@ -16,7 +12,7 @@ If you can understand code, feel free to make changes to the code and submit a p ## Thank you! -You may reinstall the extension at any time by visiting the [Chrome Web Store](https://chrome.google.com/webstore/detail/better-youtube-shorts/pehohlhkhbcfdneocgnfbnilppmfncdg) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/better-youtube-shorts). +You may reinstall the extension at any time by visiting the [Chrome Web Store](https://chrome.google.com/webstore/detail/better-youtube-shorts/icnidlkdlledahfgejnagmhgaeijokcp) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/better-youtube-shorts). Thank you for your support, and we hope to see you again in the future! diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 0000000..192c9fa --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,167 @@ +{ + "extName": { + "message": "Better YouTube Shorts", + "description": "Name of the extension" + }, + "extDescription": { + "message": "Take back the controls on YouTube Shorts with playback, volume, progress bar and more!", + "description": "Description of the extension" + }, + "announcement": { + "message": "Help us translate BYS!", + "description": "Announcement which will update from time to time" + }, + + "website": { + "message": "Website" + }, + + + "keybinds": { + "message": "Keybinds" + }, + "command": { + "message": "Command" + }, + "key": { + "message": "Key" + }, + "pressKeybinds": { + "message": "Press desired key" + }, + "pressAKey": { + "message": "Press a key" + }, + "disabled": { + "message": "" + }, + "notSupportKeyCombo": { + "message": "Does not support key combinations" + }, + "keyCanBeUsed": { + "message": "\"$1\" can be used!" + }, + "keyCannotBeUsed": { + "message": "\"$1\" cannot be used" + }, + "keyAlreadyInUse": { + "message": "\"$1\" is already in use" + }, + "disableKeybind": { + "message": "\"$1\" was disabled!" + }, + "editBinding": { + "message": "Edit binding for " + }, + "keybindDisabled": { + "message": "Keybind is currently disabled" + }, + "currentKeybind": { + "message": "Current bind is $1" + }, + "confirm": { + "message": "Confirm" + }, + "disableBind": { + "message": "Disable Bind" + }, + + "resetKB": { + "message": "Reset Keybinds" + }, + "seekBackward": { + "message": "Seek Backward" + }, + "seekForward": { + "message": "Seek Forward" + }, + "decreaseSpeed": { + "message": "Decrease Speed" + }, + "resetSpeed": { + "message": "Reset Speed" + }, + "increaseSpeed": { + "message": "Increase Speed" + }, + "decreaseVolume": { + "message": "Decrease Volume" + }, + "increaseVolume": { + "message": "Increase Volume" + }, + "toggleMute": { + "message": "Toggle Mute" + }, + "nextFrame": { + "message": "Next Frame" + }, + "previousFrame": { + "message": "Previous Frame" + }, + "restartShort": { + "message": "Restart Short" + }, + "nextShort": { + "message": "Next Short" + }, + "previousShort": { + "message": "Previous Short" + }, + + + "extraOptions": { + "message": "Extra Options" + }, + "resetOptions": { + "message": "Reset Options" + }, + + "autoSkipTitle": { + "message": "Automatically skip shorts with fewer likes" + }, + "skipThresholdTitle": { + "message": "Skip shorts with fewer than this many likes" + }, + "seekAmountTitle": { + "message": "Seek amount in seconds" + }, + "automaticallyOpenCommentsTitle": { + "message": "Open comments on new shorts automatically" + }, + "hideShortsOverlayTitle": { + "message": "Hide the overlay on shorts (title, channel, etc)" + }, + + + "toggleFeatures": { + "message": "Toggle Features" + }, + "changesAffectNewShorts": { + "message": "Changes will only affect new Shorts" + }, + "enableAll": { + "message": "Enable All" + }, + "disableAll": { + "message": "Disable All" + }, + "autoplay": { + "message": "Autoplay" + }, + "progressBar": { + "message": "Progress Bar" + }, + "timer": { + "message": "Timer" + }, + "playbackRate": { + "message": "Playback Rate" + }, + "volumeSlider": { + "message": "Volume Slider" + }, + "enable": { + "message": "Enable $1" + } +} diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json new file mode 100644 index 0000000..0f713a7 --- /dev/null +++ b/_locales/fr/messages.json @@ -0,0 +1,158 @@ +{ + "extName": { + "message": "Better YouTube Shorts", + "description": "Name of the extension" + }, + "extDescription": { + "message": "Reprenez le contrôle sur YouTube Shorts avec la lecture, le contrôle du volume, une barre de progression et plus encore !", + "description": "Description of the extension" + }, + "announcement": { + "message": "Aidez-nous à traduire BYS !", + "description": "Announcement which will update from time to time" + }, + "website": { + "message": "Site Web" + }, + "keybinds": { + "message": "Raccourcis clavier" + }, + "command": { + "message": "Commande" + }, + "key": { + "message": "Touche" + }, + "pressKeybinds": { + "message": "Appuyez sur la touche désirée" + }, + "pressAKey": { + "message": "Appuyez sur une touche" + }, + "disabled": { + "message": "" + }, + "notSupportKeyCombo": { + "message": "Ne supporte pas les combinaisons de touche" + }, + "keyCanBeUsed": { + "message": "\"$1\" peut être utilisé !" + }, + "keyCannotBeUsed": { + "message": "\"$1\" ne peut pas être utilisé" + }, + "keyAlreadyInUse": { + "message": "\"$1\" est déjà utilisé" + }, + "disableKeybind": { + "message": "\"$1\" a été désactivé !" + }, + "editBinding": { + "message": "Modifier la touche de raccourci pour " + }, + "keybindDisabled": { + "message": "Le raccourci est actuellement désactivée" + }, + "currentKeybind": { + "message": "Le raccourci actuelle est $1" + }, + "confirm": { + "message": "Confirmer" + }, + "disableBind": { + "message": "Désactiver le raccourci" + }, + "resetKB": { + "message": "Réinitialiser les raccourcis clavier" + }, + "seekBackward": { + "message": "Reculer la lecture" + }, + "seekForward": { + "message": "Avancer la lecture" + }, + "decreaseSpeed": { + "message": "Diminuer la vitesse de lecture" + }, + "resetSpeed": { + "message": "Réinitialiser la vitesse de lecture" + }, + "increaseSpeed": { + "message": "Augmenter la vitesse de lecture" + }, + "decreaseVolume": { + "message": "Diminuer le volume" + }, + "increaseVolume": { + "message": "Augmenter le volume" + }, + "toggleMute": { + "message": "Activer/Désactiver le son" + }, + "nextFrame": { + "message": "Image suivante" + }, + "previousFrame": { + "message": "Image précédente" + }, + "restartShort": { + "message": "Recommencer le short" + }, + "nextShort": { + "message": "Short suivant" + }, + "previousShort": { + "message": "Short précédent" + }, + "extraOptions": { + "message": "Options supplémentaires" + }, + "resetOptions": { + "message": "Réinitialiser les options" + }, + "autoSkipTitle": { + "message": "Passer automatiquement les shorts avec moins de likes" + }, + "skipThresholdTitle": { + "message": "Passer les shorts avec moins de likes que ceci" + }, + "seekAmountTitle": { + "message": "Nombre de secondes pour avancer/reculer la lecture" + }, + "automaticallyOpenCommentsTitle": { + "message": "Ouvrir automatiquement les commentaires sur les nouveaux shorts" + }, + "hideShortsOverlayTitle": { + "message": "Masquer l'interface superposée sur les shorts (titre, chaîne, etc.)" + }, + "toggleFeatures": { + "message": "Activer/Désactiver les fonctionnalités" + }, + "changesAffectNewShorts": { + "message": "Les modifications n'affecteront que les nouveaux shorts" + }, + "enableAll": { + "message": "Activer tout" + }, + "disableAll": { + "message": "Désactiver tout" + }, + "autoplay": { + "message": "Auto-Lecture" + }, + "progressBar": { + "message": "Barre de progression" + }, + "timer": { + "message": "Minuteur" + }, + "playbackRate": { + "message": "Vitesse de lecture" + }, + "volumeSlider": { + "message": "Curseur de volume" + }, + "enable": { + "message": "Activer $1" + } +} diff --git a/_locales/he/messages.json b/_locales/he/messages.json new file mode 100644 index 0000000..c637910 --- /dev/null +++ b/_locales/he/messages.json @@ -0,0 +1,157 @@ +{ + "extName": { + "message": "Better YouTube Shorts", + "description": "Name of the extension" + }, + "extDescription": { + "message": "לקבל בחזרה את השליטה ב-YouTube Shorts עם השמעה, עוצמת קול, סרגל התקדמות ועוד!", + "description": "Description of the extension" + }, + + "website": { + "message": "אתר" + }, + + "keybinds": { + "message": "קיצורי דרך במקלדת" + }, + "command": { + "message": "פקודה" + }, + "key": { + "message": "מקש" + }, + "pressKeybinds": { + "message": "לחץ על המקש הרצוי" + }, + "pressAKey": { + "message": "לחץ על מקש" + }, + "disabled": { + "message": "<מושבת>" + }, + "notSupportKeyCombo": { + "message": "אינו תומך בשילובי מקשים" + }, + "keyCanBeUsed": { + "message": "\"$1\" יכול להיות בשימוש!" + }, + "keyCannotBeUsed": { + "message": "\"$1\" לא יכול להיות בשימוש" + }, + "keyAlreadyInUse": { + "message": "\"$1\" כבר נמצא בשימוש" + }, + "disableKeybind": { + "message": "\"$1\" הושבת!" + }, + "editBinding": { + "message": "עריכת קיצור עבור " + }, + "keybindDisabled": { + "message": "קיצור מקשים מושבת כרגע" + }, + "currentKeybind": { + "message": "קיצור נוכחי הוא $1" + }, + "confirm": { + "message": "אישור" + }, + "disableBind": { + "message": "השבת קיצור" + }, + + "resetKB": { + "message": "איפוס קיצורי מקשים" + }, + "seekBackward": { + "message": "הרצה אחורה" + }, + "seekForward": { + "message": "הרצה קדימה" + }, + "decreaseSpeed": { + "message": "הנמכת מהירות" + }, + "resetSpeed": { + "message": "איפוס מהירות" + }, + "increaseSpeed": { + "message": "הגברת מהירות" + }, + "decreaseVolume": { + "message": "הנמכת עוצמת שמע" + }, + "increaseVolume": { + "message": "הגברת עוצמת שמע" + }, + "toggleMute": { + "message": "הפעל/כיבוי השתקה" + }, + "nextFrame": { + "message": "פריים הבא" + }, + "previousFrame": { + "message": "פריים קודם" + }, + "restartShort": { + "message": "התחלה מחדש של Short" + }, + "nextShort": { + "message": "Short הבא" + }, + "previousShort": { + "message": "Short קודם" + }, + + "extraOptions": { + "message": "אפשרויות נוספות" + }, + "resetOptions": { + "message": "איפוס אפשרויות" + }, + + "autoSkipTitle": { + "message": "דלג אוטומטית על Shorts עם פחות לייקים" + }, + "skipThresholdTitle": { + "message": "דלג על Shorts עם פחות מכזו כמות של לייקים" + }, + "seekAmountTitle": { + "message": "זמן הרצה בשניות" + }, + "automaticallyOpenCommentsTitle": { + "message": "פתח תגובות עבור Shorts חדשים באופן אוטומטי" + }, + + "toggleFeatures": { + "message": "הפעל/כיבוי פיצ'רים" + }, + "changesAffectNewShorts": { + "message": "שינווים ישפיעו על Shorts חדשים בלבד" + }, + "enableAll": { + "message": "הפעל הכל" + }, + "disableAll": { + "message": "השבת הכל" + }, + "autoplay": { + "message": "ניגון אוטומטי" + }, + "progressBar": { + "message": "סרגל התקדמות" + }, + "timer": { + "message": "שעון עצר" + }, + "playbackRate": { + "message": "קצב השמעה" + }, + "volumeSlider": { + "message": "מחוון עוצמת שמע" + }, + "enable": { + "message": "הפעל $1" + } +} diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json new file mode 100644 index 0000000..27dfc6f --- /dev/null +++ b/_locales/ja/messages.json @@ -0,0 +1,160 @@ +{ + "extName": { + "message": "Better YouTube Shorts", + "description": "Name of the extension" + }, + "extDescription": { + "message": "再生、音量、シークバーなど、YouTube Shortsのコントロールを取り戻しましょう!", + "description": "Description of the extension" + }, + + "website": { + "message": "GitHub🔗" + }, + + + "keybinds": { + "message": "ショートカットキー" + }, + "command": { + "message": "コマンド" + }, + "key": { + "message": "キー" + }, + "pressKeybinds": { + "message": "希望のキーを押下" + }, + "pressAKey": { + "message": "キーを入力..." + }, + "disabled": { + "message": "<無効>" + }, + "notSupportKeyCombo": { + "message": "このキーは設定できません。" + }, + "keyCanBeUsed": { + "message": "\"$1\" は設定可能です。" + }, + "keyCannotBeUsed": { + "message": "\"$1\" は設定できません。" + }, + "keyAlreadyInUse": { + "message": "\"$1\" は既に使用されています。" + }, + "disableKeybind": { + "message": "\"$1\" は無効に設定されます。" + }, + "editBinding": { + "message": "以下のショートカットを設定しています..." + }, + "keybindDisabled": { + "message": "現在キーは設定されていません。" + }, + "currentKeybind": { + "message": "現在のキーは $1 です。" + }, + "confirm": { + "message": "確認" + }, + "disableBind": { + "message": "無効" + }, + + "resetKB": { + "message": "リセット" + }, + "seekBackward": { + "message": "先送り" + }, + "seekForward": { + "message": "巻き戻し" + }, + "decreaseSpeed": { + "message": "再生速度を上げる" + }, + "resetSpeed": { + "message": "通常の再生速度" + }, + "increaseSpeed": { + "message": "再生速度を下げる" + }, + "decreaseVolume": { + "message": "音量を下げる" + }, + "increaseVolume": { + "message": "音量を上げる" + }, + "toggleMute": { + "message": "ミュート" + }, + "nextFrame": { + "message": "次フレーム" + }, + "previousFrame": { + "message": "前フレーム" + }, + "restartShort": { + "message": "初めから再生" + }, + "nextShort": { + "message": "次のショート" + }, + "previousShort": { + "message": "前のショート" + }, + + + "extraOptions": { + "message": "高度なオプション" + }, + "resetOptions": { + "message": "リセット" + }, + + "autoSkipTitle": { + "message": "高評価が少ないショートをスキップ" + }, + "skipThresholdTitle": { + "message": "上項の閾値" + }, + "seekAmountTitle": { + "message": "巻き戻し、先送りの秒数" + }, + "automaticallyOpenCommentsTitle": { + "message": "自動的にコメントを開く" + }, + + + "toggleFeatures": { + "message": "機能" + }, + "changesAffectNewShorts": { + "message": "変更は新しいショートで適用されます" + }, + "enableAll": { + "message": "すべて有効" + }, + "disableAll": { + "message": "すべて無効" + }, + "autoplay": { + "message": "自動再生" + }, + "progressBar": { + "message": "シークバー" + }, + "timer": { + "message": "時間表示" + }, + "playbackRate": { + "message": "再生速度" + }, + "volumeSlider": { + "message": "音量バー" + }, + "enable": { + "message": "$1 を有効にする" + } +} diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json new file mode 100644 index 0000000..5b25d57 --- /dev/null +++ b/_locales/pt/messages.json @@ -0,0 +1,164 @@ +{ + "extName": { + "message": "Better YouTube Shorts", + "description": "Name of the extension" + }, + "extDescription": { + "message": "Retome o controle do YouTube Shorts com playback, volume, barra de progresso e muito mais!", + "description": "Description of the extension" + }, + "announcement": { + "message": "Ajude-nos a traduzir BYS!", + "description": "Announcement which will update from time to time" + }, + + "website": { + "message": "Website" + }, + + + "keybinds": { + "message": "Atalhos de Teclado" + }, + "command": { + "message": "Comando" + }, + "key": { + "message": "Tecla" + }, + "pressKeybinds": { + "message": "Pressione a tecla desejada" + }, + "pressAKey": { + "message": "Pressione uma tecla" + }, + "disabled": { + "message": "" + }, + "notSupportKeyCombo": { + "message": "Não suporta combinações de teclas" + }, + "keyCanBeUsed": { + "message": "\"$1\" pode ser usada!" + }, + "keyCannotBeUsed": { + "message": "\"$1\" não pode ser usada" + }, + "keyAlreadyInUse": { + "message": "\"$1\" já está em uso" + }, + "disableKeybind": { + "message": "\"$1\" foi desabilitado!" + }, + "editBinding": { + "message": "Editando tecla para " + }, + "keybindDisabled": { + "message": "Atalho atualmente desabilitado" + }, + "currentKeybind": { + "message": "Tecla atual é $1" + }, + "confirm": { + "message": "Confirmar" + }, + "disableBind": { + "message": "Desabilitar" + }, + + "resetKB": { + "message": "Resetar Atalhos" + }, + "seekBackward": { + "message": "Retroceder" + }, + "seekForward": { + "message": "Avançar" + }, + "decreaseSpeed": { + "message": "Diminuir Velocidade" + }, + "resetSpeed": { + "message": "Resetar Velocidade" + }, + "increaseSpeed": { + "message": "Aumentar Velocidade" + }, + "decreaseVolume": { + "message": "Diminuir Volume" + }, + "increaseVolume": { + "message": "Aumentar Volume" + }, + "toggleMute": { + "message": "Ativar/Desativar Som" + }, + "nextFrame": { + "message": "Próximo Frame" + }, + "previousFrame": { + "message": "Frame Anterior" + }, + "restartShort": { + "message": "Recomeçar Short" + }, + "nextShort": { + "message": "Próximo Short" + }, + "previousShort": { + "message": "Short Anterior" + }, + + + "extraOptions": { + "message": "Opções Extras" + }, + "resetOptions": { + "message": "Resetar Opções" + }, + + "autoSkipTitle": { + "message": "Pule Shorts com menos curtidas automaticamente" + }, + "skipThresholdTitle": { + "message": "Pule Shorts com menos do que esse número de curtidas" + }, + "seekAmountTitle": { + "message": "Segundos de retrocesso/avanço" + }, + "automaticallyOpenCommentsTitle": { + "message": "Abra comentários automaticamente" + }, + + + "toggleFeatures": { + "message": "Recursos" + }, + "changesAffectNewShorts": { + "message": "Alterações afetarão apenas novos Shorts" + }, + "enableAll": { + "message": "Habilitar Tudo" + }, + "disableAll": { + "message": "Desabilitar Tudo" + }, + "autoplay": { + "message": "Autoplay" + }, + "progressBar": { + "message": "Barra de Progresso" + }, + "timer": { + "message": "Timer" + }, + "playbackRate": { + "message": "Velocidade de Reprodução" + }, + "volumeSlider": { + "message": "Barra de Volume" + }, + "enable": { + "message": "Habilitar $1" + } +} diff --git a/background.js b/background.js deleted file mode 100644 index e177d49..0000000 --- a/background.js +++ /dev/null @@ -1,4 +0,0 @@ -const browserObj = (typeof browser === 'undefined') ? chrome : browser; -const version = browserObj.runtime.getManifest().version; - -browserObj.runtime.setUninstallURL("https://github.com/ynshung/better-yt-shorts/blob/master/UNINSTALL.md"); \ No newline at end of file diff --git a/content-script.js b/content-script.js deleted file mode 100644 index 871bd2c..0000000 --- a/content-script.js +++ /dev/null @@ -1,738 +0,0 @@ -const defaultKeybinds = { - 'Seek Backward': 'ArrowLeft', - 'Seek Forward': 'ArrowRight', - 'Decrease Speed': 'KeyU', - 'Reset Speed': 'KeyI', - 'Increase Speed': 'KeyO', - 'Decrease Volume': 'Minus', - 'Increase Volume': 'Equal', - 'Toggle Mute': 'KeyM', - 'Next Frame': 'Comma', - 'Previous Frame': 'Period', - 'Next Short': 'KeyS', - 'Previous Short': 'KeyW', -}; -const defaultExtraOptions = { - skip_enabled: false, - skip_threshold: 500, - automatically_open_comments: false, - seek_amount: 5, -} -const storage = (typeof browser === 'undefined') ? chrome.storage.local : browser.storage.local; -var muted = false; -var volumeState = 0; -var actualVolume = 0; -var skippedId = null -var openedCommentsId = null -var topId = 0 // store the furthest id in the chain - -// video with no likes => https://www.youtube.com/shorts/ZFLRydDd9Mw -// video with no likes and 23k comments => https://www.youtube.com/shorts/gISsypl5xsc -// another => https://www.youtube.com/shorts/qe56pgRVrgE?feature=share -// video with 1.5M / 1,5M => https://www.youtube.com/shorts/nKZIx1bHUbQ - -function openComments() -{ - getCommentsButton().click() -} -function shouldOpenComments() -{ - let currentId = getCurrentId() - - if ( extraOptions === null ) return false - if ( !extraOptions.automatically_open_comments ) return false - if ( currentId === skippedId ) return false // prevents opening comments on skipped shorts - if ( currentId === openedCommentsId ) return false // allow closing of comments - - // change here to prevent bugs with closing comments on previous shorts - openedCommentsId = currentId - - if ( isCommentsPanelOpen() ) return false - - return true -} - -function shouldSkipShort( currentId, likeCount ) -{ - // for debugging purposes - - // console.dir({ - // "extra options check": !( extraOptions == null ), - // "video playing check": !( getVideo().currentTime === 0 ), - // "option enabled?": !( !extraOptions.skip_enabled ), - // "current id check": !( currentId < topId ), - // "skipped id check": !( skippedId === currentId ), - // "likecount null check": !( likeCount === null || isNaN( likeCount ) ), - // "threshold check": !( likeCount >= extraOptions.skip_threshold ), - // "current threshold": extraOptions.skip_threshold, - // "number of likes": likeCount - // }) - - if ( extraOptions === null ) return false - if ( getVideo().currentTime === 0 ) return false // video unstarted, likes likely not loaded - - if ( !extraOptions.skip_enabled ) return false - if ( topId === 0 ) return false // dont skip first short ever - if ( currentId < topId ) return false // allow user to scroll back up to see skipped video - if ( skippedId === currentId ) return false // prevent skip spam - if ( likeCount === null || isNaN( likeCount ) ) return false // dont skip unloaded shorts - if ( likeCount >= extraOptions.skip_threshold ) return false - return true -} - -const getCommentsButton = () => - document.querySelector( `[ id="${getCurrentId()}" ] #comments-button .yt-spec-touch-feedback-shape__fill` ) - -function isCommentsPanelOpen() -{ - // return true if the selector finds an open panel - // if panel is unfound, then the short either hasnt loaded, or the panel is not open - return document.querySelector( `[ id="${getCurrentId()}" ] #watch-while-engagement-panel [ visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED" ]` ) ?? false -} - -// fixed mac scroll issue -function skipShort( short ) -{ - var nextButton = getNextButton(); - nextButton.click(); -} - -// Using localStorage as a fallback for browser/chrome.storage.local -var keybinds = JSON.parse(localStorage.getItem("yt-keybinds")); -storage.get(["keybinds"]) -.then((result) => { - if (result.keybinds) { - // Set default keybinds if not exists - for (const [cmd, keybind] of Object.entries(defaultKeybinds)) { - if (!result.keybinds[cmd]) result.keybinds[cmd] = keybind; - } - if (result.keybinds !== keybinds) localStorage.setItem("yt-keybinds", JSON.stringify(result.keybinds)); - keybinds = result.keybinds; - } -}); - -var extraOptions = JSON.parse(localStorage.getItem("yt-extraopts")) -storage.get( ["extraopts"] ) - .then((result) => { - if (result.extraopts) - { - // Set default options if not exists - for ( const [ option, value ] of Object.entries( defaultExtraOptions ) ) { - if ( result.extraopts[ option ] ) continue - result.extraopts[ option ] = value - } - - if ( result.extraopts !== extraOptions ) - localStorage.setItem("yt-extraopts", JSON.stringify(result.extraopts) ) - - extraOptions = result.extraopts - } - }) - -document.addEventListener("keydown", (data) => { - if ( - [ ...document.querySelectorAll("input") ].includes( document.activeElement ) || - [ ...document.querySelectorAll("#contenteditable-root") ].includes( document.activeElement ) - ) return; // Avoids using keys while the user interacts with any input, like search and comment. - - const ytShorts = getVideo(); - if (!ytShorts) return; - if (!keybinds) keybinds = defaultKeybinds; - - const key = data.code; - const keyAlt = data.key.toLowerCase(); // for legacy keybinds - - let command; - for ( const [cmd, keybind] of Object.entries(keybinds) ) - if ( key === keybind || keyAlt === keybind ) - command = cmd; - - if (!command) return; - - switch (command) { - case "Seek Backward": - ytShorts.currentTime -= extraOptions?.seek_amount ?? defaultExtraOptions.seek_amount; - break; - - case "Seek Forward": - ytShorts.currentTime += extraOptions?.seek_amount ?? defaultExtraOptions.seek_amount; - break; - - case "Decrease Speed": - if (ytShorts.playbackRate > 0.25) ytShorts.playbackRate -= 0.25; - break; - - case "Reset Speed": - ytShorts.playbackRate = 1; - break; - - case "Increase Speed": - if (ytShorts.playbackRate < 16) ytShorts.playbackRate += 0.25; - break; - - case "Increase Volume": - if (ytShorts.volume <= 0.975) { - setVolume(ytShorts.volume + 0.025); - } - break; - - case "Decrease Volume": - if (ytShorts.volume >= 0.025) { - setVolume(ytShorts.volume - 0.025); - } - break; - - case "Toggle Mute": - if (!muted) { - muted = true; - volumeState = ytShorts.volume; - ytShorts.volume = 0; - } else { - muted = false; - ytShorts.volume = volumeState; - } - break; - - case "Next Frame": - if (ytShorts.paused) { - ytShorts.currentTime -= 0.04; - } - break; - - case "Previous Frame": - if (ytShorts.paused) { - ytShorts.currentTime += 0.04; - } - break; - - case "Next Short": - goToNextShort( ytShorts ) - break; - - case "Previous Short": - goToPrevShort( ytShorts ) - break; - } - setSpeed = ytShorts.playbackRate; -}); - -const getCurrentId = () => { - const videoEle = document.querySelector( - "#shorts-player > div.html5-video-container > video" - ); - if (videoEle && videoEle.closest("ytd-reel-video-renderer")) return +videoEle.closest("ytd-reel-video-renderer").id; - return null; -}; - -const getLikeCount = (id) => { - const likesElement = document.querySelector( - `[id='${id}'] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer #like-button` - ); - - // Use optional chaining and nullish coalescing to handle null values - const numberOfLikes = likesElement?.firstElementChild?.innerText.split(/\r?\n/)[0]?.trim().replace(/\s/g, "").replace(/\.$/, "").toLowerCase() ?? "0"; - - // Convert the number of likes to the appropriate format - const likeCount = convertLocaleNumber(numberOfLikes); - - // If likeCount is anything other than a number, it'll return 0. Meaning it'll translate every language. - return !isNaN(likeCount) ? likeCount : "0"; -}; - -// Checking comment count aswell, as sometimes popular videos bug out and show 0 likes, but there's 1000+ comments. -const getCommentCount = (id) => { - const commentsElement = document.querySelector( - `[id='${id}'] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer #comments-button` - ); - - // Use optional chaining and nullish coalescing to handle null values - const numberOfComments = commentsElement?.firstElementChild?.innerText.split(/\r?\n/)[0]?.replace(/ /g, "") ?? "0"; - - // Convert the number of comments to the appropriate format - const commentCount = convertLocaleNumber(numberOfComments); - - // If commentCount is anything other than a number, it'll return 0. Meaning it'll handle every language. - return !isNaN(commentCount) ? commentCount : 0; -}; - -const getActionElement = (id) => - document.querySelector( - `[id='${id}'] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #actions` - ); - -const getOverlayElement = (id) => - document.querySelector( - `[id='${id}'] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #overlay` - ); - -const getVolumeContainer = (id) => - document.querySelector( - `[id='${id}'] > #player-container > div.player-controls.style-scope.ytd-reel-video-renderer > ytd-shorts-player-controls.style-scope.ytd-reel-video-renderer` - ); - -const getNextButton = () => - document.querySelector('#navigation-button-down > .style-scope.ytd-shorts > yt-button-shape > button.yt-spec-button-shape-next.yt-spec-button-shape-next--text.yt-spec-button-shape-next--mono.yt-spec-button-shape-next--size-xl.yt-spec-button-shape-next--icon-button'); - -const setTimer = (currTime, duration) => { - const id = getCurrentId(); - if (document.getElementById(`ytTimer${id}`) === null) return false; - document.getElementById( - `ytTimer${id}` - ).innerText = `${currTime}/${duration}s`; - return true -}; - -const setVolumeSlider = (ytShorts, id) => { - const volumeContainer = getVolumeContainer(id); - const slider = document.createElement("input"); - if(!actualVolume) actualVolume = 0.5; - checkVolume(ytShorts); - slider.id = `volumeSliderController${id}`; - slider.classList.add("volume-slider"); - slider.classList.add("betterYT-volume-slider"); - slider.type = "range"; - slider.min = 0; - slider.max = 1; - slider.step = 0.01; - slider.setAttribute("orient", "vertical"); - volumeContainer.appendChild(slider); - slider.value = actualVolume; - - slider.addEventListener("input", (data) => { - setVolume(data.target.value); - }); - - // Prevent video from pausing/playing on click - slider.addEventListener("click", (data) => { - data.stopPropagation(); - }); -}; - -const setVolume = (volume) => { - const id = getCurrentId() - const volumeSliderController = document.getElementById(`volumeSliderController${id}`); - volumeSliderController.value = volume; - - const ytShorts = document.querySelector( - "#shorts-player > div.html5-video-container > video" - ); - ytShorts.volume = volume; - localStorage.setItem("yt-player-volume",`{ - "data": {\"volume\":`+volume+`,\"muted\":`+muted+`} - }`) -} - -const checkVolume = (ytShorts) => { - if(localStorage.getItem("yt-player-volume") !== null && JSON.parse(localStorage.getItem("yt-player-volume"))["data"]["volume"] !== undefined ){ - actualVolume = JSON.parse(localStorage.getItem("yt-player-volume"))["data"]["volume"]; - ytShorts.volume = actualVolume; - }else{ - actualVolume = ytShorts.volume; - } -}; - -const setPlaybackRate = (currSpeed) => { - const id = getCurrentId(); - if (document.getElementById(`ytPlayback${id}`) === null) return false; - document.getElementById( - `ytPlayback${getCurrentId()}` - ).innerText = `${currSpeed}x`; - return true -}; - -function getVideo() { return document.querySelector("#shorts-player>div>video"); } - -//WheelProgram -function wheel(Element, codeA, codeB) { - Element.addEventListener("wheel", (event) => { - if (event.wheelDelta > 0) { - codeA(); - } else { - codeB(); - } - event.preventDefault(); - }, - { passive: false } - ); -} - -var injectedItem = new Set(); -var lastTime = -1; -var lastSpeed = 0; -var setSpeed = 1; - -const timer = setInterval(() => { - if (window.location.toString().indexOf("youtube.com/shorts/") < 0) return; - - const ytShorts = getVideo(); - var currentId = getCurrentId(); - var likeCount = getLikeCount(currentId); - var actionList = getActionElement(currentId); - var overlayList = getOverlayElement(currentId); - var autoplayEnabled = localStorage.getItem("yt-autoplay") === "true" ? true : false; - if (autoplayEnabled === null) autoplayEnabled = false; - - var progBarList = overlayList.querySelector('#progress-bar-line'); - progBarList.removeAttribute( "hidden" ) - - if ( topId < currentId ) - topId = currentId - - // video has to have been playing to skip. - // I'm undecided whether to use 0.5 or 1 for currentTime, as 1 isn't quite fast enough, but sometimes with 0.5, it skips a video above the minimum like count. - if (ytShorts && ytShorts.currentTime > 0.5 && ytShorts.duration > 1) { - - if (shouldSkipShort(currentId, likeCount)) { - console.log("[Better Youtube Shorts] :: Skipping short that had", likeCount, "likes"); - skippedId = currentId; - skipShort(ytShorts); - } - - if( shouldOpenComments() ) - { - console.log("[Better Youtube Shorts] :: Opening comments"); - openComments() - } - } - - if (injectedItem.has(currentId)) { - var currTime = Math.round(ytShorts.currentTime); - var currSpeed = ytShorts.playbackRate; - - if (autoplayEnabled && ytShorts && ytShorts.currentTime >= ytShorts.duration - 0.11 && skippedId !== currentId) { - skippedId = currentId; - var nextButton = getNextButton(); - nextButton.click(); - } - - if (currTime !== lastTime) { - // Using this as a check whether the elements actually were injected on the page - var injectedSuccess = setTimer(currTime, Math.round(ytShorts.duration || 0)); - // If failed, retry injection during next interval - if (!injectedSuccess) injectedItem.delete(currentId); - lastTime = currTime; - } - if (currSpeed != lastSpeed) { - const setRateSuccess = setPlaybackRate(currSpeed); - if (setRateSuccess) lastSpeed = currSpeed; - } - - } else { - lastTime = -1; - lastSpeed = 0; - if (autoplayEnabled && ytShorts) ytShorts.loop = false; - - if (actionList) { - - const betterYTContainer = document.createElement("div"); - betterYTContainer.id = "betterYT-container"; - betterYTContainer.setAttribute("class", "button-container style-scope ytd-reel-player-overlay-renderer"); - - const ytdButtonRenderer = document.createElement("div"); - ytdButtonRenderer.setAttribute("class", "betterYT-renderer style-scope ytd-reel-player-overlay-renderer"); - - const ytButtonShape = document.createElement("div"); - ytButtonShape.setAttribute("class", "betterYT-button-shape"); - - const ytLabel = document.createElement("label"); - ytLabel.setAttribute("class", "yt-spec-button-shape-with-label"); - - const ytButton = document.createElement("button"); - ytButton.setAttribute("class", "yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-l yt-spec-button-shape-next--icon-button "); - // Playback Rate - var para0 = document.createElement("p"); - para0.classList.add("betterYT"); - para0.id = `ytPlayback${currentId}`; - - // Timer - const ytTimer = document.createElement("div"); - ytTimer.classList.add("yt-spec-button-shape-with-label__label"); - var span1 = document.createElement("span"); - span1.setAttribute("class", "yt-core-attributed-string yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--text-alignment-center yt-core-attributed-string--word-wrapping"); - span1.id = `ytTimer${currentId}`; - span1.setAttribute("role", "text"); - ytTimer.appendChild(span1); - - // Match YT's HTML structure - ytButton.appendChild(para0); - ytLabel.appendChild(ytButton); - ytLabel.appendChild(ytTimer); - ytButtonShape.appendChild(ytLabel); - ytdButtonRenderer.appendChild(ytButtonShape); - betterYTContainer.appendChild(ytdButtonRenderer); - - actionList.insertBefore(betterYTContainer, actionList.children[1]); - - // Autoplay Switch - const switchContainer = document.createElement("div"); - const autoplaySwitch = document.createElement("label"); - autoplaySwitch.classList.add("autoplay-switch"); - var checkBox = document.createElement("input"); - checkBox.type = "checkbox"; - checkBox.id = `autoplay-checkbox${currentId}`; - checkBox.checked = autoplayEnabled; - var autoplaySpan = document.createElement("span"); - autoplaySpan.classList.add("autoplay-slider"); - autoplaySwitch.append(checkBox, autoplaySpan); - switchContainer.appendChild(autoplaySwitch); - - actionList.insertBefore(switchContainer, actionList.children[1]); - - const autoplayTitle = document.createElement("div"); - autoplayTitle.classList.add("yt-spec-button-shape-with-label__label"); - var span2 = document.createElement("span"); - span2.setAttribute("class", "betterYT-auto yt-core-attributed-string yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--text-alignment-center"); - span2.setAttribute("role", "text"); - span2.textContent = "Autoplay"; - autoplayTitle.appendChild(span2); - - actionList.insertBefore(autoplayTitle, actionList.children[2]); - injectedItem.add(currentId); - - ytShorts.playbackRate = setSpeed; - setPlaybackRate(setSpeed); - injectedSuccess = setTimer(currTime || 0, Math.round(ytShorts.duration || 0)); - - betterYTContainer.addEventListener("click",() => { - ytShorts.playbackRate = 1; - setSpeed = ytShorts.playbackRate; - }); - - checkBox.addEventListener('change', () => { - if (checkBox.checked) { - localStorage.setItem("yt-autoplay", "true"); - ytShorts.loop = false; - } else { - localStorage.setItem("yt-autoplay", "false"); - ytShorts.loop = true; - } - }); - - wheel(ytButton, speedup, speeddown); - function speedup() { - if (ytShorts.playbackRate < 16) getVideo().playbackRate += 0.25; - setSpeed = getVideo().playbackRate; - } - function speeddown() { - if (ytShorts.playbackRate > 0.25) getVideo().playbackRate -= 0.25; - setSpeed = getVideo().playbackRate; - } - wheel(ytTimer, forward, backward); - function forward() { - getVideo().currentTime += 1; - } - function backward() { - getVideo().currentTime -= 1; - } - - } - // Progress bar - if (overlayList) { - var progBarList = overlayList.querySelector('#progress-bar-line'); - var progBarBG = progBarList.children[0]; - var progBarPlayed = progBarList.children[1]; // The red part of the progress bar - - const timestampTooltip = document.createElement("div"); - timestampTooltip.classList.add("betterYT-timestamp-tooltip"); - - progBarList.appendChild(timestampTooltip); - - // Styling to ensure rest of bottom overlay (shorts title/sub button) stay in place - overlayList.children[0].style.marginBottom = "-7px"; - progBarList.style.height = "10px"; - progBarList.style.paddingTop = "2px"; // Slight padding to increase hover box - - progBarList.classList.add('betterYT-progress-bar'); - progBarBG.classList.add('betterYT-progress-bar'); - progBarPlayed.classList.add('betterYT-progress-bar'); - - progBarList.addEventListener("mouseover", () => { - progBarBG.classList.add('betterYT-progress-bar-hover'); - progBarPlayed.classList.add('betterYT-progress-bar-hover'); - }); - progBarList.addEventListener("mousemove", (event) => { - let x = event.clientX - ytShorts.getBoundingClientRect().left; - // Deal with slight inaccuracies - if (x < 0) x = 0; - if (x > ytShorts.clientWidth) x = ytShorts.clientWidth; - // Get timestamp and round to nearest 0.1 - let timestamp = ((x / ytShorts.clientWidth) * ytShorts.duration).toFixed(1); - timestampTooltip.textContent = `${timestamp}s`; - // Ensure tooltip stays visible at edges of client - if ((x - (timestampTooltip.offsetWidth / 2)) > (ytShorts.clientWidth - timestampTooltip.offsetWidth)) { - timestampTooltip.style.left = `${ytShorts.clientWidth - timestampTooltip.offsetWidth}px`; - } else if ((x - (timestampTooltip.offsetWidth / 2)) <= 0) { - timestampTooltip.style.left = "0px"; - } else { - timestampTooltip.style.left = `${x - (timestampTooltip.offsetWidth / 2)}px`; - } - timestampTooltip.style.top = "-20px"; - timestampTooltip.style.display = 'block'; - }); - progBarList.addEventListener("mouseout", () => { - progBarBG.classList.remove('betterYT-progress-bar-hover'); - progBarPlayed.classList.remove('betterYT-progress-bar-hover'); - timestampTooltip.style.display = 'none'; - }); - progBarList.addEventListener("click", (event) => { - let x = event.clientX - ytShorts.getBoundingClientRect().left; - if (x < 0) x = 0; - if (x > ytShorts.clientWidth) x = ytShorts.clientWidth; - ytShorts.currentTime = (x / ytShorts.clientWidth) * ytShorts.duration; - }); - } - if (currentId !== null) setVolumeSlider(ytShorts, currentId); - } - if (ytShorts) checkVolume(ytShorts); -}, 100); - -/** - * Converts a formatted number to its full integer value. - * @param {string} string value to be converted (eg: 1.4M, 1,291 or 727) - * @returns converted number - */ -function convertLocaleNumber( string ) -{ - if ( typeof string !== "string" ) return - - // todo - add formats from other langs - const multipliers = { - // English - "b": 1_000_000_000, - "m": 1_000_000, - "k": 1_000, - - // Italian - "mln": 1_000_000, - - // Indian English - "lakh": 100_000, - - // Portuguese - "mil": 1_000, - "mi": 1_000_000, - - // Spanish - "mil": 1_000, - - // French - "mio": 1_000_000, - "md": 1_000, - - // German - "mio": 1_000_000, - "mrd": 1_000_000_000, - "tsd": 1_000, - - // Japanese - "億": 1_0000_0000, - "万": 1_0000, - - // Chinese (Simplified) - "亿": 1_0000_0000, - "万": 1_0000, - - // Chinese (Traditional) - "億": 1_0000_0000, - "萬": 1_0000, - - // Russian - "млн": 1_000_000, - "тыс": 1_000, - - // Hindi - "करोड़": 10_000_000, - "लाख": 100_000, - - // Arabic - "مليون": 1_000_000, - "مليار": 1_000_000_000, - "ألف": 1_000, - - // Korean - "억": 100_000_000, - "만": 10_000, - - // Turkish - "milyon": 1_000_000, - "milyar": 1_000_000_000, - "bin": 1_000, - - // Vietnamese - "triệu": 1_000_000, - "tỷ": 1_000_000_000, - "nghìn": 1_000, - - // Thai - "ล้าน": 1_000_000, - "พันล้าน": 1_000_000_000, - "พัน": 1_000, - - // Dutch - "mio": 1_000_000, - "mld": 1_000_000_000, - "k": 1_000, - - // Greek - "εκ": 1_000_000, - "δισ": 1_000_000_000, - "χιλ": 1_000, - - // Swedish - "mn": 1_000_000, - "md": 1_000_000_000, - "t": 1_000, - } - // const regex = /^(\d{1,3}(?:(?:,\d{3})*(?:\.\d+)?)|(?:\d+))(?:([,.])(\d+))?([a-z]*)\.?$/i; - const regex = /^([0-9\.,]+)\s?(\p{L}+)/ui - const matches = string.match( regex ) - - if (!matches) return 0 - - // 1 - number with point (now 1) - // 4 - multiplier (eg: m, b, k) (now 2) - - let numericPart = matches[1] - const multiplier = matches[2]?.toLowerCase(); - - if ( multiplier ) - { - // if has multiplier, comma is decimal point - numericPart = matches[1].replace( /,/g, "." ) - } - else - { - // remove separators - numericPart = matches[1].replace( /\.,/g, "" ) - } - - const hasMultiplier = Object.keys(multipliers).includes(multiplier) - - // debug console log - // console.log({ - // multiplier, - // matches, - // returned: hasMultiplier ? numericPart * multipliers[multiplier] : parseInt( numericPart, 10 ) - // }); - - if (hasMultiplier) { - return numericPart * multipliers[multiplier]; - } - else { - // Remove decimals and commas from the numeric part - const numericValue = parseInt( numericPart, 10 ) - return numericValue; - } -} - -function goToNextShort( short ) -{ - const scrollAmount = short.clientHeight - document.getElementById( "shorts-container" ).scrollTop += scrollAmount -} - -function goToPrevShort( short ) -{ - const scrollAmount = short.clientHeight - document.getElementById( "shorts-container" ).scrollTop -= scrollAmount -} \ No newline at end of file diff --git a/firefox-build-manifest.js b/firefox-build-manifest.js new file mode 100644 index 0000000..c24eac9 --- /dev/null +++ b/firefox-build-manifest.js @@ -0,0 +1,51 @@ +const { exec } = require("child_process"); +const fs = require("fs"); + +const RELATIVE_PATH_TO_MANIFEST = "./dist/manifest.json"; + +const REPLACED_LINES = { + service_worker: '\t\t"scripts": ["service-worker-loader.js"],', +}; + +if (process.argv[2] && process.argv[2] === "--skip-build") { + buildFirefoxManifest(); +} else { + exec("npm run build", (error) => { + if (error) { + console.error( + "An error occurred while running 'npm run build'.", + error + ); + } else { + buildFirefoxManifest(); + } + }); +} + +function buildFirefoxManifest() { + try { + const chromeManifest = fs.readFileSync( + RELATIVE_PATH_TO_MANIFEST, + "utf8" + ); + const lines = chromeManifest.split("\n"); + const newLines = []; + + lines.forEach((line) => { + for (const key in REPLACED_LINES) { + if (line.includes(key)) { + line = REPLACED_LINES[key]; + } + } + newLines.push(line); + }); + + fs.writeFileSync(RELATIVE_PATH_TO_MANIFEST, newLines.join("\n")); + console.log("Firefox manifest created successfully."); + } catch (error) { + console.error( + "An error occurred while creating the Firefox manifest.", + error + ); + } +} diff --git a/firefox-watch.js b/firefox-watch.js new file mode 100644 index 0000000..dbc56d0 --- /dev/null +++ b/firefox-watch.js @@ -0,0 +1,18 @@ +const nodemon = require('nodemon'); +const fs = require('fs'); + +const FILES_TO_WATCH = "ts,tsx,scss"; + +const watcher = nodemon({ + ext: FILES_TO_WATCH, + script: 'firefox-build-manifest.js', +}); + +watcher.on('quit', () => { + console.log('Stopped watching files.'); + process.exit(); +}); + +watcher.on('restart', (files) => { + console.log(`File changes detected: ${files.join(', ')}`); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..566351e --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Better YT Shorts + + +
+ + + diff --git a/manifest-firefox.json b/manifest-firefox.json deleted file mode 100644 index faa4e5f..0000000 --- a/manifest-firefox.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "Better YouTube Shorts", - "description": "Take back the controls on YouTube Shorts with playback, volume, timestamp controls and more.", - "version": "2.8.4", - "manifest_version": 2, - "permissions": ["storage"], - "browser_action": { - "default_popup": "popup.html" - }, - "background" : { - "scripts": ["background.js"] - }, - "content_scripts": [ - { - "matches": ["https://*.youtube.com/*"], - "js": ["content-script.js"], - "css": ["styles.css"] - } - ], - "icons": { - "16": "icons/byts16.png", - "32": "icons/byts32.png", - "48": "icons/byts48.png", - "128": "icons/byts128.png" - } -} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 9951ffa..ebaa3d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,26 +1,31 @@ { - "name": "Better YouTube Shorts", - "description": "Take back the controls on YouTube Shorts with playback, volume, timestamp controls and more.", - "version": "2.8.4", - "manifest_version": 3, - "permissions": ["storage"], - "action": { - "default_popup": "popup.html" - }, - "background" : { - "service_worker": "background.js" - }, - "content_scripts": [ - { - "matches": ["https://*.youtube.com/*"], - "js": ["content-script.js"], - "css": ["styles.css"] - } - ], - "icons": { - "16": "icons/byts16.png", - "32": "icons/byts32.png", - "48": "icons/byts48.png", - "128": "icons/byts128.png" + "manifest_version": 3, + "name": "__MSG_extName__", + "default_locale": "en", + "description": "__MSG_extDescription__", + "version": "3.0.0", + "action": { "default_popup": "index.html" }, + "permissions": [ "storage" ], + + "content_scripts": [ + { + "js": [ + "src/content.ts" + ], + "matches": [ + "https://*.youtube.com/*" + ] } -} \ No newline at end of file + ], + + "icons": { + "16": "src/assets/icons/bys-16.png", + "32": "src/assets/icons/bys-32.png", + "48": "src/assets/icons/bys-48.png", + "128": "src/assets/icons/bys-128.png" + }, + + "background": { + "service_worker": "src/background.ts" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..86bcf73 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2785 @@ +{ + "name": "better-yt-shorts", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "better-yt-shorts", + "version": "0.0.0", + "dependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-icons": "^4.10.1" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^1.0.14", + "@types/chrome": "^0.0.243", + "@types/firefox-webext-browser": "^111.0.1", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^1.3.0", + "concurrently": "^8.2.0", + "nodemon": "^3.0.1", + "sass": "^1.66.0", + "typescript": "^4.6.3", + "vite": "^2.9.15" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", + "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", + "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-1.0.14.tgz", + "integrity": "sha512-emOueVCqFRFmpcfT80Xsm4mfuFw9VSp5GY4eh5qeLDeiP81g0hddlobVQCo0pE2ZvNnWbyhLrXEYAaMAXjNL6A==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "cheerio": "^1.0.0-rc.10", + "connect-injector": "^0.4.4", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.26.0", + "picocolors": "^1.0.0", + "react-refresh": "^0.13.0", + "rollup": "^2.70.2" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@vitejs/plugin-react": ">=1.2.0" + }, + "peerDependencies": { + "vite": "^2.9.0" + } + }, + "node_modules/@crxjs/vite-plugin/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.243", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.243.tgz", + "integrity": "sha512-4PHv0kxxxpZFHWPBiJJ9TWH8kbx0567j1b2djnhpJjpiSGNI7UKkz7dSEECBtQ0B3N5nQTMwSB/5IopkWGAbEA==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "node_modules/@types/firefox-webext-browser": { + "version": "111.0.1", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-111.0.1.tgz", + "integrity": "sha512-mmHWdQTCT68X0hh0URrsIyWhJeFzZHaiprj6nni/CmsAmqYq27T0eZyu1ePeKJ/zuDD3wqtTzm5TwRFAso+oPw==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz", + "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.18.tgz", + "integrity": "sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001518", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz", + "integrity": "sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concurrently": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", + "integrity": "sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/connect-injector": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/connect-injector/-/connect-injector-0.4.4.tgz", + "integrity": "sha512-hdBG8nXop42y2gWCqOV8y1O3uVk4cIU+SoxLCPyCUKRImyPiScoNiSulpHjoktRU1BdI0UzoUdxUa87thrcmHw==", + "dev": true, + "dependencies": { + "debug": "^2.0.0", + "q": "^1.0.1", + "stream-buffers": "^0.2.3", + "uberproto": "^1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/connect-injector/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect-injector/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.482", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.482.tgz", + "integrity": "sha512-h+UqpfmEr1Qkk0zp7ej/jid7CXoq4m4QzW6wNTb0ELJ/BZCpA4wgUylBIMGCe621tnr4l5VmoHjdoSx2lbnNJA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/immutable": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.2.tgz", + "integrity": "sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", + "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-icons": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sass": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.0.tgz", + "integrity": "sha512-C3U+RgpAAlTXULZkWwzfysgbbBBo8IZudNAOJAVBLslFbIaZv4MBPkTqhuvpK4lqgdoFiWhnOGMoV4L1FyOBag==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/stream-buffers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-0.2.6.tgz", + "integrity": "sha512-ZRpmWyuCdg0TtNKk8bEqvm13oQvXMmzXDsfD4cBgcx5LouborvU5pm3JMkdTP3HcszyUI08AM1dHMXA5r2g6Sg==", + "dev": true, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uberproto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/uberproto/-/uberproto-1.2.0.tgz", + "integrity": "sha512-pGtPAQmLwh+R9w81WVHzui1FfedpQWQpiaIIfPCwhtsBez4q6DYbJFfyXPVHPUTNFnedAvNEnkoFiLuhXIR94w==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..98003c3 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "better-yt-shorts", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "concurrently --kill-others \"sass --watch src/css\" \"vite\"", + "build": "sass src/css && tsc && vite build", + "preview": "vite preview", + "dev:firefox": "node firefox-watch.js", + "build:firefox": "node firefox-build-manifest.js" + }, + "dependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-icons": "^4.10.1" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^1.0.14", + "@types/chrome": "^0.0.243", + "@types/firefox-webext-browser": "^111.0.1", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^1.3.0", + "concurrently": "^8.2.0", + "nodemon": "^3.0.1", + "sass": "^1.66.0", + "typescript": "^4.6.3", + "vite": "^2.9.15" + } +} diff --git a/popup.css b/popup.css deleted file mode 100644 index 8985b09..0000000 --- a/popup.css +++ /dev/null @@ -1,411 +0,0 @@ -:root { - --text-primary-color: #030303; - --text-secondary-color: #888; - --bg-primary-color: #fdfdfd; - --bg-primary-hover-color: #9d9ea1; - --bg-secondary-color: #f2f2f2; - --bg-secondary-hover-color: #e6e6e6; - --separation-line-color: #e5e5e5; - --yt-brand-color: #f00; - --suggested-action-color: #378de9; - --call-to-action-color: #3ea6ff; - --announcement-color: #50a5b5; -} - -[data-theme='dark'] { - --text-primary-color: #fff; - --text-secondary-color: #888; - --bg-primary-color: #0f0f0f; - --bg-primary-hover-color: #9d9ea1; - --bg-secondary-color: #272727; - --bg-secondary-hover-color: #3d3d3d; - --separation-line-color: #3f3f3f; - --suggested-action-color: #1d5fd4; - --call-to-action-color: #3978e6; - --announcement-color: #1e3055; -} - -:where( - h1, - h2, - h3, - h4, - h5, - h6, - table, - tr, - td, - th, - p, - li, - span, -){ - color: var(--text-primary-color); -} - -h3 { - font-size: 14px; - font-weight: bold; - text-align: center; -} - -body { - margin: 0; - font-family: 'Source Sans Pro', 'Roboto', 'Noto', 'Arial', sans-serif; - background: var(--bg-primary-color); - color: var(--text-primary-color); - scrollbar-gutter: stable; -} -.modal-content { - background: var(--bg-primary-color); - color: var(--text-primary-color); -} -tr:not(:first-child) { - background-color: var(--bg-secondary-color); - transition: all 0.3s ease; -} -tr:not(:first-child):hover { - background-color: var(--bg-secondary-hover-color); -} -#keybind-input { - background-color: var(--bg-primary-hover-color); - caret-color: var(--text-primary-color); - color: var(--text-primary-color); -} -.separation-line { - background-color: var(--separation-line-color); -} - -svg { - fill: var(--bg-primary-hover-color); - transition: all 0.5s ease; -} - -svg:hover { - cursor: pointer; - fill: var(--yt-brand-color) !important; -} - -.container { - margin: 8px; - width: 280px; -} - -#announcement { - background-color: var(--announcement-color); - padding: 5px 0px; - text-align: center; - margin-bottom: 10px; -} - -#announcement a { - display: inline; -} - -#announcement a:hover { - text-decoration: underline; -} - -#announcement #close-btn { - float: right; - font-size: 16px; - padding-right: 8px; -} - -#announcement #close-btn:hover { - text-decoration: none; - color: var(--yt-brand-color); -} - -.title-container { - display: flex; - justify-content: space-between; - flex-direction: row; -} - -.title { - font-size: 16px; - font-weight: bold; -} - -.version { - color: var(--bg-primary-hover-color); -} - -.separation-line { - height: 1px; - width: 100%; -} - -.textbox { - width: 100%; - font-size: 12px; - margin: 0; -} - -label { - font-size: 12px; -} - -#extra_options_skip_threshold { - margin-left: 4px; -} - -.textbox:focus { - outline: 0; - border-color: var(--call-to-action-color); -} - -.keybind-wrapper { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 10px; -} - -.keybind-span { - padding: 3px 7px; - background-color: var(--bg-secondary-hover-color); - border-radius: 3px; - box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.275); - cursor: default; -} - -.edit-svg { - fill: var(--bg-primary-hover-color); -} - -.edit-btn { - background: none; - outline: none; - border: none; -} - -table { - border-collapse: separate; - border-spacing: 0 2px; -} - -th { - font-size: 12px; - text-align: center; -} - -td { - font-size: 12px; - padding: 7px 5px; -} - -td:first-child { - padding-left: 6px; -} - -td:first-child, -th:first-child { - border-bottom-left-radius: 5px; - border-top-left-radius: 5px; - cursor: default; -} - -td:last-child, -th:last-child { - border-bottom-right-radius: 5px; - border-top-right-radius: 5px; -} - -.footer { - font-size: 10px; - text-align: center; - margin: 5px 0px; -} - -a { - color: var(--text-primary-color); - text-decoration: none; - display: flex; -} - -.btn-wrapper { - display: flex; - justify-content: center; - flex-direction: row; - gap: 8px; -} - -.btn { - margin: 5px 0px; - padding: 5px 10px; - background-color: var(--suggested-action-color); - border-radius: 3px; - box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.275); - cursor: pointer; - transition: all 0.3s ease; - font-size: 12px; -} - -.btn:hover { - background-color: var(--call-to-action-color); -} - -.modal { - display: none; - position: fixed; - z-index: 999; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); -} - -.modal-content { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - margin: auto; - padding: 0 8px 8px 8px; - width: 80%; - border-radius: 5px; - box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.7); -} - -.modal-header { - display: flex; - justify-content: space-between; - flex-direction: row; - margin-bottom: 4px; - margin-top: 2px; - align-items: center; -} - -.modal-title { - font-weight: bold; -} - -.close-btn { - color: var(--bg-primary-hover-color); - font-size: 20px; - font-weight: bold; - text-decoration: none; - cursor: pointer; -} - -.close-btn:hover, -.close-btn:focus { - color: var(--yt-brand-color); - text-decoration: none; - cursor: pointer; -} - -.input-wrapper { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - margin: 10px 0px; - gap: 10px; -} - -.prevent-selection { - -webkit-user-select: none; /* Safari */ - -ms-user-select: none; /* IE 10 and IE 11 */ - user-select: none; /* Standard syntax */ -} - -#keybind-input { - width: 30%; - height: 1.5rem; - text-align: center; -} - -#keybind-input:focus { - outline: 0; - outline: none !important; - box-shadow: 0 0 3px #00000020; -} - -.extra_options--row -{ - display: grid; - grid-template-columns: 5fr 1fr; - text-wrap: balance; - margin-bottom: 16px; - align-items: center; -} - -.extra_options--row:last-child() -{ - margin-bottom: 0; -} - - -.extra_options--row input[type="number"], -.extra_options--row input[type="text"] -{ - outline: none; - outline: 1px var( --bg-secondary-hover-color ) solid; - background: var( --bg-secondary-hover-color ); - color: var(--text-primary-color); - border:none; - padding: 0.25rem 1rem; - border-radius: 100vh; - width: 5rem; - height: fit-content; -} -.extra_options--row input[type="number"]:focus, -.extra_options--row input[type="text"]:focus -{ - outline: 2px var(--yt-brand-color) solid; -} - - -.extra_options--row input[type="checkbox"], -.extra_options--row input[type="radio"], -.extra_options--row input[type="range"] -{ - accent-color: var(--yt-brand-color); -} - -.extra_options--row input[type="checkbox"], -.extra_options--row input[type="radio"] -{ - height: 1rem; -} - -.extra_options--row input[type="number"]::-webkit-outer-spin-button, -.extra_options--row input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Firefox */ -.extra_options--row input[type="number"] { - -moz-appearance: textfield; -} - -/* width */ -::-webkit-scrollbar { - width: 0.5rem; - -} - -/* Track */ -::-webkit-scrollbar-track { - background: transparent; - height: 4rem; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: var(--bg-secondary-color); - height: 4rem; - border-radius: 1rem; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: var(--bg-secondary-hover-color); -} \ No newline at end of file diff --git a/popup.html b/popup.html deleted file mode 100644 index c4a42b6..0000000 --- a/popup.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - -
-
-
-
Better Youtube Shorts
-
v
-
- logo -
-
- -

Keybinds

- -
-

Extra Options

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
Reset keybindsGitHub
- - - - -
\ No newline at end of file diff --git a/popup.js b/popup.js deleted file mode 100644 index 60fe994..0000000 --- a/popup.js +++ /dev/null @@ -1,326 +0,0 @@ -const browserObj = (typeof browser === 'undefined') ? chrome : browser; -const version = browserObj.runtime.getManifest().version; -document.getElementById('version').textContent = version; - -const modal = document.getElementById("edit-modal"); -const resetBtn = document.querySelector(".reset-btn"); -const editBtnList = document.querySelectorAll(".edit-btn"); -const closeBtn = document.querySelector(".close-btn"); -const keybindInput = document.getElementById("keybind-input"); -const keybindTable = document.getElementById( "keybind-table" ) - -let modalTitleSpan = document.getElementById("modal-title-span"); -let invalidKeybinds = [ - 'Backspace', - 'Enter', - 'NumpadEnter', - 'Escape', - 'Tab', - 'Space', - 'PageUp', - 'PageDown', - 'ArrowUp', - 'ArrowDown', - 'F13', // printscreen - 'MetaLeft', // windows/command - 'MetaRight', - - 'ControlLeft', - 'ControlRight', - 'ShiftLeft', - 'ShiftRight', - 'AltLeft', - 'AltRight', -]; - -const defaultKeybinds = { - 'Seek Backward': 'ArrowLeft', - 'Seek Forward': 'ArrowRight', - 'Decrease Speed': 'KeyU', - 'Reset Speed': 'KeyI', - 'Increase Speed': 'KeyO', - 'Decrease Volume': 'Minus', - 'Increase Volume': 'Equal', - 'Toggle Mute': 'KeyM', - 'Next Frame': 'Comma', - 'Previous Frame': 'Period', - 'Next Short': 'KeyS', - 'Previous Short': 'KeyW', -}; -const defaultExtraOptions = { - skip_enabled: false, - skip_threshold: 500, - automatically_open_comments: false, - seek_amount: 5, -} - -// this is so that the bindings are always generated in the right order -const bindsOrder = [ - 'Seek Backward', - 'Seek Forward', - 'Decrease Speed', - 'Reset Speed', - 'Increase Speed', - 'Decrease Volume', - 'Increase Volume', - 'Toggle Mute', - 'Next Frame', - 'Previous Frame', - 'Next Short', - 'Previous Short', -] - -let currentKeybinds = Object.assign({}, defaultKeybinds); -let currentExtraOpts = Object.assign({}, defaultExtraOptions); -let currentKeybindArray = []; -let keybindState = ''; - -// Set user's prefers-color-scheme -if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - document.documentElement.setAttribute('data-theme', "dark"); -} -window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { - const newColorScheme = event.matches ? "dark" : "light"; - document.documentElement.setAttribute('data-theme', newColorScheme); -}); - -// Get keybinds from storage -browserObj.storage.local.get(['keybinds']) -.then((result) => { - let updatedkeybinds = result['keybinds']; - if (updatedkeybinds) { - // Set default keybinds if not exists - for (const [cmd, keybind] of Object.entries(defaultKeybinds)) { - if (!result.keybinds[cmd]) result.keybinds[cmd] = keybind; - } - - populateKeybindsTable( updatedkeybinds ) - currentKeybinds = updatedkeybinds; - } else { - resetKeybinds(); - } - // Keybind array for easier checking if keybind is already in use - currentKeybindArray = Object.values(currentKeybinds); -}); - -// Get extra options from storage -browserObj.storage.local.get(['extraopts']) - .then((result) => { - let updatedExtraOpts = result['extraopts']; - if ( updatedExtraOpts ) { - // Set default extraopts if not exists - for (const [ option, value ] of Object.entries( defaultExtraOptions )) { - if (result.extraopts[ option ]) continue - - result.extraopts[ option ] = value; - } - } - - if (result.extraopts) { - // set skip toggle - document.getElementById( "extra_options_skip_enabled" ).checked = result.extraopts.skip_enabled; - - // set skip threshold - document.getElementById( "extra_options_skip_threshold" ).value = result.extraopts.skip_threshold; - - // set automatically close comments - document.getElementById( "extra_options_auto_open_comments" ).checked = result.extraopts.automatically_open_comments; - - // set seek amount - document.getElementById( "extra_options_seek_amount" ).value = result.extraopts.seek_amount; - } - - }) - -// Open modal -for (let i = 0; i < editBtnList.length; i++) { - editBtnList[i].onclick = function() { - modal.style.display = "block"; - keybindInput.focus(); - keybindInput.select(); - modalTitleSpan.textContent = this.id; - keybindState = this.id; - } -} - -function resetKeybinds() { - currentKeybinds = Object.assign( {}, defaultKeybinds ); - currentKeybindArray = Object.values(currentKeybinds); - - populateKeybindsTable( defaultKeybinds ) - - browserObj.storage.local.set({ 'keybinds' : defaultKeybinds }); -} - -resetBtn.onclick = () => resetKeybinds(); - -// Close modal (x) -closeBtn.onclick = () => { - modal.style.display = "none"; -} -// Close modal (click outside) -window.onclick = (event) => { - if (event.target == modal) modal.style.display = "none"; -} - -keybindInput.addEventListener('keydown', (event) => { - event.preventDefault(); - var keybind = event.code; - var keybindAlt = event.key; // for legacy, will become obsolete - - console.log( `[BYS] :: Attempting to set key: ${keybind}` ) - - if ( invalidKeybinds.includes(keybind) ) { - keybindInput.value = ""; - closeBtn.click(); - alert("Invalid keybind: <<" + keybind + ">> is not allowed."); - return; - } - if ( currentKeybindArray.includes( keybind ) || currentKeybindArray.includes( keybindAlt ) ) { - keybindInput.value = ""; - closeBtn.click(); - alert("Invalid keybind: <<" + keybind + ">> is already in use."); - return; - } - - // document.getElementById(keybindState+'-span').textContent = keybind; - currentKeybinds[keybindState] = keybind; - currentKeybindArray = Object.values(currentKeybinds); - - browserObj.storage.local.set({ 'keybinds' : currentKeybinds }) - .then(() => { - keybindInput.value = ""; - closeBtn.click(); - }); - - populateKeybindsTable( currentKeybinds ) -}); - -document.getElementById( "extra_options_skip_enabled" ).addEventListener( "change", e => { - browserObj.storage.local.get(['extraopts']).then( result => { - if (result !== null && Object.keys(result).length !== 0) currentExtraOpts = result.extraopts; - currentExtraOpts.skip_enabled = e.target.checked; - browserObj.storage.local.set({ 'extraopts' : currentExtraOpts }); - }); -}); - -document.getElementById( "extra_options_skip_threshold" ).addEventListener( "input", e => { - browserObj.storage.local.get(['extraopts']).then( result => { - if (result !== null && Object.keys(result).length !== 0) currentExtraOpts = result.extraopts - currentExtraOpts.skip_threshold = e.target.valueAsNumber; - browserObj.storage.local.set({ 'extraopts' : currentExtraOpts }); - }); -}); - -document.getElementById( "extra_options_auto_open_comments" ).addEventListener( "change", e => { - browserObj.storage.local.get(['extraopts']).then( result => { - if (result !== null && Object.keys(result).length !== 0) currentExtraOpts = result.extraopts; - currentExtraOpts.automatically_open_comments = e.target.checked; - browserObj.storage.local.set({ 'extraopts' : currentExtraOpts }); - }); -}); - -document.getElementById( "extra_options_seek_amount" ).addEventListener( "input", e => { - browserObj.storage.local.get(['extraopts']).then( result => { - if (result !== null && Object.keys(result).length !== 0) currentExtraOpts = result.extraopts - currentExtraOpts.seek_amount = e.target.valueAsNumber; - browserObj.storage.local.set({ 'extraopts' : currentExtraOpts }); - }); -}); - -const EDIT_BUTTON_SVG_STRING = ` - - - - - - - - -` - -function populateKeybindsTable( keybinds ) -{ - keybindTable.innerHTML = ` - - Command - Key - - ` - - for ( const command of bindsOrder ) - { - const bind = keybinds[ command ] - const row = generateKeybindItem( command, bind ) - - if ( row === null ) continue - - keybindTable.appendChild( row ) - } -} - -/** - * Creates a table row for the keybinds table. - * @param {string} command - * @param {string} bind - * @returns Table Row Element - */ -function generateKeybindItem( command, bind ) -{ - const tr = document.createElement( "tr" ) - - // The command name - const td1 = document.createElement( "td" ) - td1.innerHTML = command - - // the key that is bound - const td2 = document.createElement( "td" ) - td2.innerHTML = ` -
- ${bind} -
- ` - - // the rebind icon - const td3 = document.createElement( "td" ) - td3.innerHTML = ` - - ` - td3.querySelector( `[id="${command}"]` ).addEventListener( "click", e => { - modal.style.display = "block"; - - keybindInput.focus(); - keybindInput.select(); - - modalTitleSpan.innerText = command; - keybindState = command; - - } ) - - tr.appendChild( td1 ) - tr.appendChild( td2 ) - tr.appendChild( td3 ) - - return tr -} - -browserObj.storage.local.get(['bys-announcement']) -.then((result) => { - let announcement = result['bys-announcement']; - if (announcement) { - if (parseInt(announcement) >= parseInt(document.getElementById("announcement").dataset.no)) { - document.getElementById("announcement").style.display = "none"; - } - } else { - browserObj.storage.local.set({ 'bys-announcement' : 0 }); - } -}); - -document.getElementById("close-btn").onclick = () => { - browserObj.storage.local.set({ 'bys-announcement' : parseInt(document.getElementById("announcement").dataset.no) }); - document.getElementById("announcement").style.display = "none"; -} - diff --git a/icons/byts128.png b/src/assets/icons/bys-128.png similarity index 100% rename from icons/byts128.png rename to src/assets/icons/bys-128.png diff --git a/icons/byts16.png b/src/assets/icons/bys-16.png similarity index 100% rename from icons/byts16.png rename to src/assets/icons/bys-16.png diff --git a/icons/byts32.png b/src/assets/icons/bys-32.png similarity index 100% rename from icons/byts32.png rename to src/assets/icons/bys-32.png diff --git a/icons/byts48.png b/src/assets/icons/bys-48.png similarity index 100% rename from icons/byts48.png rename to src/assets/icons/bys-48.png diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..953c594 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,3 @@ +import BROWSER from "./background/browser"; + +BROWSER.runtime.setUninstallURL( "https://github.com/ynshung/better-yt-shorts/blob/master/UNINSTALL.md" ); diff --git a/src/background/browser.ts b/src/background/browser.ts new file mode 100644 index 0000000..6ece709 --- /dev/null +++ b/src/background/browser.ts @@ -0,0 +1,2 @@ +const BROWSER = ( typeof browser === 'undefined' ) ? chrome : browser +export default BROWSER \ No newline at end of file diff --git a/src/background/i18n.ts b/src/background/i18n.ts new file mode 100644 index 0000000..316cf09 --- /dev/null +++ b/src/background/i18n.ts @@ -0,0 +1,3 @@ +import BROWSER from "./browser"; +const local = BROWSER.i18n.getMessage; +export default local; \ No newline at end of file diff --git a/src/components/Announcement.tsx b/src/components/Announcement.tsx new file mode 100644 index 0000000..756126f --- /dev/null +++ b/src/components/Announcement.tsx @@ -0,0 +1,34 @@ +import React, { useState } from "react"; +import local from '../background/i18n' + +export default function Announcement() { + const currAnnouncement = 1; + + const [showAnnouncement, setShowAnnouncement] = useState(true); + + const handleDismiss = (no: Number) => { + setShowAnnouncement(false); + localStorage.setItem("byt-announcement", no.toString()); + }; + + const announcementDismissed = + localStorage.getItem("byt-announcement"); + + if (!showAnnouncement || (announcementDismissed !== null && parseInt(announcementDismissed) >= currAnnouncement)) { + return null; + } + + return ( +
+ + {local("announcement")} + + handleDismiss(currAnnouncement)}> + × + +
+ ); +} diff --git a/src/components/EditButton.tsx b/src/components/EditButton.tsx new file mode 100644 index 0000000..f8eae38 --- /dev/null +++ b/src/components/EditButton.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { setKeybind, storage } from '../lib/declarations' +import { StringDictionary } from '../lib/definitions' + +import { MdCreate } from "react-icons/md" + +interface Props { + keybindsState: StringDictionary, + setKeybindsState: any, // ! give specific type + command: string, + setSelectedCommand: ( newState: string ) => void, + setIsModalOpen: ( newState: boolean ) => void +} +export default function EditButton( { keybindsState, setKeybindsState, command, setSelectedCommand, setIsModalOpen }: Props ) { + + function handleEditButtonClick( command: string ): void + { + setIsModalOpen( true ) + setSelectedCommand( command ) + } + + return ( + + ) +} diff --git a/src/components/EditModal.tsx b/src/components/EditModal.tsx new file mode 100644 index 0000000..80cbf5c --- /dev/null +++ b/src/components/EditModal.tsx @@ -0,0 +1,181 @@ +import { useEffect, useRef, useState } from 'react' +import Separator from './Separator' +import { DEFAULT_PRESSED_KEY, DISABLED_BIND_STRING, EXCLUDED_KEY_BINDS } from '../lib/declarations' +import { StringDictionary } from '../lib/definitions' +import { saveKeybindsToStorage } from '../lib/SaveToStorage' +import local from '../background/i18n' + + +interface Props +{ + selectedCommand: string, + isModalOpen: boolean, + setIsModalOpen: ( newState: boolean ) => void, + keybindsState: StringDictionary, + setKeybindsState: any, // ! - use proper type +} + +export default function EditModal( { selectedCommand, isModalOpen, setIsModalOpen, keybindsState, setKeybindsState }: Props ) { + + const [ inputSuccessString, setInputSuccessString ] = useState( null ) + const [ inputErrorString, setInputErrorString ] = useState( null ) + const [ pressedKey, setPressedKey ] = useState( DEFAULT_PRESSED_KEY ) // ? this is only to display this to user + + // this only exists to autofocus so inputs are immediate + const modalRef = useRef( null ) + useEffect( () => { + if ( !isModalOpen ) return + + const el = modalRef?.current as HTMLElement | null + if ( el ) + el.focus() + + }, [ isModalOpen ] ) + + useEffect( () => { + if ( + pressedKey === DEFAULT_PRESSED_KEY || + pressedKey === DISABLED_BIND_STRING + ) return + + setInputErrorString( null ) + setInputSuccessString( null ) + + if ( canUseKey() ) + setInputSuccessString( `${local("keyCanBeUsed", pressedKey)}` ) + + }, [ pressedKey ] ) + + let currentKey = "" + if ( keybindsState !== null ) + currentKey = keybindsState[ selectedCommand ] + + function handleCloseModal() + { + setIsModalOpen( false ) + + setInputErrorString( null ) + setInputSuccessString( null ) + setPressedKey( DEFAULT_PRESSED_KEY ) + } + + function handleSaveBind() + { + if ( canUseKey() ) + { + setKeybindsState( () => { + const newState = {...keybindsState} + newState[ selectedCommand ] = pressedKey + + saveKeybindsToStorage( newState ) + console.log( `[BYS] :: Bound key "${pressedKey}" to ${selectedCommand}` ) + + return newState + } ) + } + + setIsModalOpen( false ) + + setInputErrorString( null ) + setInputSuccessString( null ) + setPressedKey( DEFAULT_PRESSED_KEY ) + } + + function canUseKey() + { + if ( EXCLUDED_KEY_BINDS.includes( pressedKey ) ) + { + setInputErrorString( `${local("keyCannotBeUsed", pressedKey)}` ) + return false + } + if ( Object.values( keybindsState as Object ).includes( pressedKey ) ) + { + setInputErrorString( `${local("keyAlreadyInUse", pressedKey)}` ) + return false + } + + return true + } + + function showInputInfoString() + { + // either show the success, failure or + if ( inputSuccessString ) return
{`👍 ${inputSuccessString}`}
+ if ( inputErrorString ) return
{`❌ ${inputErrorString}`}
+ + return
{"\u00A0"}
+ } + + function handleDisableBind() + { + setKeybindsState( () => { + const newState = {...keybindsState} + newState[ selectedCommand ] = DISABLED_BIND_STRING + + saveKeybindsToStorage( newState ) + console.log( `[BYS] :: Disabled binding "${pressedKey}" that was bound to ${selectedCommand}` ) + + setInputSuccessString( `${local("disableKeybind", local(selectedCommand))}` ) + return newState + } ) + + setPressedKey( DISABLED_BIND_STRING ) + + } + + function getCurrentKeybindString() + { + if ( currentKey === DISABLED_BIND_STRING ) + return <>{local("keybindDisabled")} + + return <>{local("currentKeybind", currentKey)} + } + + function showConfirmButton() + { + if ( !inputSuccessString || inputSuccessString.includes("disabled") ) return <> + + return ( + + ) + } + + return ( + setPressedKey( e.code )} + > +
+ + + + {local("editBinding")} + {local(selectedCommand)} + + + × + + + + +
+ + {pressedKey} + {showInputInfoString()} +
+ + {showConfirmButton()} +
+
{local("notSupportKeyCombo")}
+
+
+
+ ) +} diff --git a/src/components/FeaturesPage.tsx b/src/components/FeaturesPage.tsx new file mode 100644 index 0000000..b0ae197 --- /dev/null +++ b/src/components/FeaturesPage.tsx @@ -0,0 +1,95 @@ +import { DEFAULT_FEATURES, FEATURES_ORDER, setFeature } from '../lib/declarations' +import { determineInputType } from '../lib/utils' +import { PolyDictionary } from '../lib/definitions' +import { disableAllFeatures, enableAllFeatures } from '../lib/ResetDefaults' +import { saveFeaturesToStorage } from '../lib/SaveToStorage' +import { useEffect, useState } from 'react' +import local from '../background/i18n' + +interface Props +{ + featuresState: PolyDictionary + setFeaturesState: ( features: ( previousState: PolyDictionary ) => PolyDictionary ) => void +} + +export default function FeaturesPage( { featuresState, setFeaturesState }: Props ) { + // this only exists to rerender on change + + const [ buttonEnablesAll, setButtonEnablesAll ] = useState(true) // ! - change + + useEffect( () => { + setButtonEnablesAll( () => { + if ( featuresState === null ) return true + const featuresStateLength = Object.keys( featuresState ).length + return Object.values( featuresState ).filter( ( feature: boolean ) => feature ).length !== featuresStateLength + } ) + }, [ featuresState ] ) + + function handleResetFeaturesClick() + { + setFeaturesState( () => { + let newState + + if ( buttonEnablesAll ) newState = enableAllFeatures() + else newState = disableAllFeatures() + + return newState + } ) + } + + function handleFeatureChange( e: any, feature: string ) + { + const value = e.target.checked + + setFeaturesState( () => { + const newState = setFeature( featuresState, feature, value ) + + saveFeaturesToStorage( newState ) + console.log( `[BYS] :: Set Feature "${feature}" to ${value}` ) + + return newState + } ) + } + + function populateFeaturesPage() + { + if ( featuresState === null ) return <> + + return FEATURES_ORDER.map( ( feature: string, i: number ) => { + const value = featuresState[ feature ] + + if ( featuresState === null ) return + + return ( +
+ + handleFeatureChange( e, feature ) } + checked = { value } + /> +
+ ) + } ) + } + + return ( + <> +

{local("toggleFeatures")}

+ +
+ { populateFeaturesPage() } +
+ +

{local("changesAffectNewShorts")}

+ +
+ +
+ + ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..80bc240 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { VERSION } from '../lib/declarations' +import bys_logo from "../assets/icons/bys-48.png" +import local from '../background/i18n' + +export default function Header() { + return ( +
+
+
{local("extName")}
+
v{VERSION}
+
+ Better Youtube Shorts logo +
+) +} diff --git a/src/components/KeybindsPage.tsx b/src/components/KeybindsPage.tsx new file mode 100644 index 0000000..b39492a --- /dev/null +++ b/src/components/KeybindsPage.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import { DEFAULT_KEYBINDS, DISABLED_BIND_STRING, KEYBINDS_ORDER } from '../lib/declarations' +import EditButton from './EditButton' +import { resetKeybinds } from '../lib/ResetDefaults' +import { StringDictionary } from '../lib/definitions' +import EditModal from './EditModal' +import local from '../background/i18n' + +interface Props +{ + setKeybindsState: ( keybinds: () => StringDictionary ) => void + keybindsState: StringDictionary +} + +export default function KeybindsPage( { setKeybindsState, keybindsState }: Props ) { + + const [ isModalOpen, setIsModalOpen ] = useState( false ) + const [ selectedCommand, setSelectedCommand ] = useState( "Seek Backward" ) + + const modalProps = { + selectedCommand, + isModalOpen, + setIsModalOpen, + keybindsState, + setKeybindsState + } + + function handleResetKeybinds() + { + setKeybindsState( () => { + resetKeybinds() + return DEFAULT_KEYBINDS + } ) + } + + function populateKeybindsPage() + { + if ( keybindsState === null ) return <> + + return KEYBINDS_ORDER.map( ( command: string ) => { + const bind = keybindsState[ command ] + const editButtonProps = { + keybindsState, setKeybindsState, command, setSelectedCommand, setIsModalOpen + } + + return ( + + {local(command)} + +
+ {bind} +
+ + + + + + ) + } ) + } + + return ( + <> +

{local("keybinds")}

+ + + + + + + + + + + + { populateKeybindsPage() } + +
{local("command")}{local("key")}
+ +
+ +
+ + ) +} diff --git a/src/components/OptionsPage.tsx b/src/components/OptionsPage.tsx new file mode 100644 index 0000000..825a0d2 --- /dev/null +++ b/src/components/OptionsPage.tsx @@ -0,0 +1,108 @@ +import { DEFAULT_OPTIONS, OPTIONS_ORDER, OPTION_DICTIONARY, setOption, storage } from '../lib/declarations' +import { determineInputType } from '../lib/utils' +import { PolyDictionary } from '../lib/definitions' +import { resetOptions } from '../lib/ResetDefaults' +import { saveOptionsToStorage, saveSettingsToStorage } from '../lib/SaveToStorage' +import local from '../background/i18n' + +interface Props +{ + optionsState: PolyDictionary + setOptionsState: ( options: ( previousState: PolyDictionary ) => PolyDictionary ) => void +} + +export default function OptionsPage( { optionsState, setOptionsState }: Props ) { + // this only exists to rerender on change + + function handleResetOptionsClick() + { + setOptionsState( () => { + resetOptions() + return DEFAULT_OPTIONS + } ) + } + + function handleOptionChange( e: any, option: string ) + { + if ( optionsState === null ) return + + const target = e.target as HTMLInputElement + let value: any = target.value + + // this may need changed depending on different input types + if ( target.type === "checkbox" ) value = target.checked + else if ( !isNaN( target.valueAsNumber ) ) value = target.valueAsNumber + + if ( value === null ) return console.warn( `[BYS] :: Option set handler tried to set option ${option} to null` ) + + // if value is number, handle min and max ranges + if ( [ "number", "range" ].includes( target.type ) ) + { + if ( target?.max !== "" ) + if ( +value > +target.max ) + value = +target.max + + if ( target?.min !== "" ) + if ( +value < +target.min ) + value = +target.min + } + + setOptionsState( () => { + const newState = setOption( optionsState, option, value ) + + saveOptionsToStorage( newState ) + console.log( `[BYS] :: Set Option "${option}" to ${value}` ) + + return newState + } ) + + } + + function populateOptionsPage() + { + if ( optionsState === null ) return <> + + return OPTIONS_ORDER.map( ( option: string, i: number ) => { + const value = optionsState[ option ] + + if ( OPTION_DICTIONARY === null ) return <> + + const type = determineInputType( value ) + + if ( optionsState === null ) return + + return ( +
+ + handleOptionChange( e, option ) } + /> +
+ ) + } ) + } + + return ( + <> +

{ local("extraOptions") }

+ +
+ { populateOptionsPage() } +
+ +
+ +
+ + ) +} diff --git a/src/components/PageIndicator.tsx b/src/components/PageIndicator.tsx new file mode 100644 index 0000000..2e8aaaa --- /dev/null +++ b/src/components/PageIndicator.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { PolyDictionary, PopupPageNameEnum, StrictPolyDictionary } from '../lib/definitions' +import { capitalise, getEnumWithString } from '../lib/utils' + +import { MdVideoSettings } from "react-icons/md"; +import { MdOutlineVideoSettings } from "react-icons/md"; + +import { MdKeyboard } from "react-icons/md"; +import { MdOutlineKeyboard } from "react-icons/md"; + +import { MdVisibilityOff } from "react-icons/md"; +import { MdOutlineVisibilityOff } from "react-icons/md"; + +import { saveSettingsToStorage } from '../lib/SaveToStorage'; +import local from '../background/i18n'; + +interface Props +{ + page: string + setCurrentPage: ( page: PopupPageNameEnum ) => void + isCurrentPage: boolean +} + +const ICONS = { + "OPTIONS": { + active: , + inactive: , + name: local("extraOptions"), + }, + "KEYBINDS": { + active: , + inactive: , + name: local("keybinds"), + }, + "FEATURES": { + active: , + inactive: , + name: local("toggleFeatures"), + } +} as StrictPolyDictionary + +export default function PageIndicator( { page, setCurrentPage, isCurrentPage }: Props ) { + + function handlePageIndicatorClick() + { + setCurrentPage( getEnumWithString( PopupPageNameEnum, page, 1 ) ) + } + + function getIndicatorIcon() + { + if ( !ICONS[ page ] ) return <> + return isCurrentPage ? ICONS[ page ].active : ICONS[ page ].inactive + } + + const classForIcon = ( isCurrentPage ) ? "--page-indicator-active" : "--page-indicator" + const pageTitle = ICONS[ page ].name; + + return ( + + ) +} diff --git a/src/components/PageIndicatorContainer.tsx b/src/components/PageIndicatorContainer.tsx new file mode 100644 index 0000000..4351901 --- /dev/null +++ b/src/components/PageIndicatorContainer.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import PageIndicator from './PageIndicator' +import { PolyDictionary, PopupPageNameEnum } from '../lib/definitions' +import { getEnumEntries } from '../lib/utils' +import { MdWeb } from 'react-icons/md' +import local from '../background/i18n' + +interface Props +{ + currentPage: PopupPageNameEnum + setCurrentPage: ( newPage: PopupPageNameEnum ) => void + setSettingsState: ( settings: PolyDictionary ) => void +} + +export default function PageIndicatorContainer( { currentPage, setCurrentPage, setSettingsState }: Props ) { + + function populatePageIndicators() + { + return getEnumEntries( PopupPageNameEnum ).map( ([name, page]) => { + if ( name === "UNKNOWN" ) return + + const isCurrentPage = currentPage === page + + const props = { page: name, setCurrentPage, isCurrentPage, setSettingsState } + + return + } ) + } + + return ( +
+ { populatePageIndicators() } + + + + + +
+ ) +} diff --git a/src/components/Popup.tsx b/src/components/Popup.tsx new file mode 100644 index 0000000..4597233 --- /dev/null +++ b/src/components/Popup.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from "react" +import "../css/popup.css" +import Header from "./Header" +import KeybindsPage from "./KeybindsPage" +import OptionsPage from "./OptionsPage" +import Separator from "./Separator" +import PageIndicatorContainer from "./PageIndicatorContainer" +import { BooleanDictionary, ChangedObjectStateEnum, PolyDictionary, PopupPageNameEnum, StringDictionary } from "../lib/definitions" +import { retrieveFeaturesFromStorage, retrieveKeybindsFromStorage, retrieveOptionsFromStorage } from "../lib/retrieveFromStorage" +import { pingChanges } from "../lib/chromeEmitters" +import { DEFAULT_FEATURES, DEFAULT_KEYBINDS, DEFAULT_OPTIONS, DEFAULT_SETTINGS } from "../lib/declarations" +import FeaturesPage from "./FeaturesPage" +import { saveSettingsToStorage } from "../lib/SaveToStorage" +import local from "../background/i18n" +import Announcement from "./Announcement" + +// todo - split this into its component parts + +function Popup() { + const [ keybindsState, setKeybindsState ] = useState( DEFAULT_KEYBINDS ) + const [ optionsState, setOptionsState ] = useState( DEFAULT_OPTIONS ) + const [ featuresState, setFeaturesState ] = useState( DEFAULT_FEATURES ) + const [ settingsState, setSettingsState ] = useState( DEFAULT_SETTINGS ) + + const [ currentPage, setCurrentPage ] = useState( PopupPageNameEnum.KEYBINDS ) + + const keybindsProp = { keybindsState, setKeybindsState } + const optionsProp = { optionsState, setOptionsState } + const featuresProp = { featuresState, setFeaturesState } + + const currentPageProps = { currentPage, setCurrentPage, setSettingsState } + + useEffect( () => { + // retrieve settings + retrieveOptionsFromStorage( setOptionsState ) + retrieveKeybindsFromStorage( setKeybindsState ) + retrieveFeaturesFromStorage( setFeaturesState ) + // retrieveFeaturesFromStorage( setSettingsState ) + + pingChanges( ChangedObjectStateEnum.KEYBINDS, keybindsState as Object ) + pingChanges( ChangedObjectStateEnum.OPTIONS, optionsState as Object ) + pingChanges( ChangedObjectStateEnum.FEATURES, featuresState as Object ) + // pingChanges( ChangedObjectStateEnum.SETTINGS, settingsState as Object ) + + }, [] ) + + function getCurrentPageContent() + { + if ( currentPage === PopupPageNameEnum.KEYBINDS ) return + if ( currentPage === PopupPageNameEnum.OPTIONS ) return + if ( currentPage === PopupPageNameEnum.FEATURES ) return + } + + return ( +
+ +
+
+ + + + + { getCurrentPageContent() } + +
+
+
+
Edit keybind:
+ × +
+
+
+ + +
{local("notSupportKeyCombo")}
+
+
+
+
+ +
+ +
+ +
+ ) +} + +export default Popup diff --git a/src/components/Separator.tsx b/src/components/Separator.tsx new file mode 100644 index 0000000..d6ca33e --- /dev/null +++ b/src/components/Separator.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function Separator() { + return ( +
+ ) +} diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..5f06d28 --- /dev/null +++ b/src/content.ts @@ -0,0 +1,121 @@ +import BROWSER from "./background/browser" +import { checkVolume } from "./lib/VolumeSlider" +import { DEFAULT_STATE } from "./lib/declarations" +import { StateObject } from "./lib/definitions" +import { getCurrentId, getLikeCount, getVideo } from "./lib/getters" +import { handleKeyEvent } from "./lib/handleKeyEvent" +import { retrieveFeaturesFromStorage, retrieveKeybindsFromStorage, retrieveOptionsFromStorage, retrieveSettingsFromStorage } from "./lib/retrieveFromStorage" +import { handleSkipShortsWithLowLikes, shouldSkipShort } from "./lib/SkipShortsWithLowLikes" + +// need this to ensure css is loaded in the dist +import "./css/content.css" +import { handleInjectionChecks } from "./lib/InjectionSuccess" +import { hasVideoEnded, isVideoPlaying } from "./lib/VideoState" +import { handleAutoplay, handleEnableAutoplay } from "./lib/Autoplay" +import { handleAutomaticallyOpenComments } from "./lib/AutomaticallyOpenComments" +import { handleProgressBarNotAppearing } from "./lib/ProgressBar" +import { handleHideShortsOverlay } from "./lib/HideShortsOverlay" + +/** + * content.ts + * + * Code in this file will be injected into the page itself. + * For popup code, see ./main.tsx + */ + +const state = new Proxy( DEFAULT_STATE, { + set( o: StateObject, prop: string, val: any ) + { + o[ prop ] = val + + const ytShorts = getVideo() + + // handle additional changes + if ( ytShorts !== null ) + { + switch ( prop ) + { + case "playbackRate": + ytShorts.playbackRate = val + break; + } + } + + return true + } +} ) + +var keybinds = null as any +var options = null as any +var settings = null as any +var features = null as any + +// todo - add "settings" to localstorage (merge autoplay + player volume into one) +// localStorage.getItem("yt-player-volume") !== null && JSON.parse(localStorage.getItem("yt-player-volume"))["data"]["volume"] + +retrieveKeybindsFromStorage( newBinds => { keybinds = newBinds } ) +retrieveOptionsFromStorage( newOpts => { options = newOpts } ) +retrieveSettingsFromStorage( newSettings => { settings = newSettings } ) +retrieveFeaturesFromStorage( newFeatures => { features = newFeatures } ) + +// todo - test this on firefox +BROWSER.runtime.onMessage.addListener( ( req, sender, sendResponse ) => { + if ( req?.keybinds ) + keybinds = req.keybinds + if ( req?.options ) + options = req.options + if ( req?.features ) + features = req.features + + resetIntervals() +} ) + +document.addEventListener( "keydown", e => handleKeyEvent( e, features, keybinds, settings, options, state ) ) + +var main_interval = setInterval( main, 100 ) +var volume_interval = setInterval( volumeIntervalCallback, 10 ) + +function main() { + if ( window.location.toString().indexOf("youtube.com/shorts/") < 0 ) return + + const ytShorts = getVideo() + var currentId = getCurrentId() + + if ( ytShorts === null ) return + if ( currentId === null ) return + + if ( state.topId < currentId ) + state.topId = currentId + + // video has to have been playing to skip. + // I'm undecided whether to use 0.5 or 1 for currentTime, as 1 isn't quite fast enough, but sometimes with 0.5, it skips a video above the minimum like count. + if ( isVideoPlaying() ) + { + handleSkipShortsWithLowLikes( state, options ) + handleAutomaticallyOpenComments( state, options ) // dev note: the implementation of this feature is a good starting point to figure out how to format your own + } + if ( hasVideoEnded() ) + { + handleAutoplay( settings, features[ "autoplay" ] ) + } + + handleProgressBarNotAppearing() + handleEnableAutoplay( settings, features[ "autoplay" ] ) + handleInjectionChecks( state, settings, features ) + handleHideShortsOverlay( options ) +} + +function volumeIntervalCallback() +{ + if ( window.location.toString().indexOf("youtube.com/shorts/") < 0 ) return + if ( getVideo() ) checkVolume( settings, features[ "volumeSlider" ] ) +} + +function resetIntervals() +{ + clearInterval( volume_interval ) + volume_interval = setInterval( volumeIntervalCallback, 10 ) + + clearInterval( main_interval ) + main_interval = setInterval( main, 100 ) +} \ No newline at end of file diff --git a/src/css/content.scss b/src/css/content.scss new file mode 100644 index 0000000..d885157 --- /dev/null +++ b/src/css/content.scss @@ -0,0 +1,134 @@ +@import url( "./globals.css" ); + +.betterYT { + font-size: 16px; + font-weight: bold; + text-align: center; +} + +.betterYT-renderer { + display: inline-block; +} + +.betterYT-button-shape { + display: flex; +} + +.betterYT-volume-slider{ + -webkit-appearance: slider-vertical; + position: absolute; + right: -40px; + top: 40px; + opacity: 0.4; + pointer-events: all !important; + accent-color: var( --yt-brand-color ); + cursor: pointer; +} + +@supports (-moz-appearance:none) { + .volume-slider{ + right: 16px; + } +} + +.volume-slider:hover{ + opacity: 1; +} + +.autoplay-switch { + position: relative; + display: block; + width: 48px; + height: 27px; + margin: 0 auto; +} + +/* Hide default HTML checkbox */ +.autoplay-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.autoplay-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #272727; + -webkit-transition: .4s; + transition: .4s; + border-radius: 27px; + + &:hover { + background-color: #3f3f3f; + } + + &:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; + } +} + +input:checked + .autoplay-slider { + background-color: #3f3f3f; +} + +input:checked + .autoplay-slider:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); +} + +@media screen and (max-width: 599px) { + .volumeSlider { + -webkit-appearance: slider-horizontal; + right: 44px; + top: 18px; + } + .autoplay-slider { + background-color: #FFFFFF1A; + } + input:checked + .autoplay-slider { + background-color: #FFFFFF2A; + } + .betterYT-auto { + padding-bottom: 16px; + } +} + +.betterYT-progress-bar{ + bottom: 0; + pointer-events: auto !important; +} + +.betterYT-progress-bar-hover{ + height: 10px !important; + cursor: pointer; +} + +.betterYT-timestamp-tooltip{ + position: absolute; + display: none; + background-color: #272727c4; + border-radius: 5px; + padding: 4px; + font-size: 11px; + color: white; + z-index: 50; +} + +.betterYT-hidden +{ + visibility: hidden !important; +} \ No newline at end of file diff --git a/src/css/globals.scss b/src/css/globals.scss new file mode 100644 index 0000000..b62f9d3 --- /dev/null +++ b/src/css/globals.scss @@ -0,0 +1,37 @@ +:root +{ + --text-primary-color: #030303; + --text-secondary-color: #888; + --bg-primary-color: #fff; + --bg-modal-color: #ffffff33; + --bg-primary-hover-color: #9d9ea1; + --bg-secondary-color: #f2f2f2; + --bg-secondary-hover-color: #e6e6e6; + --separation-line-color: #e5e5e5; + --yt-brand-color: #f00; + --suggested-action-color: #378de9; + --call-to-action-color: #3ea6ff; + --announcement-color: #50a5b5; + + --max-height: 600px; // of the chrome popup + + font-size: 16px; +} + +@media (prefers-color-scheme: dark) +{ + :root + { + --text-primary-color: #fff; + --text-secondary-color: #888; + --bg-primary-color: #0f0f0f; + --bg-modal-color: #0f0f0f66; + --bg-primary-hover-color: #9d9ea1; + --bg-secondary-color: #272727; + --bg-secondary-hover-color: #3d3d3d; + --separation-line-color: #3f3f3f; + --suggested-action-color: #1d5fd4; + --call-to-action-color: #3978e6; + --announcement-color: #1e3055; + } +} \ No newline at end of file diff --git a/src/css/popup.scss b/src/css/popup.scss new file mode 100644 index 0000000..9374637 --- /dev/null +++ b/src/css/popup.scss @@ -0,0 +1,772 @@ + +@import url( "./globals.css" ); + +:where( + h1, + h2, + h3, + h4, + h5, + h6, + table, + tr, + td, + th, + p, + li, + span +){ + color: var(--text-primary-color); +} + +* +{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Source Sans Pro', 'Roboto', 'Noto', 'Arial', sans-serif; + background: var(--bg-primary-color); + color: var(--text-primary-color); +} + +h3 { + font-size: 0.9rem; +} + +tr:not(:first-child) { + background-color: var(--bg-secondary-color); + transition: all 0.3s ease; +} +tr:not(:first-child):hover { + background-color: var(--bg-secondary-hover-color); +} + +// Separator +.separation-line { + background-color: var(--separation-line-color); + margin-top: 5px; + opacity: 0.9; + border: none; + outline: none; +} + +.popup_subheading +{ + text-align: center; + margin: 5px 0px +} + +.container { + width: 280px; +} + +.announcement { + background-color: var(--announcement-color); + padding: 5px 0px; + text-align: center; + + a { + display: inline; + } + + a:hover { + text-decoration: underline; + } + + .close-btn { + float: right; + font-size: 14px; + padding-right: 16px; + } + + .close-btn:hover { + text-decoration: none; + color: var(--yt-brand-color); + } +} + +.content-container { + padding: 0.75em 1em 0.5em 1em; +} + +.title-container { + display: flex; + justify-content: space-between; + flex-direction: row; +} + +.title { + font-size: 16px; + font-weight: bold; +} + +.version { + color: var(--bg-primary-hover-color); + font-size: 0.8rem; + + span { + color: var(--text-primary-color); + } +} + +.separation-line { + height: 1px; + width: 100%; +} + +.textbox { + width: 100%; + font-size: 12px; + margin: 0; + + &:focus { + outline: 0; + border-color: var(--call-to-action-color); + } +} + +label { + font-size: 12px; +} + +#label_input_skip_threshold { + margin-left: 4px; +} + +.keybind-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 10px; +} + +.keybind-span { + padding: 3px 7px; + background-color: var(--bg-secondary-hover-color); + border-radius: 3px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.275); + cursor: default; +} + +.edit-svg { + fill: var(--bg-primary-hover-color); +} + +.edit-btn { + background: none; + outline: none; + border: none; + cursor: pointer; + + svg { + fill: var(--bg-primary-hover-color); + height: 1rem; + width: 1rem; + transition: fill 200ms; + } + + &:hover svg + { + fill: var(--text-primary-color); + } +} + +table { + border-collapse: separate; + border-spacing: 0 2px; +} + +th { + font-size: 12px; + text-align: center; +} + +td { + font-size: 12px; + padding: 7px 5px; +} + +td:first-child { + padding-left: 6px; +} + +td:first-child, +th:first-child { + border-bottom-left-radius: 5px; + border-top-left-radius: 5px; + cursor: default; +} + +td:last-child, +th:last-child { + border-bottom-right-radius: 5px; + border-top-right-radius: 5px; +} + +.footer { + font-size: 10px; + text-align: center; + margin: 5px 0px; +} + +a { + color: var(--text-primary-color); + text-decoration: none; + display: flex; +} + +.btn-wrapper { + display: flex; + justify-content: center; + flex-direction: row; + gap: 8px; +} + +.modal { + display: none; + position: fixed; + z-index: 999; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + margin: auto; + padding: 0 8px 8px 8px; + width: 80%; + border-radius: 5px; + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.7); +} + +.close-btn { + color: var(--bg-primary-hover-color); + font-size: 20px; + font-weight: bold; + text-decoration: none; + cursor: pointer; + transition: color 200ms; + + &:hover, + &:focus { + color: var(--yt-brand-color); + text-decoration: none; + cursor: pointer; + } +} + +.input-wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + margin: 10px 0px; + gap: 10px; +} + +.prevent-selection { + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; /* Standard syntax */ +} + +#keybind-input { + width: 30%; + height: 1.5rem; + text-align: center; + + &:focus { + outline: 0; + outline: none !important; + box-shadow: 0 0 3px #00000020; + } +} + +.label_input--row +{ + display: grid; + grid-template-columns: 5fr 1fr; + text-wrap: balance; + margin-bottom: 16px; + align-items: center; + justify-content: center; + + [ type="number" ], + [ type="text" ] { + height: 1.75rem; + } + + [ type="checkbox" ] + { + height: 1rem; + cursor: pointer; + } + + &:last-child { + margin-bottom: 0; + } + + input[type="number"], + input[type="text"] + { + outline: none; + border: 1px var( --bg-secondary-hover-color ) solid; + background: var( --bg-secondary-hover-color ); + color: var(--text-primary-color); + padding: 0 1rem; + border-radius: 100vh; + width: 5rem; + + &:focus + { + border: 1px var(--yt-brand-color) solid; + } + } + + input[type="checkbox"], + input[type="radio"], + input[type="range"] + { + accent-color: var(--yt-brand-color); + } + + input[type="number"] { + -moz-appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } +} + +// alternate colors for clarity +// .label_input--row:nth-of-type(even) +// { +// color: var(--text-secondary-color) +// } + +/* width */ +::-webkit-scrollbar { + width: 0.5rem; + +} + +/* Track */ +::-webkit-scrollbar-track { + background: transparent; + height: 4rem; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: var(--bg-secondary-color); + height: 4rem; + border-radius: 1rem; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: var(--bg-secondary-hover-color); +} + + +.--page-indicator-container +{ + display: flex; + gap: 1rem; + width: fit-content; + margin: 0 auto; + + button, input[type="submit"], input[type="reset"] { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + } + + .--page-indicator, + .--page-indicator-active + { + border-radius: 5rem; + height: 1.5rem; + width: 1.5rem; + padding: 1rem; + + display: flex; + align-items: center; + justify-content: center; + + transition: + background 200ms, + stroke 200ms + ; + + &:hover + { + background: var(--bg-secondary-hover-color); + } + } + + .--page-indicator svg, + .--page-indicator-active svg + { + height: 1.5rem; + width: 1.5rem; + } + + .--page-indicator svg + { + fill: var(--text-secondary-color); + } + .--page-indicator-active svg + { + fill: var(--text-primary-color); + } +} + +.--flex-button-container +{ + display: flex; + width: fit-content; + align-items: center; + justify-content: center; + gap: 1rem; + max-width: 100%; + margin: 0 auto; + padding-top: 4px; + + button, input[type="submit"], input[type="reset"] { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + } + + .--flex-button + { + margin: 5px 0px; + padding: 5px 10px; + + background-color: transparent; + color: var(--text-primary-color); + outline: 1px var( --text-primary-color ) solid; + + border-radius: 3px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.275); + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; + } + .--flex-button:hover { + background-color: var(--text-primary-color); + color: var(--bg-primary-color); + } + + .--flex-button.warn { + color: var(--yt-brand-color ); + outline: 1px var( --yt-brand-color ) solid; + } + .--flex-button.warn:hover { + color: var(--text-primary-color ); + background-color: var(--yt-brand-color); + } + .--flex-button.good { + color: var(--call-to-action-color); + outline: 1px var(--call-to-action-color) solid; + } + .--flex-button.good:hover { + color: var(--text-primary-color ); + background-color: var(--call-to-action-color) + } + + +} + +// this is for the github link +.--global-footer +{ + display: flex; + width: fit-content; + max-width: 100%; + margin: 0 auto; + + .--global-footer-link + { + color: var(--text-secondary-color); + } +} + +.--modal +{ + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + align-items: center; + justify-content: center; + + background: var( --bg-modal-color ); + + .--modal-header { + display: flex; + align-items: center; + justify-content: space-between; + + .--modal-header-text + { + font-style: italic; + color: var(--text-secondary-color); + display: block; + + .--modal-command + { + font-style: normal; + display: block; + font-weight: bold; + } + + } + + } + + .--modal-content { + background: var(--bg-primary-color); + color: var(--text-primary-color); + padding: 0.5rem; + border-radius: 0.5rem; + width: 80%; + box-shadow: 0 0 0.5rem 0.5rem var(--bg-modal-color); + } + + .--modal-input { + + outline: none; + border: none; + border-radius: 1rem; + font-size: 1.5rem; + width: 1rem; + height: 1rem; + padding: 1rem 2rem; + + background-color: var(--bg-secondary-color); + caret-color: var(--text-primary-color); + color: var(--text-primary-color); + + transition: background 200ms; + + &:hover + { + background-color: var(--bg-primary-hover-color); + } + &:focus + { + background-color: var(--bg-primary-hover-color); + } + } + .--modal-input-error, + .--modal-input-success + { + display: flex; + align-items: center; + justify-content: center; + text-wrap: balance; + text-align: center; + } + .--modal-input-error + { + color: var(--yt-brand-color) + } + .--modal-input-success + { + color: var(--call-to-action-color) + } + + .--modal-label + { + color: var(--text-secondary-color); + font-style: italic; + + span + { + font-style: normal; + font-weight: bold; + } + } + +} + +.key-combo-warning +{ + color: var(--text-secondary-color); +} + +.betterYT { + font-size: 16px; + font-weight: bold; + text-align: center; +} + +.betterYT-renderer { + display: inline-block; +} + +.betterYT-button-shape { + display: flex; +} + +.betterYT-volume-slider{ + -webkit-appearance: slider-vertical; + position: absolute; + right: -40px; + top: 40px; + opacity: 0.4; + pointer-events: all !important; + cursor: pointer; +} + +@supports (-moz-appearance:none) { + .volume-slider{ + right: 16px; + } +} + +.volume-slider:hover{ + opacity: 1; +} + +.autoplay-switch { + position: relative; + display: inline-block; + width: 48px; + height: 27px; +} + +/* Hide default HTML checkbox */ +.autoplay-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.autoplay-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #272727; + -webkit-transition: .4s; + transition: .4s; + border-radius: 27px; + + &:hover { + background-color: #3f3f3f; + } + + &:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; + } +} + + + +input:checked + .autoplay-slider { + background-color: #3f3f3f; +} + +input:checked + .autoplay-slider:before { + -webkit-transform: translateX(21px); + -ms-transform: translateX(21px); + transform: translateX(21px); +} + +@media screen and (max-width: 599px) { + .volumeSlider { + -webkit-appearance: slider-horizontal; + right: 44px; + top: 18px; + } + .autoplay-slider { + background-color: #FFFFFF1A; + } + input:checked + .autoplay-slider { + background-color: #FFFFFF2A; + } + .betterYT-auto { + padding-bottom: 16px; + } +} + +.betterYT-progress-bar{ + bottom: 0; + pointer-events: auto !important; +} + +.betterYT-progress-bar-hover{ + height: 10px !important; + cursor: pointer; +} + +.betterYT-timestamp-tooltip{ + position: absolute; + display: none; + background-color: #272727c4; + border-radius: 5px; + padding: 4px; + font-size: 11px; + color: white; + z-index: 50; +} + +.page-warning +{ + color: var(--text-secondary-color); + text-align: center; + padding-top: 12px; + font-size: .75rem; + word-wrap: balance; +} + +#page-indicator { + position: sticky; + bottom: 0; + padding: 4px; + background: var(--bg-secondary-color); + z-index: 10; +} + +#extra_features, #extra_options { + padding: 4px 0; +} diff --git a/src/lib/ActionElement.ts b/src/lib/ActionElement.ts new file mode 100644 index 0000000..93092c9 --- /dev/null +++ b/src/lib/ActionElement.ts @@ -0,0 +1,131 @@ +import { createAutoplaySwitch } from "./Autoplay" +import { setPlaybackRate, setTimer } from "./PlaybackRate" +import { CYCLABLE_PLAYBACK_RATES } from "./declarations" +import { getActionElement, getCurrentId, getTitle, getVideo } from "./getters" +import { render, wheel } from "./utils" + +export function populateActionElement( state: any, settings: any, features: any ) // ! use proper types +{ + const id = getCurrentId() + const actionElement = getActionElement() + const ytShorts = getVideo() + + if ( !actionElement ) return + if ( !ytShorts ) return + + // adsu - idk how any of this works so im just going to leave it be + const betterYTContainer = document.createElement("div") + betterYTContainer.id = "betterYT-container" + betterYTContainer.setAttribute("class", "button-container style-scope ytd-reel-player-overlay-renderer") + + const ytdButtonRenderer = document.createElement("div") + ytdButtonRenderer.setAttribute("class", "betterYT-renderer style-scope ytd-reel-player-overlay-renderer") + + const ytButtonShape = document.createElement("div") + ytButtonShape.setAttribute("class", "betterYT-button-shape") + + const ytLabel = document.createElement("label") + ytLabel.setAttribute("class", "yt-spec-button-shape-with-label") + + const ytButton = document.createElement("button") + ytButton.setAttribute("class", "yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-l yt-spec-button-shape-next--icon-button ") + // Playback Rate + var para0 = document.createElement("p") + para0.classList.add("betterYT") + para0.id = `ytPlayback${id}` + + ytButton.style.display = ( features[ "playbackRate" ] === false ) ? "none" : "" // need this to check injection, so wont fully disable + + // Video title links to the main YT watch page + const videoId = document.location.pathname?.match(/\/shorts\/(.+)$/) + if (!!videoId) { + const ytTitle = getTitle() + const ytTitleLink = document.createElement('a'); + ytTitleLink.href = `https://youtube.com/watch?v=${videoId[1]}` + ytTitleLink.style.color = 'inherit' + ytTitleLink.style.textDecoration = 'none' + ytTitle.parentNode?.insertBefore(ytTitleLink, ytTitle) + ytTitleLink.appendChild(ytTitle) + } + + // Timer + const ytTimer = document.createElement("div") + ytTimer.classList.add("yt-spec-button-shape-with-label__label") + var span1 = document.createElement("span") + span1.setAttribute("class", "yt-core-attributed-string yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--text-alignment-center yt-core-attributed-string--word-wrapping") + span1.id = `ytTimer${getCurrentId()}` + span1.setAttribute("role", "text") + ytTimer.style.display = features[ "timer" ] ? "" : "none !important" // need this to check injection, so wont fully disable + ytTimer.appendChild(span1) + + + + // Match YT's HTML structure + ytButton.appendChild(para0) + ytLabel.appendChild(ytButton) + ytLabel.appendChild(ytTimer) + ytButtonShape.appendChild(ytLabel) + ytdButtonRenderer.appendChild(ytButtonShape) + betterYTContainer.appendChild(ytdButtonRenderer) + + actionElement.insertBefore(betterYTContainer, actionElement.children[1]) + + createAutoplaySwitch( settings, features[ "autoplay" ] ) + + if ( features[ "playbackRate" ] ) + ytShorts.playbackRate = state.playbackRate + + setPlaybackRate( state ) + // injectedSuccess = setTimer( currTime || 0, Math.round(ytShorts.duration || 0)) + + betterYTContainer.addEventListener("click", () => { + const index = CYCLABLE_PLAYBACK_RATES.indexOf( ytShorts.playbackRate ) + + // cycle through defined rates + if ( index !== -1 ) + { + let newIndex = ( index + 1 ) % CYCLABLE_PLAYBACK_RATES.length + state.playbackRate = CYCLABLE_PLAYBACK_RATES[newIndex] + return + } + + // note that state is a proxy, and will automatically update the video's settings + state.playbackRate = 1 + }) + + if ( features[ "timer" ] ) + wheel( + ytButton, + () => { + // speedup + const video = getVideo() + if ( video === null ) return + + if (video.playbackRate < 16) video.playbackRate += 0.25 + state.playbackRate = video.playbackRate + + }, + () => { + // speeddown + const video = getVideo() + if ( video === null ) return + + if (video.playbackRate > 0.25) video.playbackRate -= 0.25 + state.playbackRate = video.playbackRate + } + ) + + wheel( + ytTimer, + () => { + // forward + const video = getVideo() + if ( video !== null ) video.currentTime += 1 + }, + () => { + // backward + const video = getVideo() + if ( video !== null ) video.currentTime -= 1 + } + ) +} diff --git a/src/lib/AutomaticallyOpenComments.ts b/src/lib/AutomaticallyOpenComments.ts new file mode 100644 index 0000000..8000c7f --- /dev/null +++ b/src/lib/AutomaticallyOpenComments.ts @@ -0,0 +1,38 @@ +import { StateObject } from "./definitions"; +import { getCommentsButton, getCurrentId } from "./getters"; + + +export function handleAutomaticallyOpenComments( state: StateObject, options: any ) +{ + if( shouldOpenComments( state, options ) ) + openComments() +} + +function openComments() +{ + getCommentsButton()?.click() +} + +function shouldOpenComments( state: StateObject, options: any ) +{ + let currentId = getCurrentId() + + if ( options === null ) return false + if ( !options.automaticallyOpenComments ) return false + if ( currentId === state.skippedId ) return false // prevents opening comments on skipped shorts + if ( currentId === state.openedCommentsId ) return false // allow closing of comments + + // change here to prevent bugs with closing comments on previous shorts + state.openedCommentsId = currentId + + if ( isCommentsPanelOpen() ) return false + + return true +} + +function isCommentsPanelOpen() +{ + // return true if the selector finds an open panel + // if panel is unfound, then the short either hasnt loaded, or the panel is not open + return document.querySelector( `[ id="${getCurrentId()}" ] #watch-while-engagement-panel [ visibility="ENGAGEMENT_PANEL_VISIBILITY_EXPANDED" ]` ) ?? false +} \ No newline at end of file diff --git a/src/lib/Autoplay.ts b/src/lib/Autoplay.ts new file mode 100644 index 0000000..f429047 --- /dev/null +++ b/src/lib/Autoplay.ts @@ -0,0 +1,53 @@ +import local from "../background/i18n" +import { saveSettingsToStorage } from "./SaveToStorage" +import { skipShort } from "./VideoState" +import { getActionElement, getCurrentId, getVideo } from "./getters" +import { render } from "./utils" + +export function handleAutoplay( settings: any, enabled: boolean ) +{ + if ( !enabled ) return + if ( !settings.autoplay ) return + skipShort() +} + +export function handleEnableAutoplay( settings: any, enabled: boolean ) +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return false + + if ( settings.autoplay ) ytShorts.loop = !enabled + else ytShorts.loop = true +} + +export function createAutoplaySwitch( settings: any, enabled: boolean ) +{ + if ( !enabled ) return + + const actionElement = getActionElement() + + // Autoplay Switch + const autoplaySwitch = render(` +
+ +
+ ${local("autoplay")} +
+
+ `) + + actionElement.insertBefore( autoplaySwitch, actionElement.children[1] ) + + document.getElementById( `autoplay-checkbox${ getCurrentId() }` ) + ?.addEventListener( "change", ( e: any ) => { + settings.autoplay = e.target.checked + + saveSettingsToStorage( settings ) + }) +} \ No newline at end of file diff --git a/src/lib/HideShortsOverlay.ts b/src/lib/HideShortsOverlay.ts new file mode 100644 index 0000000..c4c01fa --- /dev/null +++ b/src/lib/HideShortsOverlay.ts @@ -0,0 +1,18 @@ +import { saveOptionsToStorage } from "./SaveToStorage" +import { getOverlay } from "./getters" + +export function handleHideShortsOverlay( options: any ) +{ + const overlay = getOverlay() + if ( overlay === null ) return + + if ( options.hideShortsOverlay ) + overlay.classList.add( "betterYT-hidden" ) + else + overlay.classList.remove( "betterYT-hidden" ) +} + +export function setHideShortsOverlay( newValue: boolean, options: any ) +{ + saveOptionsToStorage( { ...options, hideShortsOverlay: newValue } ) +} \ No newline at end of file diff --git a/src/lib/InjectionSuccess.ts b/src/lib/InjectionSuccess.ts new file mode 100644 index 0000000..8fed335 --- /dev/null +++ b/src/lib/InjectionSuccess.ts @@ -0,0 +1,51 @@ +import { populateActionElement } from "./ActionElement"; +import { setPlaybackRate, setTimer } from "./PlaybackRate"; +import { modifyProgressBar } from "./ProgressBar"; +import { setVolumeSlider } from "./VolumeSlider"; +import { BooleanDictionary, StateObject } from "./definitions"; +import { getCurrentId, getVideo } from "./getters"; + +export function registerInjection( state: StateObject ) +{ + state.injectedItems.add( getCurrentId() ) +} + +export function injectItems( state: StateObject, settings: any, features: any ) +{ + state.lastTime = -1 + + populateActionElement( state, settings, features ) + modifyProgressBar( features[ "progressBar" ] ) + setVolumeSlider( state, settings, features[ "volumeSlider" ] ) + + registerInjection( state ) +} + +export function injectionWasRegistered( state: StateObject ) +{ + + return state.injectedItems.has( getCurrentId() ) +} + +export function checkForInjectionSuccess( state: StateObject, features: any ) +{ + // If failed, retry injection during next interval + if ( !setTimer( state, features[ "timer" ] ) ) state.injectedItems.delete( getCurrentId() ) + + state.lastTime = state.currTime +} + +export function handleInjectionChecks( state: StateObject, settings: any, features: any ) +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return + + state.currTime = Math.round( ytShorts.currentTime ) + + if ( !injectionWasRegistered( state ) ) return injectItems( state, settings, features ) + + if ( state.currTime !== state.lastTime ) + checkForInjectionSuccess( state, features ) + + setPlaybackRate( state ) +} \ No newline at end of file diff --git a/src/lib/PlaybackRate.ts b/src/lib/PlaybackRate.ts new file mode 100644 index 0000000..e3180ca --- /dev/null +++ b/src/lib/PlaybackRate.ts @@ -0,0 +1,40 @@ +import { StateObject } from "./definitions" +import { getCurrentId, getPlaybackElement, getVideo } from "./getters" + +export function setPlaybackRate( state: any ) +{ + const id = getCurrentId() + const playBackElement = getPlaybackElement() as HTMLElement + + if ( playBackElement === null ) return false + + playBackElement.innerText = `${state.playbackRate}x` + + return true +} + +export function setTimer( state: StateObject, timerEnabled: boolean ) +{ + const id = getCurrentId() + const ytShorts = getVideo() + if ( ytShorts === null ) return + + if ( document.getElementById(`ytTimer${id}`) === null ) return false + + const timerElement = document.getElementById( `ytTimer${id}` ) as HTMLElement + + if ( !timerEnabled && timerElement ) return true + + timerElement.innerText = `${state.currTime}/${Math.round( ytShorts.duration )}s` + + return true +} + +export function createPlaybackElement( state: StateObject, enabled: boolean ) +{ + // enabled is handled differently here because this element is used to test injection + // style="display: ${enabled ? "block" : "none"};" + + + +} \ No newline at end of file diff --git a/src/lib/ProgressBar.ts b/src/lib/ProgressBar.ts new file mode 100644 index 0000000..2ef7d00 --- /dev/null +++ b/src/lib/ProgressBar.ts @@ -0,0 +1,104 @@ +import { getOverlayElement, getProgressBarList, getVideo } from "./getters" +import { render } from "./utils" + +export function modifyProgressBar( enabled: boolean ) +{ + if ( !enabled ) return + + const overlayElement = getOverlayElement() + const ytShorts = getVideo() + + if ( !overlayElement ) return + if ( ytShorts === null ) return + + //[id="0"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #overlay + let progressBar = getProgressBarList() as HTMLElement // ? the progressbar itself + let pbBackground = progressBar.children[0] as HTMLElement // ? the grey background of the bar + let pbForeground = progressBar.children[1] as HTMLElement // ? The red part of the progress bar + + const subBox = overlayElement.children[1] as HTMLElement + const tooltip = render( `
` ) as HTMLElement + + progressBar.appendChild( tooltip ); + + // Styling to ensure rest of bottom overlay (shorts title/sub button) stay in place + subBox.style.marginBottom = "-7px" // ? changed to 1 from 0 bc i think its not targeting the right thing + progressBar.style.height = "10px" + progressBar.style.paddingTop = "2px" // Slight padding to increase hover box + + progressBar.classList.add('betterYT-progress-bar') + pbBackground.classList.add('betterYT-progress-bar') + pbForeground.classList.add('betterYT-progress-bar') + + addListeners({ + progressBar, + pbBackground, + pbForeground, + tooltip + }) +} + +interface ListenerProps { + progressBar: HTMLElement, + pbBackground: HTMLElement, + pbForeground: HTMLElement, + tooltip: HTMLElement, +} +function addListeners( { progressBar, pbBackground, pbForeground, tooltip }: ListenerProps ) +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return + + + progressBar.addEventListener("mouseover", () => { + pbBackground.classList.add('betterYT-progress-bar-hover') + pbForeground.classList.add('betterYT-progress-bar-hover') + }) + progressBar.addEventListener("mousemove", ( e: MouseEvent ) => { + + let x = e.clientX - ytShorts.getBoundingClientRect().left + // Deal with slight inaccuracies + if (x < 0) x = 0 + if (x > ytShorts.clientWidth) x = ytShorts.clientWidth + // Get timestamp and round to nearest 0.1 + let timestamp = ( (x / ytShorts.clientWidth) * ytShorts.duration ).toFixed(1) + tooltip.textContent = `${timestamp}s` + + // Ensure tooltip stays visible at edges of client + if ((x - (tooltip.offsetWidth / 2)) > (ytShorts.clientWidth - tooltip.offsetWidth)) + { + tooltip.style.left = `${ytShorts.clientWidth - tooltip.offsetWidth}px` + } + else if ((x - (tooltip.offsetWidth / 2)) <= 0) + { + tooltip.style.left = "0px" + } + else + { + tooltip.style.left = `${x - (tooltip.offsetWidth / 2)}px` + } + + tooltip.style.top = "-20px" + tooltip.style.display = 'block' + }) + + + progressBar.addEventListener("mouseout", () => { + pbBackground.classList.remove('betterYT-progress-bar-hover') + pbForeground.classList.remove('betterYT-progress-bar-hover') + tooltip.style.display = 'none' + }) + + progressBar.addEventListener("click", (event) => { + let x = (event).clientX - ytShorts.getBoundingClientRect().left + if (x < 0) x = 0 + if (x > ytShorts.clientWidth) x = ytShorts.clientWidth + ytShorts.currentTime = (x / ytShorts.clientWidth) * ytShorts.duration + }) +} + +export function handleProgressBarNotAppearing() +{ + // ? to show on shorter shorts + getProgressBarList()?.removeAttribute( "hidden" ) +} \ No newline at end of file diff --git a/src/lib/ResetDefaults.ts b/src/lib/ResetDefaults.ts new file mode 100644 index 0000000..82bdb7d --- /dev/null +++ b/src/lib/ResetDefaults.ts @@ -0,0 +1,61 @@ +import { pingChanges } from "./chromeEmitters" +import { DEFAULT_FEATURES, DEFAULT_KEYBINDS, DEFAULT_OPTIONS, storage } from "./declarations" +import { BooleanDictionary, ChangedObjectStateEnum } from "./definitions" + +/** + * Resets keybinds to their factory values in local and storage as well as the live binds + */ +export function resetKeybinds() +{ + storage.set( { "keybinds" : DEFAULT_KEYBINDS } ) + localStorage.setItem( "yt-keybinds", JSON.stringify( DEFAULT_KEYBINDS ) ) + console.log( `[BYS] :: Reset Keybinds to Defaults!` ) + + pingChanges( ChangedObjectStateEnum.KEYBINDS, DEFAULT_KEYBINDS ) +} + +/** + * Resets options to their factory values in local and storage as well as the live binds + */ +export function resetOptions() +{ + storage.set( { "extraopts" : DEFAULT_OPTIONS } ) + localStorage.setItem( "yt-extraopts", JSON.stringify( DEFAULT_OPTIONS ) ) + console.log( `[BYS] :: Reset Options to Defaults!` ) + + pingChanges( ChangedObjectStateEnum.OPTIONS, DEFAULT_OPTIONS ) +} + +/** + * Features are all set to true (this is essentially a regular reset) + */ +export function enableAllFeatures() +{ + storage.set( { "features" : DEFAULT_FEATURES } ) + localStorage.setItem( "yt-features", JSON.stringify( DEFAULT_FEATURES ) ) + console.log( `[BYS] :: Enabled all features!` ) + + pingChanges( ChangedObjectStateEnum.FEATURES, DEFAULT_FEATURES ) + + return DEFAULT_FEATURES +} +/** + * Features are all set to false + */ +export function disableAllFeatures() +{ + const newState = {...DEFAULT_FEATURES} as BooleanDictionary + if ( newState === null ) return null + + Object.entries( newState ).map( ([ feature, value ]) => { + newState[ feature ] = false + } ) + + storage.set( { "features" : newState } ) + localStorage.setItem( "yt-features", JSON.stringify( newState ) ) + console.log( `[BYS] :: Disabled all features!` ) + + pingChanges( ChangedObjectStateEnum.FEATURES, newState ) + + return newState +} \ No newline at end of file diff --git a/src/lib/SaveToStorage.ts b/src/lib/SaveToStorage.ts new file mode 100644 index 0000000..676e70e --- /dev/null +++ b/src/lib/SaveToStorage.ts @@ -0,0 +1,35 @@ +import { pingChanges } from "./chromeEmitters" +import { storage } from "./declarations" +import { ChangedObjectStateEnum } from "./definitions" + +export function saveSettingsToStorage( settings: any ) +{ + storage.set( { "settings" : settings } ) + localStorage.setItem( "yt-settings", JSON.stringify( settings ) ) + + pingChanges( ChangedObjectStateEnum.SETTINGS, settings ) +} + +export function saveKeybindsToStorage( keybinds: any ) +{ + storage.set( { "keybinds" : keybinds } ) + localStorage.setItem( "yt-keybinds", JSON.stringify( keybinds ) ) + + pingChanges( ChangedObjectStateEnum.KEYBINDS, keybinds ) +} + +export function saveOptionsToStorage( options: any ) +{ + storage.set( { "extraopts" : options } ) + localStorage.setItem( "yt-extraopts", JSON.stringify( options ) ) + + pingChanges( ChangedObjectStateEnum.OPTIONS, options ) +} + +export function saveFeaturesToStorage( features: any ) +{ + storage.set( { "features" : features } ) + localStorage.setItem( "yt-features", JSON.stringify( features ) ) + + pingChanges( ChangedObjectStateEnum.FEATURES, features ) +} \ No newline at end of file diff --git a/src/lib/SkipShortsWithLowLikes.ts b/src/lib/SkipShortsWithLowLikes.ts new file mode 100644 index 0000000..ef51eb6 --- /dev/null +++ b/src/lib/SkipShortsWithLowLikes.ts @@ -0,0 +1,49 @@ +// video with no likes => https://www.youtube.com/shorts/ZFLRydDd9Mw +// video with no likes and 23k comments => https://www.youtube.com/shorts/gISsypl5xsc +// another => https://www.youtube.com/shorts/qe56pgRVrgE?feature=share +// video with 1.5M / 1,5M => https://www.youtube.com/shorts/nKZIx1bHUbQ + +import { isVideoPlaying, skipShort } from "./VideoState" +import { StateObject } from "./definitions" +import { getCurrentId, getLikeCount, getNextButton } from "./getters" + +export function shouldSkipShort( state: any, options: any ) +{ + const currentId = getCurrentId() + const likeCount = getLikeCount() + + if ( currentId === null ) return + + // console.dir({ + // "options are null": options === null, + // "is the video playing": isVideoPlaying(), + // "option isnt enabled": !options.skip_enabled, + // "current id below top id": currentId < state.topId, + // "current id is the skipped id": state.skippedId === currentId, + // "likecount is null or undefined": likeCount === null || isNaN( likeCount ), + // "likecount is above threshold": likeCount >= options.skip_threshold + // }) + + if ( options === null ) return false + if ( !isVideoPlaying() ) return false // video unstarted, likes likely not loaded + + if ( !options.skip_enabled ) return false + if ( state.topId === 0 ) return false // dont skip first short ever + if ( currentId < state.topId ) return false // allow user to scroll back up to see skipped video + if ( state.skippedId === currentId ) return false // prevent skip spam + if ( likeCount === null || isNaN( likeCount ) ) return false // dont skip unloaded shorts + if ( likeCount >= options.skip_threshold ) return false + + return true +} +export function handleSkipShortsWithLowLikes( state: StateObject, options: any ) +{ + const likeCount = getLikeCount() + + if ( shouldSkipShort( state, options ) ) + { + console.log("[BYS] :: Skipping short that had", likeCount, "likes") + state.skippedId = getCurrentId() + skipShort() + } +} \ No newline at end of file diff --git a/src/lib/VideoState.ts b/src/lib/VideoState.ts new file mode 100644 index 0000000..717310d --- /dev/null +++ b/src/lib/VideoState.ts @@ -0,0 +1,45 @@ +import { getBackButton, getMuteButton, getNextButton, getVideo } from "./getters" + +export function isVideoPlaying() +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return false + + return ytShorts.currentTime > 0.5 && ytShorts.duration > 1 +} + +export function hasVideoEnded() +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return false + + return ytShorts.currentTime >= ytShorts.duration - 0.11 +} + +export function skipShort() +{ + getNextButton()?.click() +} +export function goToNextShort() +{ + getNextButton()?.click() +} + +export function goToPreviousShort() +{ + getBackButton()?.click() +} + +export function restartShort() +{ + const ytShorts = getVideo() + if ( ytShorts === null ) return false + + ytShorts.currentTime = 0 +} + +export function mute() +{ + const muteButton = getMuteButton() + if ( muteButton === null ) return +} \ No newline at end of file diff --git a/src/lib/VolumeSlider.ts b/src/lib/VolumeSlider.ts new file mode 100644 index 0000000..5b2c272 --- /dev/null +++ b/src/lib/VolumeSlider.ts @@ -0,0 +1,68 @@ +import { saveSettingsToStorage } from "./SaveToStorage" +import { storage } from "./declarations" +import { StateObject } from "./definitions" +import { getCurrentId, getVideo, getVolumeContainer, getVolumeSliderController } from "./getters" +import { render } from "./utils" + +export function checkVolume( settings: any, sliderEnabled: boolean ) +{ + const ytShorts = getVideo() + + if ( ytShorts === null ) return + if ( !sliderEnabled ) return + + if ( settings?.volume !== null ) + ytShorts.volume = settings.volume + else + settings.volume = ytShorts.volume +} + +// todo - move this to its own lib script (probably call it volumeSlider.ts) +export function setVolume( settings: any, newVolume: number, enabled: boolean ) +{ + settings.volume = newVolume + + const volumeSliderController = getVolumeSliderController() as HTMLInputElement + if ( volumeSliderController === null ) return + + const ytShorts = getVideo() + volumeSliderController.value = "" + settings.volume + + if ( ytShorts === null ) return + + checkVolume( settings, enabled ) + + saveSettingsToStorage( settings ) + +} + +export function setVolumeSlider( state: StateObject, settings: any, enabled: boolean ) +{ + if ( !enabled ) return + + const id = getCurrentId() + + const volumeContainer = getVolumeContainer() + // const slider = document.createElement("input") + const slider = render(` + + `) + + if( settings.volume === null ) settings.volume = 0.5 + + volumeContainer.appendChild( slider ) + + // Prevent video from pausing/playing on click + slider.addEventListener( "input", (e: any) => setVolume( settings, e.target.valueAsNumber, enabled ) ) + slider.addEventListener( "click", e => e.stopPropagation() ) +} + diff --git a/src/lib/chromeEmitters.ts b/src/lib/chromeEmitters.ts new file mode 100644 index 0000000..b3d86e8 --- /dev/null +++ b/src/lib/chromeEmitters.ts @@ -0,0 +1,26 @@ +import BROWSER from "../background/browser"; +import { ChangedObjectStateEnum } from "./definitions"; +import { getKeyFromEnum } from "./utils"; + +/** + * Expects an object (the keybindsState or optionsState for exmaple) + * This is to be received by the content script, allowing immediate updates to the binds/options + * @param message + */ + +export function pingChanges( objectEnum: ChangedObjectStateEnum, message: Object ) +{ + ( async () => { + const [ tab ] = await BROWSER.tabs.query({active: true, lastFocusedWindow: true}) + const key = getKeyFromEnum( ChangedObjectStateEnum, objectEnum, null ) + + const content = {} as any + content[ key ] = message + + const response = await BROWSER.tabs.sendMessage( tab.id as number, content ); // ! - see if this works in firefox + + // do something with response here, not outside the function + console.log( `[BYS] :: Saving Changes` ) + } )() + .catch( err => {} ) +} \ No newline at end of file diff --git a/src/lib/declarations.ts b/src/lib/declarations.ts new file mode 100644 index 0000000..66255a3 --- /dev/null +++ b/src/lib/declarations.ts @@ -0,0 +1,267 @@ +import BROWSER from "../background/browser"; +import local from "../background/i18n"; +import { DefaultsDictionary, NumberDictionary, OptionsDictionary, PolyDictionary, StateObject, StringDictionary } from "./definitions"; + +export const VERSION = BROWSER.runtime.getManifest().version + +export const DEFAULT_KEYBINDS: DefaultsDictionary = { + seekBackward: "ArrowLeft", + seekForward: "ArrowRight", + decreaseSpeed: "KeyU", + resetSpeed: "KeyI", + increaseSpeed: "KeyO", + decreaseVolume: "Minus", + increaseVolume: "Equal", + toggleMute: "KeyM", + nextFrame: local("disabled"), + previousFrame: local("disabled"), + restartShort: "KeyJ", + nextShort: local("disabled"), + previousShort: local("disabled"), +}; + +export const KEYBINDS_ORDER: DefaultsDictionary = [ + "seekBackward", + "seekForward", + "decreaseSpeed", + "resetSpeed", + "increaseSpeed", + "decreaseVolume", + "increaseVolume", + "toggleMute", + "restartShort", + "nextFrame", + "previousFrame", + "nextShort", + "previousShort", +]; + + +export const DEFAULT_OPTIONS: DefaultsDictionary = { + // add new defaults for your option here + skipEnabled: false, + skipThreshold: 500, + seekAmount: 5, + automaticallyOpenComments: false, + hideShortsOverlay: false, +} + +export const OPTIONS_ORDER: DefaultsDictionary = [ + "seekAmount", + "automaticallyOpenComments", + "skipEnabled", + "skipThreshold", + "hideShortsOverlay" +]; + +export const OPTION_DICTIONARY: OptionsDictionary = { + // add details for the option (the input element type, the bounds (min/max), etc) + skipEnabled: { + desc: local("autoSkipTitle"), + type: "checkbox", + }, + skipThreshold: { + desc: local("skipThresholdTitle"), + type: "number", + min: 0, + }, + seekAmount: { + desc: local("seekAmountTitle"), + type: "number", + min: 0, + max: 60, + }, + automaticallyOpenComments: { + desc: local("automaticallyOpenCommentsTitle"), + type: "checkbox", + }, + hideShortsOverlay: + { + desc: local("hideShortsOverlayTitle"), + type: "checkbox", + } +} + +export function setKeybind( previousState: StringDictionary, command: string, newKey: string ): StringDictionary +{ + if ( previousState === null ) return null + + const newKeybinds = {...previousState} + newKeybinds[ command ] = newKey + + return newKeybinds +} + +export function setOption( previousState: PolyDictionary, option: string, value: string ): StringDictionary +{ + if ( previousState === null ) return null + + const newOptions = {...previousState} + newOptions[ option ] = value + + return newOptions +} + +export function setFeature( previousState: PolyDictionary, feature: string, value: string ): StringDictionary +{ + if ( previousState === null ) return null + + const newFeatures = { ...previousState } + newFeatures[ feature ] = value + + return newFeatures +} + + +export const storage = BROWSER.storage.local + +export const DEFAULT_STATE = { + id : 0, + topId : 0, + playbackRate: 1, + lastTime : -1, // ? this is for checking if items were injected + openedCommentsId: -1, + + injectedItems: new Set(), + + actualVolume: null, + skippedId : null, + + muted : false, +} as StateObject + +// ! - add settings +export const DEFAULT_SETTINGS = { + volume: 0.5, + autoplay: false, +} + +export const DEFAULT_FEATURES = { + autoplay: true, + progressBar: true, + timer: true, + playbackRate: true, + volumeSlider: true, + keybinds: true, +}; + +export const FEATURES_ORDER: DefaultsDictionary = [ + "autoplay", + "progressBar", + "timer", + "playbackRate", + "volumeSlider", + "keybinds", +]; + +// todo - add formats from other langs (note: dont include duplicate keys)# +export const NUMBER_MODIFIERS: NumberDictionary = { + // English + "b": 1_000_000_000, + "m": 1_000_000, + "k": 1_000, + + // Italian + "mln": 1_000_000, + + // Indian English + "lakh": 100_000, + + // Portuguese + "mil": 1_000, + "mi": 1_000_000, + + // French + "mio": 1_000_000, + "md": 1_000, + + // German + "mrd": 1_000_000_000, + "tsd": 1_000, + + // Japanese + "億": 100_000_000, + "万": 10_000, + + // Chinese (Simplified) + "亿": 100_000_000, + // "万": 10_000, + + // Chinese (Traditional) + "萬": 10_000, + // "億": 100_000_000, + + // Russian + "млн": 1_000_000, + "тыс": 1_000, + + // Hindi + "करोड़": 10_000_000, + "लाख": 100_000, + + // Arabic + "مليون": 1_000_000, + "مليار": 1_000_000_000, + "ألف": 1_000, + + // Korean + "억": 100_000_000, + "만": 10_000, + + // Turkish + "milyon": 1_000_000, + "milyar": 1_000_000_000, + "bin": 1_000, + + // Vietnamese + "triệu": 1_000_000, + "tỷ": 1_000_000_000, + "nghìn": 1_000, + + // Thai + "ล้าน": 1_000_000, + "พันล้าน": 1_000_000_000, + "พัน": 1_000, + + // Dutch + "mld": 1_000_000_000, + + // Greek + "εκ": 1_000_000, + "δισ": 1_000_000_000, + "χιλ": 1_000, + + // Swedish + "mn": 1_000_000, + "t": 1_000, +} + +export const EXCLUDED_KEY_BINDS = [ + 'Backspace', + 'Enter', + 'NumpadEnter', + 'Escape', + 'Tab', + 'Space', + 'PageUp', + 'PageDown', + 'ArrowUp', + 'ArrowDown', + 'F13', // printscreen + 'MetaLeft', // windows/command + 'MetaRight', + + 'ControlLeft', + 'ControlRight', + 'ShiftLeft', + 'ShiftRight', + 'AltLeft', + 'AltRight', +] + +export const DEFAULT_PRESSED_KEY = local("pressAKey") +export const DISABLED_BIND_STRING = local("disabled") + +export const VOLUME_INCREMENT_AMOUNT = 0.025 + +export const CYCLABLE_PLAYBACK_RATES = [ 0.5, 1, 1.5, 2 ] \ No newline at end of file diff --git a/src/lib/definitions.ts b/src/lib/definitions.ts new file mode 100644 index 0000000..bb0a176 --- /dev/null +++ b/src/lib/definitions.ts @@ -0,0 +1,49 @@ +export type NumberDictionary = { + [key: string]: number +} | null + +export type StringDictionary = { + [key: string]: string +} | null + +export type StrictStringDictionary = { + [key: string]: string +} + +export type PolyDictionary = { + [key: string]: any +} | null + +export type BooleanDictionary = { + [key: string]: any +} | null + +export type StrictPolyDictionary = { + [key: string]: any +} + +export type DefaultsDictionary = { + [key: string]: any +} + +export interface StateObject { + [key: string]: any +} + +export interface OptionsDictionary { + [key: string]: PolyDictionary +} + +export enum PopupPageNameEnum { + UNKNOWN = 0, + KEYBINDS, + OPTIONS, + FEATURES, +} + +export enum ChangedObjectStateEnum { + KEYBINDS = 0, + OPTIONS, + SETTINGS, + FEATURES, +} \ No newline at end of file diff --git a/src/lib/getters.ts b/src/lib/getters.ts new file mode 100644 index 0000000..f95c82f --- /dev/null +++ b/src/lib/getters.ts @@ -0,0 +1,123 @@ +// todo - cleanup, convert to named functions and add return types. + +import { convertLocaleNumber } from "./utils" + +export function getCurrentId() +{ + const video = document.querySelector( "#shorts-player > div.html5-video-container > video" ) as HTMLVideoElement + if ( video === null ) return null + + const closest = video.closest("ytd-reel-video-renderer" ) as HTMLElement + if ( closest === null ) return null + + return +closest.id +} + +export function getLikeCount(): number +{ + const likesElement = document.querySelector( + `[id="${getCurrentId()}"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer #like-button` + ) as HTMLElement + + // Use optional chaining and nullish coalescing to handle null values + const numberOfLikes = (likesElement?.firstElementChild)?.innerText.split(/\r?\n/)[0]?.trim().replace(/\s/g, "").replace(/\.$/, "").toLowerCase() ?? "0" + + // Convert the number of likes to the appropriate format + const likeCount = convertLocaleNumber( numberOfLikes ) as number + + // If likeCount is anything other than a number, it"ll return 0. Meaning it"ll translate every language. + return !isNaN(likeCount) ? likeCount as number : 0 +} + +// Checking comment count aswell, as sometimes popular videos bug out and show 0 likes, but there"s 1000+ comments. +export const getCommentCount = ( id: number | null ) => { + const commentsElement = document.querySelector( + `[id="${id}"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer #comments-button` + ) as HTMLElement + + // Use optional chaining and nullish coalescing to handle null values + const numberOfComments = (commentsElement?.firstElementChild)?.innerText.split(/\r?\n/)[0]?.replace(/ /g, "") ?? "0" + + // Convert the number of comments to the appropriate format + const commentCount = convertLocaleNumber(numberOfComments) + + // If commentCount is anything other than a number, it"ll return 0. Meaning it"ll handle every language. + return !isNaN(commentCount as number) ? commentCount : 0 +} + +export const getActionElement = () => + document.querySelector( + `[id="${getCurrentId()}"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #actions` + ) as HTMLElement + +export function getOverlayElement() +{ + return document.querySelector( + `[id="${ getCurrentId() }"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #overlay` + ) as HTMLElement + +} + +export function getTitle() { + return document.querySelector(`[id="${getCurrentId()}"] h2.title`) as HTMLElement +} + +export function getVolumeContainer() +{ + const id = getCurrentId() + return document.querySelector( + `[id="${id}"] > #player-container > div.player-controls.style-scope.ytd-reel-video-renderer > ytd-shorts-player-controls.style-scope.ytd-reel-video-renderer` + ) as HTMLElement +} + +export function getNextButton() +{ + return document.querySelector("#navigation-button-down > ytd-button-renderer > yt-button-shape") as HTMLElement +} + +export function getBackButton() +{ + return document.querySelector("#navigation-button-up > ytd-button-renderer > yt-button-shape") as HTMLElement +} + +export function getVideo(): HTMLVideoElement | null +{ + return document.querySelector( "#shorts-player>div>video" ) +} + +export function getPlaybackElement() +{ + const id = getCurrentId() + return document.getElementById( `ytPlayback${id}` ) +} + +export function getVolumeSliderController() +{ + const id = getCurrentId() + return document.getElementById(`volumeSliderController${id}`) +} + +export function getProgressBarList() +{ + return getOverlayElement().querySelector('#progress-bar-line') +} + +export function getMuteButton() +{ + return document.querySelector("#player-container > div > ytd-shorts-player-controls > yt-icon-button:nth-child(2)") +} + +export function getCommentsButton() +{ + return ( + document.querySelector( `[ id="${getCurrentId()}" ] #comments-button .yt-spec-touch-feedback-shape__fill` ) + ) as HTMLElement + +} + +export function getOverlay() +{ + return document.querySelector( + `[id="${ getCurrentId() }"] #overlay ytd-reel-player-header-renderer` + ) as HTMLElement +} \ No newline at end of file diff --git a/src/lib/handleKeyEvent.ts b/src/lib/handleKeyEvent.ts new file mode 100644 index 0000000..cccd379 --- /dev/null +++ b/src/lib/handleKeyEvent.ts @@ -0,0 +1,117 @@ +import { setHideShortsOverlay } from "./HideShortsOverlay" +import { goToNextShort, goToPreviousShort, restartShort } from "./VideoState" +import { setVolume } from "./VolumeSlider" +import { VOLUME_INCREMENT_AMOUNT } from "./declarations" +import { BooleanDictionary, PolyDictionary, StringDictionary } from "./definitions" +import { getVideo } from "./getters" + +export function handleKeyEvent( + e: KeyboardEvent, + features: BooleanDictionary, + keybinds: StringDictionary, + settings: any, + options: PolyDictionary, + state: any +) { + if ( + [ ...document.querySelectorAll("input") ].includes( document.activeElement as HTMLInputElement ) || + [ ...document.querySelectorAll("#contenteditable-root") ].includes( document.activeElement as HTMLElement ) + ) return // Avoids using keys while the user interacts with any input, like search and comment. + + if ( features !== null && !features[ "keybinds" ] ) return + + const ytShorts = getVideo() + if ( !ytShorts ) return + + const key = e.code + const keyAlt = e.key.toLowerCase() // for legacy keybinds + + let command + for ( const [cmd, keybind] of Object.entries( keybinds as Object ) ) + if ( key === keybind || keyAlt === keybind ) + command = cmd + + if (!command) return + + const volumeSliderEnabled = features !== null && features[ "volumeSlider" ] + + switch (command) { + case "seekBackward": + ytShorts.currentTime -= options?.seekAmount + break + + case "seekForward": + ytShorts.currentTime += options?.seekAmount + break + + case "decreaseSpeed": + if (ytShorts.playbackRate > 0.25) ytShorts.playbackRate -= 0.25 + break + + case "resetSpeed": + ytShorts.playbackRate = 1 + break + + case "increaseSpeed": + if ( ytShorts.playbackRate < 16 ) ytShorts.playbackRate += 0.25 + break + + case "increaseVolume": + if ( ytShorts.volume < 1 ) + setVolume( settings, ytShorts.volume + VOLUME_INCREMENT_AMOUNT, volumeSliderEnabled ) + + if ( ytShorts.volume > 1 ) + ytShorts.volume = 1 + + break + + case "decreaseVolume": + if ( ytShorts.volume > 0 ) + setVolume( settings, ytShorts.volume - VOLUME_INCREMENT_AMOUNT, volumeSliderEnabled ) + + if ( ytShorts.volume < 0 ) + ytShorts.volume = 0 + + break + + // case "toggleMute": + // if ( !state.muted ) + // { + // state.muted = true + // ytShorts.volume = 0 + // settings.volume = ytShorts.volume + // } + // else + // { + // state.muted = false + // ytShorts.volume = state.volumeState + // } + // break + + case "nextFrame": + if (ytShorts.paused) { + ytShorts.currentTime -= 0.04 + } + break + + case "previousFrame": + if (ytShorts.paused) { + ytShorts.currentTime += 0.04 + } + break + + case "nextShort": + goToNextShort() + break + + case "previousShort": + goToPreviousShort() + break + + case "restartShort": + restartShort() + break + } + + state.playbackRate = ytShorts.playbackRate +} \ No newline at end of file diff --git a/src/lib/retrieveFromStorage.ts b/src/lib/retrieveFromStorage.ts new file mode 100644 index 0000000..a12b970 --- /dev/null +++ b/src/lib/retrieveFromStorage.ts @@ -0,0 +1,101 @@ +import { DEFAULT_FEATURES, DEFAULT_KEYBINDS, DEFAULT_OPTIONS, DEFAULT_SETTINGS, storage } from "./declarations" +import { PolyDictionary, StringDictionary } from "./definitions" + +export async function retrieveOptionsFromStorage( setter: ( options: PolyDictionary ) => void ) +{ + const localStorageOptions = JSON.parse( localStorage.getItem("yt-extraopts") as string ) + setter( localStorageOptions ) + + storage.get( ["extraopts"] ) + .then( ( {extraopts} ) => { + if ( !extraopts ) throw Error("[BYS] :: Extra Options couldnt be loaded from storage, using defaults") + + for ( const [ option, value ] of Object.entries( DEFAULT_OPTIONS ) ) { + if ( extraopts[ option ] ) continue // * this may be an issue later on if we WANT falsy values as viable values + extraopts[ option ] = value + } + + if ( extraopts !== localStorageOptions ) + localStorage.setItem( "yt-extraopts", JSON.stringify( extraopts ) ) + + setter( extraopts ) + }) + .catch( err => { + setter( DEFAULT_OPTIONS ) + } ) +} + +export async function retrieveKeybindsFromStorage( setter: ( keybinds: StringDictionary ) => void ) +{ + const localStorageKeybinds = JSON.parse( localStorage.getItem("yt-keybinds") as string ) + setter( localStorageKeybinds ) + + storage.get( ["keybinds"] ) + .then( ( {keybinds} ) => { + if ( !keybinds ) throw Error("[BYS] :: Keybinds couldnt be loaded from storage, using defaults") + + for ( const [ option, value ] of Object.entries( DEFAULT_KEYBINDS ) ) { + if ( keybinds[ option ] !== null ) continue // * this may be an issue later on if we WANT falsy values as viable values + keybinds[ option ] = value + } + + if ( keybinds !== localStorageKeybinds ) + localStorage.setItem( "yt-keybinds", JSON.stringify( keybinds ) ) + + setter( keybinds ) + + }) + .catch( err => { + setter( DEFAULT_KEYBINDS ) + } ) +} + +export async function retrieveSettingsFromStorage( setter: ( settings: Object ) => void ) +{ + const localStorageSettings = JSON.parse( localStorage.getItem("yt-settings") as string ) + setter( localStorageSettings ) + + storage.get( ["settings"] ) + .then( ( {settings} ) => { + if ( !settings ) throw Error("[BYS] :: Settings couldnt be loaded from storage, using defaults") + + for ( const [ option, value ] of Object.entries( DEFAULT_SETTINGS ) ) { + if ( settings[ option ] ) continue // * this may be an issue later on if we WANT falsy values as viable values + settings[ option ] = value + } + + if ( settings !== localStorageSettings ) + localStorage.setItem( "yt-settings", JSON.stringify( settings ) ) + + setter( settings ) + + }) + .catch( err => { + setter( DEFAULT_SETTINGS ) + } ) +} + +export async function retrieveFeaturesFromStorage( setter: ( features: Object ) => void ) +{ + const localStorageFeatures = JSON.parse( localStorage.getItem("yt-features") as string ) + setter( localStorageFeatures ) + + storage.get( ["features"] ) + .then( ( {features} ) => { + if ( !features ) throw Error("[BYS] :: Features couldnt be loaded from storage, using defaults") + + for ( const [ feature, value ] of Object.entries( DEFAULT_FEATURES ) ) { + if ( features[ feature ] !== null ) continue + features[ feature ] = value + } + + if ( features !== localStorageFeatures ) + localStorage.setItem( "yt-features", JSON.stringify( features ) ) + + setter( features ) + + }) + .catch( err => { + setter( DEFAULT_FEATURES ) + } ) +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a7754c1 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,124 @@ +import { EXCLUDED_KEY_BINDS, NUMBER_MODIFIERS } from "./declarations" +import { PolyDictionary } from "./definitions" + +/** + * Converts a formatted number to its full integer value. + * @param {string} string value to be converted (eg: 1.4M, 1,291 or 727) + * @returns converted number + */ +export function convertLocaleNumber( string: string ): number | null +{ + if ( typeof string !== "string" ) return null + + const regex = /^(\d{1,3}(?:(?:,\d{3})*(?:\.\d+)?)|(?:\d+))(?:([,.])(\d+))?([a-z]*)\.?$/i + const matches = string.match(regex) + + if (!matches) { + return 0 + } + + let numericPart = matches[1].replace(/,/g, "") // Remove commas + if (matches[2] && matches[3]) { + // Decimal part exists, add it back + numericPart += `.${matches[3]}` + } + + const multiplier = matches[4].toLowerCase() + const modifier = ( NUMBER_MODIFIERS === null ) ? null : NUMBER_MODIFIERS[multiplier] + + if ( modifier !== null ) + { + return +numericPart * modifier + } + else + { + // Remove decimals and commas from the numeric part + const numericValue = parseInt(numericPart.replace(/[.,]/g, ""), 10) + return numericValue + } +} + +// todo - fix types on this +export function wheel( element: HTMLElement, codeA: () => void, codeB: () => void ) { + element.addEventListener( "wheel", ( e: WheelEvent ) => { + e.preventDefault() + + if (e.deltaY < 0) codeA() + else codeB() + + }, { passive: false } + ) +} + +/** + * Take an HTML string and convert it to a live HTML element. + * + * This returns the element(s) themselves, and can be manipulated as such + * + * The string must have a single parent (no siblings on the highest level, eg how react does it) + * + * @param htmlString eg: "\

\" + * @returns HTML Element (or null if the string parses to no HTML elements) + */ +export function render( htmlString: string ): Node +{ + const elements = new DOMParser().parseFromString( htmlString, "text/html" ).body.children + + if ( elements.length > 1 ) throw new Error( "ADSU | HTML String cannot have siblings!" ) + if ( elements.length < 1 ) throw new Error( "ADSU | HTML String must have an element!" ) + + return elements[0] as Node +} + +/** + * returns a standard input element type depending on the given sample value + * For example, `true` will return `"checkbox"`; `500` will return "number" + */ +export function determineInputType( sampleValue: any ): string +{ + switch( typeof sampleValue ) + { + case "boolean": + return "checkbox" + case "number": + return "number" + case "string": + return "text" + + default: + return "text" + } +} + +export function getEnumEntries( givenEnum: any ): Array<[string, any]> +{ + return Object.entries( givenEnum as Object ) + .filter( ( ( [key, val] ) => isNaN( key as any ) ) ) +} + +/** + * Assumes one word + * @param str + * @returns + */ +export function capitalise( str: string ) +{ + return str[0].toUpperCase() + str.slice( 1 ).toLowerCase() +} + + +/** + * Get enum value from key string, or return `default_return` if unfound + */ +export function getEnumWithString( givenEnum: any, key: string, default_return: any = null ) +{ + return Object.assign( {}, givenEnum )[ key ] ?? default_return +} +/** + * Get enum key from enum, or return `default_return` if unfound + * note: this will return TO LOWER CASE!! + */ +export function getKeyFromEnum( givenEnum: any, value: any, default_return: any = null ) +{ + return givenEnum[ value ].toLowerCase() +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..063f282 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +/** + * main.tsx + * + * This is where the react code is injected. + * For content-script code (that which is injected onto the page), + * see ./content.ts + */ + +import React from 'react' +import ReactDOM from 'react-dom/client' +import Popup from './components/Popup' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/store-desc/BYS-en.md b/store-desc/BYS-en.md new file mode 100644 index 0000000..0670480 --- /dev/null +++ b/store-desc/BYS-en.md @@ -0,0 +1,23 @@ +⏭️ Control your YouTube Shorts just like a normal YouTube video! Features include progress bar, seeking, playback speed, auto skip and more. You can also customize the keybinds to your liking! + +List of features: +- Progress bar at the bottom with time and duration +- Seeking 5 seconds backward and forward with arrow keys (adjustable time) +- Mini timestamp and speed above the like button (can be scrolled on!) +- Decrease and increase playback speed with keys U and O +- Toggle to auto skip short when current one ends +- Control volume with the volume slider or with - and =, mute audio with M +- Customizable keybinds + +Extra features: +- Start short from beginning with J +- Auto skip short with likes below custom threshold (e.g. 500 likes) +- Auto open comment section on each short +- Hide overlay on shorts (title, channel, etc.) +- Revert to normal speed with I or by clicking the speed button +- Navigate to previous or next short without animation with W and S +- Go to the next frame or previous frame with . and , while paused + +Fully open-source with MIT License: https://github.com/ynshung/better-yt-shorts + +If you have any suggestions or feedback, please let us know here: https://github.com/ynshung/better-yt-shorts#issues--suggestion diff --git a/store-desc/BYS-fr.md b/store-desc/BYS-fr.md new file mode 100755 index 0000000..dffb648 --- /dev/null +++ b/store-desc/BYS-fr.md @@ -0,0 +1,23 @@ +⏭️ Contrôlez les YouTube Shorts de la même manière qu'une vidéo YouTube normale ! Cette extension fournit une barre de progression, la possibilité d'aller à un moment d'une vidéo directement (contrôle de la barre de progression), l'ajustement de la vitesse de lecture, le passage automatique de vidéo et bien d'autres fonctionnalités. Il est également possible de personnaliser les raccourcis clavier selon vos préférences ! + +Liste des fonctionnalités: +- Barre de progression en dessous avec l'horodatage et la durée de la vidéo +- Avance/Recul rapide de 5 secondes avec les touches fléchées (temps modifiable dans les paramètres de l'extension) +- Horodatage de la vidéo et contrôle de la vitesse de lecture au-dessus du bouton "J'aime" (vitesse contrôlable avec la molette) +- Possibilité de diminuer ou d'augmenter la vitesse de lecture avec les touches "U" et "O" respectivement +- Interrupteur qui permet de passer automatiquement les Shorts quand ceux-ci sont terminés +- Contrôle du volume avec un curseur ou avec les touches "-" et "=", possibilité de rendre muet la vidéo avec "M" +- Raccourcis clavier personnalisables + +Fonctionnalités en plus: +- Possibilité de recommencer un Short avec la touche "J" +- Passage automatique des Shorts quand il n'y a pas un nombre minimum de "J'aime" (par exemple 500 "j'aime", la valeur est modifiable) +- Ouverture automatique des commentaires sur les Shorts +- Cacher automatiquement l'overlay (titre, chaîne, etc...) +- Retour à la vitesse normale avec la touche "I" ou en cliquant sur le bouton de vitesse de lecture +- Navigation vers le Short précédent ou suivant avec les touches "W" et "S" respectivement +- Lecture image par image avec les touches "." et "," quand le Short est en pause + +Complètement open-source avec une licence MIT : https://github.com/ynshung/better-yt-shorts + +Si vous avez des suggestions ou des retours d'expérience, n'hésitez pas à nous les indiquer ici : https://github.com/ynshung/better-yt-shorts#issues--suggestion \ No newline at end of file diff --git a/styles.css b/styles.css deleted file mode 100644 index 516c08f..0000000 --- a/styles.css +++ /dev/null @@ -1,125 +0,0 @@ -.betterYT { - font-size: 16px; - font-weight: bold; - text-align: center; -} - -.betterYT-renderer { - display: inline-block; -} - -.betterYT-button-shape { - display: flex; -} - -.betterYT-volume-slider{ - -webkit-appearance: slider-vertical; - position: absolute; - right: -40px; - top: 40px; - opacity: 0.4; - pointer-events: all !important; - cursor: pointer; -} - -@supports (-moz-appearance:none) { - .volume-slider{ - right: 16px; - } -} - -.volume-slider:hover{ - opacity: 1; -} - -.autoplay-switch { - position: relative; - display: inline-block; - width: 48px; - height: 27px; -} - -/* Hide default HTML checkbox */ -.autoplay-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.autoplay-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #272727; - -webkit-transition: .4s; - transition: .4s; - border-radius: 27px; -} - -.autoplay-slider:hover { - background-color: #3f3f3f; -} - -.autoplay-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 3px; - bottom: 3px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; -} - -input:checked + .autoplay-slider { - background-color: #3f3f3f; -} - -input:checked + .autoplay-slider:before { - -webkit-transform: translateX(21px); - -ms-transform: translateX(21px); - transform: translateX(21px); -} - -@media screen and (max-width: 599px) { - .volumeSlider { - -webkit-appearance: slider-horizontal; - right: 44px; - top: 18px; - } - .autoplay-slider { - background-color: #FFFFFF1A; - } - input:checked + .autoplay-slider { - background-color: #FFFFFF2A; - } - .betterYT-auto { - padding-bottom: 16px; - } -} - -.betterYT-progress-bar{ - bottom: 0; - pointer-events: auto !important; -} - -.betterYT-progress-bar-hover{ - height: 10px !important; - cursor: pointer; -} - -.betterYT-timestamp-tooltip{ - position: absolute; - display: none; - background-color: #272727c4; - border-radius: 5px; - padding: 4px; - font-size: 11px; - color: white; - z-index: 50; -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3d0a51a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..e993792 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "esnext", + "moduleResolution": "node" + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..29edff3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { crx } from '@crxjs/vite-plugin' +import manifest from './manifest.json' + +export default defineConfig({ + plugins: [ + react(), + crx({ manifest }), + ], +}) \ No newline at end of file