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 @@
-
+
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}) => (
+ -
+
+
+ {
+ stars ? (
+ ★ {stars}
+ ) : (
+ {parseDiffLabel(type)}
+ )
+ }
+
+ {parseDiffLabel(name)}
+
+ ))}
+
+ ))}
+
+ )}
+
+
+
+
+ {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 (
+ -
+ {text}
+ {index === 0 &&(
+
+ {isOnline && (
+ {e.stopPropagation(); onClickAdd(index)}}>
+
+
+
+
+ {t("pages.version-viewer.maps.tabs.maps.actions.add-maps.text")}
+
+ )}
+ {(!!version) && (
+ {e.stopPropagation(); onClickLink(index)}}>
+
+
+
+ )}
+
+ )}
+
+
+ )
+ }
+
+ 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 (
+
+ )
+}
\ 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 (
+
+ )
+}
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 (
+
+ )
+}
\ 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 (
+
+ )
+}
\ 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 (
+
+ )
+}
\ 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}>