diff --git a/.erb/configs/.eslintrc b/.erb/configs/.eslintrc index 12060680..778b71dc 100644 --- a/.erb/configs/.eslintrc +++ b/.erb/configs/.eslintrc @@ -4,6 +4,7 @@ "global-require": "off", "import/no-dynamic-require": "off", "prettier/prettier": 0, + "no-await-in-loop": "off", "import/prefer-default-export": "off", "no-empty-function": "off", "@typescript-eslint/no-empty-function": "off", diff --git a/.erb/configs/webpack.config.renderer.dev.ts b/.erb/configs/webpack.config.renderer.dev.ts index 324d6cbd..8bcbcc73 100644 --- a/.erb/configs/webpack.config.renderer.dev.ts +++ b/.erb/configs/webpack.config.renderer.dev.ts @@ -10,6 +10,7 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; +import { AppWindow } from "../../src/shared/models/window-manager/app-window.model" // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment @@ -39,6 +40,22 @@ if ( execSync('npm run postinstall'); } +const getHtmlPageOptions = (page: AppWindow): HtmlWebpackPlugin.Options => { + return { + filename: path.join(page), + template: path.join(webpackPaths.srcRendererPath, page.replace(".html", ".ejs")), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + nodeModules: webpackPaths.appNodeModulesPath, + } +} + const configuration: webpack.Configuration = { devtool: 'inline-source-map', @@ -147,33 +164,12 @@ const configuration: webpack.Configuration = { new ReactRefreshWebpackPlugin(), - new HtmlWebpackPlugin({ - filename: path.join('index.html'), - template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - env: process.env.NODE_ENV, - isDevelopment: process.env.NODE_ENV !== 'production', - nodeModules: webpackPaths.appNodeModulesPath, - }), + new HtmlWebpackPlugin(getHtmlPageOptions("index.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")), - new HtmlWebpackPlugin({ - filename: path.join('launcher.html'), - template: path.join(webpackPaths.srcRendererPath, 'launcher.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - env: process.env.NODE_ENV, - isDevelopment: process.env.NODE_ENV !== 'production', - nodeModules: webpackPaths.appNodeModulesPath, - }) ], node: { diff --git a/.erb/configs/webpack.config.renderer.prod.ts b/.erb/configs/webpack.config.renderer.prod.ts index 65939547..54b67a5e 100644 --- a/.erb/configs/webpack.config.renderer.prod.ts +++ b/.erb/configs/webpack.config.renderer.prod.ts @@ -14,6 +14,7 @@ import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; import checkNodeEnv from '../scripts/check-node-env'; import deleteSourceMaps from '../scripts/delete-source-maps'; +import { AppWindow } from "../../src/shared/models/window-manager/app-window.model" checkNodeEnv('production'); deleteSourceMaps(); @@ -25,6 +26,23 @@ const devtoolsConfig = } : {}; + +const getHtmlPageOptions = (page: AppWindow): HtmlWebpackPlugin.Options => { + return { + filename: path.join(page), + template: path.join(webpackPaths.srcRendererPath, page.replace(".html", ".ejs")), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: false, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + nodeModules: webpackPaths.appNodeModulesPath, + } +} + const configuration: webpack.Configuration = { ...devtoolsConfig, @@ -128,29 +146,12 @@ const configuration: webpack.Configuration = { analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', }), - new HtmlWebpackPlugin({ - filename: 'index.html', - template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - isDevelopment: process.env.NODE_ENV !== 'production', - }), + new HtmlWebpackPlugin(getHtmlPageOptions("index.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")), + new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")), - new HtmlWebpackPlugin({ - filename: 'launcher.html', - template: path.join(webpackPaths.srcRendererPath, 'launcher.ejs'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - isDevelopment: process.env.NODE_ENV !== 'production', - }) ], }; diff --git a/.eslintrc.js b/.eslintrc.js index 714be484..97c4632c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,8 @@ module.exports = { "global-require": 0, "import/prefer-default-export": "off", "no-empty-function": "off", + "no-await-in-loop": "off", + "no-continue": "off", "@typescript-eslint/no-empty-function": "off", '@typescript-eslint/lines-between-class-members': "off", "class-methods-use-this": "off", @@ -30,7 +32,10 @@ module.exports = { "@typescript-eslint/no-throw-literal": "off", "@typescript-eslint/no-unused-expressions": "off", "no-param-reassign": "off", - "no-useless-escape": "off" + "no-useless-escape": "off", + "jsx-a11y/no-noninteractive-element-interactions": "off", + "no-async-promise-executor": "off", + "new-cap": "off" }, parserOptions: { ecmaVersion: 2020, diff --git a/README.md b/README.md index 379c85cc..a2de6302 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- + logo

BSManager

diff --git a/assets/images/gifs/yuruyuri-dance.gif b/assets/images/gifs/yuruyuri-dance.gif deleted file mode 100644 index 03524548..00000000 Binary files a/assets/images/gifs/yuruyuri-dance.gif and /dev/null differ diff --git a/assets/images/third-party-icons/beast-saber.png b/assets/images/third-party-icons/beast-saber.png new file mode 100644 index 00000000..07e7a144 Binary files /dev/null and b/assets/images/third-party-icons/beast-saber.png differ diff --git a/assets/images/third-party-icons/beat-saver.png b/assets/images/third-party-icons/beat-saver.png new file mode 100644 index 00000000..0b48bdd8 Binary files /dev/null and b/assets/images/third-party-icons/beat-saver.png differ diff --git a/assets/images/third-party-icons/model-saber.svg b/assets/images/third-party-icons/model-saber.svg new file mode 100644 index 00000000..55d4a34f --- /dev/null +++ b/assets/images/third-party-icons/model-saber.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/third-party-icons/score-saber.png b/assets/images/third-party-icons/score-saber.png new file mode 100644 index 00000000..55dec558 Binary files /dev/null and b/assets/images/third-party-icons/score-saber.png differ diff --git a/assets/jsons/bs-versions.json b/assets/jsons/bs-versions.json index 05a0c336..4a469a76 100644 --- a/assets/jsons/bs-versions.json +++ b/assets/jsons/bs-versions.json @@ -501,5 +501,13 @@ "ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/d18ad9d7f45a5cc2a467d75f0bdf9d32b9141b14.png", "ReleaseDate": "1667923560", "year": "2022" + }, + { + "BSVersion": "1.27.0", + "BSManifest": "3485516174915301618", + "ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3629368753553708497", + "ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/d9ed8771498be7fc83721aaaaa5b15a08fbeadb1.png", + "ReleaseDate": "1670943003", + "year": "2022" } ] \ No newline at end of file diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index ba728426..e838d4e3 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -1,371 +1,653 @@ { - "misc":{ - "download": "Download", - "verify": "Verify", - "launch": "Launch", - "mods": "Mods", - "maps": "Maps", - "cancel": "Cancel" - }, - "pages":{ - "version-viewer":{ - "launch-mods":{ - "oculus": "Oculus Mod", - "desktop": "Desktop Mod", - "debug": "Debug Mod" - }, - "mods":{ - "loading-mods": "Loading mods...", - "buttons":{ - "more-infos": "More info", - "install-or-update": "Install or update" + "misc": { + "download": "Download", + "verify": "Verify", + "launch": "Launch", + "mods": "Mods", + "maps": "Maps", + "playlists": "Playlists", + "models": "Models", + "cancel": "Cancel", + "delete": "Delete", + "accept": "Accept", + "refuse": "Refuse" + }, + "pages": { + "version-viewer": { + "launch-mods": { + "oculus": "Oculus Mod", + "desktop": "Desktop Mod", + "debug": "Debug Mod" }, - "mods-grid":{ - "header-bar":{ - "name": "Name", - "installed": "Installed", - "latest": "Latest", - "description":"Description", - "dropdown":{ - "uninstall-all":"Uninstall all" + "maps": { + "search-bar": { + "search-placeholder": "Search a map", + "filters-btn": "Filters", + "dropdown": { + "export-maps": "Export maps", + "delete-maps": "Delete maps" + } + }, + "tabs": { + "maps": { + "actions": { + "add-maps": { + "text": "Add" + }, + "link-maps": { + "tooltips": { + "link": "Link maps", + "unlink": "Unling maps" + } + } + }, + "empty-maps":{ + "text":"No maps", + "button":"Download maps" + } } } - } - }, - "dropdown":{ - "open-folder": "Open folder", - "verify-files": "Verify files", - "clone": "Clone", - "edit": "Edit", - "uninstall": "Uninstall" - } - }, - "available-versions":{ - "title": "Select a version", - "steam-release":"Release page" - }, - "settings":{ - "steam":{ - "title":"Steam", - "description":"If you log out of Steam, you will have to log back in to download a new version of Beat Saber.", - "logout":"Log out" - }, - "appearance":{ - "title":"Appearance", - "description":"Choose the two main colours of BSManager.", - "reset":"Reset", - "sub-title":"Theme", - "themes":{ - "dark":"Dark", - "light": "Light", - "os": "Sync with computer" - } - }, - "installation-folder":{ - "title":"Installation folder", - "description":"Change the default folder for Beat Saber versions and other upcoming features.", - "choose-folder":"Choose folder" - }, - "language":{ - "title":"Language", - "description":"Select a language.", - "languages":{ - "en-EN": "English, UK", - "en-US": "English, US", - "fr-FR": "Français", - "es-ES": "Español", - "translated":{ - "en-EN": "English, United Kingdom", - "en-US": "English, USA", - "fr-FR": "French", - "es-ES": "Spanish" - } - } - }, - "patreon":{ - "title":"Support BSManager 💖", - "description": "Support the project and help us to provide continuous improvements to BSManager.", - "buttons":{ - "support": "Support BSManager", - "supporters":"Supporters 👀" }, - "view":{ - "no-supporters": "No supporter yet", - "sponsors":"Sponsors", - "supporters":"Supporters" + "mods": { + "loading-mods": "Loading mods...", + "buttons": { + "more-infos": "More info", + "install-or-update": "Install or update" + }, + "mods-grid": { + "header-bar": { + "name": "Name", + "installed": "Installed", + "latest": "Latest", + "description": "Description", + "dropdown": { + "uninstall-all": "Uninstall all" + } + } + } + }, + "dropdown": { + "open-folder": "Open folder", + "verify-files": "Verify files", + "clone": "Clone", + "edit": "Edit", + "uninstall": "Uninstall" } }, - "contribution":{ - "description":"Suggest new features or report a bug to help improve BSManager!", - "buttons":{ - "request-features":"Request a feature", - "report-bug":"Report a bug", - "open-logs": "Open logs" + "available-versions": { + "title": "Select a version", + "steam-release": "Release page" + }, + "settings": { + "steam": { + "title": "Steam", + "description": "If you log out of Steam, you will have to log back in to download a new version of Beat Saber.", + "logout": "Log out" + }, + "appearance": { + "title": "Appearance", + "description": "Choose the two main colours of BSManager.", + "reset": "Reset", + "sub-title": "Theme", + "themes": { + "dark": "Dark", + "light": "Light", + "os": "Sync with computer" + } + }, + "installation-folder": { + "title": "Installation folder", + "description": "Change the default folder for Beat Saber versions and other upcoming features.", + "choose-folder": "Choose folder" + }, + "additional-content": { + "title": "Additional content", + "description": "Additional content allows you to customize BeatSaber!", + "deep-links": { + "sub-title": "OneClick installations" + } + }, + "language": { + "title": "Language", + "description": "Select a language.", + "languages": { + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated": { + "en-EN": "English, United Kingdom", + "en-US": "English, USA", + "fr-FR": "French", + "es-ES": "Spanish" + } + } + }, + "patreon": { + "title": "Support BSManager 💖", + "description": "Support the project and help us to provide continuous improvements to BSManager.", + "buttons": { + "support": "Support BSManager", + "supporters": "Supporters 👀" + }, + "view": { + "no-supporters": "No supporter yet", + "sponsors": "Sponsors", + "supporters": "Supporters" + } + }, + "discord": { + "description": "Join the BSManager community by joining our Discord!" + }, + "contribution": { + "description": "Suggest new features or report a bug to help improve BSManager!", + "buttons": { + "request-features": "Request a feature", + "report-bug": "Report a bug", + "open-logs": "Open logs" + } } } - } - }, - "notifications":{ - "types":{ - "error": "🚨 Error", - "warning": "⚠️ Warning", - "success": "🎉 Success" - }, - "common":{ - "msg":{ + }, + "notifications": { + "types": { + "error": "🚨 Error", + "warning": "⚠️ Warning", + "success": "🎉 Success" + }, + "common": { + "msg": { "error-occurred": "An error has occurred" } }, - "shared":{ - "errors":{ - "titles":{ - "operation-running": "Operation running" + "shared": { + "errors": { + "titles": { + "operation-running": "Operation running" + }, + "msg": { + "operation-running": "Wait for the end of the current operation then start again." + } + } + }, + "bs-download": { + "success": { + "titles": { + "download-success": "Download complete", + "verification-finished": "Verification complete" + } }, - "msg":{ - "operation-running": "Wait for the end of the current operation then start again." - } - } - }, - "bs-download":{ - "success":{ - "titles":{ - "download-success": "Download complete", - "verification-finished":"Verification complete" - } - }, - "warnings":{ - "msg":{ - "ManifestChecksum": "The previously downloaded manifest does not match the new 🤔", - "ConnectionTimeout": "Your internet connection seems unstable 🥶", - "ConnectionLost": "The connection has been lost, try again...", - "ConnectionError": "Unable to connect to Steam, retry...", - "Unknown": "Something strange has happened 🤔 Your connection is probably unstable." - } - }, - "errors":{ - "msg":{ - "Password": "Password is invalid.", - "InvalidCredentials": "Your e-mail or password is invalid 😴", - "InvalidPassword": "Your password is invalid 😕", - "NoManifest": "No manifest was found", - "DirectoryCreate": "Unable to install the necessary folders.", - "NotAvailableApp": "Are you trying to download BeatSaber when you don't have it ? 🤣", - "DepotNotFound": "Unable to download BeatSaber 😥 try again later 😕", - "NotCompleted": "The download could not complete ¯\\_(ツ)_/¯", - "InvalidManifest": "Unable to download BeatSaber 😥 try again later 😕", - "NoValidKey": "Unable to download BeatSaber 😥 try again later 😕", - "NoManifestCode": "Unable to download BeatSaber 😥 try again later 😕", - "401": "Steam doesn't seem to want to let us download BeatSaber 😢", - "404": "Unable to contact Steam servers.", - "Unknown": "An unknown error occurred ¯\\_(ツ)_/¯", - "NoServer": "Unable to contact Steam servers.", - "NotAllowed": "Apparently you are not allowed to download BeatSaber 🥱", - "ConnectionTimeout": "Cannot connect to Steam 😕", - "SteamLib": "If you have this error, report the bug on GitHub with the logs pls.", - "ConnectionError": "Unable to connect to Steam after 10 tries 🤯", - "LicenceError": "Unable to get the list of licenses.", - "verification-failed": "Verification failed, try again later.", - "RateLimitExceeded": "You've tried too many times, wait a while and try again later.", - "AlreadyDownloading": "A download is already in progress", - "no-internet": "No internet connection." - } - } - }, - "settings":{ - "move-folder":{ - "success":{ - "titles":{ - "transfer-finished":"Transfer complete" - } + "warnings": { + "msg": { + "ManifestChecksum": "The previously downloaded manifest does not match the new 🤔", + "ConnectionTimeout": "Your internet connection seems unstable 🥶", + "ConnectionLost": "The connection has been lost, try again...", + "ConnectionError": "Unable to connect to Steam, retry...", + "Unknown": "Something strange has happened 🤔 Your connection is probably unstable." + } }, - "errors":{ - "titles":{ - "transfer-failed":"Transfer failed 😕" - } - } - }, - "steam":{ - "success":{ - "titles":{ - "logout": "Disconnected from Steam." - } - } - } - }, - "bs-launch":{ - "success":{ - "titles":{ - "launching": "Launching...🚀" - } - }, - "errors":{ - "titles":{ - "UNABLE_TO_LAUNCH": "Unable to launch", - "STEAM_NOT_RUNNING": "Steam is not running", - "OCULUS_NOT_RUNNING": "Oculus is not running", - "BS_ALREADY_RUNNING": "BeatSaber already running", - "EXE_NOT_FINDED": "Missing files", - "EXIT": "Abrupt stop" - }, - "msg":{ - "STEAM_NOT_RUNNING": "Steam must be running to launch BeatSaber.", - "OCULUS_NOT_RUNNING": "Oculus must be running to launch BeatSaber.", - "BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.", - "EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.", - "EXIT": "BeatSaber stopped abruptly, tries to check the files." - } - } - }, - "custom-version":{ - "errors":{ - "titles":{ - "CantEditSteam": "Unable to edit", - "CantRename": "Renaming impossible", - "VersionAlreadExist": "This version already exists", - "CantClone": "Cloning impossible" - }, - "msg":{ - "CantEditSteam": "You can't edit the Steam version. You can clone it though." - } - }, - "success":{ - "titles":{ - "CloningFinished": "Cloning complete 🎉" - } - } - }, - "mods":{ - "install-mods":{ + "errors": { "titles":{ + "dotnet-required": ".NET 6 Required" + }, + "msg": { + "401": "Steam doesn't seem to want to let us download BeatSaber 😢", + "404": "Unable to contact Steam servers.", + "Password": "Password is invalid.", + "InvalidCredentials": "Your e-mail or password is invalid 😴", + "InvalidPassword": "Your password is invalid 😕", + "NoManifest": "No manifest was found", + "DirectoryCreate": "Unable to install the necessary folders.", + "NotAvailableApp": "Are you trying to download BeatSaber when you don't have it ? 🤣", + "DepotNotFound": "Unable to download BeatSaber 😥 try again later 😕", + "NotCompleted": "The download could not complete ¯\\_(ツ)_/¯", + "InvalidManifest": "Unable to download BeatSaber 😥 try again later 😕", + "NoValidKey": "Unable to download BeatSaber 😥 try again later 😕", + "NoManifestCode": "Unable to download BeatSaber 😥 try again later 😕", + "Unknown": "An unknown error occurred ¯\\_(ツ)_/¯", + "NoServer": "Unable to contact Steam servers.", + "NotAllowed": "Apparently you are not allowed to download BeatSaber 🥱", + "ConnectionTimeout": "Cannot connect to Steam 😕", + "SteamLib": "If you have this error, report the bug on GitHub with the logs pls.", + "ConnectionError": "Unable to connect to Steam after 10 tries 🤯", + "LicenceError": "Unable to get the list of licenses.", + "verification-failed": "Verification failed, try again later.", + "RateLimitExceeded": "You've tried too many times, wait a while and try again later.", + "AlreadyDownloading": "A download is already in progress", + "no-internet": "No internet connection.", + "dotnet-required": ".NET 6 Runtime must be installed in order to download a version of BeatSaber. Download it by clicking the button below." + }, + "actions":{ + "download-dotnet": "Download .NET 6" + } + } + }, + "settings": { + "move-folder": { + "success": { + "titles": { + "transfer-finished": "Transfer complete" + } + }, + "errors": { + "titles": { + "transfer-failed": "Transfer failed 😕" + } + } + }, + "steam": { + "success": { + "titles": { + "logout": "Disconnected from Steam." + } + } + }, + "additional-content":{ + "deep-link": { + "activation":{ + "success":{ + "title":"OneClick activated!", + "description":"OneClick installations have been activated." + }, + "error":{ + "description":"Unable to activate OneClick installations." + } + }, + "deactivation":{ + "success":{ + "title":"OneClick deactivated!", + "description":"OneClick installations have been deactivated." + }, + "error":{ + "description":"An unknown error occurred." + } + }, + "check-all-enabled":{ + "title": "OneClick Disabled", + "description": "One or more OneClick installations are disabled. Go to settings to enable them.", + "actions":{ + "settings": "Settings", + "not-remind": "Do not remind me" + } + } + } + } + }, + "bs-launch": { + "success": { + "titles": { + "launching": "Launching...🚀" + } + }, + "errors": { + "titles": { + "UNABLE_TO_LAUNCH": "Unable to launch", + "STEAM_NOT_RUNNING": "Steam is not running", + "OCULUS_NOT_RUNNING": "Oculus is not running", + "BS_ALREADY_RUNNING": "BeatSaber already running", + "EXE_NOT_FINDED": "Missing files", + "EXIT": "Abrupt stop" + }, + "msg": { + "STEAM_NOT_RUNNING": "Steam must be running to launch BeatSaber.", + "OCULUS_NOT_RUNNING": "Oculus must be running to launch BeatSaber.", + "BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.", + "EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.", + "EXIT": "BeatSaber stopped abruptly, tries to check the files." + }, + "actions":{ + "STEAM_NOT_RUNNING": "Launch Steam" + } + } + }, + "steam":{ + "steam-launching":{ + "title": "Steam is launching!", + "description": "Steam may take some time to launch depending on your configuration." + } + }, + "custom-version": { + "errors": { + "titles": { + "CantEditSteam": "Unable to edit", + "CantRename": "Renaming impossible", + "VersionAlreadExist": "This version already exists", + "CantClone": "Cloning impossible" + }, + "msg": { + "CantEditSteam": "You can't edit the Steam version. You can clone it though." + } + }, + "success": { + "titles": { + "CloningFinished": "Cloning complete 🎉" + } + } + }, + "mods": { + "install-mods": { + "titles": { "success": "Mods installed 🎉", "warning": "Mods installed 🤔" }, - "msg":{ + "msg": { "success": "All mods have been installed.", "warning": "One or more mods could not be installed.", - "errors":{ + "errors": { "no-mods": "No mods to install.", "cannot-install-bsipa": "BSIPA installation failed 😨" } } }, - "uninstall-mod":{ - "titles":{ + "uninstall-mod": { + "titles": { "success": "Mod uninstalled 🎉" }, - "msg":{ - "errors":{ + "msg": { + "errors": { "no-mods": "This mod isn't installed 😑" } } }, - "uninstall-all-mods":{ - "titles":{ + "uninstall-all-mods": { + "titles": { "success": "Mods uninstalled 🎉" }, - "msg":{ + "msg": { "success": "All mods have been uninstalled.", "errors": { "no-mods": "No mod is installed in this version 😑" } } } + }, + "maps":{ + "one-click-install":{ + "success":"Map installation complete", + "error":"An error occurred while installing the map" + } + }, + "playlists":{ + "one-click-install":{ + "success":"Playlist installation complete", + "error":"An error occurred while installing the playlist" + } + }, + "models":{ + "one-click-install":{ + "success":"Model installation complete", + "error":"An error occurred while installing the model" + } } - }, - "modals":{ - "guard":{ - "title": "Steam Guard", - "inputs":{ - "guard-code":{ - "label": "Guard Code", - "placeholder": "Enter your Guard code" - } - }, - "buttons":{ - "submit": "Login" - } - }, - "steam-login":{ - "title": "Steam Login", - "inputs":{ - "username":{ - "label": "Username", - "placeholder": "Enter your username" + }, + "modals": { + "guard": { + "title": "Steam Guard", + "inputs": { + "guard-code": { + "label": "Guard Code", + "placeholder": "Enter your Guard code" + } }, - "password":{ - "label": "Password", - "placeholder": "Enter your password" - }, - "stay":{ - "label": "Stay connected" + "buttons": { + "submit": "Login" } - }, - "buttons":{ - "submit": "Login" - } - }, - "bs-uninstall":{ - "title": "Uninstall", - "description": "Are you sure you want to uninstall BeatSaber {version} ? You will have to download it again to play it.", - "buttons":{ - "submit": "Uninstall" - } - }, - "install-folder":{ - "title": "Installation folder", - "description": "Changing the default installation folder will result in moving all installed data to the new folder.", - "buttons":{ - "submit": "Choose folder" - } - }, - "edit-version":{ - "title": "Edit the version", - "buttons":{ - "submit": "Edit" - } - }, - "clone-version":{ - "title": "Clone version", - "description": "Cloning the version allows you to separate additional BeatSaber content between two versions.", - "inputs":{ - "name":{ - "label":"Name", - "placeholder": "Version name" + }, + "steam-login": { + "title": "Steam Login", + "inputs": { + "username": { + "label": "Username", + "placeholder": "Enter your username" + }, + "password": { + "label": "Password", + "placeholder": "Enter your password" + }, + "stay": { + "label": "Stay connected" + } }, - "color":{ - "label":"Color" + "buttons": { + "submit": "Login" } - }, - "buttons":{ - "submit": "Clone" - } - }, - "uninstall-mod":{ + }, + "bs-uninstall": { + "title": "Uninstall", + "description": "Are you sure you want to uninstall BeatSaber {version} ? You will have to download it again to play it.", + "buttons": { + "submit": "Uninstall" + } + }, + "install-folder": { + "title": "Installation folder", + "description": "Changing the default installation folder will result in moving all installed data to the new folder.", + "buttons": { + "submit": "Choose folder" + } + }, + "edit-version": { + "title": "Edit the version", + "buttons": { + "submit": "Edit" + } + }, + "clone-version": { + "title": "Clone version", + "description": "Cloning the version allows you to separate additional BeatSaber content between two versions.", + "inputs": { + "name": { + "label": "Name", + "placeholder": "Version name" + }, + "color": { + "label": "Color" + } + }, + "buttons": { + "submit": "Clone" + } + }, + "uninstall-mod": { "title": "Uninstall", "description": "Are you sure you want to uninstall {mod}? It could cause other installed mods to malfunction.", "description-bsipa": "Are you sure you want to uninstall BSIPA? After that all installed mods will not work anymore." }, - "uninstall-all-mods":{ + "uninstall-all-mods": { "title": "Uninstalling mods", "description": "Are you sure you want to uninstall all the mods from version {version}? This operation cannot be undone." + }, + "maps-actions": { + "delete-maps": { + "title": { + "single": "Delete the map?", + "multiple": "Delete maps ?" + }, + "desc": { + "single": "Are you sure you want to delete the map {name}?", + "multiple": "Are you sure you want to delete all {nb} maps?" + }, + "info": { + "desc": { + "single": "This map is part of the shared maps", + "multiple": "These maps are part of the shared maps" + }, + "title": { + "single": "This map will also be removed from versions using shared maps", + "multiple": "These maps will also be removed from versions using shared maps" + } + } + }, + "link-maps": { + "title": "Link maps", + "desc": "Linking maps allows to share maps between all versions. Once linked, this version will benefit from the shared maps", + "info": "Adding and deleting maps will also be shared", + "keep-maps": { + "label": "Keep maps", + "title": "Keep maps will move the maps of the current version to the shared maps folder. Otherwise they will be lost" + }, + "valid-btn": "Link maps" + }, + "unlink-maps": { + "title": "Unlink maps", + "desc": "Please note that unlinking maps will not allow the use of shared maps for this version.", + "keep-maps": { + "label": "Keep maps", + "title": "Keeping the maps will create a copy of the shared maps for the current version. Otherwise, no map will be kept for this version." + }, + "valid-btn": "Unlink maps" + } + }, + "download-maps": { + "search-btn": "Search", + "loading-maps": "Loading maps..." + }, + "mods-disclaimer":{ + "title": "disclaimer", + "p-1": "By choosing to use mods, you understand that:", + "li-1": "You may experience problems that don't exist in the vanilla game. 99.9% of bugs, crashes, and lag are due to mods.", + "li-2": "Mods are subject to being broken by updates and that's normal - be patient and respectful when this happens, as modders are volunteers with real lives.", + "li-3": "Beat Games aren't purposefully trying to break mods. They wish to work on the codebase and sometimes this breaks mods, but they are not out to kill mods.", + "p-2": "Do not attack the game developers for issues related to mods, and vice versa - modders and game developers are two separate groups. Just don't be a jerk ok." } - }, - "auto-update":{ - "checking": "Checking for updates", - "downloading": "Downloading updates" - }, - "dateformat":{ + }, + "maps": { + "map-filter-panel": { + "duration": "Duration", + "tags": "tags", + "specificities": "general", + "requirements": "requirements" + }, + "map-types": { + "accuracy": "accuracy", + "balanced": "balanced", + "challenge": "challenge", + "dancestyle": "dance", + "fitness": "fitness", + "speed": "speed", + "tech": "tech" + }, + "map-styles": { + "dance": "dance", + "swing": "swing", + "nightcore": "nightcore", + "folk": "folk", + "family": "family", + "ambient": "ambient", + "funk": "funk", + "jazz": "jazz", + "soul": "soul", + "speedcore": "speedcore", + "punk": "punk", + "rb": "r&b", + "holiday": "holiday", + "vocaloid": "vocaloid", + "jrock": "j-rock", + "trance": "trance", + "drumbass": "drum & bass", + "comedy": "comedy", + "instrumental": "instrumental", + "hardcore": "hardcore", + "kpop": "k-pop", + "indie": "indie", + "techno": "techno", + "house": "house", + "game": "video game", + "film": "film", + "alt": "alternative", + "dubstep": "dubstep", + "metal": "metal", + "anime": "anime", + "hiphop": "hiphop", + "jpop": "j-pop", + "rock": "rock", + "pop": "pop", + "electronic": "electronic", + "classical-orchestral": "Classical & Orchestral" + }, + "map-specificities": { + "automapper": "AI", + "ranked": "ranked", + "curated": "curated", + "verified": "verified", + "fullSpread": "full spread" + }, + "difficulties": { + "Easy": "easy", + "Normal": "normal", + "Hard": "hard", + "Expert": "expert", + "ExpertPlus": "expert+" + }, + "map-item": { + "by": "By {songAutor}", + "mapped-by": "mapped by" + } + }, + "beat-saver": { + "maps-sorts": { + "Latest": "Latest", + "Relevance": "Relevance", + "Rating": "Rating", + "Curated": "Curated" + } + }, + "auto-update": { + "checking": "Checking for updates", + "downloading": "Downloading updates" + }, + "dateformat": { "dayNames": [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" ], "monthNames": [ - "Jan", "Feb", "Mar", "Apr", "May", "June", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "June", + "Jul", + "Aug", + "Sept", + "Oct", + "Nov", + "Dec", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" ], "timeNames": [ - "a", "p", "am", "pm", "A", "P", "AM", "PM" + "a", + "p", + "am", + "pm", + "A", + "P", + "AM", + "PM" ] - } + } } \ No newline at end of file diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index e5ba85b3..bb4209be 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -1,372 +1,653 @@ { - "misc":{ - "download": "Descargar", - "verify": "Verificar", - "launch": "Lanzar", - "mods": "Mods", - "maps": "Maps", - "cancel": "Cancelar" - }, - "pages":{ - "version-viewer":{ - "launch-mods":{ - "oculus": "Oculus Mod", - "desktop": "Desktop Mod", - "debug": "Debug Mod" - }, - "mods":{ - "loading-mods": "Cargando mods...", - "buttons":{ - "more-infos": "Más información", - "install-or-update": "Instalar o actualizar" + "misc": { + "download": "Descargar", + "verify": "Verificar", + "launch": "Lanzar", + "mods": "Mods", + "maps": "Mapas", + "playlists": "Playlists", + "models": "Modelos", + "cancel": "Cancelar", + "delete": "Borrar", + "accept": "Aceptar", + "refuse": "Rechazar" + }, + "pages": { + "version-viewer": { + "launch-mods": { + "oculus": "Oculus Mod", + "desktop": "Desktop Mod", + "debug": "Debug Mod" }, - "mods-grid":{ - "header-bar":{ - "name": "Nombre", - "installed": "Instalado", - "latest": "Último", - "description":"Descripción", - "dropdown":{ - "uninstall-all":"Desinstalar todos" + "maps": { + "search-bar": { + "search-placeholder": "Buscar en un mapa", + "filters-btn": "Filtros", + "dropdown": { + "export-maps": "Exportar mapas", + "delete-maps": "Borrar mapas" + } + }, + "tabs": { + "maps": { + "actions": { + "add-maps": { + "text": "Añadir" + }, + "link-maps": { + "tooltips": { + "link": "Vincular los mapas", + "unlink": "Desatar los mapas" + } + } + }, + "empty-maps":{ + "text":"No hay mapas", + "button":"Descargar mapas" + } } } - } - }, - "dropdown":{ - "open-folder": "Abrir la carpeta", - "verify-files": "Verificar los archivos", - "clone": "Clonar", - "edit": "Editar", - "uninstall": "Desinstalar" - } - }, - "available-versions":{ - "title": "Seleccione una versión", - "steam-release":"Publicación Página" - }, - "settings":{ - "steam":{ - "title":"Steam", - "description":"Si sales de Steam, tendrás que volver a entrar para descargar una nueva versión de Beat Saber.", - "logout":"Desconectar" - }, - "appearance":{ - "title":"Apariencia", - "description":"Elija los dos colores principales de BSManager.", - "reset":"Reiniciar", - "sub-title":"Tema", - "themes":{ - "dark":"Oscuro", - "light": "Claro", - "os": "Sincronizar con ordenador" - } - }, - "installation-folder":{ - "title":"Carpeta de instalación", - "description":"Cambia la carpeta por defecto para las versiones de Beat Saber y otras características próximas.", - "choose-folder":"Elija la carpeta" - }, - "language":{ - "title":"Idioma", - "description":"Selecciona un idioma.", - "languages":{ - "en-EN": "English, UK", - "en-US": "English, US", - "fr-FR": "Français", - "es-ES": "Español", - "translated":{ - "en-EN": "Inglés, Reino Unido", - "en-US": "Inglés, Estados Unidos", - "fr-FR": "Francés", - "es-ES": "Español" - } - } - }, - "patreon":{ - "title":"Apoyo BSManager 💖", - "description": "Apoye el proyecto y ayúdenos a proporcionar mejoras continuas a BSManager.", - "buttons":{ - "support": "Apoyo BSManager", - "supporters":"Apoyos 👀" }, - "view":{ - "no-supporters": "Todavía no hay apoyos", - "sponsors":"Patrocinadores", - "supporters":"Apoyos" + "mods": { + "loading-mods": "Cargando mods...", + "buttons": { + "more-infos": "Más información", + "install-or-update": "Instalar o actualizar" + }, + "mods-grid": { + "header-bar": { + "name": "Nombre", + "installed": "Instalado", + "latest": "Último", + "description": "Descripción", + "dropdown": { + "uninstall-all": "Desinstalar todos" + } + } + } + }, + "dropdown": { + "open-folder": "Abrir la carpeta", + "verify-files": "Verificar los archivos", + "clone": "Clonar", + "edit": "Editar", + "uninstall": "Desinstalar" } }, - "contribution":{ - "description":"¡Sugiera nuevas características o reporte un error para ayudar a mejorar BSManager!", - "buttons":{ - "request-features":"Proponer una funcionalidad", - "report-bug":"Informar de un error", - "open-logs": "Abrir los registros" + "available-versions": { + "title": "Seleccione una versión", + "steam-release": "Publicación Página" + }, + "settings": { + "steam": { + "title": "Steam", + "description": "Si sales de Steam, tendrás que volver a entrar para descargar una nueva versión de Beat Saber.", + "logout": "Desconectar" + }, + "appearance": { + "title": "Apariencia", + "description": "Elija los dos colores principales de BSManager.", + "reset": "Reiniciar", + "sub-title": "Tema", + "themes": { + "dark": "Oscuro", + "light": "Claro", + "os": "Sincronizar con ordenador" + } + }, + "installation-folder": { + "title": "Carpeta de instalación", + "description": "Cambia la carpeta por defecto para las versiones de Beat Saber y otras características próximas.", + "choose-folder": "Elija la carpeta" + }, + "additional-content": { + "title": "Contenido adicional", + "description": "¡El contenido adicional te permite personalizar BeatSaber!", + "deep-links": { + "sub-title": "Instalaciones OneClick" + } + }, + "language": { + "title": "Idioma", + "description": "Selecciona un idioma.", + "languages": { + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated": { + "en-EN": "Inglés, Reino Unido", + "en-US": "Inglés, Estados Unidos", + "fr-FR": "Francés", + "es-ES": "Español" + } + } + }, + "patreon": { + "title": "Apoyo BSManager 💖", + "description": "Apoye el proyecto y ayúdenos a proporcionar mejoras continuas a BSManager.", + "buttons": { + "support": "Apoyo BSManager", + "supporters": "Apoyos 👀" + }, + "view": { + "no-supporters": "Todavía no hay apoyos", + "sponsors": "Patrocinadores", + "supporters": "Apoyos" + } + }, + "discord": { + "description": "¡Únete a la comunidad de BSManager uniéndote a nuestro Discord!" + }, + "contribution": { + "description": "¡Sugiera nuevas características o reporte un error para ayudar a mejorar BSManager!", + "buttons": { + "request-features": "Proponer una funcionalidad", + "report-bug": "Informar de un error", + "open-logs": "Abrir los registros" + } } } - } - }, - - "notifications":{ - "types":{ - "error": "🚨 Error", - "warning": "⚠️ Advertencia", - "success": "🎉 Éxito" - }, - "common":{ - "msg":{ + }, + "notifications": { + "types": { + "error": "🚨 Error", + "warning": "⚠️ Advertencia", + "success": "🎉 Éxito" + }, + "common": { + "msg": { "error-occurred": "Se ha producido un error" } }, - "shared":{ - "errors":{ - "titles":{ - "operation-running": "Operación en curso" + "shared": { + "errors": { + "titles": { + "operation-running": "Operación en curso" + }, + "msg": { + "operation-running": "Espere a que termine la operación actual y vuelva a empezar." + } + } + }, + "bs-download": { + "success": { + "titles": { + "download-success": "Descargar completo", + "verification-finished": "Verificación completa" + } }, - "msg":{ - "operation-running": "Espere a que termine la operación actual y vuelva a empezar." - } - } - }, - "bs-download":{ - "success":{ - "titles":{ - "download-success": "Descargar completo", - "verification-finished":"Verificación completa" - } - }, - "warnings":{ - "msg":{ - "ManifestChecksum": "El manifiesto descargado anteriormente no coincide con el nuevo 🤔", - "ConnectionTimeout": "Su conexión a Internet parece inestable 🥶", - "ConnectionLost": "Se ha perdido la conexión, inténtelo de nuevo...", - "ConnectionError": "No se puede conectar a Steam, reintentar...", - "Unknown": "Algo extraño ha ocurrido 🤔 Tu conexión es probablemente inestable." - } - }, - "errors":{ - "msg":{ - "Password": "La contraseña es inválida.", - "InvalidCredentials": "Su correo electrónico o contraseña no son válidos 😴", - "InvalidPassword": "Su contraseña no es válida 😕", - "NoManifest": "No se ha encontrado el manifiesto", - "DirectoryCreate": "No se pueden instalar las carpetas necesarias.", - "NotAvailableApp": "¿Intentas descargar BeatSaber cuando no lo tienes? 🤣", - "DepotNotFound": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", - "NotCompleted": "La descarga no pudo completarse ¯\\_(ツ)_/¯", - "InvalidManifest": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", - "NoValidKey": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", - "NoManifestCode": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", - "401": "Parece que Steam no quiere dejarnos descargar BeatSaber 😢", - "404": "No se puede contactar con los servidores de Steam", - "Unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯", - "NoServer": "No se puede contactar con los servidores de Steam.", - "NotAllowed": "Aparentemente no tienes permiso para descargar BeatSaber 🥱", - "ConnectionTimeout": "No se puede conectar a Steam 😕", - "SteamLib": "Si tienes este error, reporta el bug en GitHub con los logs por favor.", - "ConnectionError": "No se puede conectar a Steam después de 10 intentos 🤯", - "LicenceError": "No se puede obtener la lista de licencias.", - "verification-failed": "Verificación fallida, inténtelo más tarde.", - "RateLimitExceeded": "Lo has intentado demasiadas veces, espera un poco y vuelve a intentarlo más tarde.", - "AlreadyDownloading": "La descarga ya está en curso", - "no-internet": "Sin conexión a Internet." - } - } - }, - "settings":{ - "move-folder":{ - "success":{ - "titles":{ - "transfer-finished":"Transferencia completa" - } + "warnings": { + "msg": { + "ManifestChecksum": "El manifiesto descargado anteriormente no coincide con el nuevo 🤔", + "ConnectionTimeout": "Su conexión a Internet parece inestable 🥶", + "ConnectionLost": "Se ha perdido la conexión, inténtelo de nuevo...", + "ConnectionError": "No se puede conectar a Steam, reintentar...", + "Unknown": "Algo extraño ha ocurrido 🤔 Tu conexión es probablemente inestable." + } }, - "errors":{ - "titles":{ - "transfer-failed":"Transferencia fallida 😕" - } - } - }, - "steam":{ - "success":{ - "titles":{ - "logout": "Desconectado de Steam." - } - } - } - }, - "bs-launch":{ - "success":{ - "titles":{ - "launching": "Lanzamiento...🚀" - } - }, - "errors":{ - "titles":{ - "UNABLE_TO_LAUNCH": "No se puede lanzar", - "STEAM_NOT_RUNNING": "Steam no funciona", - "OCULUS_NOT_RUNNING": "Oculus no funciona", - "BS_ALREADY_RUNNING": "BeatSaber ya está en marcha", - "EXE_NOT_FINDED": "Archivos perdidos", - "EXIT": "Parada abrupta" - }, - "msg":{ - "STEAM_NOT_RUNNING": "Para iniciar BeatSaber es necesario que Steam esté en funcionamiento.", - "OCULUS_NOT_RUNNING": "Oculus debe estar funcionando para lanzar BeatSaber.", - "BS_ALREADY_RUNNING": "Cierra BeatSaber antes de volver a lanzarlo.", - "EXE_NOT_FINDED": "Parece que faltan algunos archivos, intente verificar los archivos.", - "EXIT": "BeatSaber se detuvo abruptamente, trata de revisar los archivos." - } - } - }, - "custom-version":{ - "errors":{ - "titles":{ - "CantEditSteam": "Imposible editar", - "CantRename": "El renombramiento es imposible", - "VersionAlreadExist": "Esta versión ya existe", - "CantClone": "Clonación imposible" - }, - "msg":{ - "CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla." - } - }, - "success":{ - "titles":{ - "CloningFinished": "Clonación completa 🎉" - } - } - }, - "mods":{ - "install-mods":{ + "errors": { "titles":{ + "dotnet-required": ".NET 6 Requerido" + }, + "msg": { + "401": "Parece que Steam no quiere dejarnos descargar BeatSaber 😢", + "404": "No se puede contactar con los servidores de Steam", + "Password": "La contraseña es inválida.", + "InvalidCredentials": "Su correo electrónico o contraseña no son válidos 😴", + "InvalidPassword": "Su contraseña no es válida 😕", + "NoManifest": "No se ha encontrado el manifiesto", + "DirectoryCreate": "No se pueden instalar las carpetas necesarias.", + "NotAvailableApp": "¿Intentas descargar BeatSaber cuando no lo tienes? 🤣", + "DepotNotFound": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", + "NotCompleted": "La descarga no pudo completarse ¯\\_(ツ)_/¯", + "InvalidManifest": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", + "NoValidKey": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", + "NoManifestCode": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕", + "Unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯", + "NoServer": "No se puede contactar con los servidores de Steam.", + "NotAllowed": "Aparentemente no tienes permiso para descargar BeatSaber 🥱", + "ConnectionTimeout": "No se puede conectar a Steam 😕", + "SteamLib": "Si tienes este error, reporta el bug en GitHub con los logs por favor.", + "ConnectionError": "No se puede conectar a Steam después de 10 intentos 🤯", + "LicenceError": "No se puede obtener la lista de licencias.", + "verification-failed": "Verificación fallida, inténtelo más tarde.", + "RateLimitExceeded": "Lo has intentado demasiadas veces, espera un poco y vuelve a intentarlo más tarde.", + "AlreadyDownloading": "La descarga ya está en curso", + "no-internet": "Sin conexión a Internet.", + "dotnet-required": "Se debe instalar el tiempo de ejecución de .NET 6 para descargar una versión de BeatSaber. Descárguelo haciendo clic en el botón de abajo." + }, + "actions":{ + "download-dotnet": "Descargar .NET 6" + } + } + }, + "settings": { + "move-folder": { + "success": { + "titles": { + "transfer-finished": "Transferencia completa" + } + }, + "errors": { + "titles": { + "transfer-failed": "Transferencia fallida 😕" + } + } + }, + "steam": { + "success": { + "titles": { + "logout": "Desconectado de Steam." + } + } + }, + "additional-content":{ + "deep-link": { + "activation":{ + "success":{ + "title":"¡OneClick activado!", + "description":"Las instalaciones OneClick han sido activadas." + }, + "error":{ + "description":"No se pueden activar las instalaciones OneClick." + } + }, + "deactivation":{ + "success":{ + "title":"¡OneClick desactivado!", + "description":"Las instalaciones OneClick han sido desactivadas." + }, + "error":{ + "description":"Ocurrió un error desconocido." + } + }, + "check-all-enabled":{ + "title": "OneClick deshabilitado", + "description": "Una o más instalaciones de OneClick están deshabilitadas. Vaya a la configuración para habilitarlas.", + "actions":{ + "settings": "Ajustes", + "not-remind": "No volver a recordarme" + } + } + } + } + }, + "bs-launch": { + "success": { + "titles": { + "launching": "Lanzamiento...🚀" + } + }, + "errors": { + "titles": { + "UNABLE_TO_LAUNCH": "No se puede lanzar", + "STEAM_NOT_RUNNING": "Steam no funciona", + "OCULUS_NOT_RUNNING": "Oculus no funciona", + "BS_ALREADY_RUNNING": "BeatSaber ya está en marcha", + "EXE_NOT_FINDED": "Archivos perdidos", + "EXIT": "Parada abrupta" + }, + "msg": { + "STEAM_NOT_RUNNING": "Para iniciar BeatSaber es necesario que Steam esté en funcionamiento.", + "OCULUS_NOT_RUNNING": "Oculus debe estar funcionando para lanzar BeatSaber.", + "BS_ALREADY_RUNNING": "Cierra BeatSaber antes de volver a lanzarlo.", + "EXE_NOT_FINDED": "Parece que faltan algunos archivos, intente verificar los archivos.", + "EXIT": "BeatSaber se detuvo abruptamente, trata de revisar los archivos." + }, + "actions":{ + "STEAM_NOT_RUNNING": "Iniciar Steam" + } + } + }, + "steam":{ + "steam-launching":{ + "title": "¡Steam se está iniciando!", + "description": "Steam puede tardar un poco en iniciarse dependiendo de tu configuración." + } + }, + "custom-version": { + "errors": { + "titles": { + "CantEditSteam": "Imposible editar", + "CantRename": "El renombramiento es imposible", + "VersionAlreadExist": "Esta versión ya existe", + "CantClone": "Clonación imposible" + }, + "msg": { + "CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla." + } + }, + "success": { + "titles": { + "CloningFinished": "Clonación completa 🎉" + } + } + }, + "mods": { + "install-mods": { + "titles": { "success": "Mods instalados 🎉", "warning": "Mods instalados 🤔" }, - "msg":{ + "msg": { "success": "Todos los mods han sido instalados.", "warning": "No se han podido instalar uno o más mods.", - "errors":{ - "no-mods": "No hay que instalar mods.", + "errors": { + "no-mods": "No hay que instalar mods.", "cannot-install-bsipa": "La instalación de BSIPA falló 😨" } } }, - "uninstall-mod":{ - "titles":{ + "uninstall-mod": { + "titles": { "success": "Mod desinstalado 🎉" }, - "msg":{ - "errors":{ + "msg": { + "errors": { "no-mods": "Este mod no está instalado 😑" } } }, - "uninstall-all-mods":{ - "titles":{ + "uninstall-all-mods": { + "titles": { "success": "Mods desinstalados 🎉" }, - "msg":{ + "msg": { "success": "Todos los mods han sido desinstalados.", "errors": { "no-mods": "No se instala ningún mod en esta versión 😑" } } } + }, + "maps":{ + "one-click-install":{ + "success":"Instalación del mapa completada", + "error":"Se produjo un error durante la instalación del mapa" + } + }, + "playlists":{ + "one-click-install":{ + "success":"Instalación de la lista de reproducción completada", + "error":"Se produjo un error durante la instalación de la lista de reproducción" + } + }, + "models":{ + "one-click-install":{ + "success":"Instalación del modelo completada", + "error":"Se produjo un error durante la instalación del modelo" + } } - }, - "modals":{ - "guard":{ - "title": "Steam Guard", - "inputs":{ - "guard-code":{ - "label": "Guard Código", - "placeholder": "Introduzca su código Guard" - } - }, - "buttons":{ - "submit": "Conéctate" - } - }, - "steam-login":{ - "title": "Steam Conectar", - "inputs":{ - "username":{ - "label": "Nombre de usuario", - "placeholder": "Introduzca su nombre de usuario" + }, + "modals": { + "guard": { + "title": "Steam Guard", + "inputs": { + "guard-code": { + "label": "Guard Código", + "placeholder": "Introduzca su código Guard" + } }, - "password":{ - "label": "Contraseña", - "placeholder": "Introduzca su contraseña" - }, - "stay":{ - "label": "Manténgase conectado" + "buttons": { + "submit": "Conéctate" } - }, - "buttons":{ - "submit": "Inicio de sesión" - } - }, - "bs-uninstall":{ - "title": "Desinstalar", - "description": "¿Estás seguro de que quieres desinstalar BeatSaber {version}? Tendrás que descargarlo de nuevo para poder jugar.", - "buttons":{ - "submit": "Desinstalar" - } - }, - "install-folder":{ - "title": "Instalación Carpeta", - "description": "Al cambiar la carpeta de instalación por defecto, se moverán todos los datos instalados a la nueva carpeta.", - "buttons":{ - "submit": "Elija la carpeta" - } - }, - "edit-version":{ - "title": "Editar la versión", - "buttons":{ - "submit": "Editar" - } - }, - "clone-version":{ - "title": "Versión clonada", - "description": "La clonación de la versión le permite separar el contenido adicional de BeatSaber entre dos versiones.", - "inputs":{ - "name":{ - "label":"Nombre", - "placeholder": "Nombre de la versión" + }, + "steam-login": { + "title": "Steam Conectar", + "inputs": { + "username": { + "label": "Nombre de usuario", + "placeholder": "Introduzca su nombre de usuario" + }, + "password": { + "label": "Contraseña", + "placeholder": "Introduzca su contraseña" + }, + "stay": { + "label": "Manténgase conectado" + } }, - "color":{ - "label":"Color" + "buttons": { + "submit": "Inicio de sesión" } - }, - "buttons":{ - "submit": "Clonar" - } - }, - "uninstall-mod":{ + }, + "bs-uninstall": { + "title": "Desinstalar", + "description": "¿Estás seguro de que quieres desinstalar BeatSaber {version}? Tendrás que descargarlo de nuevo para poder jugar.", + "buttons": { + "submit": "Desinstalar" + } + }, + "install-folder": { + "title": "Instalación Carpeta", + "description": "Al cambiar la carpeta de instalación por defecto, se moverán todos los datos instalados a la nueva carpeta.", + "buttons": { + "submit": "Elija la carpeta" + } + }, + "edit-version": { + "title": "Editar la versión", + "buttons": { + "submit": "Editar" + } + }, + "clone-version": { + "title": "Versión clonada", + "description": "La clonación de la versión le permite separar el contenido adicional de BeatSaber entre dos versiones.", + "inputs": { + "name": { + "label": "Nombre", + "placeholder": "Nombre de la versión" + }, + "color": { + "label": "Color" + } + }, + "buttons": { + "submit": "Clonar" + } + }, + "uninstall-mod": { "title": "Desinstalar", "description": "¿Estás seguro de que quieres desinstalar {mod}? Podría provocar el mal funcionamiento de otros mods instalados.", "description-bsipa": "¿Estás seguro de que quieres desinstalar BSIPA? Después de eso, todos los mods instalados dejarán de funcionar." }, - "uninstall-all-mods":{ + "uninstall-all-mods": { "title": "Desinstalación de mods", "description": "¿Estás seguro de que quieres desinstalar todos los mods de la versión {version}? Esta operación no se puede deshacer." + }, + "maps-actions": { + "delete-maps": { + "title": { + "single": "¿Borrar el mapa?", + "multiple": "¿Eliminar mapas?" + }, + "desc": { + "single": "¿Estás seguro de que quieres eliminar el mapa {name}?", + "multiple": "¿Seguro que quieres eliminar los {nb} mapas?" + }, + "info": { + "desc": { + "single": "Este mapa es parte de los mapas compartidos", + "multiple": "Estos mapas son parte de los mapas compartidos" + }, + "title": { + "single": "Este mapa también se eliminará de las versiones que usan mapas compartidos.", + "multiple": "Estos mapas también se eliminarán de las versiones que usan mapas compartidos." + } + } + }, + "link-maps": { + "title": "Vincular mapas", + "desc": "La vinculación de mapas permite compartir mapas entre todas las versiones. Una vez vinculada, esta versión se beneficiará de los mapas compartidos", + "info": "También se compartirá la adición y eliminación de mapas.", + "keep-maps": { + "label": "Mantener mapas", + "title": "Mantener mapas moverá los mapas de la versión actual a la carpeta de mapas compartidos. De lo contrario, se perderán" + }, + "valid-btn": "Vincular mapas" + }, + "unlink-maps": { + "title": "Desvincular mapas", + "desc": "Tenga en cuenta que desvincular mapas no permitirá el uso de mapas compartidos para esta versión.", + "keep-maps": { + "label": "Mantener mapas", + "title": "Mantener los mapas creará una copia de los mapas compartidos para la versión actual. De lo contrario, no se mantendrá ningún mapa para esta versión." + }, + "valid-btn": "Desvincular mapas" + } + }, + "download-maps": { + "search-btn": "buscar", + "loading-maps": "Cargando mapas..." + }, + "mods-disclaimer":{ + "title": "descargo de responsabilidad", + "p-1": "Al elegir usar mods, entiendes y aceptas que:", + "li-1": "Es posible que encuentres problemas que no existen en el juego base. El 99,9% de los errores, bloqueos y ralentizaciones son debidos a los mods.", + "li-2": "Es probable que los mods dejen de funcionar debido a las actualizaciones y eso es normal. Sé paciente y respetuoso cuando suceda esto, ya que los modders son voluntarios y tienen una vida.", + "li-3": "Beat Games no está intencionalmente tratando de romper los mods. Quieren mejorar el juego base y a veces esto rompe los mods, pero no es su objetivo matar a los mods.", + "p-2": "No ataques a los desarrolladores por problemas relacionados con los mods y viceversa. Los modders y los desarrolladores son dos grupos distintos. No seas tonto, ¿de acuerdo?" } - }, - "auto-update":{ - "checking": "Comprobar las actualizaciones", - "downloading": "Descargar actualizaciones" - }, - "dateformat":{ + }, + "maps": { + "map-filter-panel": { + "duration": "Duración", + "tags": "tags", + "specificities": "general", + "requirements": "requisitos" + }, + "map-types": { + "accuracy": "precisión", + "balanced": "equilibrado", + "challenge": "desafío", + "dancestyle": "baile", + "fitness": "fitness", + "speed": "speed", + "tech": "tec" + }, + "map-styles": { + "dance": "baile", + "swing": "swing", + "nightcore": "nightcore", + "folk": "folk", + "family": "familia", + "ambient": "ambiente", + "funk": "funk", + "jazz": "jazz", + "soul": "soul", + "speedcore": "speedcore", + "punk": "punk", + "rb": "r&b", + "holiday": "vacaciones", + "vocaloid": "vocaloid", + "jrock": "j-rock", + "trance": "trance", + "drumbass": "drum & bass", + "comedy": "comedia", + "instrumental": "instrumental", + "hardcore": "hardcore", + "kpop": "k-pop", + "indie": "indie", + "techno": "tecno", + "house": "house", + "game": "videojuego", + "film": "film", + "alt": "alternativa", + "dubstep": "dubstep", + "metal": "metal", + "anime": "anime", + "hiphop": "hiphop", + "jpop": "j-pop", + "rock": "rock", + "pop": "pop", + "electronic": "electrónico", + "classical-orchestral": "Clásico y orquestal" + }, + "map-specificities": { + "automapper": "IA", + "ranked": "rankado", + "curated": "recomendado", + "verified": "verificado", + "fullSpread": "panel completo" + }, + "difficulties": { + "Easy": "fácil", + "Normal": "normal", + "Hard": "difícil", + "Expert": "experto", + "ExpertPlus": "experto+" + }, + "map-item": { + "by": "Por {songAutor}", + "mapped-by": "mapeado por" + } + }, + "beat-saver": { + "maps-sorts": { + "Latest": "Última", + "Relevance": "Relevancia", + "Rating": "Notas", + "Curated": "Recomendado" + } + }, + "auto-update": { + "checking": "Comprobar las actualizaciones", + "downloading": "Descargar actualizaciones" + }, + "dateformat": { "dayNames": [ - "Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", - "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado" + "Dom", + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb", + "Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado" ], "monthNames": [ - "Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic", - "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", - "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" + "Ene", + "Feb", + "Mar", + "Abr", + "May", + "Jun", + "Jul", + "Ago", + "Sep", + "Oct", + "Nov", + "Dic", + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre" ], "timeNames": [ - "a", "p", "am", "pm", "A", "P", "AM", "PM" + "a", + "p", + "am", + "pm", + "A", + "P", + "AM", + "PM" ] } -} +} \ No newline at end of file diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index e78c0f5f..cdf58eb6 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -1,371 +1,653 @@ { - "misc":{ - "download": "Télécharger", - "verify": "Vérifier", - "launch": "Lancer", - "mods": "Mods", - "maps": "Maps", - "cancel": "Annuler" - }, - "pages":{ - "version-viewer":{ - "launch-mods":{ - "oculus": "Mode Oculus", - "desktop": "Mode Bureau", - "debug": "Mode Debug" - }, - "mods":{ - "loading-mods": "Chargement des mods...", - "buttons":{ - "more-infos": "Plus d'infos", - "install-or-update": "Installer ou mettre à jour" + "misc": { + "download": "Télécharger", + "verify": "Vérifier", + "launch": "Lancer", + "mods": "Mods", + "maps": "Maps", + "playlists": "Playlists", + "models": "Modèles", + "cancel": "Annuler", + "delete": "Supprimer", + "accept": "Accepter", + "refuse": "Refuser" + }, + "pages": { + "version-viewer": { + "launch-mods": { + "oculus": "Mode Oculus", + "desktop": "Mode Bureau", + "debug": "Mode Debug" }, - "mods-grid":{ - "header-bar":{ - "name": "Nom", - "installed": "Installé", - "latest": "Récent", - "description":"Description", - "dropdown":{ - "uninstall-all":"Tout désinstaller" + "maps": { + "search-bar": { + "search-placeholder": "Rechercher une map", + "filters-btn": "Filtres", + "dropdown": { + "export-maps": "Exporter les maps", + "delete-maps": "Supprimer les maps" + } + }, + "tabs": { + "maps": { + "actions": { + "add-maps": { + "text": "Ajouter" + }, + "link-maps": { + "tooltips": { + "link": "Lier les maps", + "unlink": "Délier les maps" + } + } + }, + "empty-maps":{ + "text":"Aucune map", + "button":"Télécharger des maps" + } } } - } - }, - "dropdown":{ - "open-folder": "Ouvrir le dossier", - "verify-files": "Vérifier les fichiers", - "clone": "Cloner", - "edit": "Editer", - "uninstall": "Désinstaller" - } - }, - "available-versions":{ - "title": "Choisi une version", - "steam-release":"Notes de version" - }, - "settings":{ - "steam":{ - "title":"Steam", - "description":"Si tu te déconnectes de Steam, tu devras te reconnecter pour télécharger une nouvelle version de Beat Saber.", - "logout":"Déconnexion" - }, - "appearance":{ - "title":"Apparence", - "description":"Choisi les deux couleurs principales de BSManager.", - "reset":"Réinitialiser", - "sub-title":"Thème", - "themes":{ - "dark":"Sombre", - "light": "Clair", - "os": "Synchroniser avec l'ordinateur" - } - }, - "installation-folder":{ - "title":"Dossier d'installation", - "description":"Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.", - "choose-folder":"Choisir un dossier" - }, - "language":{ - "title":"Langage", - "description":"Sélectionne un langage.", - "languages":{ - "en-EN": "English, UK", - "en-US": "English, US", - "fr-FR": "Français", - "es-ES": "Español", - "translated":{ - "en-EN": "Anglais, Royaume-Uni", - "en-US": "Anglais, États-Unis", - "fr-FR": "Français", - "es-ES": "Espagnol" - } - } - }, - "patreon":{ - "title":"Soutenir BSManager 💖", - "description": "Soutien le projet et aide-nous à fournir des améliorations continues à BSManager.", - "buttons":{ - "support": "Soutenir BSManager", - "supporters":"Supporteurs 👀" }, - "view":{ - "no-supporters": "Pas encore de supporteurs", - "sponsors":"Sponsors", - "supporters":"Supporteurs" + "mods": { + "loading-mods": "Chargement des mods...", + "buttons": { + "more-infos": "Plus d'infos", + "install-or-update": "Installer ou mettre à jour" + }, + "mods-grid": { + "header-bar": { + "name": "Nom", + "installed": "Installé", + "latest": "Récent", + "description": "Description", + "dropdown": { + "uninstall-all": "Tout désinstaller" + } + } + } + }, + "dropdown": { + "open-folder": "Ouvrir le dossier", + "verify-files": "Vérifier les fichiers", + "clone": "Cloner", + "edit": "Editer", + "uninstall": "Désinstaller" } }, - "contribution":{ - "description":"Propose de nouvelles fonctionnalités ou signale un bug pour contribuer à l'amélioration de BSManager !", - "buttons":{ - "request-features":"Proposer une fonctionnalité", - "report-bug":"Signaler un bug", - "open-logs": "Ouvrir les logs" + "available-versions": { + "title": "Choisi une version", + "steam-release": "Notes de version" + }, + "settings": { + "steam": { + "title": "Steam", + "description": "Si tu te déconnectes de Steam, tu devras te reconnecter pour télécharger une nouvelle version de Beat Saber.", + "logout": "Déconnexion" + }, + "appearance": { + "title": "Apparence", + "description": "Choisi les deux couleurs principales de BSManager.", + "reset": "Réinitialiser", + "sub-title": "Thème", + "themes": { + "dark": "Sombre", + "light": "Clair", + "os": "Synchroniser avec l'ordinateur" + } + }, + "installation-folder": { + "title": "Dossier d'installation", + "description": "Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.", + "choose-folder": "Choisir un dossier" + }, + "additional-content": { + "title": "Contenus additionnels", + "description": "Les contenus additionnels te permet de personaliser BeatSaber !", + "deep-links": { + "sub-title": "Installations OneClick" + } + }, + "language": { + "title": "Langage", + "description": "Sélectionne un langage.", + "languages": { + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "translated": { + "en-EN": "Anglais, Royaume-Uni", + "en-US": "Anglais, États-Unis", + "fr-FR": "Français", + "es-ES": "Espagnol" + } + } + }, + "patreon": { + "title": "Soutenir BSManager 💖", + "description": "Soutien le projet et aide-nous à fournir des améliorations continues à BSManager.", + "buttons": { + "support": "Soutenir BSManager", + "supporters": "Supporteurs 👀" + }, + "view": { + "no-supporters": "Pas encore de supporteurs", + "sponsors": "Sponsors", + "supporters": "Supporteurs" + } + }, + "discord":{ + "description": "Prends part à la communauté de BSManager en rejoignant notre Discord !" + }, + "contribution": { + "description": "Propose de nouvelles fonctionnalités ou signale un bug pour contribuer à l'amélioration de BSManager !", + "buttons": { + "request-features": "Proposer une fonctionnalité", + "report-bug": "Signaler un bug", + "open-logs": "Ouvrir les logs" + } } } - } - }, - "notifications":{ - "types":{ - "error": "🚨 Erreur", - "warning": "⚠️ Attention", - "success": "🎉 Succès" - }, - "common":{ - "msg":{ + }, + "notifications": { + "types": { + "error": "🚨 Erreur", + "warning": "⚠️ Attention", + "success": "🎉 Succès" + }, + "common": { + "msg": { "error-occurred": "Une erreur c'est produite" } }, - "shared":{ - "errors":{ - "titles":{ - "operation-running": "Opération en cours" + "shared": { + "errors": { + "titles": { + "operation-running": "Opération en cours" + }, + "msg": { + "operation-running": "Attend la fin de l'opération en cours puis recommences." + } + } + }, + "bs-download": { + "success": { + "titles": { + "download-success": "Téléchargement terminé", + "verification-finished": "Vérification terminée" + } }, - "msg":{ - "operation-running": "Attend la fin de l'opération en cours puis recommences." - } - } - }, - "bs-download":{ - "success":{ - "titles":{ - "download-success": "Téléchargement terminé", - "verification-finished":"Vérification terminée" - } - }, - "warnings":{ - "msg":{ - "ManifestChecksum": "Le manifest télécharger précédemment ne correspond pas au nouveau 🤔", - "ConnectionTimeout": "Ta connexion internet semble instable 🥶", - "ConnectionLost": "La connexion a été perdue, retentative...", - "ConnectionError": "Impossible de se connecter à Steam, retentative...", - "Unknown": "Quelque chose d'étrange c'est produit 🤔 Votre connexion est surement instable." - } - }, - "errors":{ - "msg":{ - "Password": "Le mot de passe est invalide.", - "InvalidCredentials": "Ton e-mail ou mot de passe est invalide 😴", - "InvalidPassword": "Ton mot de passe est invalide 😕", - "NoManifest": "Aucun manifest n'a été trouvé.", - "DirectoryCreate": "Impossible d'installer les dossiers nécessaires.", - "NotAvailableApp": "Tu essayes de télécharger BeatSaber alors que tu ne l'as pas ? 🤣", - "DepotNotFound": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", - "NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯", - "InvalidManifest": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", - "NoValidKey": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", - "NoManifestCode": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", - "401": "Steam semble pas vouloir nous laisser télécharger BeatSaber 😢", - "404": "Impossible de contacter les serveurs de Steam.", - "Unknown": "Une erreur inconnue c'est produite ¯\\_(ツ)_/¯", - "NoServer": "Impossible de contacter les serveurs de Steam.", - "NotAllowed": "Apparemment tu n'es pas autorisé à télécharger BeatSaber 🥱", - "ConnectionTimeout": "Impossible de se connecter à Steam 😕", - "SteamLib": "Si tu à cette erreur, report le bug sur GitHub avec les logs stp.", - "ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯", - "LicenceError": "Impossible d'obtenir la liste des licences.", - "verification-failed": "La vérification a échoué, réessaye plus tard.", - "RateLimitExceeded": "Tu as essayé trop de fois attend un peu et recommence plus tard.", - "AlreadyDownloading": "Un téléchargement est déjà en cours", - "no-internet": "Pas de connexion internet." - } - } - }, - "settings":{ - "move-folder":{ - "success":{ - "titles":{ - "transfer-finished":"Transfert terminé 👌" - } + "warnings": { + "msg": { + "ManifestChecksum": "Le manifest télécharger précédemment ne correspond pas au nouveau 🤔", + "ConnectionTimeout": "Ta connexion internet semble instable 🥶", + "ConnectionLost": "La connexion a été perdue, retentative...", + "ConnectionError": "Impossible de se connecter à Steam, retentative...", + "Unknown": "Quelque chose d'étrange c'est produit 🤔 Votre connexion est surement instable." + } }, - "errors":{ - "titles":{ - "transfer-failed":"Le transfert a échoué 😕" - } - } - }, - "steam":{ - "success":{ - "titles":{ - "logout": "Déconnecté de Steam." - } - } - } - }, - "bs-launch":{ - "success":{ - "titles":{ - "launching": "Lancement...🚀" - } - }, - "errors":{ - "titles":{ - "UNABLE_TO_LAUNCH": "Lancement impossible", - "STEAM_NOT_RUNNING": "Steam n'est pas lancé", - "OCULUS_NOT_RUNNING": "Oculus n'est pas lancé", - "BS_ALREADY_RUNNING": "BeatSaber est déjà lancé", - "EXE_NOT_FINDED": "Fichiers manquants", - "EXIT": "Arrêt brutal" - }, - "msg":{ - "STEAM_NOT_RUNNING": "Steam doit être en cours d'exécution pour lancer BeatSaber.", - "OCULUS_NOT_RUNNING": "Oculus doit être en cours d'exécution pour lancer BeatSaber.", - "BS_ALREADY_RUNNING": "Ferme BeatSaber avant de le lancer à nouveau.", - "EXE_NOT_FINDED": "Quelques fichiers semblent manquants. Essaye de vérifier les fichiers.", - "EXIT": "BeatSaber s'est arrêté brusquement, essaye de vérifier les fichiers." - } - } - }, - "custom-version":{ - "errors":{ - "titles":{ - "CantEditSteam": "Edition impossible", - "CantRename": "Renommage impossible", - "VersionAlreadExist": "Cette version existe déjà", - "CantClone": "Clonage impossible" - }, - "msg":{ - "CantEditSteam": "Tu ne peut pas éditer la version Steam. Tu peut la cloner par contre." - } - }, - "success":{ - "titles":{ - "CloningFinished": "Clonage terminer 🎉" - } - } - }, - "mods":{ - "install-mods":{ + "errors": { "titles":{ + "dotnet-required": ".NET 6 Requis" + }, + "msg": { + "401": "Steam semble pas vouloir nous laisser télécharger BeatSaber 😢", + "404": "Impossible de contacter les serveurs de Steam.", + "Password": "Le mot de passe est invalide.", + "InvalidCredentials": "Ton e-mail ou mot de passe est invalide 😴", + "InvalidPassword": "Ton mot de passe est invalide 😕", + "NoManifest": "Aucun manifest n'a été trouvé.", + "DirectoryCreate": "Impossible d'installer les dossiers nécessaires.", + "NotAvailableApp": "Tu essayes de télécharger BeatSaber alors que tu ne l'as pas ? 🤣", + "DepotNotFound": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", + "NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯", + "InvalidManifest": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", + "NoValidKey": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", + "NoManifestCode": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕", + "Unknown": "Une erreur inconnue c'est produite ¯\\_(ツ)_/¯", + "NoServer": "Impossible de contacter les serveurs de Steam.", + "NotAllowed": "Apparemment tu n'es pas autorisé à télécharger BeatSaber 🥱", + "ConnectionTimeout": "Impossible de se connecter à Steam 😕", + "SteamLib": "Si tu à cette erreur, report le bug sur GitHub avec les logs stp.", + "ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯", + "LicenceError": "Impossible d'obtenir la liste des licences.", + "verification-failed": "La vérification a échoué, réessaye plus tard.", + "RateLimitExceeded": "Tu as essayé trop de fois attend un peu et recommence plus tard.", + "AlreadyDownloading": "Un téléchargement est déjà en cours", + "no-internet": "Pas de connexion internet.", + "dotnet-required": ".NET 6 Runtime doit être installé pour pouvoir télécharger une version de BeatSaber. Télécharge-le en cliquant sur le bouton ci-dessous." + }, + "actions":{ + "download-dotnet": "Télécharger .NET 6" + } + } + }, + "settings": { + "move-folder": { + "success": { + "titles": { + "transfer-finished": "Transfert terminé 👌" + } + }, + "errors": { + "titles": { + "transfer-failed": "Le transfert a échoué 😕" + } + } + }, + "steam": { + "success": { + "titles": { + "logout": "Déconnecté de Steam." + } + } + }, + "additional-content":{ + "deep-link": { + "activation":{ + "success":{ + "title":"OneClick activé !", + "description":"Les installations OneClick ont été activées." + }, + "error":{ + "description":"Impossible d'activer les installations OneClick." + } + }, + "deactivation":{ + "success":{ + "title":"OneClick désactivé !", + "description":"Les installations OneClick ont été désactivées." + }, + "error":{ + "description":"Une erreur inconnue s'est produite." + } + }, + "check-all-enabled":{ + "title": "OneClick désactivée(s)", + "description": "Une ou plusieurs installations OneClick sont désactivées. Rendez-vous dans les paramètres pour les activer.", + "actions":{ + "settings": "Paramètres", + "not-remind": "Ne plus me rappeler" + } + } + } + } + }, + "bs-launch": { + "success": { + "titles": { + "launching": "Lancement...🚀" + } + }, + "errors": { + "titles": { + "UNABLE_TO_LAUNCH": "Lancement impossible", + "STEAM_NOT_RUNNING": "Steam n'est pas lancé", + "OCULUS_NOT_RUNNING": "Oculus n'est pas lancé", + "BS_ALREADY_RUNNING": "BeatSaber est déjà lancé", + "EXE_NOT_FINDED": "Fichiers manquants", + "EXIT": "Arrêt brutal" + }, + "msg": { + "STEAM_NOT_RUNNING": "Steam doit être en cours d'exécution pour lancer BeatSaber.", + "OCULUS_NOT_RUNNING": "Oculus doit être en cours d'exécution pour lancer BeatSaber.", + "BS_ALREADY_RUNNING": "Ferme BeatSaber avant de le lancer à nouveau.", + "EXE_NOT_FINDED": "Quelques fichiers semblent manquants. Essaye de vérifier les fichiers.", + "EXIT": "BeatSaber s'est arrêté brusquement, essaye de vérifier les fichiers." + }, + "actions":{ + "STEAM_NOT_RUNNING": "Lancer Steam" + } + } + }, + "steam":{ + "steam-launching":{ + "title": "Steam se lance !", + "description": "Steam peut mettre quelque temps à se lancer selon votre configuration." + } + }, + "custom-version": { + "errors": { + "titles": { + "CantEditSteam": "Edition impossible", + "CantRename": "Renommage impossible", + "VersionAlreadExist": "Cette version existe déjà", + "CantClone": "Clonage impossible" + }, + "msg": { + "CantEditSteam": "Tu ne peut pas éditer la version Steam. Tu peut la cloner par contre." + } + }, + "success": { + "titles": { + "CloningFinished": "Clonage terminer 🎉" + } + } + }, + "mods": { + "install-mods": { + "titles": { "success": "Mods installés 🎉", "warning": "Mods installés 🤔" }, - "msg":{ + "msg": { "success": "Tous les mods on été installés.", "warning": "Un ou plusieurs mods n'ont pas pu être installés.", - "errors":{ + "errors": { "no-mods": "Aucun mods à installer.", "cannot-install-bsipa": "L'installation de BSIPA à échoué 😨" } } }, - "uninstall-mod":{ - "titles":{ + "uninstall-mod": { + "titles": { "success": "Mod désintallé 🎉" }, - "msg":{ - "errors":{ + "msg": { + "errors": { "no-mods": "Ce mod n'est pas installé 😑" } } }, - "uninstall-all-mods":{ - "titles":{ + "uninstall-all-mods": { + "titles": { "success": "Mods désintallés 🎉" }, - "msg":{ + "msg": { "success": "Tous les mods on été désintallés.", "errors": { "no-mods": "Aucun mod n'est installé dans dans cette version 😑" } } } + }, + "maps":{ + "one-click-install":{ + "success":"Installation de la map terminée", + "error":"Une erreur s'est produite lors de l'installation de la map" + } + }, + "playlists":{ + "one-click-install":{ + "success":"Installation de la playlist terminée", + "error":"Une erreur s'est produite lors de l'installation de la playlist" + } + }, + "models":{ + "one-click-install":{ + "success":"Installation du modèle terminée", + "error":"Une erreur s'est produite lors de l'installation du modèle" + } } - }, - "modals":{ - "guard":{ - "title": "Steam Guard", - "inputs":{ - "guard-code":{ - "label": "Code Guard", - "placeholder": "Entre ton code Guard" - } - }, - "buttons":{ - "submit": "Se connecter" - } - }, - "steam-login":{ - "title": "Connexion Steam", - "inputs":{ - "username":{ - "label": "Nom d'utilisateur", - "placeholder": "Entre ton nom d'utilisateur" + }, + "modals": { + "guard": { + "title": "Steam Guard", + "inputs": { + "guard-code": { + "label": "Code Guard", + "placeholder": "Entre ton code Guard" + } }, - "password":{ - "label": "Mot de passe", - "placeholder": "Entre ton mot de passe" - }, - "stay":{ - "label": "Rester connecté" + "buttons": { + "submit": "Se connecter" } - }, - "buttons":{ - "submit": "Se connecter" - } - }, - "bs-uninstall":{ - "title": "Désinstaller", - "description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.", - "buttons":{ - "submit": "Désinstaller" - } - }, - "install-folder":{ - "title": "Dossier d'installation", - "description": "Changer le dossier d'installation par défaut, va engendrer le déplacement de toutes les données installer vers le nouveau dossier.", - "buttons":{ - "submit": "Choisir un dossier" - } - }, - "edit-version":{ - "title": "Editer la version", - "buttons":{ - "submit": "Editer" - } - }, - "clone-version":{ - "title": "Cloner la version", - "description": "Cloner la version te permet de séparer les contenus additionnels de BeatSaber entre deux mêmes versions.", - "inputs":{ - "name":{ - "label":"Nom", - "placeholder": "Nom de la version" + }, + "steam-login": { + "title": "Connexion Steam", + "inputs": { + "username": { + "label": "Nom d'utilisateur", + "placeholder": "Entre ton nom d'utilisateur" + }, + "password": { + "label": "Mot de passe", + "placeholder": "Entre ton mot de passe" + }, + "stay": { + "label": "Rester connecté" + } }, - "color":{ - "label":"Couleur" + "buttons": { + "submit": "Se connecter" } - }, - "buttons":{ - "submit": "Cloner" - } - }, - "uninstall-mod":{ + }, + "bs-uninstall": { + "title": "Désinstaller", + "description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.", + "buttons": { + "submit": "Désinstaller" + } + }, + "install-folder": { + "title": "Dossier d'installation", + "description": "Changer le dossier d'installation par défaut, va engendrer le déplacement de toutes les données installer vers le nouveau dossier.", + "buttons": { + "submit": "Choisir un dossier" + } + }, + "edit-version": { + "title": "Editer la version", + "buttons": { + "submit": "Editer" + } + }, + "clone-version": { + "title": "Cloner la version", + "description": "Cloner la version te permet de séparer les contenus additionnels de BeatSaber entre deux mêmes versions.", + "inputs": { + "name": { + "label": "Nom", + "placeholder": "Nom de la version" + }, + "color": { + "label": "Couleur" + } + }, + "buttons": { + "submit": "Cloner" + } + }, + "uninstall-mod": { "title": "Désinstaller", "description": "Es-tu sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.", "description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus." }, - "uninstall-all-mods":{ + "uninstall-all-mods": { "title": "Désinstaller les mods", "description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée." + }, + "maps-actions": { + "delete-maps": { + "title": { + "single": "Supprimer la map ?", + "multiple": "Supprimer les maps ?" + }, + "desc": { + "single": "Est-tu sur de vouloir supprimer la map {name} ?", + "multiple": "Est-tu sur de vouloir supprimer les {nb} maps ?" + }, + "info": { + "desc": { + "single": "Cette map fait partie des maps paratagées", + "multiple": "Ces maps font parties des maps partagées" + }, + "title": { + "single": "Cette map sera également supprimée des versions utilisant les maps partagées", + "multiple": "Ces maps seront égalements supprimées des versions utilisant les maps partagées" + } + } + }, + "link-maps": { + "title": "Lier les maps", + "desc": "La liaison des maps permet de partager les maps entre toute les version. Une fois liée, cette version profitera des maps partagées", + "info": "L'ajout et la suppression de maps sera également partagé", + "keep-maps": { + "label": "Conserver les maps", + "title": "Conserver les maps déplacera les maps de la version actuelle dans le dossier des maps partagées. Dans le cas contraire elles seront perdues" + }, + "valid-btn": "Lier les maps" + }, + "unlink-maps": { + "title": "Délier les maps", + "desc": "Attention, délier les maps ne permettra plus l'utilisation des maps paratagées pour cette version.", + "keep-maps": { + "label": "Conserver les maps", + "title": "Conserver les maps créera une copie des maps partagées pour la version actuelle. Dans le cas contraire, aucune map ne sera conservée pour cette version." + }, + "valid-btn": "Délier les maps" + } + }, + "download-maps": { + "search-btn": "Rechercher", + "loading-maps": "Chargement des maps..." + }, + "mods-disclaimer":{ + "title": "avis de non-responsabilité", + "p-1": "En choisissant d'utiliser des mods, vous comprenez et acceptez que :", + "li-1": "Vous pouvez rencontrer des problèmes qui n'existent pas dans le jeu de base. 99.9% des bugs, plantages et ralentissements sont dus aux mods.", + "li-2": "Les mods sont susceptibles de ne plus fonctionner à cause des mises à jour et c'est normal - soyez patient et respectueux lorsque cela se produit, parce que les moddeurs sont bénévoles et ont une vie.", + "li-3": "Beat Games ne cherche pas volontairement à casser les mods. Ils souhaitent améliorer la base du jeu et parfois cela casse les mods, mais ce n'est pas dans leur objectif de tuer les mods.", + "p-2": "N'attaquez pas les développeurs pour des problèmes relatifs aux mods, et inversement - les moddeurs et les développeurs sont deux groupes distincts. Ne sois pas idiot, d'accord ?" } - }, - "auto-update":{ - "checking": "Vérification des mises à jour", - "downloading": "Téléchargement des mises à jour" - }, - "dateformat":{ + }, + "maps": { + "map-filter-panel": { + "duration": "Durée", + "tags": "tags", + "specificities": "général", + "requirements": "requis" + }, + "map-types": { + "accuracy": "précision", + "balanced": "équilibrée", + "challenge": "challenge", + "dancestyle": "dance", + "fitness": "fitness", + "speed": "vitesse", + "tech": "tech" + }, + "map-styles": { + "dance": "dance", + "swing": "swing", + "nightcore": "nightcore", + "folk": "folk", + "family": "famille", + "ambient": "ambiante", + "funk": "funk", + "jazz": "jazz", + "soul": "soul", + "speedcore": "speedcore", + "punk": "punk", + "rb": "r&b", + "holiday": "vacance", + "vocaloid": "vocaloid", + "jrock": "j-rock", + "trance": "trance", + "drumbass": "drum & bass", + "comedy": "comédie", + "instrumental": "instrumental", + "hardcore": "hardcore", + "kpop": "k-pop", + "indie": "indé", + "techno": "techno", + "house": "house", + "game": "jeu vidéo", + "film": "film", + "alt": "alternative", + "dubstep": "dubstep", + "metal": "metal", + "anime": "anime", + "hiphop": "hiphop", + "jpop": "j-pop", + "rock": "rock", + "pop": "pop", + "electronic": "éléctronique", + "classical-orchestral": "Classique & Orchestral" + }, + "map-specificities": { + "automapper": "IA", + "ranked": "classée", + "curated": "recommandée", + "verified": "vérifiée", + "fullSpread": "panel complet" + }, + "difficulties": { + "Easy": "facile", + "Normal": "normal", + "Hard": "difficile", + "Expert": "expert", + "ExpertPlus": "expert+" + }, + "map-item": { + "by": "Par {songAutor}", + "mapped-by": "mappée par" + } + }, + "beat-saver": { + "maps-sorts": { + "Latest": "Dernière", + "Relevance": "Pertinence", + "Rating": "Notes", + "Curated": "Recommandée" + } + }, + "auto-update": { + "checking": "Vérification des mises à jour", + "downloading": "Téléchargement des mises à jour" + }, + "dateformat": { "dayNames": [ - "Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", - "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi" + "Dim", + "Lun", + "Mar", + "Mer", + "Jeu", + "Ven", + "Sam", + "Dimanche", + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi" ], "monthNames": [ - "Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Juil", "Aou", "Sept", "Oct", "Nov", "Déc", - "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", - "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" + "Jan", + "Fév", + "Mar", + "Avr", + "Mai", + "Jun", + "Juil", + "Aou", + "Sept", + "Oct", + "Nov", + "Déc", + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Août", + "Septembre", + "Octobre", + "Novembre", + "Décembre" ], "timeNames": [ - "a", "p", "am", "pm", "A", "P", "AM", "PM" + "a", + "p", + "am", + "pm", + "A", + "P", + "AM", + "PM" ] - } -} + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ff8a2745..198d71cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1484,59 +1484,59 @@ "dev": true }, "@motionone/animation": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.14.0.tgz", - "integrity": "sha512-h+1sdyBP8vbxEBW5gPFDnj+m2DCqdlAuf2g6Iafb1lcMnqjsRXWlPw1AXgvUMXmreyhqmPbJqoNfIKdytampRQ==", + "version": "10.15.1", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", + "integrity": "sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==", "requires": { - "@motionone/easing": "^10.14.0", - "@motionone/types": "^10.14.0", - "@motionone/utils": "^10.14.0", + "@motionone/easing": "^10.15.1", + "@motionone/types": "^10.15.1", + "@motionone/utils": "^10.15.1", "tslib": "^2.3.1" } }, "@motionone/dom": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.13.1.tgz", - "integrity": "sha512-zjfX+AGMIt/fIqd/SL1Lj93S6AiJsEA3oc5M9VkUr+Gz+juRmYN1vfvZd6MvEkSqEjwPQgcjN7rGZHrDB9APfQ==", + "version": "10.15.3", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.15.3.tgz", + "integrity": "sha512-FQ7a2zMBXc1UeU8CG9G3yDpst55fbb0+C9A0VGfwOITitBCzigKZnXRgsRSWWR+FW57GSc13eGQxtYB0lKG0Ng==", "requires": { - "@motionone/animation": "^10.13.1", - "@motionone/generators": "^10.13.1", - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", + "@motionone/animation": "^10.15.1", + "@motionone/generators": "^10.15.1", + "@motionone/types": "^10.15.1", + "@motionone/utils": "^10.15.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "@motionone/easing": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.14.0.tgz", - "integrity": "sha512-2vUBdH9uWTlRbuErhcsMmt1jvMTTqvGmn9fHq8FleFDXBlHFs5jZzHJT9iw+4kR1h6a4SZQuCf72b9ji92qNYA==", + "version": "10.15.1", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.15.1.tgz", + "integrity": "sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==", "requires": { - "@motionone/utils": "^10.14.0", + "@motionone/utils": "^10.15.1", "tslib": "^2.3.1" } }, "@motionone/generators": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.14.0.tgz", - "integrity": "sha512-6kRHezoFfIjFN7pPpaxmkdZXD36tQNcyJe3nwVqwJ+ZfC0e3rFmszR8kp9DEVFs9QL/akWjuGPSLBI1tvz+Vjg==", + "version": "10.15.1", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.15.1.tgz", + "integrity": "sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==", "requires": { - "@motionone/types": "^10.14.0", - "@motionone/utils": "^10.14.0", + "@motionone/types": "^10.15.1", + "@motionone/utils": "^10.15.1", "tslib": "^2.3.1" } }, "@motionone/types": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.14.0.tgz", - "integrity": "sha512-3bNWyYBHtVd27KncnJLhksMFQ5o2MSdk1cA/IZqsHtA9DnRM1SYgN01CTcJ8Iw8pCXF5Ocp34tyAjY7WRpOJJQ==" + "version": "10.15.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.15.1.tgz", + "integrity": "sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA==" }, "@motionone/utils": { - "version": "10.14.0", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.14.0.tgz", - "integrity": "sha512-sLWBLPzRqkxmOTRzSaD3LFQXCPHvDzyHJ1a3VP9PRzBxyVd2pv51/gMOsdAcxQ9n+MIeGJnxzXBYplUHKj4jkw==", + "version": "10.15.1", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.15.1.tgz", + "integrity": "sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==", "requires": { - "@motionone/types": "^10.14.0", + "@motionone/types": "^10.15.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } @@ -1660,6 +1660,11 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + }, "@remix-run/router": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.3.tgz", @@ -1838,6 +1843,14 @@ } } }, + "@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "requires": { + "tippy.js": "^6.3.1" + } + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -2236,12 +2249,12 @@ } }, "@types/react-dom": { - "version": "17.0.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz", - "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", "dev": true, "requires": { - "@types/react": "^17" + "@types/react": "*" } }, "@types/react-outside-click-handler": { @@ -2355,12 +2368,6 @@ "@types/react": "*" } }, - "@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true - }, "@types/verror": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", @@ -2877,22 +2884,6 @@ "indent-string": "^4.0.0" } }, - "airbnb-prop-types": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", - "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", - "requires": { - "array.prototype.find": "^2.1.1", - "function.prototype.name": "^1.1.2", - "is-regex": "^1.1.0", - "object-is": "^1.1.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.2", - "prop-types": "^15.7.2", - "prop-types-exact": "^1.2.0", - "react-is": "^16.13.1" - } - }, "ajv": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", @@ -3120,17 +3111,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "array.prototype.find": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.1.tgz", - "integrity": "sha512-I2ri5Z9uMpMvnsNrHre9l3PaX+z9D0/z6F7Yt2u15q7wt0I62g5kX6xUKR1SJiefgG+u2/gJUmM8B47XRvQR6w==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, "array.prototype.flat": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", @@ -3749,6 +3729,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -3991,6 +3972,16 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4006,6 +3997,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -4204,11 +4205,6 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true }, - "consolidated-events": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz", - "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" - }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4330,6 +4326,12 @@ "which": "^2.0.1" } }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==", + "dev": true + }, "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -4447,6 +4449,12 @@ } } }, + "css-unit-converter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true + }, "css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -4563,6 +4571,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -4704,6 +4717,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -4856,22 +4870,44 @@ "fs-extra": "^10.0.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" + } + }, + "dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "requires": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" }, "dependencies": { - "dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "optional": true, "requires": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "crc": "^3.8.0", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "optional": true } } }, @@ -4906,14 +4942,6 @@ "esutils": "^2.0.2" } }, - "document.contains": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/document.contains/-/document.contains-1.0.2.tgz", - "integrity": "sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==", - "requires": { - "define-properties": "^1.1.3" - } - }, "dom-accessibility-api": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", @@ -5395,6 +5423,7 @@ "version": "1.20.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", @@ -5456,6 +5485,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -5464,6 +5494,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -6692,6 +6723,15 @@ "pend": "~1.2.0" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6795,11 +6835,6 @@ "locate-path": "^3.0.0" } }, - "fitty": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/fitty/-/fitty-2.3.6.tgz", - "integrity": "sha512-ENp/+MqqYmXBGHC0dlltCX75+5zL5m0uiIGCgYdoThao99BrJpNYzEh3xrvhiy4b7O5X82CQT7dPTiB53H6stw==" - }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -6842,6 +6877,14 @@ "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6855,31 +6898,13 @@ "dev": true }, "framer-motion": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-7.6.7.tgz", - "integrity": "sha512-vEGsjXygf4qSmgXXsCT1FC56DjiZau9tSQTCchwAP2mOHnYHUy5gbthc4RXFWJh4Z/gFtqE8bzEmjahwOrfT7w==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-8.0.2.tgz", + "integrity": "sha512-xuIiQchVh/cLqUzzu5/8ok4o0nkwYPyIRtkDl8wDvrQUNRhSfRaZu1MMdzN0TjpBtG66oP03PSLO7Qtu1YqPrA==", "requires": { "@emotion/is-prop-valid": "^0.8.2", - "@motionone/dom": "10.13.1", - "framesync": "6.1.2", + "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", - "popmotion": "11.0.5", - "style-value-types": "5.1.2", - "tslib": "2.4.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "framesync": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", - "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", - "requires": { "tslib": "2.4.0" }, "dependencies": { @@ -6941,12 +6966,14 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "function.prototype.name": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", @@ -6963,7 +6990,8 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, "gauge": { "version": "4.0.4", @@ -6997,6 +7025,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -7021,6 +7050,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -7192,6 +7222,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -7199,7 +7230,8 @@ "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true }, "has-flag": { "version": "4.0.0", @@ -7211,6 +7243,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } @@ -7218,12 +7251,14 @@ "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -7240,6 +7275,12 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, "hey-listen": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", @@ -7291,6 +7332,18 @@ } } }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==", + "dev": true + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -7335,6 +7388,12 @@ } } }, + "html-tags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", + "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "dev": true + }, "html-webpack-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -7467,6 +7526,13 @@ "ms": "^2.0.0" } }, + "iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7598,6 +7664,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, "requires": { "get-intrinsic": "^1.1.0", "has": "^1.0.3", @@ -7647,6 +7714,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -7664,6 +7732,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -7672,7 +7741,8 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true }, "is-ci": { "version": "3.0.1", @@ -7683,6 +7753,20 @@ "ci-info": "^3.2.0" } }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, "is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -7696,6 +7780,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -7762,7 +7847,8 @@ "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true }, "is-number": { "version": "7.0.0", @@ -7774,6 +7860,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -7825,6 +7912,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -7840,6 +7928,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -7854,6 +7943,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -7862,6 +7952,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -7901,6 +7992,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -9604,6 +9696,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "dev": true + }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -10019,6 +10117,12 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, + "modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true + }, "mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -10104,6 +10208,30 @@ "semver": "^7.3.5" } }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "requires": { + "lodash": "^4.17.21" + } + }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -10226,17 +10354,26 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -10245,12 +10382,14 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -10262,6 +10401,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -10293,6 +10433,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -10674,24 +10815,6 @@ "xmlbuilder": "^15.1.1" } }, - "popmotion": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz", - "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==", - "requires": { - "framesync": "6.1.2", - "hey-listen": "^1.0.8", - "style-value-types": "5.1.2", - "tslib": "2.4.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "postcss": { "version": "8.4.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", @@ -10770,6 +10893,15 @@ "resolve": "^1.1.7" } }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, "postcss-load-config": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", @@ -10888,6 +11020,15 @@ "icss-utils": "^5.0.0" } }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, "postcss-normalize-charset": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", @@ -11093,6 +11234,12 @@ } } }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -11134,22 +11281,13 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, - "prop-types-exact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", - "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", - "requires": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" - } - }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -11175,6 +11313,109 @@ } } }, + "ps-scrollbar-tailwind": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ps-scrollbar-tailwind/-/ps-scrollbar-tailwind-0.0.1.tgz", + "integrity": "sha512-IPAAIXn0gNx4AzwHA9YY2+jZm18WTOzR+Xk+M3HfyEWWqRrGhBzQFUkgpQNe6mI2Bch1P4Bl62wh8rlR2W75jw==", + "dev": true, + "requires": { + "tailwindcss": "^2.0.2" + }, + "dependencies": { + "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, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true + }, + "postcss-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", + "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^8.1.6" + } + }, + "tailwindcss": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.19.tgz", + "integrity": "sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==", + "dev": true, + "requires": { + "arg": "^5.0.1", + "bytes": "^3.0.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", + "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", + "lodash": "^4.17.21", + "lodash.topath": "^4.5.2", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", + "normalize-path": "^3.0.0", + "object-hash": "^2.2.0", + "postcss-js": "^3.0.3", + "postcss-load-config": "^3.1.0", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.6", + "postcss-value-parser": "^4.1.0", + "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", + "quick-lru": "^5.1.1", + "reduce-css-calc": "^2.1.8", + "resolve": "^1.20.0", + "tmp": "^0.2.1" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -11324,6 +11565,26 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "purgecss": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.1.3.tgz", + "integrity": "sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==", + "dev": true, + "requires": { + "commander": "^8.0.0", + "glob": "^7.1.7", + "postcss": "^8.3.5", + "postcss-selector-parser": "^6.0.6" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + } + } + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -11410,30 +11671,16 @@ "scheduler": "^0.23.0" } }, - "react-fitty": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-fitty/-/react-fitty-1.0.1.tgz", - "integrity": "sha512-mO+Sbon+bO8Kazv38qu3uwi52vYDdty6P8fla91TkQEc2KwfZjNLT4j31cXix3ZTlBMWmavWAu5SfG6J9SpaFQ==", - "requires": { - "fitty": "2" - } - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, - "react-outside-click-handler": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz", - "integrity": "sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==", - "requires": { - "airbnb-prop-types": "^2.15.0", - "consolidated-events": "^1.1.1 || ^2.0.0", - "document.contains": "^1.0.1", - "object.values": "^1.1.0", - "prop-types": "^15.7.2" - } + "react-range": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/react-range/-/react-range-1.8.14.tgz", + "integrity": "sha512-v2nyD5106rHf9dwHzq+WRlhCes83h1wJRHIMFjbZsYYsO6LF4mG/mR3cH7Cf+dkeHq65DItuqIbLn/3jjYjsHg==" }, "react-refresh": { "version": "0.12.0", @@ -11493,14 +11740,6 @@ } } }, - "react-visibility-sensor": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz", - "integrity": "sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==", - "requires": { - "prop-types": "^15.7.2" - } - }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -11595,10 +11834,23 @@ "strip-indent": "^3.0.0" } }, - "reflect.ownkeys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==" + "reduce-css-calc": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", + "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } }, "regedit": { "version": "5.1.1", @@ -11620,6 +11872,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", @@ -11674,6 +11927,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -11747,6 +12005,18 @@ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "dev": true }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11803,6 +12073,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -11819,7 +12090,6 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, "requires": { "truncate-utf8-bytes": "^1.0.0" } @@ -12140,6 +12410,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -12152,6 +12423,23 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, "simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -12434,6 +12722,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -12444,6 +12733,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -12500,22 +12790,6 @@ "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", "dev": true }, - "style-value-types": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz", - "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==", - "requires": { - "hey-listen": "^1.0.8", - "tslib": "2.4.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -12640,21 +12914,6 @@ "is-glob": "^4.0.3" } }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, "postcss-nested": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", @@ -12815,6 +13074,14 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "requires": { + "@popperjs/core": "^2.9.0" + } + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -12910,7 +13177,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, "requires": { "utf8-byte-length": "^1.0.1" } @@ -13131,6 +13397,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -13256,16 +13523,28 @@ "prepend-http": "^2.0.0" } }, + "use-delayed-state": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-delayed-state/-/use-delayed-state-1.2.0.tgz", + "integrity": "sha512-i/7nyyHZsJ+2qGwbb9XGTViBDs43j/57pxdbhbyhTD6XNTzqCvaZQSfIa/SLAGR7eFPK9+UV62Dfzia/aYdwPw==" + }, "use-double-click": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/use-double-click/-/use-double-click-1.0.5.tgz", "integrity": "sha512-71LUca6NtzpzHYlcfM/dOdmwvmvpMbzeIVQpN87w+DctpLiMCXtZpsN8FNWPgHpPBtNhvucPUHIDh5al8D8C7w==" }, + "use-fit-text": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/use-fit-text/-/use-fit-text-2.4.0.tgz", + "integrity": "sha512-Iy4LMrXcdxWlyZ5phntMpJMgyXGB1p3tV73y2r0QrZ6f/thPh+/QU3ie6RCXmjF8tHMs20FKMPskXeDYIla/Ww==", + "requires": { + "resize-observer-polyfill": "^1.5.1" + } + }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", - "dev": true + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==" }, "util-deprecate": { "version": "1.0.2", @@ -13284,11 +13563,6 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -13408,6 +13682,11 @@ "defaults": "^1.0.3" } }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -13696,6 +13975,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", diff --git a/package.json b/package.json index 63037887..ec84ada6 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "target": [ "nsis" ], - "icon": "assets/favicon.ico" + "icon": "assets/favicon.ico", + "certificateSha1": "f3363a89b084f8f6b616babea3c91f9d79e15000" }, "linux": { "target": [ @@ -153,12 +154,11 @@ "@types/jest": "^27.5.2", "@types/node": "17.0.23", "@types/react": "^17.0.48", - "@types/react-dom": "^17.0.17", + "@types/react-dom": "^18.0.10", "@types/react-outside-click-handler": "^1.3.1", "@types/react-test-renderer": "^17.0.2", "@types/terser-webpack-plugin": "^5.0.4", "@types/use-double-click": "^1.0.1", - "@types/uuid": "^8.3.4", "@types/webpack-bundle-analyzer": "^4.4.2", "@types/webpack-env": "^1.18.0", "@typescript-eslint/eslint-plugin": "^5.34.0", @@ -173,7 +173,7 @@ "css-minimizer-webpack-plugin": "^4.1.0", "detect-port": "^1.3.0", "electron": "^20.0.3", - "electron-builder": "^23.3.3", + "electron-builder": "^23.6.0", "electron-devtools-installer": "^3.2.0", "electron-notarize": "^1.2.1", "electron-rebuild": "^3.2.9", @@ -200,6 +200,7 @@ "postcss": "^8.4.16", "postcss-loader": "^6.2.1", "prettier": "^2.7.1", + "ps-scrollbar-tailwind": "0.0.1", "react-refresh": "^0.12.0", "react-refresh-typescript": "^2.0.7", "react-test-renderer": "^18.2.0", @@ -208,7 +209,7 @@ "sass-loader": "^12.6.0", "style-loader": "^3.3.1", "tailwind-scrollbar": "^2.0.1", - "tailwindcss": "^3.1.8", + "tailwindcss": "^3.2.4", "terser-webpack-plugin": "^5.3.5", "ts-jest": "^27.1.5", "ts-loader": "^9.3.0", @@ -223,6 +224,7 @@ }, "dependencies": { "@node-steam/vdf": "^2.2.0", + "@tippyjs/react": "^4.2.6", "archiver": "^5.3.1", "dateformat": "^5.0.3", "dot-prop": "^7.2.0", @@ -230,25 +232,28 @@ "electron-log": "^4.4.8", "electron-store": "^8.1.0", "electron-updater": "^5.2.4", - "framer-motion": "^7.2.1", + "fast-deep-equal": "^3.1.3", + "framer-motion": "^8.0.2", + "fs-extra": "^10.1.0", "history": "^5.3.0", "is-online": "^9.0.1", "md5-file": "^5.0.0", + "node-fetch": "^3.3.0", "node-stream-zip": "^1.15.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", - "react-fitty": "^1.0.1", - "react-outside-click-handler": "^1.3.0", + "react-range": "^1.8.14", "react-router-dom": "^6.3.0", - "react-visibility-sensor": "^5.1.1", "regedit": "^5.1.1", "rxjs": "^7.5.6", + "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss-scoped-groups": "^2.0.0", + "use-delayed-state": "^1.2.0", "use-double-click": "^1.0.5", - "uuid": "^9.0.0" + "use-fit-text": "^2.4.0" }, "devEngines": { "node": ">=14.x", diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 6a1de2a6..0eeebd3e 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; -import App from '../renderer/App'; +import App from '../renderer/windows/App'; describe('App', () => { it('should render', () => { diff --git a/src/main/constants.ts b/src/main/constants.ts index 24c72eda..74ca8198 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -2,3 +2,4 @@ export const BS_EXECUTABLE = "Beat Saber.exe"; export const OCULUS_BS_DIR = "hyperbolic-magnetism-beat-saber" export const BS_APP_ID = "620980"; export const BS_DEPOT = "620981"; +export const APP_NAME = "BSManager"; diff --git a/src/main/helpers/array-tools.ts b/src/main/helpers/array-tools.ts new file mode 100644 index 00000000..e7967792 --- /dev/null +++ b/src/main/helpers/array-tools.ts @@ -0,0 +1,7 @@ +export function splitIntoChunk(arr: T[], chunkSize: number): T[][] { + const resArr = []; + for(let i = 0; i < arr.length; i += chunkSize){ + resArr.push(arr.slice(i, i + chunkSize)); + } + return resArr; +} \ No newline at end of file diff --git a/src/main/helpers/url.helpers.ts b/src/main/helpers/url.helpers.ts new file mode 100644 index 00000000..ea1981b9 --- /dev/null +++ b/src/main/helpers/url.helpers.ts @@ -0,0 +1,9 @@ +export function isValidUrl(url: string): boolean{ + try{ + new URL(url); + return true; + } + catch(e){ + return false; + } +} \ No newline at end of file diff --git a/src/main/ipcs/beat-saver-ipcs.ts b/src/main/ipcs/beat-saver-ipcs.ts new file mode 100644 index 00000000..980666b1 --- /dev/null +++ b/src/main/ipcs/beat-saver-ipcs.ts @@ -0,0 +1,49 @@ +import { ipcMain } from "electron"; +import { UtilsService } from "../services/utils.service"; +import { IpcRequest } from "shared/models/ipc"; +import { SearchParams } from "shared/models/maps/beat-saver.model"; +import { BeatSaverService } from "../services/thrid-party/beat-saver/beat-saver.service"; + +ipcMain.on("bsv-search-map", async (event, request: IpcRequest) => { + const utlis = UtilsService.getInstance(); + const bsvService = BeatSaverService.getInstance(); + + bsvService.searchMaps(request.args).then(maps => { + utlis.ipcSend(request.responceChannel, {success: true, data: maps}); + }).catch(e => { + utlis.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); + +ipcMain.on("bsv-get-map-details-from-hashs", async (event, request: IpcRequest) => { + const utlis = UtilsService.getInstance(); + const bsvService = BeatSaverService.getInstance(); + + bsvService.getMapDetailsFromHashs(request.args).then(maps => { + utlis.ipcSend(request.responceChannel, {success: true, data: maps}); + }).catch(e => { + utlis.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); + +ipcMain.on("bsv-get-map-details-by-id", async (event, request: IpcRequest) => { + const utlis = UtilsService.getInstance(); + const bsvService = BeatSaverService.getInstance(); + + bsvService.getMapDetailsById(request.args).then(maps => { + utlis.ipcSend(request.responceChannel, {success: true, data: maps}); + }).catch(e => { + utlis.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); + +ipcMain.on("bsv-get-playlist-details-by-id", async (event, request: IpcRequest) => { + const utlis = UtilsService.getInstance(); + const bsvService = BeatSaverService.getInstance(); + + bsvService.getPlaylistPage(request.args).then(maps => { + utlis.ipcSend(request.responceChannel, {success: true, data: maps}); + }).catch(e => { + utlis.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); \ No newline at end of file diff --git a/src/main/ipcs/bs-download-ipcs.ts b/src/main/ipcs/bs-download-ipcs.ts index 6d592c01..80acc27b 100644 --- a/src/main/ipcs/bs-download-ipcs.ts +++ b/src/main/ipcs/bs-download-ipcs.ts @@ -5,6 +5,7 @@ import { IpcRequest } from 'shared/models/ipc'; import { InstallationLocationService } from '../services/installation-location.service'; import { UtilsService } from '../services/utils.service'; import { BsmException } from 'shared/models/bsm-exception.model'; +import { LocalMapsManagerService } from '../services/additional-content/local-maps-manager.service'; export interface InitDownloadInfoInterface { @@ -24,8 +25,17 @@ export interface DownloadInfo { stay?: boolean } +ipcMain.on('is-dotnet-6-installed', async (event, request: IpcRequest) => { + const installer = BSInstallerService.getInstance(); + const utils = UtilsService.getInstance(); + installer.isDotNet6Installed().then(installed => { + utils.ipcSend(request.responceChannel, {success: true, data: installed}); + }); +}); + ipcMain.on('bs-download.start', async (event, request: IpcRequest) => { - BSInstallerService.getInstance().downloadBsVersion(request.args).then(res => { + BSInstallerService.getInstance().downloadBsVersion(request.args).then(async res => { + await LocalMapsManagerService.getInstance().linkVersionMaps(request.args.bsVersion, true).catch(e => {}); UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res}); }).catch(e => { UtilsService.getInstance().ipcSend(request.responceChannel, {success: false, data: e}); diff --git a/src/main/ipcs/bs-maps-ipcs.ts b/src/main/ipcs/bs-maps-ipcs.ts new file mode 100644 index 00000000..4868df52 --- /dev/null +++ b/src/main/ipcs/bs-maps-ipcs.ts @@ -0,0 +1,140 @@ +import { ipcMain } from "electron"; +import { LocalMapsManagerService } from "../services/additional-content/local-maps-manager.service"; +import { UtilsService } from "../services/utils.service"; +import { BSVersion } from "shared/bs-version.interface"; +import { IpcRequest } from "shared/models/ipc"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +import { BsvMapDetail } from "shared/models/maps"; + +ipcMain.on('get-version-maps', (event, request: IpcRequest) => { + const utilsService = UtilsService.getInstance(); + const localMaps = LocalMapsManagerService.getInstance(); + + localMaps.getMaps(request.args).then(maps => { + utilsService.ipcSend(request.responceChannel, {success: true, data: maps}); + }).catch(() => { + utilsService.ipcSend(request.responceChannel, {success: false}); + }) + +}); + +ipcMain.on("verion-have-maps-linked", async (event, request: IpcRequest) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + utils.ipcSend(request.responceChannel, {success: true, data: await maps.versionIsLinked(request.args)}); + + }); + +ipcMain.on("link-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.linkVersionMaps(request.args.version, request.args.keepMaps).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: true, error: err}); + }); + +}); + +ipcMain.on("unlink-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.unlinkVersionMaps(request.args.version, request.args.keepMaps).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: true, error: err}); + }); + +}); + +ipcMain.on("delete-maps", async (event, request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[]}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.deleteMaps(request.args.maps, request.args.version).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: false, error: err}); + }); +}); + +ipcMain.on("export-maps", async (event, request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[], outPath: string}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.exportMaps(request.args.version, request.args.maps, request.args.outPath).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: false, error: err}); + }); +}); + +ipcMain.on("download-map", async (event, request: IpcRequest<{map: BsvMapDetail, version: BSVersion}>) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.downloadMap(request.args.map, request.args.version).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: false, error: err}); + }); +}); + +ipcMain.on("one-click-install-map", async (event, request: IpcRequest) => { + const utils = UtilsService.getInstance(); + const maps = LocalMapsManagerService.getInstance(); + + maps.oneClickDownloadMap(request.args).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(err => { + utils.ipcSend(request.responceChannel, {success: false, error: err}); + }); +}); + +ipcMain.on("register-maps-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalMapsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.enableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("unregister-maps-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalMapsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.disableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("is-map-deep-links-enabled", async (event, request: IpcRequest) => { + + const maps = LocalMapsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.isDeepLinksEnabled(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); \ No newline at end of file diff --git a/src/main/ipcs/bs-model-ipcs.ts b/src/main/ipcs/bs-model-ipcs.ts new file mode 100644 index 00000000..a56c9a81 --- /dev/null +++ b/src/main/ipcs/bs-model-ipcs.ts @@ -0,0 +1,61 @@ +import { ipcMain } from "electron"; +import { UtilsService } from "../services/utils.service"; +import { IpcRequest } from "shared/models/ipc"; +import { MSModel } from "shared/models/model-saber/model-saber.model"; +import { LocalModelsManagerService } from "../services/additional-content/local-models-manager.service"; + +ipcMain.on("one-click-install-model", async (event, request: IpcRequest) => { + const utils = UtilsService.getInstance(); + const models = LocalModelsManagerService.getInstance(); + + models.oneClickDownloadModel(request.args).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(e => { + utils.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); + +ipcMain.on("register-models-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalModelsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.enableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("unregister-models-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalModelsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.disableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("is-models-deep-links-enabled", async (event, request: IpcRequest) => { + + const maps = LocalModelsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.isDeepLinksEnabled(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); \ No newline at end of file diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts new file mode 100644 index 00000000..7ed970f3 --- /dev/null +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -0,0 +1,61 @@ +import { ipcMain } from "electron"; +import { IpcRequest } from "shared/models/ipc"; +import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service"; +import { UtilsService } from "../services/utils.service"; + +ipcMain.on("one-click-install-playlist", async (event, request: IpcRequest) => { + + const utils = UtilsService.getInstance(); + const playlists = LocalPlaylistsManagerService.getInstance(); + + playlists.oneClickInstallPlaylist(request.args).then(() => { + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(e => { + utils.ipcSend(request.responceChannel, {success: false, error: e}); + }); +}); + +ipcMain.on("register-playlists-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalPlaylistsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.enableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("unregister-playlists-deep-link", async (event, request: IpcRequest) => { + + const maps = LocalPlaylistsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.disableDeepLinks(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); + +ipcMain.on("is-playlists-deep-links-enabled", async (event, request: IpcRequest) => { + + const maps = LocalPlaylistsManagerService.getInstance(); + const utils = UtilsService.getInstance(); + + try{ + const res = maps.isDeepLinksEnabled(); + utils.ipcSend(request.responceChannel, {success: true, data: res}); + } + catch(e){ + utils.ipcSend(request.responceChannel, {success: false}); + } + +}); \ No newline at end of file diff --git a/src/main/ipcs/index.ts b/src/main/ipcs/index.ts index 397a23be..0fc184bf 100644 --- a/src/main/ipcs/index.ts +++ b/src/main/ipcs/index.ts @@ -3,8 +3,12 @@ import './os-controls-ipcs'; import './bs-launcher-ipcs'; import './bs-version-ipcs'; import './bs-uninstall-ipcs'; -import './map-ipcs'; import './supporters-ipcs'; import './launcher-ipcs'; import './window-manager-ipcs'; import './bs-mods-ipcs'; +import './bs-maps-ipcs'; +import './beat-saver-ipcs'; +import './bs-playlist-ipcs'; +import './model-saber.ipcs'; +import './bs-model-ipcs'; \ No newline at end of file diff --git a/src/main/ipcs/map-ipcs.ts b/src/main/ipcs/map-ipcs.ts deleted file mode 100644 index 19ea6ff8..00000000 --- a/src/main/ipcs/map-ipcs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ipcMain} from 'electron'; -import { MapService } from '../services/map.service'; -import { UtilsService } from '../services/utils.service'; -import { IpcRequest } from 'shared/models/ipc'; -import { ExportVersionMapsOption } from 'shared/models/maps/export-version-maps.model'; - -ipcMain.on("map.export-version", (event, request: IpcRequest) => { - const utils = UtilsService.getInstance(); - const mapService = MapService.getInstance(); - mapService.exportVersionMaps(request.args.version, request.args.path).then(() => { - utils.ipcSend(request.responceChannel, {success: true}); - }).catch(() => utils.ipcSend(request.responceChannel, {success: false, error: {title: "", msg: ""}})); -}); \ No newline at end of file diff --git a/src/main/ipcs/model-saber.ipcs.ts b/src/main/ipcs/model-saber.ipcs.ts new file mode 100644 index 00000000..911d7542 --- /dev/null +++ b/src/main/ipcs/model-saber.ipcs.ts @@ -0,0 +1,15 @@ +import { ipcMain } from "electron"; +import { IpcRequest } from "shared/models/ipc"; +import { ModelSaberService } from "../services/thrid-party/model-saber/model-saber.service"; +import { UtilsService } from "../services/utils.service"; + +ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest) => { + const utils = UtilsService.getInstance(); + const ms = ModelSaberService.getInstance(); + + ms.getModelById(request.args).then(model => { + utils.ipcSend(request.responceChannel, {success: true, data: model}); + }).catch(e => { + utils.ipcSend(request.responceChannel, {success: false, error: e}); + }) +}); \ No newline at end of file diff --git a/src/main/ipcs/os-controls-ipcs.ts b/src/main/ipcs/os-controls-ipcs.ts index fe258a84..aaec73c8 100644 --- a/src/main/ipcs/os-controls-ipcs.ts +++ b/src/main/ipcs/os-controls-ipcs.ts @@ -1,25 +1,31 @@ import { ipcMain, shell, dialog, app } from 'electron'; import { UtilsService } from '../services/utils.service'; import { IpcRequest } from 'shared/models/ipc'; +import { SystemNotificationOptions } from 'shared/models/notification/system-notification.model'; +import { NotificationService } from '../services/notification.service'; +import { SteamService } from '../services/steam.service'; + + +// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE ipcMain.on('window.close', async () => { const utils = UtilsService.getInstance(); - utils.getMainWindow()?.close(); + utils.getMainWindows("index.html")?.close(); }); ipcMain.on('window.maximize', async () => { const utils = UtilsService.getInstance(); - utils.getMainWindow()?.maximize(); + utils.getMainWindows("index.html")?.maximize(); }); ipcMain.on('window.minimize', async () => { const utils = UtilsService.getInstance(); - utils.getMainWindow()?.minimize(); + utils.getMainWindows("index.html")?.minimize(); }); ipcMain.on('window.reset', async () => { const utils = UtilsService.getInstance(); - utils.getMainWindow()?.restore(); + utils.getMainWindows("index.html")?.restore(); }); ipcMain.on('new-window', async (event, request: IpcRequest) => { @@ -34,7 +40,7 @@ ipcMain.on('choose-folder', async (event, request: IpcRequest) => { ipcMain.on("window.progression", async (event, request: IpcRequest) => { const utils = UtilsService.getInstance(); - utils.getMainWindow().setProgressBar(request.args / 100); + utils.getMainWindows("index.html")?.setProgressBar(request.args / 100); }); ipcMain.on('save-file', async (event, request: IpcRequest<{filename?: string, filters?: Electron.FileFilter[]}>) => { @@ -52,3 +58,17 @@ ipcMain.on("current-version", async (event, request: IpcRequest) => { ipcMain.on("open-logs", async (event, request: IpcRequest) => { shell.openPath(app.getPath("logs")); }); + +ipcMain.on("notify-system", async (event, request: IpcRequest) => { + NotificationService.getInstance().notify(request.args) +}); + +ipcMain.on("open-steam", async (event, request: IpcRequest) => { + const steam = SteamService.getInstance(); + const utils = UtilsService.getInstance(); + steam.openSteam().then(res => { + utils.ipcSend(request.responceChannel, {success: true, data: res}); + }).catch((e) => { + utils.ipcSend(request.responceChannel, {success: false, error: e}); + }); +}); \ No newline at end of file diff --git a/src/main/ipcs/window-manager-ipcs.ts b/src/main/ipcs/window-manager-ipcs.ts index b0551ddc..e0393d68 100644 --- a/src/main/ipcs/window-manager-ipcs.ts +++ b/src/main/ipcs/window-manager-ipcs.ts @@ -9,4 +9,14 @@ ipcMain.on("open-window-then-close-all", async (event, request: IpcRequest { windowManager.closeAllWindows(request.args); }); +}); + +ipcMain.on("close-all-windows", async (event, request: IpcRequest) => { + const windowManager = WindowManagerService.getInstance(); + windowManager.closeAllWindows(request.args); +}); + +ipcMain.on("close-windows", async (event, request: IpcRequest) => { + const windowManager = WindowManagerService.getInstance(); + windowManager.close(...request.args); }); \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index bf35882a..10bd4c52 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -9,13 +9,17 @@ * `./src/main.js` using webpack. This gives us some performance wins. */ import path from 'path'; -import { app } from 'electron'; +import { app, protocol } from 'electron'; import log from 'electron-log'; import './ipcs'; import { UtilsService } from './services/utils.service'; import { WindowManagerService } from './services/window-manager.service'; - -export const PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js') +import { DeepLinkService } from './services/deep-link.service'; +import { AppWindow } from 'shared/models/window-manager/app-window.model'; +import { LocalMapsManagerService } from './services/additional-content/local-maps-manager.service'; +import { LocalPlaylistsManagerService } from './services/additional-content/local-playlists-manager.service'; +import { LocalModelsManagerService } from './services/additional-content/local-models-manager.service'; +import { APP_NAME } from './constants'; const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; @@ -47,11 +51,18 @@ const installExtensions = async () => { return installer.default(extensions.map((name) => installer[name]), forceDownload).catch(console.log); }; -const createWindow = async () => { +const createWindow = async (window: AppWindow = "launcher.html") => { if(isDebug){ await installExtensions(); } - WindowManagerService.getInstance().openWindow("launcher.html"); + WindowManagerService.getInstance().openWindow(window); }; +const initServicesMustBeInitialized = () => { + LocalMapsManagerService.getInstance(); + LocalPlaylistsManagerService.getInstance(); + LocalModelsManagerService.getInstance(); + // Model +} + app.on('window-all-closed', () => { // Respect the OSX convention of having the application in memory even // after all windows have been closed @@ -60,6 +71,42 @@ app.on('window-all-closed', () => { } }); -app.whenReady().then(() => { - createWindow(); -}).catch(log.error); \ No newline at end of file +const gotTheLock = app.requestSingleInstanceLock(); + +if(!gotTheLock){ + app.quit(); +} +else{ + + app.on('second-instance', (e, argv) => { + + const deepLink = argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); + + if(!deepLink){ return; } + + DeepLinkService.getInstance().dispatchLinkOpened(deepLink); + + }); + + app.whenReady().then(() => { + + app.setAppUserModelId(APP_NAME); + + initServicesMustBeInitialized(); + + const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); + + if(!deepLink){ + createWindow(); + } + else{ + DeepLinkService.getInstance().dispatchLinkOpened(deepLink); + } + + protocol.registerFileProtocol('file', (request, callback) => { + const pathname = decodeURI(request.url.replace('file:///', '')); + callback(pathname); + }); + + }).catch(log.error); +} \ No newline at end of file diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts new file mode 100644 index 00000000..12580c40 --- /dev/null +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -0,0 +1,306 @@ +import path from "path"; +import { BSVersion } from "shared/bs-version.interface"; +import { BsvMapDetail, RawMapInfoData } from "shared/models/maps"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +import { BSLocalVersionService } from "../bs-local-version.service"; +import { InstallationLocationService } from "../installation-location.service"; +import { UtilsService } from "../utils.service"; +import crypto from "crypto"; +import { lstatSync, symlinkSync, unlinkSync, readdirSync, createWriteStream } from "fs"; +import { copySync } from "fs-extra"; +import StreamZip from "node-stream-zip"; +import { RequestService } from "../request.service"; +import sanitize from "sanitize-filename"; +import archiver from "archiver"; +import { DeepLinkService } from "../deep-link.service"; +import log from 'electron-log'; +import { WindowManagerService } from "../window-manager.service"; +import { ipcMain } from "electron"; +import { IpcRequest } from 'shared/models/ipc'; + +export class LocalMapsManagerService { + + private static instance: LocalMapsManagerService; + + public static getInstance(): LocalMapsManagerService{ + if(!LocalMapsManagerService.instance){ LocalMapsManagerService.instance = new LocalMapsManagerService(); } + return LocalMapsManagerService.instance; + } + + private readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data"; + private readonly CUSTOM_LEVELS_FOLDER = "CustomLevels"; + + private readonly DEEP_LINKS = { + BeatSaver: "beatsaver", + ScoreSaber: "web+bsmap" + }; + + private readonly localVersion: BSLocalVersionService; + private readonly installLocation: InstallationLocationService; + private readonly utils: UtilsService; + private readonly reqService: RequestService; + private readonly deepLink: DeepLinkService; + private readonly windows: WindowManagerService; + + private constructor(){ + this.localVersion = BSLocalVersionService.getInstance(); + this.installLocation = InstallationLocationService.getInstance(); + this.utils = UtilsService.getInstance(); + this.reqService = RequestService.getInstance(); + this.deepLink = DeepLinkService.getInstance(); + this.windows = WindowManagerService.getInstance(); + + this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, (link) => { + log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link); + this.openOneClickDownloadMapWindow(new URL(link).host); + }); + + this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, (link) => { + log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ScoreSaber, link); + this.openOneClickDownloadMapWindow(new URL(link).host, true); + }); + } + + public async getMapsFolderPath(version?: BSVersion): Promise{ + if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); } + const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER); + if(!(await this.utils.pathExist(sharedMapsPath))){ + this.utils.createFolderIfNotExist(sharedMapsPath); + } + return sharedMapsPath; + } + + private async computeMapHash(mapPath: string, rawInfoString: string): Promise{ + const mapRawInfo = JSON.parse(rawInfoString); + let content = rawInfoString; + for(const set of mapRawInfo._difficultyBeatmapSets){ + for(const diff of set._difficultyBeatmaps){ + const diffFilePath = path.join(mapPath, diff._beatmapFilename); + if(!await this.utils.pathExist(diffFilePath)){ continue; } + const diffContent = (await this.utils.readFileAsync(diffFilePath)).toString(); + content += diffContent; + } + } + + const shasum = crypto.createHash("sha1"); + shasum.update(content); + return shasum.digest("hex"); + } + + private async loadMapInfoFromPath(mapPath: string): Promise{ + const infoFilePath = path.join(mapPath, "Info.dat"); + + if(!(await this.utils.pathExist(infoFilePath))){ return null; } + + const rawInfoString = await (await (this.utils.readFileAsync(infoFilePath))).toString(); + + const rawInfo: RawMapInfoData = JSON.parse(rawInfoString); + const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href; + const songUrl = new URL(`file:///${path.join(mapPath, rawInfo._songFilename)}`).href; + + const hash = await this.computeMapHash(mapPath, rawInfoString); + + return {rawInfo, coverUrl, songUrl, hash}; + } + + private async downloadMapZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{ + const fileName = path.basename(zipUrl); + const tempPath = this.utils.getTempPath(); + this.utils.createFolderIfNotExist(this.utils.getTempPath()); + const dest = path.join(tempPath, fileName); + + const zipPath = await this.reqService.downloadFile(zipUrl, dest); + const zip = new StreamZip.async({file : zipPath}); + + return {zip, zipPath}; + } + + private async getAbsoluteFolderOfMaps(maps: BsmLocalMap[], version: BSVersion): Promise{ + + const mapsFolder = await this.getMapsFolderPath(version); + + const res: string[] = []; + const mapHashs = maps.map(map => map.hash); + + const mapsFolders = readdirSync(mapsFolder, {withFileTypes: true}); + + for(const content of mapsFolders){ + if(!content.isDirectory()){ continue; } + const mapFolderPath = path.join(mapsFolder, content.name); + const { hash } = await this.loadMapInfoFromPath(mapFolderPath); + if(mapHashs.includes(hash)){ + res.push(mapFolderPath); + } + } + + return res; + + } + + private openOneClickDownloadMapWindow(mapId: string, isHash = false): void{ + + ipcMain.once("one-click-map-info", async (event, req: IpcRequest) => { + this.utils.ipcSend(req.responceChannel, {success: true, data: {id: mapId, isHash}}); + }); + + this.windows.openWindow("oneclick-download-map.html"); + + } + + public async getMaps(version?: BSVersion): Promise{ + const levelsFolder = await this.getMapsFolderPath(version); + + const levelsPath = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : []; + + const mapsInfo = await Promise.all(levelsPath.map(levelPath => this.loadMapInfoFromPath(levelPath))); + + return mapsInfo.filter(info => !!info); + } + + public async versionIsLinked(version: BSVersion): Promise{ + + const levelsPath = await this.getMapsFolderPath(version); + + const isPathExist = await this.utils.pathExist(levelsPath); + + if(!isPathExist){ return false; } + + return lstatSync(levelsPath).isSymbolicLink() + } + + public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise{ + + if(await this.versionIsLinked(version)){ return; } + + const sharedMapsPath = await this.getMapsFolderPath(); + const versionMapsPath = await this.getMapsFolderPath(version); + + if(keepMaps){ + await this.utils.moveDirContent(versionMapsPath, sharedMapsPath); + } + + await this.utils.deleteFolder(versionMapsPath); + + symlinkSync(sharedMapsPath, versionMapsPath, "junction"); + } + + public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise{ + + const sharedMapsPath = await this.getMapsFolderPath(); + const versionMapsPath = await this.getMapsFolderPath(version); + + if(await this.versionIsLinked(version)){ + unlinkSync(versionMapsPath); + } + + this.utils.createFolderIfNotExist(versionMapsPath); + + if(keepMaps){ + copySync(sharedMapsPath, versionMapsPath); + } + + } + + public async deleteMaps(maps: BsmLocalMap[], verion?: BSVersion){ + + const mapsFolders = await this.getAbsoluteFolderOfMaps(maps, verion); + const mapsHashsToDelete = maps.map(map => map.hash); + + for(const folder of mapsFolders){ + const { hash } = await this.loadMapInfoFromPath(folder); + if(mapsHashsToDelete.includes(hash)){ + await this.utils.deleteFolder(folder); + } + } + + } + + public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise{ + + if(!map.versions.at(0).hash){ throw "Cannot download map, no hash found"; } + + const zipUrl = map.versions.at(0).downloadURL; + + const mapsFolder = await this.getMapsFolderPath(version); + + const {zip, zipPath} = await this.downloadMapZip(zipUrl); + + const mapFolderName = sanitize(`${map.id}-${map.name}`); + + const mapPath = path.join(mapsFolder, mapFolderName); + + if(!zip){ throw `Cannot download ${zipUrl}`; } + + this.utils.createFolderIfNotExist(mapPath); + + await zip.extract(null, mapPath); + await zip.close(); + + unlinkSync(zipPath); + + return mapPath; + } + + public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string){ + + const output = createWriteStream(outPath); + const archive = archiver("zip", {zlib: {level: 9}}); + + archive.pipe(output); + archive.on("error", (e) => {throw e}); + + if(!maps || maps.length === 0){ + + const mapsFolder = await this.getMapsFolderPath(version); + archive.directory(mapsFolder, false); + + } + else{ + + const mapsFolders = await this.getAbsoluteFolderOfMaps(maps, version); + + for(const folder of mapsFolders){ + archive.directory(folder, path.basename(folder)); + } + + } + + await archive.finalize(); + + } + + public async oneClickDownloadMap(map: BsvMapDetail): Promise{ + + const downloadedMap = await this.downloadMap(map); + + const versions = await this.localVersion.getInstalledVersions(); + + for(const version of versions){ + + if(await this.versionIsLinked(version)){ continue; } + + const versionMapsPath = await this.getMapsFolderPath(version); + + this.utils.createFolderIfNotExist(versionMapsPath); + + copySync(downloadedMap, path.join(versionMapsPath, path.basename(downloadedMap)), {overwrite: true}); + + } + + } + + public enableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link)); + } + + public disableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link)); + } + + public isDeepLinksEnabled(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link)); + } + + + +} \ No newline at end of file diff --git a/src/main/services/additional-content/local-models-manager.service.ts b/src/main/services/additional-content/local-models-manager.service.ts new file mode 100644 index 00000000..f266d93a --- /dev/null +++ b/src/main/services/additional-content/local-models-manager.service.ts @@ -0,0 +1,126 @@ +import { DeepLinkService } from "../deep-link.service"; +import log from "electron-log" +import { ipcMain } from "electron"; +import { IpcRequest } from "shared/models/ipc"; +import { UtilsService } from "../utils.service"; +import { WindowManagerService } from "../window-manager.service"; +import { MSModel, MSModelType } from "shared/models/model-saber/model-saber.model"; +import { BSVersion } from "shared/bs-version.interface"; +import { BSLocalVersionService } from "../bs-local-version.service"; +import path from "path"; +import { RequestService } from "../request.service"; +import { copyFileSync } from "fs-extra"; +import sanitize from "sanitize-filename"; + +export class LocalModelsManagerService { + + private static instance: LocalModelsManagerService; + + public static getInstance(): LocalModelsManagerService{ + if(!LocalModelsManagerService.instance){ LocalModelsManagerService.instance = new LocalModelsManagerService(); } + return LocalModelsManagerService.instance; + } + + private readonly DEEP_LINKS = { + ModelSaber: "modelsaber", + }; + + private readonly MODEL_TYPE_FOLDER: Record, string> = { + avatar: "CustomAvatars", + bloq: "CustomNotes", + platform: "CustomPlatforms", + saber: "CustomSabers" + } + + private readonly deepLink: DeepLinkService; + private readonly utils: UtilsService; + private readonly windows: WindowManagerService; + private readonly localVersion: BSLocalVersionService; + private readonly request: RequestService; + + private constructor(){ + this.deepLink = DeepLinkService.getInstance(); + this.utils = UtilsService.getInstance(); + this.windows = WindowManagerService.getInstance(); + this.localVersion = BSLocalVersionService.getInstance(); + this.request = RequestService.getInstance(); + + this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, (link) => { + log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link); + const url = new URL(link); + + const type = url.host + const id = url.pathname.replace("/", '').split("/").at(0); + + this.openOneClickDownloadModelWindow(id, type); + }); + } + + private openOneClickDownloadModelWindow(id: string, type: string){ + + ipcMain.once("one-click-model-info", async (event, req: IpcRequest) => { + this.utils.ipcSend(req.responceChannel, {success: true, data: {id, type}}); + }); + + this.windows.openWindow("oneclick-download-model.html"); + + } + + private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise{ + + if(!version){ throw "will be implemented whith models management" } + if(type === "misc"){ throw "model type not supported"; } + + const versionPath = await this.localVersion.getVersionPath(version); + const modelFolderPath = path.join(versionPath, this.MODEL_TYPE_FOLDER[type]); + + this.utils.createFolderIfNotExist(modelFolderPath); + + return modelFolderPath; + + } + + public async downloadModel(model: MSModel, version: BSVersion): Promise{ + + const modelFolder = await this.getModelFolderPath(model.type, version); + const modelDest = path.join(modelFolder, sanitize(path.basename(model.download))); + + return this.request.downloadFile(model.download, modelDest); + + } + + public async oneClickDownloadModel(model: MSModel): Promise{ + + if(!model){ return; } + + const versions = await this.localVersion.getInstalledVersions(); + + if(versions?.length === 0){ return; } + + const fisrtVersion = versions.shift(); + + const downloaded = await this.downloadModel(model, fisrtVersion); + + for(const version of versions){ + + const modelDest = path.join(await this.getModelFolderPath(model.type, version), path.basename(downloaded)); + + copyFileSync(downloaded, modelDest); + + } + + } + + public enableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link)); + } + + public disableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link)); + } + + public isDeepLinksEnabled(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link)); + } + +} \ No newline at end of file diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts new file mode 100644 index 00000000..3955efb5 --- /dev/null +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -0,0 +1,226 @@ +import path from "path"; +import { BehaviorSubject, Observable } from "rxjs"; +import { BSVersion } from "shared/bs-version.interface"; +import { BSLocalVersionService } from "../bs-local-version.service"; +import { DeepLinkService } from "../deep-link.service"; +import { RequestService } from "../request.service"; +import { UtilsService } from "../utils.service"; +import { LocalMapsManagerService } from "./local-maps-manager.service"; +import log from "electron-log" +import { isValidUrl } from "../../helpers/url.helpers"; +import { ipcMain } from "electron"; +import { WindowManagerService } from "../window-manager.service"; +import { IpcRequest } from "shared/models/ipc"; +import { BPList, DownloadPlaylistProgression } from "shared/models/playlists/playlist.interface"; +import { copyFileSync, readFileSync } from "fs"; +import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; +import { copySync } from "fs-extra"; + +export class LocalPlaylistsManagerService { + + private static instance: LocalPlaylistsManagerService + + public static getInstance(): LocalPlaylistsManagerService { + if (!LocalPlaylistsManagerService.instance) { LocalPlaylistsManagerService.instance = new LocalPlaylistsManagerService(); } + return LocalPlaylistsManagerService.instance; + } + + private readonly PLAYLISTS_FOLDER = "Playlists"; + private readonly DEEP_LINKS = { + BeatSaver: "bsplaylist", + }; + + + private readonly versions: BSLocalVersionService; + private readonly maps: LocalMapsManagerService; + private readonly utils: UtilsService; + private readonly request: RequestService; + private readonly deepLink: DeepLinkService; + private readonly windows: WindowManagerService; + private readonly bsaver: BeatSaverService; + + private constructor(){ + this.maps = LocalMapsManagerService.getInstance(); + this.versions = BSLocalVersionService.getInstance(); + this.utils = UtilsService.getInstance(); + this.request = RequestService.getInstance(); + this.deepLink = DeepLinkService.getInstance(); + this.windows = WindowManagerService.getInstance(); + this.bsaver = BeatSaverService.getInstance(); + + this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => { + + log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link); + const url = new URL(link); + const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : ""; + + this.openOneClickDownloadPlaylistWindow(bplistUrl); + + }); + + } + + private getPlaylistIdFromDownloadUrl(url: string): string{ + + if(!isValidUrl(url)){ return ""; } + + const splited = url.split("/") + const idIndex = splited.indexOf("id"); + + if(idIndex < 0){ return ""; } + + return splited[idIndex + 1]; + } + + private async getPlaylistsFolder(version?: BSVersion){ + + if(!version){ throw "Playlists are not available to be linked yet" } + + const versionFolder = await this.versions.getVersionPath(version); + + const folder = path.join(versionFolder, this.PLAYLISTS_FOLDER); + + await this.utils.createFolderIfNotExist(folder) + + return folder; + + } + + private async installBPListFile(bpListUrlOrPath: string, version: BSVersion): Promise{ + + const playlistFolder = await this.getPlaylistsFolder(version); + + const bpListDest = path.join(playlistFolder, path.basename(bpListUrlOrPath)); + + if(this.utils.pathExist(bpListUrlOrPath)){ + copyFileSync(bpListUrlOrPath, bpListDest); + } + else{ + await this.request.downloadFile(bpListUrlOrPath, bpListDest); + } + + return bpListDest; + + } + + private async readPlaylistFile(path: string): Promise{ + + if(!this.utils.pathExist(path)){ throw `bplist file not exist at ${path}`; } + + const rawContent = readFileSync(path).toString(); + + return JSON.parse(rawContent); + + } + + private openOneClickDownloadPlaylistWindow(downloadUrl: string): void{ + + ipcMain.once("one-click-playlist-info", async (event, req: IpcRequest) => { + this.utils.ipcSend(req.responceChannel, {success: true, data: {bpListUrl: downloadUrl, id: this.getPlaylistIdFromDownloadUrl(downloadUrl)}}); + }); + + this.windows.openWindow("oneclick-download-playlist.html"); + + } + + public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable{ + + const res = new BehaviorSubject({progression: 0, current: null, downloadedMaps: [], mapsPath: [], bpListPath: ""}); + + const sub = res.subscribe(process => { + this.utils.ipcSend("download-playlist-progress", {success: true, data: process}); + }, err => { + this.utils.ipcSend("download-playlist-progress", {success: false, error: err}); + }); + + const observer = async () => { + + try{ + + const bpListPath = await this.installBPListFile(bpListUrl, version); + + res.next({...res.value, bpListPath}); + + const bpList = await this.readPlaylistFile(bpListPath); + + for(const song of bpList.songs){ + + if(!song.key){ continue; } + + const map = await this.bsaver.getMapDetailsById(song.key); + + res.next({ + ...res.value, + current: map, + progression: ((res.value.downloadedMaps.length + .5) / bpList.songs.length) * 100 + }); + + const mapPath = await this.maps.downloadMap(map, version); + + const progression = ((res.value.downloadedMaps.length + 1) / bpList.songs.length) * 100; + + res.next({ + ...res.value, + current: null, + downloadedMaps: [...res.value.downloadedMaps, map], + mapsPath: [...res.value.mapsPath, mapPath], + progression + }); + + } + + } + catch(e){ + res.error(e); + } + + res.complete(); + + } + + observer().finally(() => sub.unsubscribe()); + return res.asObservable(); + + } + + public async oneClickInstallPlaylist(bpListUrl: string): Promise{ + + const versions = await this.versions.getInstalledVersions(); + + const firstVersion = versions.shift(); + const fistVersionLinked = await this.maps.versionIsLinked(firstVersion); + + const {bpListPath, mapsPath} = await this.downloadPlaylist(bpListUrl, firstVersion).toPromise(); + + for(const version of versions){ + + await this.installBPListFile(bpListPath, version); + + const versionIsLinked = await this.maps.versionIsLinked(version); + + if(fistVersionLinked && versionIsLinked){ continue; } + + for(const mapPath of mapsPath){ + const versionMapsFolder = await this.maps.getMapsFolderPath(version); + const mapDest = path.join(versionMapsFolder, path.basename(mapPath)); + + copySync(mapPath, mapDest, {overwrite: true}); + } + + } + + } + + public enableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link)); + } + + public disableDeepLinks(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link)); + } + + public isDeepLinksEnabled(): boolean{ + return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link)); + } + +} \ No newline at end of file diff --git a/src/main/services/bs-installer.service.ts b/src/main/services/bs-installer.service.ts index 06971716..ffdfd5b4 100644 --- a/src/main/services/bs-installer.service.ts +++ b/src/main/services/bs-installer.service.ts @@ -2,12 +2,13 @@ import { BS_APP_ID, BS_DEPOT } from "../constants"; import path from "path"; import { BSVersion } from 'shared/bs-version.interface'; import { UtilsService } from "./utils.service"; -import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process"; import log from "electron-log"; import { InstallationLocationService } from "./installation-location.service"; import { ctrlc } from "ctrlc-windows"; import { BSLocalVersionService } from "./bs-local-version.service"; import isOnline from 'is-online'; +import { WindowManagerService } from "./window-manager.service"; export class BSInstallerService{ @@ -16,6 +17,7 @@ export class BSInstallerService{ private readonly utils: UtilsService; private readonly installLocationService: InstallationLocationService; private readonly localVersionService: BSLocalVersionService; + private readonly windows: WindowManagerService; private downloadProcess: ChildProcessWithoutNullStreams; @@ -23,8 +25,9 @@ export class BSInstallerService{ this.utils = UtilsService.getInstance(); this.installLocationService = InstallationLocationService.getInstance(); this.localVersionService = BSLocalVersionService.getInstance(); + this.windows = WindowManagerService.getInstance(); - this.utils.getMainWindow().on("close", () => { + this.windows.getWindows("index.html")?.on("close", () => { this.killDownloadProcess(); }); } @@ -62,6 +65,18 @@ export class BSInstallerService{ }); } + public async isDotNet6Installed(): Promise{ + try{ + const process = spawnSync(this.getDepotDownloaderExePath()); + const out = process.output.toString(); + if(out.includes(".NET runtime can be found at")){ return false; } + return true; + } + catch(e){ + return false; + } + } + public async downloadBsVersion(downloadInfos: DownloadInfo): Promise{ if(this.downloadProcess && this.downloadProcess.connected){ throw "AlreadyDownloading"; } diff --git a/src/main/services/deep-link.service.ts b/src/main/services/deep-link.service.ts new file mode 100644 index 00000000..095f9e35 --- /dev/null +++ b/src/main/services/deep-link.service.ts @@ -0,0 +1,99 @@ +import { app } from "electron"; +import path from "path"; +import { URL } from "url"; +import log from "electron-log" + +export class DeepLinkService { + + // See docs : https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs + + private static instance: DeepLinkService; + + public static getInstance(): DeepLinkService{ + if(!DeepLinkService.instance){ DeepLinkService.instance = new DeepLinkService(); } + return DeepLinkService.instance; + } + + private readonly listeners = new Map() + + private constructor(){} + + public registerDeepLink(protocol: string): boolean{ + + if(process.defaultApp && process.argv.length >= 2){ + return app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]); + } + + return app.setAsDefaultProtocolClient(protocol); + } + + public unRegisterDeepLink(protocol: string): boolean{ + + if(process.defaultApp && process.argv.length >= 2) { + return app.removeAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]); + } + + return app.removeAsDefaultProtocolClient(protocol); + } + + public isDeepLinkRegistred(protocol: string): boolean{ + + if(process.defaultApp && process.argv.length >= 2) { + return app.isDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]); + } + + return app.isDefaultProtocolClient(protocol); + } + + public addLinkOpenedListener(protocol: string, fn: Listerner){ + + if(!this.listeners.has(protocol)){ + this.listeners.set(protocol, [] as Listerner[]); + } + + this.listeners.get(protocol).push(fn); + } + + public removeLinkOpenedListener(protocol: string, fn: Listerner){ + + if(!this.listeners.get(protocol)?.length){ return; } + + const listeners = this.listeners.get(protocol); + const fnIndex = listeners.findIndex(listener => listener === fn); + + if(fnIndex < 0){ return; } + + listeners.splice(fnIndex, 1); + + } + + public dispatchLinkOpened(link: string){ + + log.info("DEISPATCH", link); + + const url = new URL(link); + + const protocolListeners = this.listeners.get(url.protocol.replace(":", "")) ?? []; + + protocolListeners.forEach(listerner => { + listerner(link); + }); + + } + + public isDeepLink(link: string): boolean{ + + try{ + const url = new URL(link); + const protocol = url.protocol.replace(":", ""); + return Array.from(this.listeners.keys()).some(key => key === protocol); + } + catch(e){ + return false; + } + + } + +} + +type Listerner = (link: string) => void; \ No newline at end of file diff --git a/src/main/services/installation-location.service.ts b/src/main/services/installation-location.service.ts index 48a58c66..17a77986 100644 --- a/src/main/services/installation-location.service.ts +++ b/src/main/services/installation-location.service.ts @@ -12,6 +12,11 @@ export class InstallationLocationService { private readonly INSTALLATION_FOLDER = "BSManager"; private readonly VERSIONS_FOLDER = "BSInstances"; + + private readonly SHARED_CONTENT_FOLDER = "SharedContent"; + private readonly SHARED_MAPS_FOLDER = "SharedMaps"; + private readonly SHARED_PLAYLISTS_FOLDER = "SharedPlaylists"; + private readonly STORE_INSTALLATION_PATH_KEY = "installation-folder"; @@ -36,9 +41,6 @@ export class InstallationLocationService { this._installationDirectory = this.configService.get(this.STORE_INSTALLATION_PATH_KEY) || app.getPath("documents"); } - public get installationDirectory(): string{ return path.join(this._installationDirectory, this.INSTALLATION_FOLDER); } - public get versionsDirectory(): string { return path.join(this.installationDirectory, this.VERSIONS_FOLDER); } - public setInstallationDirectory(newDir: string): Promise{ const oldDir = this.installationDirectory; const newDest = path.join(newDir, this.INSTALLATION_FOLDER); @@ -53,7 +55,15 @@ export class InstallationLocationService { log.error(err); }) }) - } + public get installationDirectory(): string{ return path.join(this._installationDirectory, this.INSTALLATION_FOLDER); } + + public get versionsDirectory(): string { return path.join(this.installationDirectory, this.VERSIONS_FOLDER); } + + public get sharedContentPath(): string { return path.join(this.installationDirectory, this.SHARED_CONTENT_FOLDER); } + public get sharedMapsPath(): string { return path.join(this.sharedContentPath, this.SHARED_MAPS_FOLDER); } + public get sharedPlaylistsPath(): string { return path.join(this.sharedContentPath, this.SHARED_PLAYLISTS_FOLDER); } + + } diff --git a/src/main/services/map.service.ts b/src/main/services/map.service.ts deleted file mode 100644 index 596321c8..00000000 --- a/src/main/services/map.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import path from "path"; -import { BSVersion } from "shared/bs-version.interface"; -import { BSLocalVersionService } from "./bs-local-version.service"; -import archiver from "archiver"; -import { createWriteStream } from "fs"; -import log from "electron-log"; - -export class MapService{ - - private readonly MAP_PATH = path.join("Beat Saber_Data", "CustomLevels"); - - private static instance: MapService; - - private readonly localVersionService: BSLocalVersionService; - - public static getInstance(): MapService{ - if(!MapService.instance){ MapService.instance = new MapService(); } - return MapService.instance; - } - - private constructor(){ - this.localVersionService = BSLocalVersionService.getInstance(); - } - - private getMapsPath(versionPath: string): string{ - return path.join(versionPath, this.MAP_PATH); - } - - public async exportVersionMaps(version: BSVersion, outputZip: string): Promise{ - const versionPath = await this.localVersionService.getVersionPath(version); - const mapsPath = this.getMapsPath(versionPath); - - const output = createWriteStream(outputZip); - const archive = archiver("zip", {zlib: {level: 9}}); - - const promise = new Promise((resolve, reject) => { - archive.pipe(output); - archive.directory(mapsPath, false); - archive.on("error", reject); - output.on("close", resolve); - archive.finalize() - }); - - return promise; - } - -} \ No newline at end of file diff --git a/src/main/services/notification.service.ts b/src/main/services/notification.service.ts new file mode 100644 index 00000000..74277bad --- /dev/null +++ b/src/main/services/notification.service.ts @@ -0,0 +1,27 @@ +import { Notification } from "electron"; +import { SystemNotificationOptions } from "shared/models/notification/system-notification.model"; +import { UtilsService } from "./utils.service"; + +export class NotificationService { + + private static instance: NotificationService; + + public static getInstance(): NotificationService{ + if(!NotificationService.instance){ NotificationService.instance = new NotificationService(); } + return NotificationService.instance; + } + + private readonly APP_ICON: string; + + private readonly utils: UtilsService; + + private constructor(){ + this.utils = UtilsService.getInstance(); + this.APP_ICON = this.utils.getAssetsPath("favicon.ico"); + } + + public notify(options: SystemNotificationOptions){ + new Notification({...options, icon: this.APP_ICON}).show(); + } + +} \ No newline at end of file diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 03ce76fc..5c2c2237 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -27,6 +27,7 @@ export class RequestService { public downloadFile(url: string, dest: string): Promise{ return new Promise((resolve, reject) => { + const file = createWriteStream(dest); get(url, res => { res.pipe(file); diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index c880f97b..622d175d 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -3,6 +3,7 @@ import regedit from 'regedit' import path from "path"; import { parse } from "@node-steam/vdf"; import { readFile } from "fs/promises"; +import { spawn } from "child_process"; export class SteamService{ @@ -62,4 +63,13 @@ export class SteamService{ return null; } + public openSteam(): Promise{ + const process = spawn("start", ["steam://open/games"], {shell: true}); + + return new Promise(resolve => { + process.on("exit", () => resolve(true)); + process.on("error", () => resolve(false)); + }); + } + } diff --git a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts new file mode 100644 index 00000000..7f443857 --- /dev/null +++ b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts @@ -0,0 +1,130 @@ +import { ApiResult } from "renderer/models/api/api.model"; +import { BsvMapDetail } from "shared/models/maps"; +import { BsvPlaylist, BsvPlaylistPage, MapFilter, SearchParams, SearchResponse } from "shared/models/maps/beat-saver.model"; +import fetch from "node-fetch" + +export class BeatSaverApiService { + + private static instance: BeatSaverApiService; + + public static getInstance(): BeatSaverApiService{ + if(!BeatSaverApiService.instance){ BeatSaverApiService.instance = new BeatSaverApiService(); } + return BeatSaverApiService.instance; + } + + private readonly bsaverApiUrl = "https://beatsaver.com/api" + + private constructor(){} + + private mapFilterToUrlParams(filter: MapFilter): URLSearchParams{ + + if(!filter){ return new URLSearchParams(); } + + const enbledTagsString = filter.enabledTags ? Array.from(filter.enabledTags) : null; + const excludedTagsString = filter.excludedTags ? Array.from(filter.excludedTags).map(tag => `!${tag}`) : null; + + const tags = (enbledTagsString || excludedTagsString) ? [...enbledTagsString, excludedTagsString].join("|") : null; + + const params = { + ...(filter.automapper && {automapper: String(filter.automapper)}), + ...(filter.chroma && {chroma: String(filter.chroma)}), + ...(filter.cinema && {cinema: String(filter.cinema)}), + ...(filter.me && {me: String(filter.me)}), + ...(filter.noodle && {noodle: String(filter.noodle)}), + ...(filter.ranked && {ranked: String(filter.ranked)}), + ...(filter.verified && {verified: String(filter.verified)}), + ...(filter.curated && {curated: String(filter.curated)}), + ...(filter.fullSpread && {fullSpread: String(filter.fullSpread)}), + ...(filter.from && {from: String(filter.from)}), + ...(filter.to && {to: String(filter.to)}), + ...(tags && {tags: String(tags)}), + ...(filter.minDuration && {minDuration: String(filter.minDuration)}), + ...(filter.maxDuration && {maxDuration: String(filter.maxDuration)}), + ...(filter.minNps && {minNps: String(filter.minNps)}), + ...(filter.maxNps && {maxNps: String(filter.maxNps)}) + }; + + return new URLSearchParams(params); + + } + + private searchParamsToUrlParams(search: SearchParams): URLSearchParams{ + + if(!search){ return new URLSearchParams(); } + + const searchParams = { + ...(search.includeEmpty && {includeEmpty: String(search.includeEmpty)}), + ...(search.sortOrder && {sortOrder: search.sortOrder}), + ...(search.q && {q: search.q}) + }; + + const filterUrlParms = this.mapFilterToUrlParams(search.filter); + + return new URLSearchParams({ + ...searchParams, + ...Object.fromEntries(filterUrlParms) + }); + + } + + public async getMapsDetailsByHashs(hashs: T[]): Promise, BsvMapDetail>>>{ + + if(hashs.length > 50){ throw "too musch map hashs"; } + + const paramsHashs = hashs.join(","); + const resp = await fetch(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`); + + const data = await resp.json() as Record, BsvMapDetail> | BsvMapDetail; + + if((data as BsvMapDetail).id){ + const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase(); + const parsedData = { + [key]: data as BsvMapDetail + } as Record, BsvMapDetail>; + + return {status: resp.status, data: parsedData} + } + + return {status: resp.status, data: (data as Record, BsvMapDetail>)}; + + } + + public async getMapDetailsById(id: string): Promise>{ + + const res = await fetch(`${this.bsaverApiUrl}/maps/id/${id}`); + + const data = await res.json() as BsvMapDetail; + + return {status: res.status, data}; + + } + + public async searchMaps(search: SearchParams): Promise>{ + + const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`); + + url.search = this.searchParamsToUrlParams(search).toString(); + + const res = await fetch(url.toString()); + + if(!res.ok){ + return {status: res.status, data: null}; + } + + const data: any = await res.json(); + + return {status: res.status, data}; + + } + + public async getPlaylistDetails(id: string): Promise>{ + + const res = await fetch(`${this.bsaverApiUrl}/playlists/id/${id}/0`); + + const data = await res.json() as BsvPlaylistPage; + + return {status: res.status, data: data.playlist}; + + } + +} \ No newline at end of file diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts new file mode 100644 index 00000000..660e98d1 --- /dev/null +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -0,0 +1,76 @@ +import { splitIntoChunk } from "../../../helpers/array-tools"; +import { BsvMapDetail } from "shared/models/maps"; +import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model"; +import { BeatSaverApiService } from "./beat-saver-api.service"; + +export class BeatSaverService { + + private static instance: BeatSaverService; + + public static getInstance(): BeatSaverService{ + if(!BeatSaverService.instance){ BeatSaverService.instance = new BeatSaverService(); } + return BeatSaverService.instance; + } + + private readonly bsaverApi: BeatSaverApiService; + + private readonly cachedMapsDetails = new Map(); + + private constructor(){ + this.bsaverApi = BeatSaverApiService.getInstance(); + } + + public async getMapDetailsFromHashs(hashs: string[]): Promise{ + + const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !Array.from(this.cachedMapsDetails.keys()).includes(hash)); + const chunkHash = splitIntoChunk(filtredHashs, 50); + + const mapDetails = Array.from(this.cachedMapsDetails.entries()).reduce((res , [hash, details]) => { + if(hashs.includes(hash)){ + res.push(details); + } + return res; + }, [] as BsvMapDetail[]); + + for(const hashs of chunkHash){ + + const res = await this.bsaverApi.getMapsDetailsByHashs(hashs); + + if(res.status === 200){ + mapDetails.push(...Object.values(res.data).filter(detail => !!detail)); + mapDetails.forEach(detail => { + this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail); + }); + } + } + + return mapDetails; + } + + public async getMapDetailsById(id: string): Promise{ + + const res = await this.bsaverApi.getMapDetailsById(id); + return res.data; + + } + + public searchMaps(search: SearchParams): Promise{ + + return this.bsaverApi.searchMaps(search).then(res => { + return res.status === 200 ? res.data.docs : []; + }).catch(err => { + return []; + }); + + } + + public async getPlaylistPage(id: string): Promise{ + + const res = await this.bsaverApi.getPlaylistDetails(id); + return res.data; + + } + + + +} \ No newline at end of file diff --git a/src/main/services/thrid-party/model-saber/model-saber-api.service.ts b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts new file mode 100644 index 00000000..f24ed97c --- /dev/null +++ b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts @@ -0,0 +1,70 @@ +import fetch from "node-fetch"; +import { ApiResult } from "renderer/models/api/api.model"; +import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "shared/models/model-saber/model-saber.model"; + +export class ModelSaberApiService { + + private static instance: ModelSaberApiService; + + public static getInstance(): ModelSaberApiService{ + if(!ModelSaberApiService.instance){ ModelSaberApiService.instance = new ModelSaberApiService(); } + return ModelSaberApiService.instance; + } + + private readonly API_URL = "https://modelsaber.com/api/v2/"; + private readonly ENDPOINTS = {get: "get.php", types: "types.php"}; + + private constructor(){} + + private parseFilters(filters: MSGetQueryFilter[]): string{ + + if(!filters){ return null; } + + const parsed = filters.map(filter => { + const stringFilter = filter.type === "searchName" ? filter.value : `${filter.type}:${filter.value}`; + return filter.isNegative ? `-${stringFilter}` : stringFilter; + }); + + return parsed.join(","); + + } + + private buildUrlQuery(query: MSGetQuery): URLSearchParams{ + + if(!query){ return new URLSearchParams(); } + + const filterQuery = this.parseFilters(query.filter); + + const searchParams = { + ...(query.type && {type: query.type}), + ...(query.platform && {platform: query.platform}), + ...(query.start && {start: `${query.start}`}), + ...(query.end && {end: `${query.end}`}), + ...(query.sort && {sort: query.sort}), + ...(query.sortDirection && {sortDirection: query.sortDirection}), + ...(filterQuery && {filter: filterQuery}), + } + + return new URLSearchParams(searchParams); + + } + + public async searchModel(query: MSGetQuery): Promise>{ + + const url = new URL(this.ENDPOINTS.get, this.API_URL); + + url.search = this.buildUrlQuery(query).toString(); + + const res = await fetch(url.toString()); + + if(!res.ok){ + return {data: null, status: res.status}; + } + + const data = await res.json() as MSGetResponse; + + return {data, status: res.status}; + + } + +} \ No newline at end of file diff --git a/src/main/services/thrid-party/model-saber/model-saber.service.ts b/src/main/services/thrid-party/model-saber/model-saber.service.ts new file mode 100644 index 00000000..c694987e --- /dev/null +++ b/src/main/services/thrid-party/model-saber/model-saber.service.ts @@ -0,0 +1,45 @@ +import { MSGetQuery, MSGetQueryFilter, MSModel } from "shared/models/model-saber/model-saber.model"; +import { ModelSaberApiService } from "./model-saber-api.service"; + +export class ModelSaberService { + + private static instance: ModelSaberService; + + public static getInstance(): ModelSaberService{ + if(!ModelSaberService.instance){ ModelSaberService.instance = new ModelSaberService(); } + return ModelSaberService.instance; + } + + private readonly modelSaberApi: ModelSaberApiService; + + private constructor(){ + this.modelSaberApi = ModelSaberApiService.getInstance(); + } + + public async getModelById(id: number|string): Promise{ + + const query: MSGetQuery = { + start: 0, + end: 1, + platform: "pc", + filter: [{type: "id", value: id}] + } + + try{ + const res = await this.modelSaberApi.searchModel(query); + + if(res.status !== 200){ return null; } + + if(Object.keys(res.data).length === 0){ + return null; + } + + return res.data[`${id}`]; + } + catch(e){ + return null; + } + + } + +} \ No newline at end of file diff --git a/src/main/services/utils.service.ts b/src/main/services/utils.service.ts index 328a9ac7..5735bc4c 100644 --- a/src/main/services/utils.service.ts +++ b/src/main/services/utils.service.ts @@ -1,4 +1,5 @@ -import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs"; +import { existsSync, mkdirSync, readdirSync, readFile, rmSync } from "fs"; +import { moveSync } from "fs-extra" import { spawnSync } from "child_process"; import { homedir } from "os"; import path from "path"; @@ -6,6 +7,9 @@ import { app, BrowserWindow } from "electron"; import { rm, unlink } from "fs/promises"; import { IpcResponse } from "shared/models/ipc"; import log from "electron-log"; +import { AppWindow } from "shared/models/window-manager/app-window.model"; + +// TODO : REFACTOR export class UtilsService{ @@ -13,7 +17,7 @@ export class UtilsService{ private assetsPath: string = ''; - private mainWindow: BrowserWindow; + private windows: Map = new Map(); private constructor(){} @@ -29,8 +33,8 @@ export class UtilsService{ public getAssestsJsonsPath(): string { return this.getAssetsPath("jsons"); } public getTempPath(): string{ return path.join(app.getPath("temp"), app.getName()) } - public setMainWindow(win: BrowserWindow){ this.mainWindow = win; } - public getMainWindow(){ return this.mainWindow; } + public setMainWindows(windows: Map){ this.windows = windows; } + public getMainWindows(win: AppWindow){ return this.windows.get(win); } public pathExist(path: string): boolean{ return existsSync(path); } @@ -43,7 +47,7 @@ export class UtilsService{ return unlink(pathToFile); } - public rmDirIfExist(path: string): Promise{ + public async rmDirIfExist(path: string): Promise{ if(!this.pathExist(path)){ return; } return rm(path, {recursive: true, force: true}); } @@ -68,19 +72,33 @@ export class UtilsService{ }); } - public listDirsInDir(dirPath: string): string[]{ + public listDirsInDir(dirPath: string, fullPath = false): string[]{ let files = readdirSync(dirPath, { withFileTypes:true}); files = files.filter(f => f.isDirectory()) - return files.map(f => f.name); + return files.map(f => fullPath ? path.join(dirPath, f.name) : f.name); } - public deleteFolder(folderPath: string): Promise{ - return rm(folderPath, {recursive: true}); + public async deleteFolder(folderPath: string): Promise{ + const folderExist = this.pathExist(folderPath); + if(!folderExist){ return; } + return rmSync(folderPath, {recursive: true}); } + public async moveDirContent(src: string, dest: string, overwrite = false): Promise{ + const [srcExist, destExist] = await Promise.all([this.pathExist(src), this.pathExist(dest)]); + if(!srcExist){ return; } + if(!destExist){ await this.createFolderIfNotExist(dest); } + readdirSync(src, {encoding: "utf-8"}).forEach(file => { + const srcFullPath = path.join(src, file); + const destFullPath = path.join(dest, file); + if(!overwrite && this.pathExist(destFullPath)){ return; } + moveSync(srcFullPath, destFullPath, {overwrite}); + }); + } + public ipcSend(channel: string, response: IpcResponse): void{ try { - this.mainWindow.webContents.send(channel, response); + Array.from(this.windows.values()).forEach(window => window.webContents.send(channel, response)); } catch (error) { log.error(error); } diff --git a/src/main/services/window-manager.service.ts b/src/main/services/window-manager.service.ts index d4d075f0..e81a3ec4 100644 --- a/src/main/services/window-manager.service.ts +++ b/src/main/services/window-manager.service.ts @@ -2,25 +2,32 @@ import { app, BrowserWindow, BrowserWindowConstructorOptions } from "electron"; import { resolveHtmlPath } from "../util"; import { UtilsService } from "./utils.service"; import { AppWindow } from "shared/models/window-manager/app-window.model"; -import { PRELOAD_PATH } from "../main"; +import path from "path"; +import { APP_NAME } from "../constants"; export class WindowManagerService{ private static instance: WindowManagerService; + private readonly PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../../.erb/dll/preload.js') + private readonly utilsService: UtilsService = UtilsService.getInstance(); private readonly appWindowsOptions: Record = { "launcher.html": {width: 380, height: 500, minWidth: 380, minHeight: 500, resizable: false}, - "index.html": {width: 1080, height: 720, minWidth: 900, minHeight: 500} + "index.html": {width: 1080, height: 720, minWidth: 900, minHeight: 500}, + "oneclick-download-map.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false}, + "oneclick-download-playlist.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false}, + "oneclick-download-model.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false}, } private readonly baseWindowOption: BrowserWindowConstructorOptions = { + title: APP_NAME, icon: this.utilsService.getAssetsPath("favicon.ico"), show: false, frame: false, titleBarOverlay: false, - webPreferences: { preload: PRELOAD_PATH } + webPreferences: { preload: this.PRELOAD_PATH, webSecurity: false } } private readonly windows: Map = new Map(); @@ -32,15 +39,16 @@ export class WindowManagerService{ private constructor(){} - public openWindow(windowType: AppWindow): Promise{ - const window = new BrowserWindow({...this.appWindowsOptions[windowType], ...this.baseWindowOption}); + public openWindow(windowType: AppWindow, options?: BrowserWindowConstructorOptions): Promise{ + const window = new BrowserWindow({...this.appWindowsOptions[windowType], ...this.baseWindowOption, ...options}); + const promise = window.loadURL(resolveHtmlPath(windowType)); window.removeMenu(); window.setMenu(null); window.once("ready-to-show", () => { if (!window) { throw new Error('"window" is not defined'); } - return window.show(); + window.show(); }); window.once("closed", () => { @@ -49,15 +57,11 @@ export class WindowManagerService{ }); this.windows.set(windowType, window); - this.utilsService.setMainWindow(window); + this.utilsService.setMainWindows(this.windows); return promise.then(() => window); } - public closeWindow(window: AppWindow){ - this.windows.get(window).close(); - } - public closeAllWindows(except?: AppWindow){ this.windows.forEach((window, key) => { if(key === except){ return; } @@ -65,4 +69,14 @@ export class WindowManagerService{ }) } + public close(...win: AppWindow[]){ + win.forEach(window => { + this.windows.get(window)?.close(); + }); + } + + public getWindows(window: AppWindow): BrowserWindow{ + return this.windows.get(window); + } + } \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx deleted file mode 100644 index e41a0308..00000000 --- a/src/renderer/App.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { NavBar } from "./components/nav-bar/nav-bar.component"; -import TitleBar from "./components/title-bar/title-bar.component"; -import { Routes, Route, useLocation } from "react-router-dom"; -import { AvailableVersionsList } from "./pages/available-versions-list.components"; -import { VersionViewer } from "./pages/version-viewer.component"; -import { Modal } from "./components/modal/modal.component"; -import { SettingsPage } from "./pages/settings-page.component"; -import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.component"; -import { useEffect } from "react"; -import { ThemeService } from "./services/theme.service"; -import { NotificationOverlay } from "./components/notification/notification-overlay.component"; -import { PageStateService } from "./services/page-state.service"; -import "tailwindcss/tailwind.css"; - -export default function App() { - - const themeService = ThemeService.getInstance(); - const pageState = PageStateService.getInstance(); - - const location = useLocation(); - - pageState.setState(location.state); - - useEffect(() => { - themeService.theme$.subscribe(() => { - if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); } - document.documentElement.classList.remove('dark'); - }); - }, []); - - - return ( -
- - - -
- -
- - }/> - }/> - }/> - - -
-
-
- ); -} diff --git a/src/renderer/components/available-versions/available-versions-slide.component.tsx b/src/renderer/components/available-versions/available-versions-slide.component.tsx index ecfc970e..5cbb3484 100644 --- a/src/renderer/components/available-versions/available-versions-slide.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slide.component.tsx @@ -16,10 +16,6 @@ export function AvailableVersionsSlide(props: {year: string}) { }); }, []) -//w-full max-w-full max-h-full flex items-start justify-center overflow-x-hidden overflow-y-scroll content-start scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 - -//relative left-[2px] flex justify-center items-start content-start flex-wrap max-w-6xl - return (
    {availableVersions.map((version, index) => diff --git a/src/renderer/components/available-versions/available-versions-slider.component.tsx b/src/renderer/components/available-versions/available-versions-slider.component.tsx index aa5d1250..e87439ce 100644 --- a/src/renderer/components/available-versions/available-versions-slider.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slider.component.tsx @@ -23,10 +23,10 @@ export function AvailableVersionsSlider() { return (
    - +
      - { availableYears.map((year, index) => - + { availableYears.map(year => + )}
    diff --git a/src/renderer/components/maps-mangement-components/filter-panel.component.tsx b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx new file mode 100644 index 00000000..647da87d --- /dev/null +++ b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx @@ -0,0 +1,183 @@ +import { MapFilter, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model" +import {motion} from "framer-motion" +import { MutableRefObject} from "react" +import { MAP_TYPES } from "renderer/partials/maps/map-tags/map-types" +import { MAP_STYLES } from "renderer/partials/maps/map-tags/map-styles" +import { BsmCheckbox } from "../shared/bsm-checkbox.component" +import { min_to_s } from "renderer/helpers/time-utils" +import dateFormat from "dateformat" +import { BsmRange } from "../shared/bsm-range.component" +import { useTranslation } from "renderer/hooks/use-translation.hook" +import { MAP_SPECIFICITIES } from "renderer/partials/maps/map-general/map-specificity" +import { MAP_REQUIREMENTS } from "renderer/partials/maps/map-requirements/map-requirements" +import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors" + +export type Props = { + className?: string, + ref?: MutableRefObject + playlist?: boolean, + filter: MapFilter + onChange?: (filter: MapFilter) => void +} + +export function FilterPanel({className, ref, playlist = false, filter, onChange}: Props) { + + const t = useTranslation(); + + const MIN_NPS = 0; + const MAX_NPS = 17; + + const MIN_DURATION = 0; + const MAX_DURATION = min_to_s(30); + + const npss = [filter?.minNps || MIN_NPS, filter?.maxNps || MAX_NPS]; + const durations = [filter?.minDuration || MIN_DURATION, filter?.maxDuration || MAX_DURATION]; + + const isTagActivated = (tag: MapTag): boolean => filter?.enabledTags?.has(tag) || filter?.excludedTags?.has(tag); + const isTagExcluded = (tag: MapTag): boolean => filter?.excludedTags?.has(tag); + + const renderDurationLabel = (sec: number): JSX.Element => { + + const textValue = (() => { + if(sec === MIN_DURATION){ return t("maps.map-filter-panel.duration"); } + if(sec === MAX_DURATION){ return "∞"; } + const date = new Date(0); + date.setSeconds(sec); + return sec > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + })(); + + return renderLabel(textValue, sec === MAX_DURATION); + } + + const renderNpsLabel = (nps: number): JSX.Element => { + + const textValue = (() => { + if(nps === MIN_NPS){ return "NPS"; } + if(nps === MAX_NPS){ return "∞"; } + return nps; + })(); + + return renderLabel(textValue, nps === MAX_NPS); + + } + + const renderLabel = (text: unknown, isMax: boolean): JSX.Element => { + return ( + + {text} + + ) + } + + const onNpssChange = ([min, max]: number[]) => { + const newFilter: MapFilter = {...filter, minNps: min, maxNps: max}; + if(max === MAX_NPS){ + delete newFilter["maxNps"]; + } + onChange(newFilter); + } + + const onDurationsChange = ([min, max]: number[]) => { + const newFilter: MapFilter = {...filter, minDuration: min, maxDuration: max}; + if(max === MAX_DURATION){ + delete newFilter["maxDuration"]; + } + onChange(newFilter); + } + + const handleTagClick = (tag: MapTag) => { + + const enabledTags = filter.enabledTags ?? new Set(); + const excludedTags = filter.excludedTags ?? new Set(); + + if(isTagExcluded(tag)){ + excludedTags.delete(tag); + } + else if(isTagActivated(tag)){ + excludedTags.add(tag); + enabledTags.delete(tag); + } + else{ + enabledTags.add(tag); + } + + onChange({ + ...filter, + enabledTags, + excludedTags + }); + + } + + const translateMapType = (type: MapType): string => { + return t(`maps.map-types.${type}`); + } + + const translateMapStyle = (style: MapStyle): string => { + return t(`maps.map-styles.${style}`) + } + + const translateMapSpecificity = (specificity: MapSpecificity): string => { + return t(`maps.map-specificities.${specificity}`); + } + + type BooleanKeys = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T]; + + const handleCheckbox = (key: BooleanKeys) => { + const newFilter = {...(filter ?? {})}; + if(newFilter[key] === true){ + delete newFilter[key]; + } + else{ + newFilter[key] = true; + } + onChange(newFilter); + } + + return !playlist ? ( + +
    + + +
    +
    +
    + +

    {t("maps.map-filter-panel.specificities")}

    + {MAP_SPECIFICITIES.map(specificity => ( +
    handleCheckbox(specificity)}> + handleCheckbox(specificity)}/> + {translateMapSpecificity(specificity)} +
    + ))} + +

    {t("maps.map-filter-panel.requirements")}

    + {MAP_REQUIREMENTS.map(requirement => ( +
    handleCheckbox(requirement)}> + handleCheckbox(requirement)}/> + {requirement} +
    + ))} + +
    +
    +

    {t("maps.map-filter-panel.tags")}

    +
    + {MAP_TYPES.map(tag => ( + handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-90"}`} style={{backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Normal}}>{translateMapType(tag)} + ))} +
    +
    + {MAP_STYLES.map(tag => ( + handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-90"}`} style={{backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Easy}}>{translateMapStyle(tag)} + ))} +
    + + +
    +
    +
    + ) : ( + <> + ) +} diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx new file mode 100644 index 00000000..8528e642 --- /dev/null +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -0,0 +1,260 @@ +import { MapsManagerService } from "renderer/services/maps-manager.service" +import { BSVersion } from "shared/bs-version.interface" +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react" +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" +import { Subscription } from "rxjs" +import { MapItem, ParsedMapDiff } from "./map-item.component" +import { BsvMapCharacteristic, MapFilter } from "shared/models/maps/beat-saver.model" +import { useInView } from "framer-motion" +import { MapsDownloaderService } from "renderer/services/maps-downloader.service" +import { BsmImage } from "../shared/bsm-image.component" +import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png" +import { BsmButton } from "../shared/bsm-button.component" +import { useTranslation } from "renderer/hooks/use-translation.hook" + +type Props = { + version: BSVersion, + className?: string, + filter?: MapFilter + search?: string, +} + +export const LocalMapsListPanel = forwardRef(({version, className, filter, search} : Props, forwardRef) => { + + const mapsManager = MapsManagerService.getInstance(); + const mapsDownloader = MapsDownloaderService.getInstance(); + + const ref = useRef(null) + const isVisible = useInView(ref, {once: true}); + const [maps, setMaps] = useState([]); + const [subs] = useState([]); + const [selectedMaps, setSelectedMaps] = useState([]); + const t = useTranslation(); + + useImperativeHandle(forwardRef ,()=>({ + deleteMaps(){ + const mapsToDelete = selectedMaps.length === 0 ? maps : selectedMaps + mapsManager.deleteMaps(mapsToDelete, version).finally(loadMaps); + }, + exportMaps(){ + mapsManager.exportMaps(version, selectedMaps) + } + }), [selectedMaps, maps, version]); + + useEffect(() => { + + if(isVisible){ + loadMaps(); + subs.push(mapsManager.versionLinked$.subscribe(loadMaps)); + subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps)); + mapsDownloader.addOnMapDownloadedListener(loadMaps); + } + + return () => { + setMaps(() => []); + subs.forEach(s => s.unsubscribe()); + mapsDownloader.removeOnMapDownloadedListene(loadMaps); + } + }, [isVisible, version]); + + const loadMaps = () => { + subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => [...localMaps]))); + } + + const handleDelete = useCallback((map: BsmLocalMap) => { + mapsManager.deleteMaps([map], version).then(res => res && loadMaps()) + }, [version]); + + const extractMapDiffs = (map: BsmLocalMap): Map => { + const res = new Map(); + if(map.bsaverInfo?.versions[0]?.diffs){ + map.bsaverInfo.versions[0].diffs.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + const diffName = map.rawInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty + arr.push({name: diffName, type: diff.difficulty, stars: diff.stars}); + res.set(diff.characteristic, arr); + }); + return res; + } + + map.rawInfo._difficultyBeatmapSets.forEach(set => { + set._difficultyBeatmaps.forEach(diff => { + const arr = res.get(set._beatmapCharacteristicName) || []; + arr.push({name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null}); + res.set(set._beatmapCharacteristicName, arr); + }); + }); + + return res; + } + + const onMapSelected = useCallback((map: BsmLocalMap) => { + const maps = [...selectedMaps]; + if(maps.some(selectedMap => selectedMap.hash === map.hash)){ + const i = maps.findIndex(selectedMap => selectedMap.hash === map.hash); + maps.splice(i, 1); + } + else{ + maps.push(map); + } + setSelectedMaps(() => maps); + }, [selectedMaps]); + + const isMapFitFilter = (map: BsmLocalMap): boolean => { + + // Can be more clean and optimized i think + + const fitEnabledTags = (() => { + if(!filter?.enabledTags || filter.enabledTags.size === 0){ return true; } + if(!map?.bsaverInfo?.tags){ return false; } + return Array.from(filter.enabledTags.values()).every(tag => map.bsaverInfo.tags.some(mapTag => mapTag === tag)); + })(); + + if(!fitEnabledTags){ return false; } + + const fitExcluedTags = (() => { + if(!filter?.excludedTags || filter.excludedTags.size === 0){ return true; } + if(!map?.bsaverInfo?.tags){ return true; } + return !map.bsaverInfo.tags.some(tag => filter.excludedTags.has(tag)); + })(); + + if(!fitExcluedTags){ return false; } + + const fitMinNps = (() => { + if(!filter?.minNps){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return !map.bsaverInfo.versions.some(version => { + return version.diffs.some(diff => diff.nps < filter.minNps); + }); + })(); + + if(!fitMinNps){ return false; } + + const fitMaxNps = (() => { + if(!filter?.maxNps){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return !map.bsaverInfo.versions.some(version => { + return version.diffs.some(diff => diff.nps > filter.maxNps); + }); + })(); + + if(!fitMaxNps){ return false; } + + const fitMinDuration = (() => { + if(!filter?.minDuration){ return true; } + + if(!map?.bsaverInfo?.metadata?.duration){ return false; } + return map.bsaverInfo.metadata.duration >= filter.minDuration; + })(); + + if(!fitMinDuration){ return false; } + + const fitMaxDuration = (() => { + if(!filter?.maxDuration){ return true; } + if(!map?.bsaverInfo?.metadata?.duration){ return false; } + return map.bsaverInfo.metadata.duration <= filter.maxDuration; + })(); + + if(!fitMaxDuration){ return false; } + + const fitNoodle = (() => { + if(!filter?.noodle){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.ne)); + })(); + + if(!fitNoodle){ return false; } + + const fitMe = (() => { + if(!filter?.me){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.me)); + })(); + + if(!fitMe){ return false; } + + const fitCinema = (() => { + if(!filter?.cinema){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.cinema)); + })(); + + if(!fitCinema){ return false; } + + const fitChroma =(() => { + if(!filter?.chroma){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.chroma)); + })(); + + if(!fitChroma){ return false; } + + const fitFullSpread = (() => { + if(!filter?.fullSpread){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version?.diffs?.length >= 5); + })(); + + if(!fitFullSpread){ return false; } + + if(!(filter?.automapper ? map.bsaverInfo?.automapper === filter.automapper : true)){ return false } + if(!(filter?.ranked ? map.bsaverInfo?.ranked === filter.ranked : true)){ return false } + if(!(filter?.curated ? !!map.bsaverInfo?.curatedAt : true)){ return false } + if(!(filter?.verified ? !!map.bsaverInfo?.uploader?.verifiedMapper : true)){ return false } + + const searchCheck = (() => { + return ( + ((map.rawInfo?._songName ?? map.bsaverInfo?.name) || "")?.toLowerCase().includes(search.toLowerCase()) || + ((map.rawInfo?._songAuthorName ?? map.bsaverInfo?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) || + ((map.rawInfo?._levelAuthorName ?? map.bsaverInfo?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase())); + })(); + + if(!searchCheck){ return false }; + + return true; + + } + + const renderMaps = (): JSX.Element[] => { + return maps.reduce((acc, current) => { + if(isMapFitFilter(current)){ + acc.push(renderMapItem(current)); + } + return acc + }, []); + } + + const renderMapItem = (map: BsmLocalMap) => { + + return _map.hash === map.hash)} + diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} qualified={null} ranked={map.bsaverInfo?.ranked} autorId={map.bsaverInfo?.uploader?.id} likes={map.bsaverInfo?.stats?.upvotes} createdAt={map.bsaverInfo?.createdAt} + onDelete={handleDelete} + onSelected={onMapSelected} + callBackParam={map} + />; + } + + return ( +
    +
      + {maps?.length ? renderMaps() : ( +
      + + {t("pages.version-viewer.maps.tabs.maps.empty-maps.text")} + {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/> +
      + )} +
    +
    + ) +}) diff --git a/src/renderer/components/maps-mangement-components/map-item.component.tsx b/src/renderer/components/maps-mangement-components/map-item.component.tsx new file mode 100644 index 00000000..23bb0116 --- /dev/null +++ b/src/renderer/components/maps-mangement-components/map-item.component.tsx @@ -0,0 +1,242 @@ +import { BsmImage } from "../shared/bsm-image.component"; +import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model" +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { BsmLink } from "../shared/bsm-link.component"; +import { BsmIcon } from "../svgs/bsm-icon.component"; +import { BsmButton } from "../shared/bsm-button.component"; +import { AnimatePresence, motion } from "framer-motion"; +import { useState, Fragment, memo, useRef } from "react"; +import { LinkOpenerService } from "renderer/services/link-opener.service"; +import dateFormat from "dateformat"; +import { AudioPlayerService } from "renderer/services/audio-player.service"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { map } from "rxjs/operators"; +import useDelayedState from "use-delayed-state"; +import equal from "fast-deep-equal/es6"; +import { getMapZipUrlFromHash } from "renderer/helpers/maps-utils"; +import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component"; +import defaultImage from '../../../../assets/images/default-version-img.jpg' +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-difficulties"; +import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors" +import useDoubleClick from "use-double-click"; + +export type ParsedMapDiff = {type: BsvMapDifficultyType, name: string, stars: number} + +export type MapItemProps = { + hash: string, + title: string, + autor: string, + songAutor: string, + coverUrl: string, + songUrl: string, + autorId: number, + mapId: string, + diffs: Map, + qualified: boolean, + ranked: boolean, + bpm: number, + duration: number, + likes: number, + createdAt: string, + selected?: boolean, + downloading?: boolean, + callBackParam: T + onDelete?: (param: T) => void, + onDownload?: (param: T) => void, + onSelected?: (param: T) => void, + onCancelDownload?: (param: T) => void, + onDoubleClick?: (param: T) => void +} + +export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, createdAt, selected, downloading, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick}: MapItemProps) => { + + const linkOpener = LinkOpenerService.getInstance(); + const audioPlayer = AudioPlayerService.getInstance(); + + const color = useThemeColor("first-color"); + const t = useTranslation(); + + const ref = useRef(null); + const [hovered, setHovered] = useState(false); + const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false); + const [diffsPanelHovered, setDiffsPanelHovered] = useState(false); + + useDoubleClick({ + ref, + latency: onDoubleClick ? 200 : 0, + onSingleClick: e => onSelected?.(callBackParam), + onDoubleClick: e => onDoubleClick?.(callBackParam) + }) + + const songPlaying = useObservable(audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); + + const zipUrl = getMapZipUrlFromHash(hash); + const previewUrl = mapId ? `https://skystudioapps.com/bs-viewer/?url=${zipUrl}` : null; + const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; + const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null; + const createdDate = createdAt ? dateFormat(createdAt, "d mmm yyyy") : null; + const likesText = likes ? Intl.NumberFormat(undefined, {notation: "compact"}).format(likes).split(" ").join("") : null; + + const durationText = (() => { + if(!duration){ return null; } + const date = new Date(0); + date.setSeconds(duration); + return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + })(); + + const parseDiffLabel = (diffLabel: string) => { + if(MAP_DIFFICULTIES.includes(diffLabel as BsvMapDifficultyType)){ + return t(`maps.difficulties.${diffLabel}`); + } + return diffLabel; + } + + const openPreview = () => linkOpener.open(previewUrl, true); + const copyBsr = () => navigator.clipboard.writeText(`!bsr ${mapId}`); + const toogleMusic = () => { + if(songPlaying){ + return audioPlayer.pause(); + } + if(!audioPlayer.playing && audioPlayer.src === songUrl){ + return audioPlayer.resume(); + } + audioPlayer.play(songUrl, bpm); + } + + const bottomBarHoverStart = () => { + cancelBottomBarHovered(); + setBottomBarHovered(true, (diffsPanelHovered || bottomBarHovered) ? 0 : 300); + } + + const bottomBarHoverEnd = () => { + cancelBottomBarHovered(); + setBottomBarHovered(false, 100); + } + + const diffsPanelHoverStart = () => setDiffsPanelHovered(true); + const diffsPanelHoverEnd = () => setDiffsPanelHovered(false); + + + const renderDiffPreview = () => { + + const diffSets = Array.from(diffs.entries()); + + if(diffSets.length === 1){ + const [diffType, diffSet] = diffSets[0]; + + return ( + <> + +
    + {diffSet.map(diff => ( + + ))} +
    + + ) + } + if(diffSets.length > 1){ + return diffSets.map(([diffType, diffSet]) => ( + + + {diffSet.length} + + )); + } + } + + return ( + setHovered(true)} onHoverEnd={() => setHovered(false)} style={{zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden"}}> + {(hovered || selected) && onSelected && } + + {(diffsPanelHovered || bottomBarHovered) && ( + + {Array.from(diffs.entries()).map(([charac, diffSet]) => ( +
      + {diffSet.map(({type, name, stars}) => ( +
    1. + + + { + stars ? ( + ★ {stars} + ) : ( + {parseDiffLabel(type)} + ) + } + + {parseDiffLabel(name)} +
    2. + ))} +
    + ))} +
    + )} +
    +
    +
    + + {e.stopPropagation(); e.preventDefault(); toogleMusic()}}> + + +
    +
    + +
    +

    {title}

    +

    {songAutor && t("maps.map-item.by", {songAutor})}

    +

    {autor && (<> {t("maps.map-item.mapped-by")} {autor})}

    +
    + {likesText && ( +
    + + {likesText} +
    + )} + {durationText && ( +
    + + {durationText} +
    + )} + {createdAt && ( +
    + + +
    + )} +
    + + {ranked && ( +
    + {t("maps.map-specificities.ranked")} +
    + )} +
    + {renderDiffPreview()} +
    +
    +
    +
    +
    + + + +
    + {onDelete && !downloading && {e.stopPropagation(); onDelete(callBackParam)}}/>} + {onDownload && !downloading && {e.stopPropagation(); onDownload(callBackParam)}}/>} + {onCancelDownload && !downloading && {e.stopPropagation(); onCancelDownload(callBackParam)}}/>} + {downloading && } + {previewUrl && {e.stopPropagation(); openPreview()}}/>} + {mapId && {e.stopPropagation(); copyBsr()}}/>} +
    +
    +
    +
    + ) +}, areEqual) + +function areEqual(prevProps: MapItemProps, nextProps: MapItemProps): boolean { + return equal(prevProps, nextProps); +} diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx new file mode 100644 index 00000000..4c9a1fb7 --- /dev/null +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -0,0 +1,142 @@ +import { DetailedHTMLProps, useEffect, useRef, useState } from "react" +import { BSVersion } from "shared/bs-version.interface" +import { TabNavBar } from "../shared/tab-nav-bar.component" +import { LocalMapsListPanel } from "./local-maps-list-panel.component" +import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component" +import { FilterPanel } from "./filter-panel.component" +import { MapFilter } from "shared/models/maps/beat-saver.model" +import { useThemeColor } from "renderer/hooks/use-theme-color.hook" +import { MapsManagerService } from "renderer/services/maps-manager.service" +import { motion, Variants } from "framer-motion"; +import { MapsDownloaderService } from "renderer/services/maps-downloader.service" +import { BsmImage } from "../shared/bsm-image.component" +import wipGif from "../../../../assets/images/gifs/wip.gif" +import { OsDiagnosticService } from "renderer/services/os-diagnostic.service" +import { useObservable } from "renderer/hooks/use-observable.hook" +import { BsmIcon } from "../svgs/bsm-icon.component" +import { useTranslation } from "renderer/hooks/use-translation.hook" + +type Props = { + version?: BSVersion +} + +export function MapsPlaylistsPanel({version}: Props) { + + const mapsService = MapsManagerService.getInstance(); + const mapsDownloader = MapsDownloaderService.getInstance(); + const osDiagnostic = OsDiagnosticService.getInstance(); + + const [tabIndex, setTabIndex] = useState(0); + const [mapFilter, setMapFilter] = useState({}); + const [mapSearch, setMapSearch] = useState(""); + const [playlistSearch, setPlaylistSearch] = useState(""); + const [mapsLinked, setMapsLinked] = useState(false); + const isOnline = useObservable(osDiagnostic.isOnline$); + const color = useThemeColor("first-color"); + const t = useTranslation(); + const mapsRef = useRef(); + + useEffect(() => { + loadMapIsLinked(); + }, [version]); + + const loadMapIsLinked = () => { + mapsService.versionHaveMapsLinked(version).then(setMapsLinked); + } + + const handleSearch = (value: string) => { + if(tabIndex === 0){ + return setMapSearch(() => value); + } + return setPlaylistSearch(() => value); + } + + const handleMapsLinkClick = () => { + if(!mapsLinked){ + return mapsService.linkVersion(version).then(loadMapIsLinked); + } + return mapsService.unlinkVersion(version).then(loadMapIsLinked); + } + + const handleMapsAddClick = () => { + mapsDownloader.openDownloadMapModal(version); + } + + const renderTab = (props: DetailedHTMLProps, HTMLLIElement>, text: string, index: number): JSX.Element => { + + const linkedColor = mapsLinked ? color : "red"; + + const onClickLink = (index: number) => { + if(index === 0){ handleMapsLinkClick(); } + } + + const onClickAdd = (index: number) => { + if(index === 0){ handleMapsAddClick(); } + } + + const variants: Variants = { hover: {rotate: 22.5}, tap: {rotate: 45} }; + + return ( +
  1. + {text} + {index === 0 &&( +
    + {isOnline && ( + {e.stopPropagation(); onClickAdd(index)}}> + + + + + {t("pages.version-viewer.maps.tabs.maps.actions.add-maps.text")} + + )} + {(!!version) && ( + {e.stopPropagation(); onClickLink(index)}}> + + + + )} +
    + )} + +
  2. + ) + } + + const dropDownItems = ((): DropDownItem[] => { + if(tabIndex === 1){ + return [ + + ] + } + return [ + {icon:"export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.()}, + {icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.()} + ] + + })() + + return ( +
    + +
    + +
    + +
    + + Work in progress +
    +
    +
    +
    + ) +} diff --git a/src/renderer/components/maps-mangement-components/maps-toolbar.component.tsx b/src/renderer/components/maps-mangement-components/maps-toolbar.component.tsx new file mode 100644 index 00000000..b78c2b57 --- /dev/null +++ b/src/renderer/components/maps-mangement-components/maps-toolbar.component.tsx @@ -0,0 +1,11 @@ +type Props = { + className?: string +} + +export function MapsToolbar({className}: Props) { + return ( +
    + +
    + ) +} diff --git a/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx new file mode 100644 index 00000000..601df8c3 --- /dev/null +++ b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx @@ -0,0 +1,31 @@ +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png' + +export const DeleteMapsModal: ModalComponent = ({resolver, data:{linked, maps}}) => { + + const t = useTranslation(); + + const multiple = maps.length > 1; + + const titleText = multiple ? "modals.maps-actions.delete-maps.title.multiple" : "modals.maps-actions.delete-maps.title.single"; + const descText = multiple ? "modals.maps-actions.delete-maps.desc.multiple" : "modals.maps-actions.delete-maps.desc.single"; + const infoText = multiple ? "modals.maps-actions.delete-maps.info.desc.multiple" : "modals.maps-actions.delete-maps.info.desc.single"; + const infoTitleText = multiple ? "modals.maps-actions.delete-maps.info.title.multiple" : "modals.maps-actions.delete-maps.info.title.single"; + + return ( +
    +

    {t(titleText)}

    + +

    {t(descText, multiple ? {nb: maps.length.toString()} : {name: maps.at(0).rawInfo._songName})}

    + {linked &&

    {t(infoText)}

    } +
    + resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/> + resolver({exitCode: ModalExitCode.COMPLETED})} withBar={false} text="misc.delete"/> +
    + + ) +} \ No newline at end of file diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx new file mode 100644 index 00000000..d63c7592 --- /dev/null +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -0,0 +1,181 @@ +import { motion } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { FilterPanel } from "renderer/components/maps-mangement-components/filter-panel.component"; +import { MapItem, ParsedMapDiff } from "renderer/components/maps-mangement-components/map-item.component"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component"; +import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { BSV_SORT_ORDER } from "renderer/partials/beat-saver/sort-order"; +import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"; +import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; +import { MapsManagerService } from "renderer/services/maps-manager.service"; +import { ModalComponent } from "renderer/services/modale.service"; +import { BSVersion } from "shared/bs-version.interface"; +import { BsvMapCharacteristic, BsvMapDetail, MapFilter, SearchOrder, SearchParams } from "shared/models/maps/beat-saver.model"; +import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png"; +import equal from "fast-deep-equal/es6"; +import { ProgressBarService } from "renderer/services/progress-bar.service"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; + +export const DownloadMapsModal: ModalComponent = ({data}) => { + + const beatSaver = BeatSaverService.getInstance(); + const mapsManager = MapsManagerService.getInstance(); + const mapsDownloader = MapsDownloaderService.getInstance(); + const progressBar = ProgressBarService.getInstance(); + + const currentDownload = useObservable(mapsDownloader.currentMapDownload$); + const mapsInQueue = useObservable(mapsDownloader.mapsInQueue$); + const t = useTranslation(); + const [filter, setFilter] = useState({}); + const [query, setQuery] = useState(""); + const [maps, setMaps] = useState([]); + const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0)); + const [ownedMapHashs, setOwnedMapHashs] = useState([]); + const [searchParams, setSearchParams] = useState({ + sortOrder, + filter, + page: 0, + q: query + }); + + const loaderRef = useRef(null); + + const sortOptions: BsmSelectOption[] = (() => { + return BSV_SORT_ORDER.map(sort => ({text: `beat-saver.maps-sorts.${sort}`, value: sort})); + })(); + + useEffect(() => { + loadMaps(searchParams); + }, [searchParams]); + + + useEffect(() => { + mapsManager.getMaps(data, false).toPromise().then(maps => setOwnedMapHashs(maps.map(map => map.hash))); + + const onMapDownloaded = (map: BsvMapDetail, verion: BSVersion) => { + if(!equal(verion, data) || !map?.versions){ return; } + const downloadedHash = map.versions.at(0).hash; + setOwnedMapHashs((prev) => [...prev, downloadedHash]); + } + mapsDownloader.addOnMapDownloadedListener(onMapDownloaded); + + if(mapsDownloader.isDownloading){ + progressBar.setStyle(mapsDownloader.progressBarStyle); + } + + return () => { + mapsDownloader.removeOnMapDownloadedListene(onMapDownloaded); + progressBar.setStyle(null); + } + }, []) + + const loadMaps = (params: SearchParams) => { + beatSaver.searchMaps(params).then((maps => setMaps(prev => [...prev, ...maps]))); + } + + const extractMapDiffs = (map: BsvMapDetail): Map => { + const res = new Map(); + if(map.versions.at(0).diffs){ + map.versions.at(0).diffs.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + arr.push({name: diff.difficulty, type: diff.difficulty, stars: diff.stars}); + res.set(diff.characteristic, arr); + }); + + } + return res; + } + + const renderMap = (map: BsvMapDetail) => { + + const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash)); + const isDownloading = map.id === currentDownload?.map?.id; + const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, data) && toDownload.map.id === map.id); + + return ( + + ) + } + + const handleDownloadMap = useCallback((map: BsvMapDetail) => { + mapsDownloader.addMapToDownload({map, version: data}) + }, []); + + const handleCancelDownload = useCallback((map: BsvMapDetail) => { + mapsDownloader.removeMapToDownload({map, version: data}); + }, []); + + const handleSortChange = (newSort: string) => { + setSortOrder(() => newSort as SearchOrder); + setMaps(() => []); + setSearchParams(() => ({...searchParams, sortOrder: (newSort as SearchOrder)})); + } + + const handleSearch = () => { + const searchParams: SearchParams = { + sortOrder, + filter, + q: query.trim(), + page: 0, + }; + + setMaps(() => []); + setSearchParams(() => searchParams); + } + + const handleLoadMore = () => { + setSearchParams((prev) => { + return {...prev, page: prev.page + 1} + }); + } + + return ( +
    {e.preventDefault(); handleSearch()}}> +
    + + + + setQuery(e.target.value)}/> + {e.preventDefault(); handleSearch()}}/> + +
    +
      + {maps.length === 0 ? ( +
      +  + {t("modals.download-maps.loading-maps")} +
      + ) : ( + <> + {maps.map(renderMap)} + + + )} +
    +
    + ) +} diff --git a/src/renderer/components/modal/modal-types/link-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/link-maps-modal.component.tsx new file mode 100644 index 00000000..3cd82119 --- /dev/null +++ b/src/renderer/components/modal/modal-types/link-maps-modal.component.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import BeatRunning from '../../../../../assets/images/apngs/beat-running.png' + +export const LinkMapsModal: ModalComponent = ({resolver}) => { + + const t = useTranslation(); + const [keepMaps, setKeepMaps] = useState(true); + + return ( +
    +

    {t("modals.maps-actions.link-maps.title")}

    + +

    {t("modals.maps-actions.link-maps.desc")}

    +

    {t("modals.maps-actions.link-maps.info")}

    +
    + + {t("modals.maps-actions.link-maps.keep-maps.label")} +
    +
    + resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/> + resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text="modals.maps-actions.link-maps.valid-btn"/> +
    + + ) +} \ No newline at end of file diff --git a/src/renderer/components/modal/modal-types/mods-disclaimer-modal.component.tsx b/src/renderer/components/modal/modal-types/mods-disclaimer-modal.component.tsx new file mode 100644 index 00000000..3dfa2d5f --- /dev/null +++ b/src/renderer/components/modal/modal-types/mods-disclaimer-modal.component.tsx @@ -0,0 +1,28 @@ +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png' + +export const ModsDisclaimerModal: ModalComponent = ({resolver}) => { + + const t = useTranslation(); + + return ( +
    +

    {t("modals.mods-disclaimer.title")}

    + +

    {t("modals.mods-disclaimer.p-1")}

    +
      +
    • {t("modals.mods-disclaimer.li-1")}
    • +
    • {t("modals.mods-disclaimer.li-2")}
    • +
    • {t("modals.mods-disclaimer.li-3")}
    • +
    +

    {t("modals.mods-disclaimer.p-2")}

    +
    + {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.refuse"/> + {resolver({exitCode: ModalExitCode.COMPLETED})}} withBar={false} text="misc.accept"/> +
    + + ) +} \ No newline at end of file diff --git a/src/renderer/components/modal/modal-types/unlink-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/unlink-maps-modal.component.tsx new file mode 100644 index 00000000..40822438 --- /dev/null +++ b/src/renderer/components/modal/modal-types/unlink-maps-modal.component.tsx @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png' + +export const UnlinkMapsModal: ModalComponent = ({resolver}) => { + + const t = useTranslation(); + const [keepMaps, setKeepMaps] = useState(true); + + return ( +
    +

    {t("modals.maps-actions.unlink-maps.title")}

    + +

    {t("modals.maps-actions.unlink-maps.desc")}

    +
    + + {t("modals.maps-actions.unlink-maps.keep-maps.label")} +
    +
    + resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/> + resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text="modals.maps-actions.unlink-maps.valid-btn"/> +
    + + ) +} \ No newline at end of file diff --git a/src/renderer/components/modal/modal.component.tsx b/src/renderer/components/modal/modal.component.tsx index 1b610182..b74b8517 100644 --- a/src/renderer/components/modal/modal.component.tsx +++ b/src/renderer/components/modal/modal.component.tsx @@ -2,6 +2,8 @@ import { ModalExitCode, ModalService } from "renderer/services/modale.service"; import { AnimatePresence, motion } from "framer-motion"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { useEffect } from "react"; +import { BsmIcon } from "../svgs/bsm-icon.component"; export function Modal() { @@ -14,14 +16,39 @@ export function Modal() { const {firstColor, secondColor} = useThemeColor(); + useEffect(() => { + + const onEscape = (e: KeyboardEvent) => { + if(e.key !== "Escape"){ return; } + resolver?.(ModalExitCode.NO_CHOICE); + } + + if(!!ModalComponent){ + window.addEventListener("keyup", onEscape) + } + else{ + window.removeEventListener("keyup", onEscape); + } + + return () => { + window.removeEventListener("keyup", onEscape); + } + }, [ModalComponent]) + + return ( {ModalComponent && (
    modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: ModalComponent && .60}} exit={{opacity: 0}} transition={{duration: .2}}/> -
    - +
    +
    + +
    +
    {e.stopPropagation(); resolver(ModalExitCode.CLOSED)}}> + +
    diff --git a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx index f938016d..dc9b9ece 100644 --- a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx +++ b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx @@ -1,13 +1,34 @@ import { memo } from "react" import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; -import { motion } from "framer-motion"; +import { motion, Variants } from "framer-motion"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { AudioPlayerService } from "renderer/services/audio-player.service"; export const BsManagerIcon = memo(({className}: {className?: string}) => { + const audioPlayer = AudioPlayerService.getInstance(); + const {firstColor, secondColor} = useThemeColor(); + const playing= useObservable(audioPlayer.playing$); + + const bpm = audioPlayer.bpm; + + const transitions: Variants = { + playing: { + scale: [1, 1.05, 1], + transition: {repeat: Infinity, duration: ((60/bpm))/2, repeatDelay: ((60/bpm))/2} + }, + idle: {} + } + + const clickAction = () => { + if(playing){ + audioPlayer.pause(); + } + } return ( - + 0 ? "playing" : "idle"} onClick={clickAction}> diff --git a/src/renderer/components/nav-bar/bs-version-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx similarity index 65% rename from src/renderer/components/nav-bar/bs-version-item.component.tsx rename to src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx index c7d8aa0a..1a7bbb09 100644 --- a/src/renderer/components/nav-bar/bs-version-item.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar-items/bs-version-item.component.tsx @@ -5,12 +5,13 @@ import { useEffect, useState } from "react"; import { combineLatest, Subscription } from "rxjs"; import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service"; import { ConfigurationService } from "renderer/services/configuration.service"; -import { BsmButton } from "../shared/bsm-button.component"; import { BSUninstallerService } from "renderer/services/bs-uninstaller.service"; import { BSVersionManagerService } from "renderer/services/bs-version-manager.service"; -import { BsmIcon } from "../svgs/bsm-icon.component"; -import { ReactFitty } from "react-fitty"; +import { BsmIcon } from "../../svgs/bsm-icon.component"; import { useThemeColor } from 'renderer/hooks/use-theme-color.hook'; +import { NavBarItem } from './nav-bar-item.component'; +import useFitText from 'use-fit-text'; +import Tippy from '@tippyjs/react'; export function BsVersionItem(props: {version: BSVersion}) { @@ -21,10 +22,11 @@ export function BsVersionItem(props: {version: BSVersion}) { const bsUninstallerService = BSUninstallerService.getInstance(); const { state } = useLocation() as { state: BSVersion}; + const { fontSize, ref } = useFitText(); const [downloading, setDownloading] = useState(false); const [downloadPercent, setDownloadPercent] = useState(0); - const {firstColor, secondColor} = useThemeColor(); + const secondColor = useThemeColor("second-color"); const isActive = (): boolean => { return props.version?.BSVersion === state?.BSVersion && props?.version.steam === state?.steam && props?.version.oculus === state?.oculus && props?.version.name === state?.name; @@ -76,20 +78,30 @@ export function BsVersionItem(props: {version: BSVersion}) { return } - - return ( -
  3. - {downloading &&
    } -
    - - {renderIcon()} -
    - {props.version.name || props.version.BSVersion} + const renderVersionText = () => { + if(props.version.name){ + return ( + +
    +
    {props.version.name}
    +
    +
    + ) + } + return ( +
    + {props.version.BSVersion}
    - - {downloading && } -
    -
  4. + ) + } - ) + + return ( + + + {renderIcon()} + {renderVersionText()} + + + ) } \ No newline at end of file diff --git a/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx new file mode 100644 index 00000000..0b366265 --- /dev/null +++ b/src/renderer/components/nav-bar/nav-bar-items/maps-nav-bar-item.component.tsx @@ -0,0 +1,24 @@ +import { Link } from "react-router-dom"; +import { BsmIcon } from "renderer/components/svgs/bsm-icon.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { PageStateService } from "renderer/services/page-state.service"; +import { NavBarItem } from "./nav-bar-item.component"; + +export function MapsNavBarItem() { + + const pageState = PageStateService.getInstance(); + + const route = useObservable(pageState.route$); + + const color = useThemeColor("first-color"); + + return ( + + + + Maps + + + ) +} diff --git a/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx new file mode 100644 index 00000000..91a47f11 --- /dev/null +++ b/src/renderer/components/nav-bar/nav-bar-items/nav-bar-item.component.tsx @@ -0,0 +1,25 @@ +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { BsmButton } from "../../shared/bsm-button.component"; + +type Props = { + children: JSX.Element, + isDownloading?: boolean + progress?: number, + isActive?: boolean, + onCancel?: (e: React.MouseEvent) => void +} + +export function NavBarItem({progress, isDownloading, children, isActive, onCancel}: Props) { + + const {firstColor, secondColor} = useThemeColor(); + + return ( +
  5. + {isDownloading &&
    } +
    + {children} + {isDownloading && } +
    +
  6. + ) +} diff --git a/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx b/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx new file mode 100644 index 00000000..35521307 --- /dev/null +++ b/src/renderer/components/nav-bar/nav-bar-spliter.component.tsx @@ -0,0 +1,5 @@ +export function NavBarSpliter() { + return ( + + ) +} diff --git a/src/renderer/components/nav-bar/nav-bar.component.tsx b/src/renderer/components/nav-bar/nav-bar.component.tsx index 2b2f73dd..fe3421e3 100644 --- a/src/renderer/components/nav-bar/nav-bar.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar.component.tsx @@ -1,10 +1,12 @@ import './nav-bar.component.css' -import { BsVersionItem } from './bs-version-item.component'; +import { BsVersionItem } from './nav-bar-items/bs-version-item.component'; import { BSVersionManagerService } from '../../services/bs-version-manager.service'; import { Link } from 'react-router-dom'; import { BsmIcon } from '../svgs/bsm-icon.component'; import { useObservable } from 'renderer/hooks/use-observable.hook'; import { BsManagerIcon } from './bsmanager-icon.component'; +import { MapsNavBarItem } from './nav-bar-items/maps-nav-bar-item.component'; +import { NavBarSpliter } from './nav-bar-spliter.component'; export function NavBar() { @@ -16,10 +18,13 @@ export function NavBar() {