Compare commits
472 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed1960c469 | |||
| dc9e181c54 | |||
| 4a91fa04e7 | |||
| af3e5d1a56 | |||
| 7b4e6340ca | |||
| 2e3febf66a | |||
| a5ff3482e5 | |||
| 8de279ac90 | |||
| 0efc56f8d6 | |||
| b59c37ebbd | |||
| ff194e724b | |||
| ca636c86e6 | |||
| 8b1117543e | |||
| ef123d8513 | |||
| 5aac3ba47e | |||
| d2f3bdf29a | |||
| e70313f66a | |||
| 70926283ff | |||
| 8357c09e9f | |||
| 21f97df788 | |||
| 00d6d7de30 | |||
| b10900618a | |||
| 9b8d6ddda0 | |||
| 4e6948693d | |||
| 6e3c65cced | |||
| c499fb83e7 | |||
| be347dc6ba | |||
| 716f068adf | |||
| e9d2cb1482 | |||
| 132394524e | |||
| 20499b821e | |||
| 803842d579 | |||
| eb119a31f0 | |||
| 9d4acea654 | |||
| 6a2f982292 | |||
| 7c88ea46e0 | |||
| 0f3de55e9e | |||
| d5b7dff7a9 | |||
| 7468554ea0 | |||
| 0d93c984be | |||
| 87f134ebb5 | |||
| 8fe346b1a7 | |||
| e9ca4bb7ac | |||
| eac6192afb | |||
| 679b28e44f | |||
| 36e95d0d96 | |||
| 59bd480445 | |||
| d38a3b5720 | |||
| 9d59bfb956 | |||
| 50322b69cc | |||
| fc212cecb4 | |||
| 3d6a9b59bf | |||
| b97a58c278 | |||
| 6f55f4283d | |||
| 9ee33f2c7d | |||
| 55f45e438c | |||
| cce6b71993 | |||
| 3d24e97abd | |||
| 1eea031438 | |||
| dcb45927a1 | |||
| d234a504b5 | |||
| cac09cf032 | |||
| 6d6f7d492e | |||
| 01bae7c59b | |||
| db5af48250 | |||
| 0576ea0159 | |||
| f73ab4a6a4 | |||
| 501c0d9ae1 | |||
| cc810ae84e | |||
| 3533402197 | |||
| 03a8377773 | |||
| 138002d7a0 | |||
| 18854650ab | |||
| e51d47ac73 | |||
| 32bf20a55f | |||
| b702b33def | |||
| d74a730a04 | |||
| 793af822ce | |||
| c05b9a4b8e | |||
| 883f7594b0 | |||
| 7161f0861e | |||
| 5f35a43098 | |||
| 63ef3acc72 | |||
| 3ff3c5af67 | |||
| 50ebef30fb | |||
| 6d6da765a6 | |||
| 031312e90e | |||
| f0e32bdae1 | |||
| 81071d920e | |||
| ea320c0e08 | |||
| 8335494a9f | |||
| 96fc2b4efb | |||
| 3280ca1984 | |||
| aae23c18d6 | |||
| 7a8eee9266 | |||
| 757069d657 | |||
| 15178f0e60 | |||
| 02d0aef497 | |||
| ee6b49f028 | |||
| 5c383b66a7 | |||
| 50cef104f0 | |||
| 37a44b54f3 | |||
| 0027832bdb | |||
| ad4da530eb | |||
| e4a894f75a | |||
| 1a62ae67c9 | |||
| 78951f0de1 | |||
| fd52fb8d78 | |||
| e14ba2727b | |||
| f6cd2fc316 | |||
| 57f0679f57 | |||
| d407b396cf | |||
| 4bfc00d698 | |||
| bd371b56e1 | |||
| 7ae097b990 | |||
| 9734d714d7 | |||
| 9c86adf2d4 | |||
| 6258de5b06 | |||
| d3c797041f | |||
| 5e6837b8cd | |||
| eba2b6fb03 | |||
| 1b2b8baa48 | |||
| 846945b75a | |||
| f84758c42d | |||
| 55aeab896c | |||
| ac62926f8d | |||
| 98bfbfa868 | |||
| 150efb1a5c | |||
| b68c84dd6a | |||
| 3145d2fdfa | |||
| dac67ebe4e | |||
| aea325a254 | |||
| 462499fe81 | |||
| f6f69e2f13 | |||
| c7ac3f2d9c | |||
| cb95d5d88d | |||
| 388ad76780 | |||
| be4afb71e3 | |||
| 11ddfc29b5 | |||
| b1eaad8df0 | |||
| 36fc7cb9e2 | |||
| 8df9516c11 | |||
| 6bd8727067 | |||
| b620b11eb5 | |||
| 3ff3ec7410 | |||
| 8f9b3405bc | |||
| 698058710f | |||
| 5fbc69e772 | |||
| daa2f864cc | |||
| f073f66533 | |||
| ff9bede111 | |||
| 650338d25b | |||
| be32418b93 | |||
| 3bb9250269 | |||
| 13da659c0d | |||
| 9f4a133b74 | |||
| 25b647630f | |||
| 373c6b979d | |||
| ded5b58020 | |||
| 763b3c65f5 | |||
| ef69e64ad7 | |||
| d82b7bab54 | |||
| 095ceccc19 | |||
| 32e5ee98ad | |||
| b2a57ea307 | |||
| eb30400b7c | |||
| 0fd1e211ba | |||
| 64484bfd4e | |||
| 2e750f3d5a | |||
| e1c3115668 | |||
| a6462530bb | |||
| 8316a9ef80 | |||
| 152313289c | |||
| c96ad6f7cd | |||
| b7cf3c5d20 | |||
| 97d3c2a3ae | |||
| a20ebe2075 | |||
| 2f5a74b155 | |||
| a20c73d0e9 | |||
| aeca1c1e9e | |||
| b9ce95968c | |||
| 490ccfdc1e | |||
| 6d0c6b4f64 | |||
| 89c3b0906b | |||
| 8cd74c3b0b | |||
| 769dab2896 | |||
| 8f1f998c5a | |||
| 48d03cccbb | |||
| 7d54ae79f7 | |||
| c46fdd56f6 | |||
| 22dc1ca511 | |||
| 709105b9f4 | |||
| 1c0cfb6b89 | |||
| 7ebc1bff16 | |||
| 0e1cda5050 | |||
| 331626bd1e | |||
| ea62f2443f | |||
| 532ab08e46 | |||
| a6fa55719d | |||
| 163857a14f | |||
| 854e0e2761 | |||
| eba6f916a3 | |||
| 9790f35749 | |||
| 93d3cbc8bf | |||
| 51ea1b130b | |||
| d020b72f20 | |||
| f2ac952cd9 | |||
| 189d2388fb | |||
| 1c989a2402 | |||
| 959e60127d | |||
| 0faa0f1288 | |||
| 5ad25f58d5 | |||
| 8ace5e6a7a | |||
| 185d01e7b4 | |||
| 755e407372 | |||
| 4f123b69ef | |||
| 7e2334f8fc | |||
| 5be533ccfb | |||
| b8eafd5f9b | |||
| 3f3bb959c4 | |||
| f30cad74d7 | |||
| ede82393da | |||
| 9ee468af03 | |||
| c4d5db809b | |||
| 8ecc9d30b3 | |||
| ca722b2a18 | |||
| c219e9655d | |||
| a6c1ba9fee | |||
| 6e9ea1bd9a | |||
| 84f27d571b | |||
| 51549161cd | |||
| 198002238e | |||
| ae0ad4d2f6 | |||
| e470782bea | |||
| b440c1b9e9 | |||
| 163b95ff26 | |||
| 6fa3b95f32 | |||
| 773432a407 | |||
| 45bab6ccce | |||
| 748b75ef3b | |||
| 116e4d77fa | |||
| c1322c4ed7 | |||
| f2ecd34672 | |||
| 657a683d13 | |||
| 0f038ccdc2 | |||
| 6d38985581 | |||
| 0edee845d7 | |||
| 517db1d056 | |||
| 65bf528a11 | |||
| 07e32df91a | |||
| 05a4386479 | |||
| 8c242e607b | |||
| a721c16c85 | |||
| 0957080e22 | |||
| 3427cae437 | |||
| 33be4de2ff | |||
| d83507a865 | |||
| 44c63a63c6 | |||
| d299553d1e | |||
| 415d47a9e5 | |||
| 9b8f1a7c42 | |||
| 12a8a49ccb | |||
| 3cfc59982b | |||
| 5e1a021c3a | |||
| d6ba587db0 | |||
| 4075b2fcde | |||
| b1ee9f054d | |||
| 15ebe8e929 | |||
| 901137ada9 | |||
| ab07334cb8 | |||
| 7407b7dc83 | |||
| 46824eb3ed | |||
| 1c9289f989 | |||
| 64d6aa25e3 | |||
| 126024b70b | |||
| cb08374cbb | |||
| 22a217528c | |||
| e9f297f186 | |||
| 934f857f96 | |||
| 56dbeea691 | |||
| bff5123373 | |||
| a48db98001 | |||
| 850f39339a | |||
| f0cbb066e4 | |||
| 8fefefd5ac | |||
| 365818266c | |||
| 764eeff57a | |||
| e713782b9f | |||
| d8cfad32a6 | |||
| 86c73239c9 | |||
| 9af6c9a94a | |||
| 9ffa0db43e | |||
| 5cf80ec6f2 | |||
| 0ea355d75c | |||
| 044174c711 | |||
| 7562b07832 | |||
| 0196206ec1 | |||
| d54464401f | |||
| 969be1e816 | |||
| d91a95ec0b | |||
| 89066411b1 | |||
| 604dec8540 | |||
| 32f03eaaa2 | |||
| 22769da7c5 | |||
| 69e573a12c | |||
| 7f726537ff | |||
| 40b12c6285 | |||
| 28f0a4d8c6 | |||
| 6e9c6e6c9e | |||
| 281ae0bbc6 | |||
| aad4cb9a2e | |||
| cc0ec76d83 | |||
| 7b7cdd5559 | |||
| 4a2f7697f9 | |||
| cbab039ca4 | |||
| 576cd2dec5 | |||
| c02ac39184 | |||
| 5eda9463c4 | |||
| 41ef0164fd | |||
| c833df9884 | |||
| ba3cca79b4 | |||
| ad6d9b57f8 | |||
| d21271693e | |||
| 589c191ac2 | |||
| 6a326fd584 | |||
| 0f430293a5 | |||
| 61ad8ba75b | |||
| 0cea1cd89f | |||
| 6836f18e89 | |||
| 0093c3970c | |||
| 644a0c4e43 | |||
| c71c7a96e3 | |||
| 0e02e2e2b9 | |||
| 4537cbd539 | |||
| 0c185066f6 | |||
| 5d5985f305 | |||
| 744c3f18c7 | |||
| 2a52ae3644 | |||
| e822738a3b | |||
| d185614d3b | |||
| c1a6a43825 | |||
| b98e1ccdfa | |||
| 4797aab0aa | |||
| 0460a0dab3 | |||
| 9cd5c48831 | |||
| e0f597e252 | |||
| ebbe2963b1 | |||
| 541a14d2a7 | |||
| 169a6e99b0 | |||
| 3da5af354e | |||
| 031c9044a1 | |||
| 6d271d8c48 | |||
| 8bc37ef3f1 | |||
| d7f3936619 | |||
| 49358b7006 | |||
| 067b967da1 | |||
| 31e1ae2273 | |||
| 7237e49c7f | |||
| 501def3a94 | |||
| f2cd9a1174 | |||
| d0fee6aaca | |||
| 7c034550fb | |||
| 1e6f051f59 | |||
| 08bdc03745 | |||
| de0575aea4 | |||
| 487d0c1f8b | |||
| ac5a9b16e5 | |||
| e7f5e98fcd | |||
| a36ccb0c13 | |||
| 59b04ee33e | |||
| b42231b134 | |||
| 11661cc1da | |||
| d7c43a2ba9 | |||
| 14a65f1d69 | |||
| bc9ab121b9 | |||
| 0f0f7c1abb | |||
| 1d9a261bc5 | |||
| 9a3ebf2971 | |||
| e51ef0992c | |||
| d9f025e28b | |||
| b4efa9583e | |||
| 1b4b59fbf0 | |||
| 8394581544 | |||
| 159d478e94 | |||
| 90bb8f2c19 | |||
| 097b18e72f | |||
| 9b744fb1e2 | |||
| 31f1ee1549 | |||
| c540308c2c | |||
| 778b4f6ee6 | |||
| df057cf82e | |||
| 5f94190b68 | |||
| 7718d239b2 | |||
| b29c9515d3 | |||
| 535831c03a | |||
| b03b670d56 | |||
| 1fd185fe88 | |||
| 05154f577f | |||
| c62896f078 | |||
| 2403f02851 | |||
| b8262a941d | |||
| 8e8d608120 | |||
| c92ca87c4c | |||
| b02cd7fc77 | |||
| 86c1f5aa7b | |||
| cd5237aff3 | |||
| 5449792294 | |||
| 5c35eae53a | |||
| 61889c0b5b | |||
| 4cf242f683 | |||
| 3217addaa6 | |||
| 76ed1b09a1 | |||
| 9401f1cf4a | |||
| 6b4d919afe | |||
| e92efa0ada | |||
| 99a9f5b1de | |||
| 98bdbba23c | |||
| ddde1f8b8a | |||
| b951be9511 | |||
| b91d74f3a3 | |||
| 002b11d54c | |||
| 4e15d1c1b6 | |||
| 961e629de8 | |||
| 02ad4b9c68 | |||
| a981adede0 | |||
| 33bab3ea0e | |||
| 7dae25ce32 | |||
| 71f317dd37 | |||
| 8333e238a6 | |||
| 39f2a16c96 | |||
| 2c08f1e07c | |||
| f4560b9190 | |||
| f0f83e2b3b | |||
| 0776de8d79 | |||
| 504462258a | |||
| fdef870e83 | |||
| 7d58fec63b | |||
| c77f4a9448 | |||
| 37a3951f7b | |||
| e5599b78d8 | |||
| 2da329367f | |||
| 0b64722092 | |||
| fbec5a0e46 | |||
| e21b731e10 | |||
| 19998facc7 | |||
| c8017939da | |||
| 75100e6919 | |||
| 834dad0cc0 | |||
| f57926e41b | |||
| 57157b4553 | |||
| b2c1338650 | |||
| 1dde1b8916 | |||
| 7e9b8ec86d | |||
| ec0d2fb8c6 | |||
| 214ffc243d | |||
| d243475bf8 | |||
| 9872fcb856 | |||
| 29f7754ba9 | |||
| 9c440a0430 | |||
| aaf75278d0 | |||
| ae2cf08c7d | |||
| 4a0848fb41 | |||
| 76c863608e | |||
| 8963c08e64 | |||
| ba51e76691 | |||
| cb0faad418 | |||
| 00bf26081b | |||
| 385c403bee | |||
| 6c3a241481 | |||
| 34c0ad03d3 | |||
| 8c9846d751 | |||
| 0d7c160b24 |
@@ -6,8 +6,20 @@ import webpack from "webpack";
|
||||
import webpackPaths from "./webpack.paths";
|
||||
import { dependencies as externals } from "../../release/app/package.json";
|
||||
|
||||
function createExternals(): string[] {
|
||||
const webpackExternals: string[] = [...Object.keys(externals || {})];
|
||||
const excludedExternals: string[] = [];
|
||||
|
||||
if (process.platform === "linux") {
|
||||
// Linux only uses regedit-rs types
|
||||
excludedExternals.push("regedit-rs");
|
||||
}
|
||||
|
||||
return webpackExternals.filter(external => !excludedExternals.includes(external));
|
||||
}
|
||||
|
||||
const configuration: webpack.Configuration = {
|
||||
externals: [...Object.keys(externals || {})],
|
||||
externals: createExternals(),
|
||||
|
||||
stats: "errors-only",
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Webpack config for development electron main process
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import { merge } from 'webpack-merge';
|
||||
import checkNodeEnv from '../scripts/check-node-env';
|
||||
import baseConfig from './webpack.config.base';
|
||||
import webpackPaths from './webpack.paths';
|
||||
|
||||
// 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
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
checkNodeEnv('development');
|
||||
}
|
||||
|
||||
const configuration: webpack.Configuration = {
|
||||
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
mode: 'development',
|
||||
|
||||
target: 'electron-main',
|
||||
|
||||
entry: {
|
||||
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
|
||||
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
|
||||
},
|
||||
|
||||
output: {
|
||||
path: webpackPaths.dllPath,
|
||||
filename: '[name].bundle.dev.js',
|
||||
library: {
|
||||
type: 'umd',
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
|
||||
analyzerPort: 8888,
|
||||
}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
'process.type': '"browser"',
|
||||
}),
|
||||
],
|
||||
|
||||
/**
|
||||
* Disables webpack processing of __dirname and __filename.
|
||||
* If you run the bundle in node.js it falls back to these values of node.js.
|
||||
* https://github.com/webpack/webpack/issues/2010
|
||||
*/
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default merge(baseConfig, configuration);
|
||||
@@ -40,6 +40,7 @@ const configuration: webpack.Configuration = {
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
import "webpack-dev-server";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
@@ -2,6 +2,9 @@ const path = require("path");
|
||||
|
||||
const rootPath = path.join(__dirname, "../..");
|
||||
|
||||
const erbPath = path.join(__dirname, '..');
|
||||
const erbNodeModulesPath = path.join(erbPath, 'node_modules');
|
||||
|
||||
const dllPath = path.join(__dirname, "../dll");
|
||||
|
||||
const srcPath = path.join(rootPath, "src");
|
||||
@@ -22,6 +25,7 @@ const buildPath = path.join(releasePath, "build");
|
||||
|
||||
export default {
|
||||
rootPath,
|
||||
erbNodeModulesPath,
|
||||
dllPath,
|
||||
srcPath,
|
||||
srcMainPath,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import path from 'path';
|
||||
import { copyFileSync, existsSync, readdirSync } from 'fs-extra';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const eternalsFolder = path.join(__dirname, '..', '..', 'externals');
|
||||
|
||||
// Get rust project folders from externals
|
||||
const rustProjects = readdirSync(eternalsFolder).filter(folder => existsSync(path.join(eternalsFolder, folder, 'Cargo.toml')));
|
||||
|
||||
// Build each rust project in release mode
|
||||
rustProjects.forEach(project => {
|
||||
console.log(`Building ${project}`);
|
||||
execSync(`cargo build --release`, {
|
||||
cwd: path.join(eternalsFolder, project),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
|
||||
// Copy the built files exe to the assests/scripts folder
|
||||
rustProjects.forEach(project => {
|
||||
// read the project name from Cargo.toml using toml parser
|
||||
const projectMetadata = execSync('cargo metadata --no-deps --format-version 1', {
|
||||
cwd: path.join(eternalsFolder, project),
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const projectMetadataJson = JSON.parse(projectMetadata);
|
||||
const projectName = projectMetadataJson.packages[0].name;
|
||||
|
||||
const source = path.join(eternalsFolder, project, 'target', 'release', `${projectName}.exe`);
|
||||
const destination = path.join(__dirname, '..', '..', 'assets', 'scripts', `${projectName}.exe`);
|
||||
console.log(`Copying ${source} to ${destination}`);
|
||||
copyFileSync(source, destination);
|
||||
});
|
||||
@@ -1,17 +1,13 @@
|
||||
import rimraf from "rimraf";
|
||||
import process from "process";
|
||||
import webpackPaths from "../configs/webpack.paths";
|
||||
import { rimrafSync } from 'rimraf';
|
||||
import fs from 'fs';
|
||||
import webpackPaths from '../configs/webpack.paths';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const commandMap = {
|
||||
dist: webpackPaths.distPath,
|
||||
release: webpackPaths.releasePath,
|
||||
dll: webpackPaths.dllPath,
|
||||
};
|
||||
const foldersToRemove = [
|
||||
webpackPaths.distPath,
|
||||
webpackPaths.buildPath,
|
||||
webpackPaths.dllPath,
|
||||
];
|
||||
|
||||
args.forEach(x => {
|
||||
const pathToRemove = commandMap[x];
|
||||
if (pathToRemove !== undefined) {
|
||||
rimraf.sync(pathToRemove);
|
||||
}
|
||||
foldersToRemove.forEach((folder) => {
|
||||
if (fs.existsSync(folder)) rimrafSync(folder);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import path from "path";
|
||||
import rimraf from "rimraf";
|
||||
import webpackPaths from "../configs/webpack.paths";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { rimrafSync } from 'rimraf';
|
||||
import webpackPaths from '../configs/webpack.paths';
|
||||
|
||||
export default function deleteSourceMaps() {
|
||||
rimraf.sync(path.join(webpackPaths.distMainPath, "*.js.map"));
|
||||
rimraf.sync(path.join(webpackPaths.distRendererPath, "*.js.map"));
|
||||
if (fs.existsSync(webpackPaths.distMainPath))
|
||||
rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), {
|
||||
glob: true,
|
||||
});
|
||||
if (fs.existsSync(webpackPaths.distRendererPath))
|
||||
rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), {
|
||||
glob: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import fs from "fs";
|
||||
import webpackPaths from "../configs/webpack.paths";
|
||||
import fs from 'fs';
|
||||
import webpackPaths from '../configs/webpack.paths';
|
||||
|
||||
const { srcNodeModulesPath } = webpackPaths;
|
||||
const { appNodeModulesPath } = webpackPaths;
|
||||
const { srcNodeModulesPath, appNodeModulesPath, erbNodeModulesPath } = webpackPaths;
|
||||
|
||||
if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
|
||||
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, "junction");
|
||||
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(erbNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
|
||||
fs.symlinkSync(appNodeModulesPath, erbNodeModulesPath, 'junction');
|
||||
}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
const { notarize } = require("electron-notarize");
|
||||
const { build } = require("../../package.json");
|
||||
const { notarize } = require('@electron/notarize');
|
||||
const { build } = require('../../package.json');
|
||||
|
||||
exports.default = async function notarizeMacos(context) {
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== "darwin") {
|
||||
return;
|
||||
}
|
||||
const { electronPlatformName, appOutDir } = context;
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.CI !== "true") {
|
||||
console.warn("Skipping notarizing step. Packaging is not running in CI");
|
||||
return;
|
||||
}
|
||||
if (process.env.CI !== 'true') {
|
||||
console.warn('Skipping notarizing step. Packaging is not running in CI');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("APPLE_ID" in process.env && "APPLE_ID_PASS" in process.env)) {
|
||||
console.warn("Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env)
|
||||
) {
|
||||
console.warn(
|
||||
'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
|
||||
await notarize({
|
||||
appBundleId: build.appId,
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_ID_PASS,
|
||||
});
|
||||
await notarize({
|
||||
appBundleId: build.appId,
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,19 +47,30 @@ module.exports = {
|
||||
"import/no-cycle": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"@typescript-eslint/ban-types": ["error", {
|
||||
types: {
|
||||
Function: false,
|
||||
}
|
||||
}]
|
||||
"import/extensions": "off",
|
||||
"lines-between-class-members": "off",
|
||||
"no-throw-literal": "warn",
|
||||
"no-use-before-define": "off",
|
||||
"no-useless-constructor": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
'react/jsx-filename-extension': [2, { 'extensions': ['.js', '.jsx', '.ts', '.tsx'] }],
|
||||
"no-shadow": "off",
|
||||
"react/function-component-definition": "off",
|
||||
"jsx-a11y/control-has-associated-label": "off",
|
||||
"react/button-has-type": "off",
|
||||
"no-labels": ["error", { "allowLoop": true }]
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
NodeJS: true
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
||||
@@ -73,4 +84,5 @@ module.exports = {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
"plugins": ["@typescript-eslint"]
|
||||
};
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
assets/scripts/* binary
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: "[BUG] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] : "
|
||||
labels: bug
|
||||
assignees: Zagrios
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## Reproduction Steps
|
||||
<!-- Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
## Expected Behavior
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
## System Specs
|
||||
<!-- **Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context about the problem here. -->
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: "[FEAT.] Feature request"
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT.] : "
|
||||
labels: enhancement
|
||||
assignees: Zagrios
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!-- **Is your feature request related to a problem? Please describe.** -->
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
## Solution
|
||||
<!-- **Describe the solution you'd like** -->
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
## Alternative solutions (if any)
|
||||
<!-- **Describe alternatives you've considered** -->
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
|
||||
name: "[BUG] Bug report"
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG] : "
|
||||
labels: bug
|
||||
assignees: Zagrios
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for taking the time to fill out this bug report! 😊
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Issue encountered
|
||||
description: Tell us what issue you've encountered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Tell us what should expected to happen.
|
||||
- type: textarea
|
||||
id: replication
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- Windows 10
|
||||
- Windows 11
|
||||
- Linux
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of BSManager are you running?
|
||||
placeholder: eg. 1.4.8 or 1.5.0-alpha-4
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context that you may share about the issue. You may add your log files here.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
name: "[FEAT.] Feature request"
|
||||
description: Suggest an idea for this project
|
||||
title: "[FEAT.] : "
|
||||
labels: enhancement
|
||||
assignees: Zagrios
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for taking the time to fill out this feature request! 😊
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
placeholder: Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Describe the solution you'd like.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative-solution
|
||||
attributes:
|
||||
label: Alternative Solution
|
||||
description: Describe alternatives you've considered.
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: If the feature is only applicable to a specific OS. Leave it to `None` if it's applicable to all OS'es.
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
lank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discord Support
|
||||
url: https://discord.gg/uSqbHVpKdV
|
||||
about: You can join our Discord server for a quick and interactive support.
|
||||
@@ -0,0 +1,34 @@
|
||||
# This workflow will...
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run package
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-${{ matrix.os }}
|
||||
path: release
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Publish wiki
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- docs/wiki/**
|
||||
- .github/workflows/docs.yml
|
||||
concurrency:
|
||||
group: publish-wiki
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
publish-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: docs/wiki/
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
label-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Search Linux Term
|
||||
uses: actions-ecosystem/action-regex-match@v2
|
||||
id: regex-match
|
||||
with:
|
||||
text: ${{ github.event.issue.body }}
|
||||
regex: '\# Operating System(\s*)Linux'
|
||||
flags: m
|
||||
|
||||
- name: Add Linux label
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
if: ${{ steps.regex-match.outputs.match != '' }}
|
||||
with:
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
labels: linux
|
||||
@@ -0,0 +1,35 @@
|
||||
# This workflow will...
|
||||
|
||||
name: Release Linux
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run publish:linux
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: release
|
||||
@@ -18,6 +18,7 @@ We'd also love PRs. If you're thinking of a large PR, we advise opening up an is
|
||||
## Submitting a pull request
|
||||
|
||||
1. [Fork][fork] and clone the repository.
|
||||
1. Install the correct NodeJS version (highly recommend installing [Volta](https://volta.sh/) for that).
|
||||
1. Configure and install the dependencies: `npm install`.
|
||||
1. Create a new branch following naming convention: `git checkout -b (feature|bugfix|hotfix|chore)/(short-description)(/issue-id)`.
|
||||
1. Make your change, test, and make sure BSManager work fine.
|
||||
|
||||
@@ -57,10 +57,12 @@
|
||||
<a href="https://discord.gg/uSqbHVpKdV"><img
|
||||
src="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff"
|
||||
alt="discord" /></a>
|
||||
<a href="https://www.patreon.com/bsmanager"><img
|
||||
src="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation" /></a>
|
||||
<a href="https://twitter.com/BSManager_"><img
|
||||
src="https://img.shields.io/badge/-Twitter-F5F8FA?style=for-the-badge&logo=Twitter" alt="Twitter" /></a>
|
||||
src="https://img.shields.io/badge/-Twitter-black?style=for-the-badge&logo=X" alt="Twitter" /></a>
|
||||
<a href="https://www.bsmanager.io">
|
||||
<img src="https://img.shields.io/badge/-WebSite-00649c?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPgogIDxnIGZpbGw9IiMzYjgyZmYiPgogICAgPHBhdGggZD0ibTYyNi43NSA0ODUuOTEgMjAuODIgMzUxLjk0YTM1LjIxIDM1LjIxIDAgMCAxLTE1LjQ5IDMxLjM2bC0xNDQuNzEgOTYuMTRoLS4wNmEzNS41NiAzNS41NiAwIDAgMS0xMS4zMSA0Ljc3IDM0Ljg1IDM0Ljg1IDAgMCAxLTIzLjUxLTIuODdMMTM2LjU4IDgwOS45YTExNS40NSAxMTUuNDUgMCAwIDEtNjMuNzUtOTYuNDlMNTIgMzYxLjQ2YTM1LjI2IDM1LjI2IDAgMCAxIDE1LjIzLTMxLjIxbDE0NS4wNS05Ni4zNmEzNS43MiAzNS43MiAwIDAgMSAxMS4yNy00LjcgMzQuODcgMzQuODcgMCAwIDEgMjMuNTEgMi44N0w1NjMgMzg5LjQyYTExNS40MSAxMTUuNDEgMCAwIDEgNjMuNzUgOTYuNDlaIi8+CiAgICA8cGF0aCBmaWx0ZXI9ImJyaWdodG5lc3MoMzAlKSIgZD0ibTYxMy43OCA0ODYuNjcgMjAuODIgMzUyYTIyLjM0IDIyLjM0IDAgMCAxLTMyLjI2IDIxLjMxTDI4Ni40IDcwMi41N0ExMDIuNDMgMTAyLjQzIDAgMCAxIDIyOS44MyA2MTdMMjA5IDI2NWEyMi4zNSAyMi4zNSAwIDAgMSAzMi4yNi0yMS4zMkw1NTcuMiA0MDEuMDVhMTAyLjQyIDEwMi40MiAwIDAgMSA1Ni41OCA4NS42MloiLz4KICA8L2c+CiAgPHBhdGggZD0iTTcxOC4yOSA3NzYuNDggNzAwIDgwNC4xOGMtNDAuNjMgMzkuNS04Ny4wNiA2Ny4yNS0xMzMuODEgOTAuNzUtODcuMzIgNDMuOS0yMDMuNjcgNzcuMTktMjQ5IDkwLjM0YTQ4IDQ4IDAgMCAxLTI2LjE3LjE2Yy02NC0xNy43Mi0xODguNjItNzItMjM2LjI4LTEyNS40OWE0LjIzIDQuMjMgMCAwIDEgNS4zLTYuNDVjMzUuNjQgMjAuODcgMTExLjIyIDU3LjY0IDE1Ni4zNyA3NC4yMiA0OS4xOCAxOC4wNiA4MS4yOSA4Ljc0IDkyLjI4LTMuMjZhNC43NyA0Ljc3IDAgMCAxIDcuMjQuMjFjMTkuNTEgMjQgMTA3LjI2IDQuNiAxNjYuNjQtMTMgNjQuMzQtMTkgMTg1Ljg1LTc2LjU2IDIzNS43Mi0xMzUuMThabS0xOTYuMiAxNTcuMzMtMTQ3LjQ1IDU1LjY4YTMuOTIgMy45MiAwIDAgMCAxLjQ0IDcuNThsNTQuNjItLjc3YTM1LjYzIDM1LjYzIDAgMCAwIDE5LTUuNzlsNzYtNDkuNTVhNCA0IDAgMCAwLTMuNjEtNy4xNVpNMjE4LjkyIDE5NS41NmMtMjEuMTQuMDctMzMuNTUgMzUuODYtMzMuMTYgODkuMDguMiAyOCAyLjU4IDkyLjc0IDQuNzkgMTQ3LjczLjIzIDUuNzEtOC4xOCA2LjQ4LTkgLjgxbC0zOC40Mi0yNzAuNzFhNC4zOCA0LjM4IDAgMCAxIDcuMTItNCA2My42MyA2My42MyAwIDAgMCAyMS45MyAxMS44NSA0LjYxIDQuNjEgMCAwIDAgNS45LTMuNzJjNC4yLTI3LjcgMTYuNjMtNTguNzQgMzcuMzUtODguNzRhNC4zNiA0LjM2IDAgMCAxIDcuNTYuNjljMTAuODYgMjMuODcgNDkgMTAzLjggOTMuMzQgMTUyLjc5YTQuNjUgNC42NSAwIDAgMS01LjYyIDcuMjJjLTM2LjU3LTE5LjM4LTgzLjAxLTQzLjAyLTkxLjc5LTQzWm0tMTUuNDcgMzg0LjhhNC41MSA0LjUxIDAgMCAxLTQuNDYtMy45MmwtMTMuODItMTA3LjI5YTQuNSA0LjUgMCAwIDEgOC45My0xLjE1bDEzLjgyIDEwNy4yOWE0LjUgNC41IDAgMCAxLTMuODkgNSAzLjg2IDMuODYgMCAwIDEtLjU4LjA3Wk01NzcuMDggNTQuNThjLTQ2Ljc5IDE1Ljg1LTExNS40NSA0MC41Ni0xNTguNzMgNjIuNzlhMzUuNSAzNS41IDAgMCAwLTE4LjU3IDI0LjUzYy0zLjk1IDE5LjQ5LTExIDU2LjA5LTE5IDEwNy4yNi0uNyA0LjQ4LTcuMzYgMy43OC03LjExLS43NWwxMC42Ni0xOTQuNzNhMy43OCAzLjc4IDAgMCAxIDcuNDYtLjY2bDMuNDkgMTQuODVhMi41NyAyLjU3IDAgMCAwIDQuNjkuNzdjNi4xOC0xMCAyMi4zNy0zMi43OSA1Ny4zMi02NWE0LjEzIDQuMTMgMCAwIDEgNi41NiA0LjcybC0zMi41NCA3Mi44OXM3OC43OC0xOS4zNyAxNDQtMzMuMDljNC4xMS0uODcgNS42OSA1LjA4IDEuNzcgNi40MloiIGZpbGw9IiNmZmYiIHN0cm9rZT0iYmxhY2siLz4KICA8ZyBmaWxsPSIjZjQ0Ij4KICAgIDxwYXRoIGZpbHRlcj0iYnJpZ2h0bmVzcyg2MCUpIHNhdHVyYXRlKDEyMCUpIiBkPSJtOTQ3LjA3IDQxMC4zNS01Ny43NSAzNDcuNzlhMzUuMTggMzUuMTggMCAwIDEtMjIuMDYgMjcuMTVsLTE2Mi40MyA2MS42NWgtLjA2YTM1LjUyIDM1LjUyIDAgMCAxLTEyLjA2IDIuMTEgMzQuODMgMzQuODMgMCAwIDEtMjIuMjktOGwtMjczLjE3LTIyMy41YTExNS40MyAxMTUuNDMgMCAwIDEtNDAuNzYtMTA4LjIybDU3Ljc1LTM0Ny44QTM1LjI1IDM1LjI1IDAgMCAxIDQzNiAxMzQuNDdsMTYyLjgzLTYxLjc5YTM1LjUxIDM1LjUxIDAgMCAxIDEyLTIuMDggMzQuODMgMzQuODMgMCAwIDEgMjIuMjkgOGwyNzMuMTkgMjIzLjUyYTExNS4zOCAxMTUuMzggMCAwIDEgNDAuNzYgMTA4LjIzWiIvPgogICAgPHBhdGggZD0iTTkzNC4yNSA0MDguMjIgODc2LjUgNzU2YTIyLjM0IDIyLjM0IDAgMCAxLTM2LjE4IDEzLjYzbC0yNzMuMTctMjIzLjVhMTAyLjQyIDEwMi40MiAwIDAgMS0zNi4xNy05Nmw1Ny43NS0zNDcuODNhMjIuMzQgMjIuMzQgMCAwIDEgMzYuMTgtMTMuNjNsMjczLjE3IDIyMy41MWExMDIuNDIgMTAyLjQyIDAgMCAxIDM2LjE3IDk2LjA0WiIvPgogIDwvZz4KICA8ZWxsaXBzZSBmaWxsPSIjZmZmIiBzdHJva2U9ImJsYWNrIiBjeD0iNzMyLjYxIiBjeT0iNDI5LjE2IiByeD0iNjIuODMiIHJ5PSIxMTAuNzMiIHRyYW5zZm9ybT0icm90YXRlKC0xNS40NSA3MzIuNzAzIDQyOS4xOTkpIi8+Cjwvc3ZnPg==" alt="Website" /></a>
|
||||
<a href="https://www.patreon.com/bsmanager"><img
|
||||
src="https://img.shields.io/badge/-🤍%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation" /></a>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -287,13 +289,11 @@
|
||||
<div>
|
||||
<ul>
|
||||
<li><strong>via Oculus</strong>: For Oculus users, <a href="https://github.com/Zagrios/bs-manager">BSManager</a>
|
||||
uses authentication directly via the <a href="https://about.meta.com/fr/">META</a> website
|
||||
to retrieve the connection token, ensuring reliable and secure access to your account data.</li>
|
||||
requires you to retrieve a connection token by following the instructions in this guide: <a href="https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a>. Once obtained, please insert it into the form.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img height=450 src="https://github.com/Zagrios/bs-manager/assets/40648115/1e4a2f98-af16-45aa-821d-0c4e90e1e54b" />
|
||||
<img height=450 src="https://github.com/Zagrios/bs-manager/assets/40648115/3674e629-542d-4de3-b8db-b248f25126d7" />
|
||||
</div>
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
<div>
|
||||
<h2>Credits</h2>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios</a> - Lead Developer & Founder.</li>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios - Mathieu Gries</a> - Lead Developer & Founder.</li>
|
||||
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Co-Developer & Co-Founder, Discord Bot Developer.</li>
|
||||
<li><a href="https://github.com/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
|
||||
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
|
||||
|
||||
|
After Width: | Height: | Size: 401 KiB |
@@ -598,8 +598,7 @@
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/6169409105101272202",
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans/32055887/c328e407367e9914abaf92f609501877ee5abb63.png",
|
||||
"ReleaseDate": "1680623885",
|
||||
"year": "2023",
|
||||
"recommended": true
|
||||
"year": "2023"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.29.4",
|
||||
@@ -700,7 +699,7 @@
|
||||
"ReleaseDate": "1708091473",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
{
|
||||
"BSVersion": "1.35.0",
|
||||
"BSManifest": "1490986193481243578",
|
||||
"OculusBinaryId": "6720809361352119",
|
||||
@@ -708,5 +707,85 @@
|
||||
"ReleaseImg": "https://clan.cloudflare.steamstatic.com/images//32055887/9a141be5c44ca6fc81551a2c7e7f28407df413ed.png",
|
||||
"ReleaseDate": "1709831174",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.36.0",
|
||||
"BSManifest": "8533509040167838423",
|
||||
"OculusBinaryId": "6863072637125790",
|
||||
"ReleaseURL": "https://store.steampowered.com/news/app/620980/view/4205873192610459250",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images//32055887/b8bd8a050e2bc3903cf18a277c5489ade01f7e75.png",
|
||||
"ReleaseDate": "1712681263",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.36.2",
|
||||
"BSManifest": "4832237377879752064",
|
||||
"OculusBinaryId": "6983660338400352",
|
||||
"ReleaseURL": "https://store.steampowered.com/news/app/620980/view/4181105931135110118",
|
||||
"ReleaseImg": "https://clan.cloudflare.steamstatic.com/images//32055887/74541417f14dee210bcfd89eefeabd7c0cc27c7d.png",
|
||||
"ReleaseDate": "1715008020",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.0",
|
||||
"BSManifest": "7270690075584855288",
|
||||
"OculusBinaryId": "7062062833893435",
|
||||
"ReleaseURL": "https://store.steampowered.com/news/app/620980/view/4161968170153497460",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images//32055887/f82d34c7582137a6748a28c5b02670ff4b235356.png",
|
||||
"ReleaseDate": "1717513346",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.1",
|
||||
"BSManifest": "9129356330007601179",
|
||||
"OculusBinaryId": "7313068492126200",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4253168598913260422",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/039be718c1494f948257cd5b09f0082986484a58.png",
|
||||
"ReleaseDate": "1720710529",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.2",
|
||||
"BSManifest": "6848299977215652352",
|
||||
"OculusBinaryId": "7438773609555687",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4349999161850643744",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/7c03af6d12fad23f1d439666d4be86b492d40003.png",
|
||||
"ReleaseDate": "1722945814",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.3",
|
||||
"BSManifest": "5834150512217183366",
|
||||
"OculusBinaryId": "7553569971409383",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/6808965920900622902",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/a1c9a6737cc5182fca8cddc7c8088eda6dd4d82d.png",
|
||||
"ReleaseDate": "1724061712",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.4",
|
||||
"BSManifest": "7585106640515547731",
|
||||
"OculusBinaryId": "7630971613669218",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4601078012425941968",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/23e6e6f219a56d2f9ea4a27b560c54ac1214aec3.png",
|
||||
"ReleaseDate": "1725548547",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.5",
|
||||
"BSManifest": "3122533396458693889",
|
||||
"OculusBinaryId": "7685108391588873",
|
||||
"ReleaseDate": "1725628356",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.38.0",
|
||||
"BSManifest": "3482902979520746178",
|
||||
"OculusBinaryId": "7834818833284494",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4669760442595134838",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/57cfa043407c091e67b7e0a8ae86fe5590deae33.png",
|
||||
"ReleaseDate": "1728399815",
|
||||
"year": "2024",
|
||||
"recommended": true
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"1.5.0-alpha.1":{
|
||||
"htmlBody" :"<strong>BSManager continues to get better, thanks to your continued support and contributions!</strong><center>🎈🎉🎈</center><p>Note: This is an alpha release, encountering bugs is expected. Please file new issues for any bugs you find.</p><h2>Features</h2><ul><li>Added playlist support</li><li>Added Linux support (Thanks to @Insprill)</li><li>Added reset button to reset values when editing a Beat Saber version (Thanks to @Liborsaf)</li><li>Added a changelog modal</li></ul><h2>Fixes</h2><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version</li><li>Fixed an issue where the launch window from a shortcut would never close</li><li>Advanced launch arguments are used even when the input field is closed</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded</li></ul><h2>Other changes</h2><ul><li>.NET Framework is no longer required</li><li>Clear English translations for the Steam credentials popup (Thanks to @Aeywoo)</li><li>NSFW models are now blurred when browsing for models</li><li>When no mods are available for a Beat Saber version, the version number is now shown</li><li>Mods load faster</li><li>After a first full maps loading, maps load almost instantly</li><li>Improved performance in lists with lot of contents</li></ul>",
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721048400,
|
||||
"version" : "1.5.0-alpha.1"
|
||||
},
|
||||
"1.5.0-alpha.2":{
|
||||
"htmlBody" :"<strong>BSManager continues to get better, thanks to your continued support and contributions!</strong><center>🎈🎉🎈</center><p>Note: This is an alpha release, encountering bugs is expected. Please file new issues for any bugs you find.</p><h4>Alpha changes</h4><ul><li>Fixed an issue where BSIPA could not be installed if the BS version's path contained spaces #523</li><li>Fixed a crash when changing audio volume #518</li><li>Fixed an issue where playlist maps with a malformed hash were not loaded #517</li><li>Improved reliability of reading maps from a playlist file #517</li><li>Playlists in sub-folders are now loaded #514</li><li>Fixed a crash when loading an invalid playlist file #513</li></ul><h2>Features</h2><ul> <li>Added playlist support</li> <li>Added Linux support (Thanks to @Insprill)</li> <li>Added reset button to reset values when editing a Beat Saber version (Thanks to @Liborsaf)</li> <li>Added a changelog modal</li></ul><h3>Fixes</h3><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version #472</li><li>Fixed an issue where the launch window from a shortcut would never close #485</li><li>Advanced launch arguments are used even when the input field is closed #496</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded #503</li><li>Fixed an issue where downloading maps sometimes resulted in a timeout error #524</li><li>Fixed an issue where the duration of maps could be wrong in some cases #522</li></ul><h2>Other changes</h2><ul> <li>.NET Framework is no longer required</li> <li>Clear English translations for the Steam credentials popup (Thanks to @Aeywoo)</li> <li>NSFW models are now blurred when browsing for models</li> <li>When no mods are available for a Beat Saber version, the version number is now shown</li> <li>Mods load faster</li> <li>After a first full maps loading, maps load almost instantly</li> <li>Improved performance in lists with lot of contents</li></ul>",
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721665382,
|
||||
"version" : "1.5.0-alpha.2"
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,6 @@
|
||||
{
|
||||
"username": "Falmil"
|
||||
},
|
||||
{
|
||||
"username": "K1Lc4m"
|
||||
},
|
||||
{
|
||||
"username": "Anonymously42",
|
||||
"type": "diamond",
|
||||
@@ -30,9 +27,6 @@
|
||||
"username": "Burt",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Ilnahro"
|
||||
},
|
||||
{
|
||||
"username": "Phil",
|
||||
"type": "gold"
|
||||
@@ -41,18 +35,10 @@
|
||||
"username": "Protocrush",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "ServerMensch",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Karlito",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Cathery",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": ".sharkey"
|
||||
},
|
||||
@@ -74,9 +60,6 @@
|
||||
{
|
||||
"username": "Minescence"
|
||||
},
|
||||
{
|
||||
"username": "Finlay"
|
||||
},
|
||||
{
|
||||
"username": "mereknom"
|
||||
},
|
||||
@@ -89,5 +72,47 @@
|
||||
},
|
||||
{
|
||||
"username": "rhythmshade"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "liborsaf"
|
||||
},
|
||||
{
|
||||
"username": "aatame3",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Joshua"
|
||||
},
|
||||
{
|
||||
"username": "Stuijvi",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "_ monaka"
|
||||
},
|
||||
{
|
||||
"username": "Better_Axel"
|
||||
},
|
||||
{
|
||||
"username": "Riley",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Austin"
|
||||
},
|
||||
{
|
||||
"username": "Fatalution",
|
||||
"type": "diamond",
|
||||
"link": "https://x.com/fatalution"
|
||||
},
|
||||
{
|
||||
"username": "Taurus Arcade",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "Refuse",
|
||||
"apply": "Apply",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!"
|
||||
"copied": "Copied!",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Add a version",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "Filters",
|
||||
"dropdown": {
|
||||
"export-maps": "Export maps",
|
||||
"delete-maps": "Delete maps"
|
||||
"delete-maps": "Delete maps",
|
||||
"delete-duplicate-maps": "Delete duplicates"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Add"
|
||||
"drop-down": {
|
||||
"browse-maps": "Browse maps",
|
||||
"import-maps": "Import maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No maps",
|
||||
"button": "Download maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your maps",
|
||||
"subtext": "Drop your zip files here to import your maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Browse playlists",
|
||||
"create-a-playlist": "Create a playlist",
|
||||
"import-playlists": "Import playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your playlists",
|
||||
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "No mods are available yet for this version of Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "More info",
|
||||
"install-or-update": "Install or update"
|
||||
"install-or-update": "Install or update",
|
||||
"reinstall-all": "Reinstall all"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "Uninstall all"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods already installed",
|
||||
"description": "All selected mods are already installed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Logging out will allow you to switch accounts on the next Beat Saber download.",
|
||||
"logout": "Log out",
|
||||
"logout-success": "Logout Successful",
|
||||
"download-platform": {
|
||||
"title": "Default Platform",
|
||||
"desc": "Choose the default platform that will be used to download versions of Beat Saber.",
|
||||
@@ -131,9 +157,14 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Installation folder",
|
||||
"description": "Change the default folder for Beat Saber versions and other upcoming features.",
|
||||
"description": "Change the folder that will contain all the content downloaded by BSManager.",
|
||||
"choose-folder": "Choose folder"
|
||||
},
|
||||
"proton-path": {
|
||||
"title": "Proton path",
|
||||
"description": "Change the path to the Proton binary",
|
||||
"choose-file": "Choose file"
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Additional content",
|
||||
"description": "Additional content that allows you to customize Beat Saber!",
|
||||
@@ -190,6 +221,38 @@
|
||||
"report-bug": "Report a bug",
|
||||
"open-logs": "Open logs"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Check Out the Changelog!",
|
||||
"not-founds": "😕 Oops, No Changelog Here!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Advanced settings for BSManager.",
|
||||
"hardware-acceleration": {
|
||||
"title": "Hardware Acceleration",
|
||||
"description": "Enable Hardware Acceleration to use your GPU and improve BSManager's performance. Turn this off if you're experiencing frame drops.",
|
||||
"modal": {
|
||||
"title": "Restart Needed",
|
||||
"body": "Changing hardware acceleration setting will quit and re-launch BSManager. Are you sure you want to do this?",
|
||||
"confirm-btn": "Yes I'm sure"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "An error occur, unable to disable hardware acceleration."
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "Use Symlinks",
|
||||
"description": "Use Symlinks instead of Junctions to link folders. Turn this on only if you really need it.",
|
||||
"modal": {
|
||||
"title": "Symlink Permissions",
|
||||
"body": "When creating symlinks, BSManager will require administrator privileges or developer mode enabled on your system. Are you sure you want to continue?",
|
||||
"confirm-btn": "Yes I'm sure"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "An error occur, unable to change symlinks settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +271,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operation running",
|
||||
"no-internet": "No internet"
|
||||
"no-internet": "No internet",
|
||||
"file-not-supported": "File not supported"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Wait for the current operation to finish, then try again.",
|
||||
"no-internet": "Check your connection and try again."
|
||||
"no-internet": "Check your connection and try again.",
|
||||
"file-not-supported": "Only {types} files are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +299,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": ".NET 6 Required"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Steam doesn't seem to want to let us download Beat Saber 😢",
|
||||
"404": "Unable to contact Steam servers.",
|
||||
@@ -259,11 +321,7 @@
|
||||
"LicenceError": "Unable to get the list of licenses.",
|
||||
"RateLimitExceeded": "You've tried too many times, wait a while and try again later.",
|
||||
"TokenRejected": "Your login token has been rejected 😕 Please try again.",
|
||||
"AccessDenied": "Access to Steam has been denied.",
|
||||
"dotnet-required": ".NET 6 Runtime must be installed in order to download a version of Beat Saber. Download it by clicking the button below."
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "Download .NET 6"
|
||||
"AccessDenied": "Access to Steam has been denied."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -379,6 +437,7 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus is not running",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber already running",
|
||||
"EXE_NOT_FINDED": "Missing files",
|
||||
"PROTON_NOT_SET": "Proton path not set",
|
||||
"EXIT": "Abrupt stop",
|
||||
"OCULUS_LIB_NOT_FOUND": "Oculus library not found"
|
||||
},
|
||||
@@ -389,7 +448,8 @@
|
||||
"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.",
|
||||
"OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus."
|
||||
"OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus.",
|
||||
"PROTON_NOT_SET": "Set the Proton path in settings"
|
||||
},
|
||||
"actions": {
|
||||
"STEAM_NOT_RUNNING": "Launch Steam"
|
||||
@@ -408,7 +468,8 @@
|
||||
"CantEditSteam": "Unable to edit",
|
||||
"CantRename": "Renaming impossible",
|
||||
"VersionAlreadExist": "This version already exists",
|
||||
"CantClone": "Cloning impossible"
|
||||
"CantClone": "Cloning impossible",
|
||||
"UnknownError": "An unknown error occurred"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "You can't edit the Steam version. You can clone it though."
|
||||
@@ -461,6 +522,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Map installation complete",
|
||||
"error": "An error occurred while installing the map"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "No Duplicates",
|
||||
"msg": "No maps were deleted"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicates Deleted",
|
||||
"msg": "Duplicates were deleted"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Maps import completed",
|
||||
"error": "An error occurred during the maps import"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Maps successfully imported.",
|
||||
"some-success": "Some maps were successfully imported.",
|
||||
"only-accept-zip": "Only zip files are supported.",
|
||||
"invalid-zip": "The zip file(s) do not contain any maps.",
|
||||
"unknown": "An unknown error occurred."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +563,15 @@
|
||||
"title": "Backup created",
|
||||
"msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "Error while linking folder",
|
||||
"msg": {
|
||||
"EPERM": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"EACCES": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"ENOSPC": "The disk is full, make some space and try again.",
|
||||
"UNKNOWN_ERROR":"An unknown error has occurred while linking the folder."
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -550,8 +641,8 @@
|
||||
},
|
||||
"steam-credentials": {
|
||||
"title": "Steam Credentials",
|
||||
"p-1": "The credentials are only used to download the game because steam need to verify that you paid the game in order to be allowed to download it. They aren't saved and directly passed to DepotDownloader. If you don't want to enter your credentials you can follow this tutorial :",
|
||||
"p-2": "and then click on the gear icon in the top right corner and select \"Import a version\", select the folder where beat saber has been downloaded (If you follow the tutorial above you should have the correct location)"
|
||||
"p-1": "Your Steam credentials are only required to download versions of Beat Saber, we use DepotDownloader to achieve this which requires your Steam credentials to verify that you own the game within your library before allowing you to retrieve the game files, your credentials are not stored or saved and are passed directly to DepotDownloader. However, if you don't want to do that, you can follow this tutorial instead: ",
|
||||
"p-2": "Afterwards you can click on the gear icon in the top right corner and select \"Import a version\", then you can select the folder where Beat Saber was downloaded to. (If you followed the tutorial, you will have the right location)"
|
||||
},
|
||||
"bs-import-version": {
|
||||
"title": "Import a version",
|
||||
@@ -646,6 +737,11 @@
|
||||
"title": "Keeping maps will copy all maps from the shared folder to the current version after unlinking. Maps will not be lost if this is disabled."
|
||||
},
|
||||
"valid-btn": "Unlink maps"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Delete maps?",
|
||||
"desc": "Only the map \"{map}\" is a duplicate. Are you sure you want to delete it?",
|
||||
"desc-plural": "{nb} duplicate maps have been found. Are you sure you want to delete them?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +822,20 @@
|
||||
},
|
||||
"launch-as-admin": "Launch as Administrator",
|
||||
"not-remind-me": "Do not remind me"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "Installation folder",
|
||||
"choose-folder-description": "Choose the folder that will contain all the content downloaded by BSManager. (versions, mods, maps, playlists, etc.)",
|
||||
"choose-folder": "Choose folder",
|
||||
"default": "Default",
|
||||
"default-tooltip": "Defaults to your home folder"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Duration",
|
||||
"nps" : "Notes Per Second",
|
||||
"njs": "Note Jump Speed",
|
||||
"tags": "tags",
|
||||
"specificities": "general",
|
||||
"requirements": "requirements",
|
||||
@@ -741,7 +845,7 @@
|
||||
"accuracy": "accuracy",
|
||||
"balanced": "balanced",
|
||||
"challenge": "challenge",
|
||||
"dancestyle": "dance",
|
||||
"dance-style": "dance",
|
||||
"fitness": "fitness",
|
||||
"speed": "speed",
|
||||
"tech": "tech"
|
||||
@@ -750,10 +854,10 @@
|
||||
"dance": "dance",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "family",
|
||||
"folk-acoustic": "folk & acoustic",
|
||||
"kids-family": "kids & family",
|
||||
"ambient": "ambient",
|
||||
"funk": "funk",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -761,24 +865,24 @@
|
||||
"rb": "r&b",
|
||||
"holiday": "holiday",
|
||||
"vocaloid": "vocaloid",
|
||||
"jrock": "j-rock",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedy",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedy & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"kpop": "k-pop",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "video game",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "video game",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternative",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"jpop": "j-pop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electronic",
|
||||
@@ -809,7 +913,8 @@
|
||||
"bsr-code" : "BSR code",
|
||||
"download" : "Download map",
|
||||
"downloading" :"Downloading map",
|
||||
"cancel-download" : "Cancel download"
|
||||
"cancel-download" : "Cancel download",
|
||||
"hightlight-difficulty": "Highlight difficulty"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,6 +1106,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Or browse files"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error creating playlist",
|
||||
"error-playlist-creation-desc": "An error occurred while creating the playlist.",
|
||||
"playlist-created-title": "Playlist created",
|
||||
"playlist-created-desc": "The playlist has been successfully created. You can now sync its maps!",
|
||||
"download-playlist": "Download playlist",
|
||||
"synchronize-playlist": "Synchronize playlist",
|
||||
"synchronize-maps": "Synchronize maps",
|
||||
"error-playlists-synchronization-title": "Error synchronizing playlists",
|
||||
"error-playlists-synchronization-desc": "An error occurred while synchronizing playlists.",
|
||||
"playlists-synchronized-title": "Playlists synchronized!",
|
||||
"playlists-synchronized-desc": "Playlists and their maps have been downloaded.",
|
||||
"playlists-export-error-title": "Error exporting playlists",
|
||||
"playlists-export-error-desc": "An error occurred while exporting playlists.",
|
||||
"playlists-exported-title": "Playlists exported!",
|
||||
"playlists-exported-desc": "Playlists have been successfully exported.",
|
||||
"playlists-with-maps-exported-desc": "Playlists and their maps have been successfully exported.",
|
||||
"playlist-delete-error-title": "Error deleting playlist",
|
||||
"playlist-delete-error-desc": "An error occurred while deleting the playlist.",
|
||||
"playlists-deleted-title": "Playlists deleted!",
|
||||
"playlists-deleted-desc": "Playlists have been successfully deleted.",
|
||||
"edit-playlist": "Edit playlist",
|
||||
"playlist-edit-error-title": "Error editing playlist",
|
||||
"playlist-edit-error-desc": "An error occurred while editing the playlist.",
|
||||
"playlist-edited-title": "Playlist edited!",
|
||||
"playlist-edited-desc": "The playlist has been successfully modified. You can now sync its maps!",
|
||||
"playlists-loading": "Loading playlists...",
|
||||
"no-playlists": "No playlists",
|
||||
"download-playlists": "Download playlists",
|
||||
"created-by": "Created by",
|
||||
"stop-download": "Stop download",
|
||||
"cancel-download": "Cancel download",
|
||||
"open-file": "Open file",
|
||||
"link-playlists": "Link playlists",
|
||||
"link-playlist-desc": "Linking playlists allows sharing playlists between all versions. Once linked, this version will benefit from shared playlists",
|
||||
"link-playlist-info": "Adding and deleting playlists will also be shared",
|
||||
"keep-playlists": "Keep playlists",
|
||||
"keep-playlists-tip": "Keeping playlists will move the playlists from the current version to the shared playlists folder. Otherwise, they will be lost",
|
||||
"unlink-playlists": "Unlink playlists",
|
||||
"unlink-playlist-desc": "Warning, unlinking playlists will no longer allow the use of shared playlists for this version.",
|
||||
"unlink-keep-playlists-tip": "Keeping playlists will create a copy of shared playlists for the current version. Otherwise, no playlists will be kept for this version.",
|
||||
"delete-playlist-ask": "Delete playlist?",
|
||||
"delete-playlists-ask": "Delete playlists?",
|
||||
"delete-playlist-desc": "Are you sure you want to delete the playlist \"{playlistTitle}\"?",
|
||||
"delete-playlists-desc": "Are you sure you want to delete {nb} playlists?",
|
||||
"delete-maps": "Delete maps",
|
||||
"delete-playlist-maps-tip": "If enabled, all maps in the playlist will be deleted",
|
||||
"delete-playlists-maps-tip": "If enabled, all maps in the playlists will be deleted",
|
||||
"export-playlist-ask": "Export playlist?",
|
||||
"export-playlists-ask": "Export playlists?",
|
||||
"export-playlist-desc": "Are you sure you want to export the playlist \"{playlistTitle}\"?",
|
||||
"export-playlists-desc": "Are you sure you want to export {nb} playlists?",
|
||||
"export-maps": "Export maps",
|
||||
"export-playlist-maps-tip": "If enabled, all maps in the playlist will also be exported",
|
||||
"export-playlists-maps-tip": "If enabled, all maps in the playlists will also be exported",
|
||||
"export": "Export",
|
||||
"need-clone-title": "Warning",
|
||||
"need-clone-desc-1": "This playlist has been downloaded from an external site and contains a synchronization link.",
|
||||
"need-clone-desc-2": "To avoid losing your changes during synchronization, the playlist will be duplicated and its synchronization link removed.",
|
||||
"need-clone-desc-3": "You can then, if you wish, delete the original playlist.",
|
||||
"understood": "I understand",
|
||||
"synchronize-playlist-ask": "Synchronize playlist?",
|
||||
"synchronize-playlists-ask": "Synchronize playlists?",
|
||||
"synchronize-playlist-desc": "Are you sure you want to synchronize the playlist \"{playlistTitle}\"?",
|
||||
"synchronize-playlists-desc": "Are you sure you want to synchronize {nb} playlists?",
|
||||
"synchronize-playlist-tip": "This action updates playlists and downloads missing maps; it may take several minutes.",
|
||||
"synchronize": "Synchronize",
|
||||
"curated": "Recommended",
|
||||
"verified-mapper": "Verified mapper",
|
||||
"empty-playlists": "Empty playlists",
|
||||
"search-playlist": "Search for a playlist",
|
||||
"no-playlists-found": "No playlists found",
|
||||
"error-occur-while-loading-playlists": "An error occurred while loading playlists",
|
||||
"error-occur-while-loading-playlist": "An error occurred while loading the playlist",
|
||||
"loading-maps": "Loading maps...",
|
||||
"no-maps-found-for-playlist": "No maps found for this playlist",
|
||||
"playlist-contain-no-maps": "The playlist contains no maps",
|
||||
"no-map-installed-for-playlist": "No maps installed for this playlist",
|
||||
"playlist-is-waiting-to-download": "The playlist is waiting to download",
|
||||
"download-maps": "Download maps",
|
||||
"download-missing-maps": "Download missing maps",
|
||||
"playlist-is-downloading": "The playlist is downloading",
|
||||
"some-playlist-maps-are-missing": "Some maps in this playlist are missing",
|
||||
"create-a-playlist": "Create a playlist",
|
||||
"synchronize-playlists": "Synchronize playlists",
|
||||
"export-playlists": "Export playlists",
|
||||
"delete-playlists": "Delete playlists",
|
||||
"choose-image": "Choose an image",
|
||||
"title": "Title",
|
||||
"playlist-title": "Playlist title",
|
||||
"description": "Description",
|
||||
"playlist-description": "Playlist description",
|
||||
"author": "Author",
|
||||
"playlist-author": "Playlist author",
|
||||
"save": "Save",
|
||||
"loading": "Loading...",
|
||||
"installed": "Installed",
|
||||
"no-map-found": "No map found",
|
||||
"edit-playlist-shortcuts": "Hold Shift or Ctrl to select multiple maps",
|
||||
"add-to-playlist": "Add to playlist",
|
||||
"remove-from-playlist": "Remove from playlist",
|
||||
"playlist-is-empty": "The playlist is empty",
|
||||
"continue": "Continue",
|
||||
"nb-maps": "Number of maps",
|
||||
"nb-mappers": "Number of mappers",
|
||||
"duration": "Duration",
|
||||
"nps": "Notes per second",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "Start date — End date",
|
||||
"all": "All",
|
||||
"last-24h": "Last 24h",
|
||||
"last-week": "Last week",
|
||||
"last-month": "Last month",
|
||||
"3-last-month": "Last 3 months"
|
||||
},
|
||||
"playlists-imported": "Playlists imported",
|
||||
"all-playlists-have-been-successfully-imported": "All playlists have been successfully imported",
|
||||
"no-playlist-found": "No playlist found",
|
||||
"no-playlist-found-in-selected-files": "No playlist found in the selected files",
|
||||
"some-playlists-not-imported": "Some playlists not imported",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Some playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Some playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Some playlists are unreadable",
|
||||
"unknown": "Some playlists could not be imported"
|
||||
},
|
||||
"no-playlists-imported": "No playlists imported",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Playlists are unreadable",
|
||||
"unknown": "No playlist could be imported"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["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"],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "Rechazar",
|
||||
"apply": "Aplicar",
|
||||
"copy": "Copiar",
|
||||
"copied": "¡Copiado!"
|
||||
"copied": "¡Copiado!",
|
||||
"confirm": "Confirmar"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Agregar una versión",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "Filtros",
|
||||
"dropdown": {
|
||||
"export-maps": "Exportar mapas",
|
||||
"delete-maps": "Borrar mapas"
|
||||
"delete-maps": "Borrar mapas",
|
||||
"delete-duplicate-maps": "Borrar duplicados"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Añadir"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorar mapas",
|
||||
"import-maps": "Importar mapas"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No hay mapas",
|
||||
"button": "Descargar mapas"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importar tus mapas",
|
||||
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorar listas de reproducción",
|
||||
"create-a-playlist": "Crear una lista de reproducción",
|
||||
"import-playlists": "Importar listas de reproducción"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importa tus listas de reproducción",
|
||||
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "Más información",
|
||||
"install-or-update": "Instalar o actualizar"
|
||||
"install-or-update": "Instalar o actualizar",
|
||||
"reinstall-all": "Reinstalar todo"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "Desinstalar todos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods ya instalados",
|
||||
"description": "Todos los mods seleccionados ya están instalados"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Desconectarte te permitirá cambiar de cuenta en la próxima descarga de Beat Saber.",
|
||||
"logout": "Cerrar sesión",
|
||||
"logout-success": "Cierre de sesión exitoso",
|
||||
"download-platform": {
|
||||
"title": "Plataforma predeterminada",
|
||||
"desc": "Elige la plataforma predeterminada que se utilizará para descargar las versiones de Beat Saber.",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Carpeta de instalación",
|
||||
"description": "Cambia la carpeta por defecto para las versiones de Beat Saber y próximas funciones.",
|
||||
"description": "Cambiar la carpeta que contendrá todo el contenido descargado por BSManager.",
|
||||
"choose-folder": "Elige la carpeta"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "Informar de un error",
|
||||
"open-logs": "Abrir los registros"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 ¡Descubre lo nuevo en el Changelog!",
|
||||
"not-founds": "😕 Uy, ¡Changelog no encontrado!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avanzados",
|
||||
"description": "Configuraciones avanzadas para BSManager.",
|
||||
"hardware-acceleration": {
|
||||
"title": "Aceleración de hardware",
|
||||
"description": "Habilite la aceleración de hardware para usar su GPU y mejorar el rendimiento de BSManager. Desactive esta opción si experimenta caídas de fotogramas.",
|
||||
"modal": {
|
||||
"title": "Reinicio necesario",
|
||||
"body": "Cambiar la configuración de aceleración de hardware cerrará y reiniciará BSManager. ¿Estás seguro de que quieres hacer esto?",
|
||||
"confirm-btn": "Sí, estoy seguro"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Ocurrió un error, no se puede desactivar la aceleración de hardware."
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "Usar enlaces simbólicos",
|
||||
"description": "Utilice enlaces simbólicos en lugar de uniones para enlazar carpetas. Actívelo solo si realmente lo necesita.",
|
||||
"modal": {
|
||||
"title": "Permisos de enlace simbólico",
|
||||
"body": "Al crear enlaces simbólicos, BSManager requerirá privilegios de administrador o el modo desarrollador activado en su sistema. ¿Estás seguro de que quieres continuar?",
|
||||
"confirm-btn": "Sí, estoy seguro"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Ocurrió un error, no se pueden cambiar los ajustes de los enlaces simbólicos."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operación en curso",
|
||||
"no-internet": "Sin internet"
|
||||
"no-internet": "Sin internet",
|
||||
"file-not-supported": "Archivo no compatible"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Espera a que termine la operación actual y vuelve a empezar.",
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo."
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo.",
|
||||
"file-not-supported": "Solo se admiten archivos {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": ".NET 6 Requerido"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Parece que Steam no quiere dejarnos descargar Beat Saber 😢",
|
||||
"404": "No se puede contactar con los servidores de Steam",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "No se puede obtener la lista de licencias.",
|
||||
"RateLimitExceeded": "Lo has intentado demasiadas veces, espera un poco y vuelve a intentarlo más tarde.",
|
||||
"TokenRejected": "Tu token de inicio de sesión ha sido rechazada 😕 Por favor, inténtalo de nuevo.",
|
||||
"AccessDenied": "El acceso a Steam ha sido denegado.",
|
||||
"dotnet-required": "Se debe instalar el tiempo de ejecución de .NET 6 para descargar una versión de Beat Saber. Descárgalo haciendo clic en el botón de abajo."
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "Descargar .NET 6"
|
||||
"AccessDenied": "El acceso a Steam ha sido denegado."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "Imposible editar",
|
||||
"CantRename": "Cambio de nombre imposible",
|
||||
"VersionAlreadExist": "Esta versión ya existe",
|
||||
"CantClone": "Clonación imposible"
|
||||
"CantClone": "Clonación imposible",
|
||||
"UnknownError": "Ocurrió un error desconocido"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla."
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Instalación del mapa completada",
|
||||
"error": "Se produjo un error durante la instalación del mapa"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Sin duplicados",
|
||||
"msg": "No se eliminó ninguna carta"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicados eliminados",
|
||||
"msg": "Se eliminaron los duplicados"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importación de mapas completada",
|
||||
"error": "Ocurrió un error durante la importación de mapas"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Los mapas se han importado con éxito.",
|
||||
"some-success": "Algunos mapas se han importado con éxito.",
|
||||
"only-accept-zip": "Solo se admiten archivos zip.",
|
||||
"invalid-zip": "El o los archivos zip no contienen mapas.",
|
||||
"unknown": "Se produjo un error desconocido."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +556,15 @@
|
||||
"title": "Backup creado",
|
||||
"msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincula la carpeta para restaurar la copia de seguridad"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "Error al enlazar la carpeta",
|
||||
"msg": {
|
||||
"EPERM": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
|
||||
"EACCES": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
|
||||
"ENOSPC": "El disco está lleno, libera espacio e inténtalo de nuevo.",
|
||||
"UNKNOWN_ERROR": "Se ha producido un error desconocido al enlazar la carpeta."
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -646,6 +730,11 @@
|
||||
"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"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "¿Borrar mapas?",
|
||||
"desc": "Solo el mapa \"{map}\" está duplicado. ¿Estás seguro de que quieres eliminarlo?",
|
||||
"desc-plural": "Se han encontrado {nb} mapas duplicados. ¿Estás seguro de que quieres eliminarlos?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +815,20 @@
|
||||
},
|
||||
"launch-as-admin": "Iniciar como Administrador",
|
||||
"not-remind-me": "No volver a recordármelo"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "Carpeta de instalación",
|
||||
"choose-folder-description": "Elija la carpeta que contendrá todo el contenido descargado por BSManager. (versiones, mods, mapas, listas de reproducción, etc.)",
|
||||
"choose-folder": "Elige la carpeta",
|
||||
"default": "Predeterminado",
|
||||
"default-tooltip": "Por defecto, en su carpeta personal"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Duración",
|
||||
"nps" : "Notas Por Segundo",
|
||||
"njs": "Velocidad de salto de nota",
|
||||
"tags": "tags",
|
||||
"specificities": "general",
|
||||
"requirements": "requisitos",
|
||||
@@ -741,19 +838,19 @@
|
||||
"accuracy": "precisión",
|
||||
"balanced": "equilibrado",
|
||||
"challenge": "desafío",
|
||||
"dancestyle": "baile",
|
||||
"dance-style": "baile",
|
||||
"fitness": "fitness",
|
||||
"speed": "speed",
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "baile",
|
||||
"dance": "danza",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "familia",
|
||||
"ambient": "ambiente",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acústico",
|
||||
"kids-family": "niños & familia",
|
||||
"ambient": "ambiental",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -761,28 +858,28 @@
|
||||
"rb": "r&b",
|
||||
"holiday": "vacaciones",
|
||||
"vocaloid": "vocaloid",
|
||||
"jrock": "j-rock",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedia",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedia & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"kpop": "k-pop",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "tecno",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "videojuego",
|
||||
"film": "film",
|
||||
"alt": "alternativa",
|
||||
"video-game-soundtrack": "videojuego",
|
||||
"tv-movie-soundtrack": "TV & cine",
|
||||
"alternative": "alternativo",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"jpop": "j-pop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electrónico",
|
||||
"classical-orchestral": "Clásico y orquestal"
|
||||
"electronic": "electrónica",
|
||||
"classical-orchestral": "Clásica & Orquestal"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -809,7 +906,8 @@
|
||||
"bsr-code" : "Código BSR",
|
||||
"download" : "Descargar mapa",
|
||||
"downloading" :"Descargando mapa",
|
||||
"cancel-download" : "Cancelar descarga"
|
||||
"cancel-download" : "Cancelar descarga",
|
||||
"hightlight-difficulty": "Resaltar la dificultad"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,6 +1099,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "O navegar por los archivos"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error al crear la lista de reproducción",
|
||||
"error-playlist-creation-desc": "Ocurrió un error al crear la lista de reproducción.",
|
||||
"playlist-created-title": "Lista de reproducción creada",
|
||||
"playlist-created-desc": "La lista de reproducción se ha creado con éxito. ¡Ahora puedes sincronizar sus mapas!",
|
||||
"download-playlist": "Descargar lista de reproducción",
|
||||
"synchronize-playlist": "Sincronizar lista de reproducción",
|
||||
"synchronize-maps": "Sincronizar mapas",
|
||||
"error-playlists-synchronization-title": "Error al sincronizar listas de reproducción",
|
||||
"error-playlists-synchronization-desc": "Ocurrió un error al sincronizar las listas de reproducción.",
|
||||
"playlists-synchronized-title": "¡Listas de reproducción sincronizadas!",
|
||||
"playlists-synchronized-desc": "Las listas de reproducción y sus mapas han sido descargados.",
|
||||
"playlists-export-error-title": "Error al exportar listas de reproducción",
|
||||
"playlists-export-error-desc": "Ocurrió un error al exportar las listas de reproducción.",
|
||||
"playlists-exported-title": "¡Listas de reproducción exportadas!",
|
||||
"playlists-exported-desc": "Las listas de reproducción se han exportado con éxito.",
|
||||
"playlists-with-maps-exported-desc": "Las listas de reproducción y sus mapas se han exportado con éxito.",
|
||||
"playlist-delete-error-title": "Error al eliminar la lista de reproducción",
|
||||
"playlist-delete-error-desc": "Ocurrió un error al eliminar la lista de reproducción.",
|
||||
"playlists-deleted-title": "¡Listas de reproducción eliminadas!",
|
||||
"playlists-deleted-desc": "Las listas de reproducción se han eliminado con éxito.",
|
||||
"edit-playlist": "Editar lista de reproducción",
|
||||
"playlist-edit-error-title": "Error al editar la lista de reproducción",
|
||||
"playlist-edit-error-desc": "Ocurrió un error al editar la lista de reproducción.",
|
||||
"playlist-edited-title": "¡Lista de reproducción editada!",
|
||||
"playlist-edited-desc": "La lista de reproducción se ha modificado con éxito. ¡Ahora puedes sincronizar sus mapas!",
|
||||
"playlists-loading": "Cargando listas de reproducción...",
|
||||
"no-playlists": "No hay listas de reproducción",
|
||||
"download-playlists": "Descargar listas de reproducción",
|
||||
"created-by": "Creado por",
|
||||
"stop-download": "Detener descarga",
|
||||
"cancel-download": "Cancelar descarga",
|
||||
"open-file": "Abrir archivo",
|
||||
"link-playlists": "Vincular listas de reproducción",
|
||||
"link-playlist-desc": "Vincular listas de reproducción permite compartirlas entre todas las versiones. Una vez vinculada, esta versión se beneficiará de las listas de reproducción compartidas",
|
||||
"link-playlist-info": "Añadir y eliminar listas de reproducción también se compartirá",
|
||||
"keep-playlists": "Mantener listas de reproducción",
|
||||
"keep-playlists-tip": "Mantener las listas de reproducción moverá las listas de la versión actual a la carpeta de listas compartidas. De lo contrario, se perderán",
|
||||
"unlink-playlists": "Desvincular listas de reproducción",
|
||||
"unlink-playlist-desc": "Advertencia, desvincular las listas de reproducción ya no permitirá el uso de listas compartidas para esta versión.",
|
||||
"unlink-keep-playlists-tip": "Mantener las listas de reproducción creará una copia de las listas compartidas para la versión actual. De lo contrario, no se mantendrán listas para esta versión.",
|
||||
"delete-playlist-ask": "¿Eliminar lista de reproducción?",
|
||||
"delete-playlists-ask": "¿Eliminar listas de reproducción?",
|
||||
"delete-playlist-desc": "¿Estás seguro de que quieres eliminar la lista de reproducción \"{playlistTitle}\"?",
|
||||
"delete-playlists-desc": "¿Estás seguro de que quieres eliminar {nb} listas de reproducción?",
|
||||
"delete-maps": "Eliminar mapas",
|
||||
"delete-playlist-maps-tip": "Si está activado, se eliminarán todos los mapas de la lista de reproducción",
|
||||
"delete-playlists-maps-tip": "Si está activado, se eliminarán todos los mapas de las listas de reproducción",
|
||||
"export-playlist-ask": "¿Exportar lista de reproducción?",
|
||||
"export-playlists-ask": "¿Exportar listas de reproducción?",
|
||||
"export-playlist-desc": "¿Estás seguro de que quieres exportar la lista de reproducción \"{playlistTitle}\"?",
|
||||
"export-playlists-desc": "¿Estás seguro de que quieres exportar {nb} listas de reproducción?",
|
||||
"export-maps": "Exportar mapas",
|
||||
"export-playlist-maps-tip": "Si está activado, también se exportarán todos los mapas de la lista de reproducción",
|
||||
"export-playlists-maps-tip": "Si está activado, también se exportarán todos los mapas de las listas de reproducción",
|
||||
"export": "Exportar",
|
||||
"need-clone-title": "Advertencia",
|
||||
"need-clone-desc-1": "Esta lista de reproducción se ha descargado de un sitio externo y contiene un enlace de sincronización.",
|
||||
"need-clone-desc-2": "Para evitar perder tus cambios durante la sincronización, la lista de reproducción se duplicará y se eliminará su enlace de sincronización.",
|
||||
"need-clone-desc-3": "Luego puedes, si lo deseas, eliminar la lista de reproducción original.",
|
||||
"understood": "Entendido",
|
||||
"synchronize-playlist-ask": "¿Sincronizar lista de reproducción?",
|
||||
"synchronize-playlists-ask": "¿Sincronizar listas de reproducción?",
|
||||
"synchronize-playlist-desc": "¿Estás seguro de que quieres sincronizar la lista de reproducción \"{playlistTitle}\"?",
|
||||
"synchronize-playlists-desc": "¿Estás seguro de que quieres sincronizar {nb} listas de reproducción?",
|
||||
"synchronize-playlist-tip": "Esta acción actualiza las listas de reproducción y descarga los mapas faltantes; puede tardar varios minutos.",
|
||||
"synchronize": "Sincronizar",
|
||||
"curated": "Recomendado",
|
||||
"verified-mapper": "Mapeador verificado",
|
||||
"empty-playlists": "Listas de reproducción vacías",
|
||||
"search-playlist": "Buscar una lista de reproducción",
|
||||
"no-playlists-found": "No se encontraron listas de reproducción",
|
||||
"error-occur-while-loading-playlists": "Ocurrió un error al cargar las listas de reproducción",
|
||||
"error-occur-while-loading-playlist": "Ocurrió un error al cargar la lista de reproducción",
|
||||
"loading-maps": "Cargando mapas...",
|
||||
"no-maps-found-for-playlist": "No se encontraron mapas para esta lista de reproducción",
|
||||
"playlist-contain-no-maps": "La lista de reproducción no contiene mapas",
|
||||
"no-map-installed-for-playlist": "No hay mapas instalados para esta lista de reproducción",
|
||||
"playlist-is-waiting-to-download": "La lista de reproducción está esperando para descargar",
|
||||
"download-maps": "Descargar mapas",
|
||||
"download-missing-maps": "Descargar mapas faltantes",
|
||||
"playlist-is-downloading": "La lista de reproducción se está descargando",
|
||||
"some-playlist-maps-are-missing": "Faltan algunos mapas en esta lista de reproducción",
|
||||
"create-a-playlist": "Crear una lista de reproducción",
|
||||
"synchronize-playlists": "Sincronizar listas de reproducción",
|
||||
"export-playlists": "Exportar listas de reproducción",
|
||||
"delete-playlists": "Eliminar listas de reproducción",
|
||||
"choose-image": "Elegir una imagen",
|
||||
"title": "Título",
|
||||
"playlist-title": "Título de la lista de reproducción",
|
||||
"description": "Descripción",
|
||||
"playlist-description": "Descripción de la lista de reproducción",
|
||||
"author": "Autor",
|
||||
"playlist-author": "Autor de la lista de reproducción",
|
||||
"save": "Guardar",
|
||||
"loading": "Cargando...",
|
||||
"installed": "Instalado",
|
||||
"no-map-found": "No se encontró ningún mapa",
|
||||
"edit-playlist-shortcuts": "Mantén presionado Shift o Ctrl para seleccionar múltiples mapas",
|
||||
"add-to-playlist": "Añadir a la lista de reproducción",
|
||||
"remove-from-playlist": "Quitar de la lista de reproducción",
|
||||
"playlist-is-empty": "La lista de reproducción está vacía",
|
||||
"continue": "Continuar",
|
||||
"nb-maps": "Número de mapas",
|
||||
"nb-mappers": "Número de mapeadores",
|
||||
"duration": "Duración",
|
||||
"nps": "Notas por segundo",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "Fecha de inicio — Fecha de fin",
|
||||
"all": "Todo",
|
||||
"last-24h": "Últimas 24h",
|
||||
"last-week": "Última semana",
|
||||
"last-month": "Último mes",
|
||||
"3-last-month": "Últimos 3 meses"
|
||||
},
|
||||
"playlists-imported": "Listas de reproducción importadas",
|
||||
"all-playlists-have-been-successfully-imported": "Todas las listas de reproducción se han importado con éxito",
|
||||
"no-playlist-found": "No se encontró ninguna lista de reproducción",
|
||||
"no-playlist-found-in-selected-files": "No se encontró ninguna lista de reproducción en los archivos seleccionados",
|
||||
"some-playlists-not-imported": "Algunas listas de reproducción no se importaron",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar algunas listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Algunas listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Algunas listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudieron importar algunas listas de reproducción"
|
||||
},
|
||||
"no-playlists-imported": "No se importaron listas de reproducción",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar las listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Las listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Las listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudo importar ninguna lista de reproducción"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["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"],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "Refuser",
|
||||
"apply": "Appliquer",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié !"
|
||||
"copied": "Copié !",
|
||||
"confirm": "Confirmer"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Ajouter une version",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "Filtres",
|
||||
"dropdown": {
|
||||
"export-maps": "Exporter les maps",
|
||||
"delete-maps": "Supprimer les maps"
|
||||
"delete-maps": "Supprimer les maps",
|
||||
"delete-duplicate-maps": "Supprimer les doublons"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Ajouter"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorer les maps",
|
||||
"import-maps": "Importer des maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Aucune map",
|
||||
"button": "Télécharger des maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos maps",
|
||||
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorer les playlists",
|
||||
"create-a-playlist": "Créer une playlist",
|
||||
"import-playlists": "Importer des playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos playlists",
|
||||
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "Plus d'infos",
|
||||
"install-or-update": "Installer ou mettre à jour"
|
||||
"install-or-update": "Installer ou mettre à jour",
|
||||
"reinstall-all": "Tout réinstaller"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "Tout désinstaller"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods déjà installées",
|
||||
"description": "Tous les mods séléctionnées sont déjà installées"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
|
||||
"logout": "Déconnexion",
|
||||
"logout-success": "Déconnexion réussie",
|
||||
"download-platform": {
|
||||
"title": "Plateforme par défaut",
|
||||
"desc": "Choisi la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"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.",
|
||||
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager.",
|
||||
"choose-folder": "Choisir un dossier"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "Signaler un bug",
|
||||
"open-logs": "Ouvrir les logs"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Jetez un œil au Changelog !",
|
||||
"not-founds": "😕 Oups, Changelog introuvable !"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avancés",
|
||||
"description": "Paramètres avancés pour BSManager.",
|
||||
"hardware-acceleration": {
|
||||
"title": "Accélération matérielle",
|
||||
"description": "Activez l'accélération matérielle pour utiliser votre GPU et améliorer les performances de BSManager. Désactivez cette option si vous rencontrez des chutes d'IPS.",
|
||||
"modal": {
|
||||
"title": "Redémarrage nécessaire",
|
||||
"body": "Changer le paramètre d'accélération matérielle va quitter et relancer BSManager. Êtes-vous sûr de vouloir continuer ?",
|
||||
"confirm-btn": "Oui, je suis sûr"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Une erreur s'est produite, impossible de désactiver l'accélération matérielle."
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "Utiliser des liens symboliques",
|
||||
"description": "Utilisez des liens symboliques au lieu de jonctions pour lier des dossiers. Activez cette option uniquement si vous en avez besoin.",
|
||||
"modal": {
|
||||
"title": "Permissions des liens symboliques",
|
||||
"body": "Lors de la création de liens symboliques, BSManager nécessitera des privilèges administrateur ou le mode développeur activé sur votre système. Êtes-vous sûr de vouloir continuer ?",
|
||||
"confirm-btn": "Oui, je suis sûr"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Une erreur s'est produite, impossible de changer les paramètres des liens symboliques."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Opération en cours",
|
||||
"no-internet": "Pas d'accès Internet"
|
||||
"no-internet": "Pas d'accès Internet",
|
||||
"file-not-supported": "Fichier non supporté"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Attends la fin de l'opération en cours puis recommence.",
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye."
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye.",
|
||||
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": ".NET 6 Requis"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Steam ne semble pas vouloir nous laisser télécharger Beat Saber 😢",
|
||||
"404": "Impossible de contacter les serveurs de Steam.",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "Impossible d'obtenir la liste des licences.",
|
||||
"RateLimitExceeded": "Tu as essayé trop de fois attends un peu et recommence plus tard.",
|
||||
"TokenRejected": "Votre token de connexion a été rejeté 😕 Veuillez réessayer.",
|
||||
"AccessDenied": "L'accès à Steam a été refusé.",
|
||||
"dotnet-required": ".NET 6 Runtime doit être installé pour pouvoir télécharger une version de Beat Saber. Télécharge-le en cliquant sur le bouton ci-dessous."
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "Télécharger .NET 6"
|
||||
"AccessDenied": "L'accès à Steam a été refusé."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "Modification impossible",
|
||||
"CantRename": "Renommage impossible",
|
||||
"VersionAlreadExist": "Cette version existe déjà",
|
||||
"CantClone": "Clonage impossible"
|
||||
"CantClone": "Clonage impossible",
|
||||
"UnknownError": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Tu ne peux pas modifier la version Steam, cependant tu peux la cloner."
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Installation de la map terminée",
|
||||
"error": "Une erreur s'est produite lors de l'installation de la map"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Pas de doublons",
|
||||
"msg": "Aucune carte n'a été supprimée"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Doublons supprimés",
|
||||
"msg": "Les doublons ont été supprimés"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importation des maps terminée",
|
||||
"error": "Une erreur est survenue lors de l'importation des maps"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Les maps ont été importées avec succès.",
|
||||
"some-success": "Certaines maps ont été importées avec succès.",
|
||||
"only-accept-zip": "Seules les fichiers zip sont pris en charge.",
|
||||
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps.",
|
||||
"unknown": "Une erreur inconnue s'est produite."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,7 +556,17 @@
|
||||
"title": "Sauvegarde créée",
|
||||
"msg": "Le partage du dossier 'UserData' peut générer des erreurs, en cas de soucis déliez le dossier pour restaurer la sauvegarde"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "Erreur lors de la liaison du dossier",
|
||||
"msg": {
|
||||
"EPERM": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
|
||||
"EACCES": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
|
||||
"ENOSPC": "Le disque est plein, faites de la place et réessayez.",
|
||||
"UNKNOWN_ERROR": "Une erreur inconnue est survenue lors de la liaison du dossier."
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
@@ -646,6 +731,11 @@
|
||||
"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"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Supprimer les maps ?",
|
||||
"desc": "Seule la map \"{map}\" est en double. Es-tu sûr de vouloir la supprimer ?",
|
||||
"desc-plural": "{nb} maps en double ont été trouvées. Es-tu sûr de vouloir les supprimer ?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +816,20 @@
|
||||
},
|
||||
"launch-as-admin": "Lancer en administateur",
|
||||
"not-remind-me": "Ne plus me rappeler"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "Dossier d'installation",
|
||||
"choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, cartes, playlists, etc.)",
|
||||
"choose-folder": "Choisir un dossier",
|
||||
"default": "Par défaut",
|
||||
"default-tooltip": "Par défaut, dans votre dossier personnel"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Durée",
|
||||
"nps" : "Notes Par Seconde",
|
||||
"njs": "Vitesse de saut des notes",
|
||||
"tags": "tags",
|
||||
"specificities": "général",
|
||||
"requirements": "requis",
|
||||
@@ -741,48 +839,48 @@
|
||||
"accuracy": "précision",
|
||||
"balanced": "équilibrée",
|
||||
"challenge": "challenge",
|
||||
"dancestyle": "dance",
|
||||
"dance-style": "dance",
|
||||
"fitness": "fitness",
|
||||
"speed": "vitesse",
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "dance",
|
||||
"dance": "danse",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "famille",
|
||||
"ambient": "ambiante",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acoustique",
|
||||
"kids-family": "enfants & famille",
|
||||
"ambient": "ambiant",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"rb": "r&b",
|
||||
"holiday": "vacance",
|
||||
"holiday": "vacances",
|
||||
"vocaloid": "vocaloid",
|
||||
"jrock": "j-rock",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comédie",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comédie & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"kpop": "k-pop",
|
||||
"indie": "indé",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "jeu vidéo",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "jeu vidéo",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternatif",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"metal": "métal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"jpop": "j-pop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "électronique",
|
||||
"classical-orchestral": "classique & orchestrale"
|
||||
"classical-orchestral": "Classique & Orchestral"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -809,7 +907,8 @@
|
||||
"bsr-code" : "Code BSR",
|
||||
"download" : "Télécharger la carte",
|
||||
"downloading" :"Téléchargement de la carte",
|
||||
"cancel-download" : "Annuler le téléchargement"
|
||||
"cancel-download" : "Annuler le téléchargement",
|
||||
"hightlight-difficulty" : "Surligner la difficulté"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,6 +1100,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Ou parcourir les fichiers"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
|
||||
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
|
||||
"playlist-created-title": "Playlist créée",
|
||||
"playlist-created-desc": "La playlist a été créée avec succès. Tu peut maintenant synchroniser ses maps !",
|
||||
"download-playlist": "Télécharger la playlist",
|
||||
"synchronize-playlist": "Synchroniser la playlist",
|
||||
"synchronize-maps": "Synchroniser les maps",
|
||||
"error-playlists-synchronization-title": "Erreur lors de la synchronisation des playlists",
|
||||
"error-playlists-synchronization-desc": "Une erreur est survenue lors de la synchronisation des playlists.",
|
||||
"playlists-synchronized-title": "Playlists synchronisées !",
|
||||
"playlists-synchronized-desc": "Les playlists et leurs maps ont été téléchargées.",
|
||||
"playlists-export-error-title": "Erreur lors de l'exportation des playlists",
|
||||
"playlists-export-error-desc": "Une erreur est survenue lors de l'exportation des playlists.",
|
||||
"playlists-exported-title": "Playlists exportées !",
|
||||
"playlists-exported-desc": "Les playlists ont été exportées avec succès.",
|
||||
"playlists-with-maps-exported-desc": "Les playlists et leurs maps ont été exportées avec succès.",
|
||||
"playlist-delete-error-title": "Erreur lors de la suppression de la playlist",
|
||||
"playlist-delete-error-desc": "Une erreur est survenue lors de la suppression de la playlist.",
|
||||
"playlists-deleted-title": "Playlists supprimées !",
|
||||
"playlists-deleted-desc": "Les playlists ont été supprimées avec succès.",
|
||||
"edit-playlist": "Éditer la playlist",
|
||||
"playlist-edit-error-title": "Erreur lors de l'édition de la playlist",
|
||||
"playlist-edit-error-desc": "Une erreur est survenue lors de l'édition de la playlist.",
|
||||
"playlist-edited-title": "Playlist éditée !",
|
||||
"playlist-edited-desc": "La playlist a été modifiée avec succès. Tu peut maintenant synchroniser ses maps !",
|
||||
"playlists-loading": "Chargement des playlist...",
|
||||
"no-playlists": "Aucune playlist",
|
||||
"download-playlists": "Télécharger des playlists",
|
||||
"created-by": "Créée par",
|
||||
"stop-download": "Arrêter le téléchargement",
|
||||
"cancel-download": "Annuler le téléchargement",
|
||||
"open-file": "Ouvrir le fichier",
|
||||
"link-playlists": "Lier les playlists",
|
||||
"link-playlist-desc": "La liaison des playlists permet de partager les playlists entre toute les version. Une fois liée, cette version profitera des playlists partagées",
|
||||
"link-playlist-info": "L'ajout et la suppression de playlists sera également partagé",
|
||||
"keep-playlists": "Conserver les playlists",
|
||||
"keep-playlists-tip": "Conserver les playlists déplacera les playlists de la version actuelle dans le dossier des playlists partagées. Dans le cas contraire elles seront perdues",
|
||||
"unlink-playlists": "Délier les playlists",
|
||||
"unlink-playlist-desc": "Attention, délier les playlists ne permettra plus l'utilisation des playlists paratagées pour cette version.",
|
||||
"unlink-keep-playlists-tip": "Conserver les playlists créera une copie des playlists partagées pour la version actuelle. Dans le cas contraire, aucune playlist ne sera conservée pour cette version.",
|
||||
"delete-playlist-ask": "Supprimer la playlist ?",
|
||||
"delete-playlists-ask": "Supprimer les playlists ?",
|
||||
"delete-playlist-desc": "Es-tu sur de vouloir supprimer la playlist \"{playlistTitle}\" ?",
|
||||
"delete-playlists-desc": "Es-tu sur de vouloir supprimer {nb} playlists ?",
|
||||
"delete-maps": "Supprimer les maps",
|
||||
"delete-playlist-maps-tip": "Si activé, toutes les maps de la playlist seront supprimées",
|
||||
"delete-playlists-maps-tip": "Si activé, toutes les maps des playlists seront supprimées",
|
||||
"export-playlist-ask": "Exporter la playlist ?",
|
||||
"export-playlists-ask": "Exporter les playlists ?",
|
||||
"export-playlist-desc": "Es-tu sur de vouloir exporter la playlist \"{playlistTitle}\" ?",
|
||||
"export-playlists-desc": "Es-tu sur de vouloir exporter les {nb} playlists ?",
|
||||
"export-maps": "Exporter les maps",
|
||||
"export-playlist-maps-tip": "Si activé, toutes les maps de la playlist seront également exportées",
|
||||
"export-playlists-maps-tip": "Si activé, toutes les maps des playlists seront également exportées",
|
||||
"export": "Exporter",
|
||||
"need-clone-title": "Attention",
|
||||
"need-clone-desc-1": "Cette playlist a été téléchargée depuis un site externe et contient un lien de synchronisation.",
|
||||
"need-clone-desc-2": "Pour éviter de perdre vos modifications lors d'une synchronisation, la playlist va être dupliquée et son lien de synchronisation supprimé.",
|
||||
"need-clone-desc-3": "Vous pourrez ensuite, si vous le souhaitez, supprimer la playlist originale.",
|
||||
"understood": "J'ai compris",
|
||||
"synchronize-playlist-ask": "Synchroniser la playlist ?",
|
||||
"synchronize-playlists-ask": "Synchroniser les playlists ?",
|
||||
"synchronize-playlist-desc": "Es-tu sur de vouloir synchroniser la playlist \"{playlistTitle}\" ?",
|
||||
"synchronize-playlists-desc": "Es-tu sur de vouloir synchroniser les {nb} playlists ?",
|
||||
"synchronize-playlist-tip": "Cette action met à jour les playlists et télécharge les maps manquantes; cela peut durer plusieurs minutes.",
|
||||
"synchronize": "Synchroniser",
|
||||
"curated": "Recommandée",
|
||||
"verified-mapper": "Mapper vérifié",
|
||||
"empty-playlists": "Playlists vides",
|
||||
"search-playlist": "Rechercher une playlist",
|
||||
"no-playlists-found": "Aucune playlists trouvées",
|
||||
"error-occur-while-loading-playlists": "Une erreur est survenue lors du chargement des playlists",
|
||||
"error-occur-while-loading-playlist": "Une erreur est survenue lors du chargement de la playlist",
|
||||
"loading-maps":"Chargement des maps...",
|
||||
"no-maps-found-for-playlist":"Aucune map trouvée pour cette playlist",
|
||||
"playlist-contain-no-maps":"La playlist ne contient aucune map",
|
||||
"no-map-installed-for-playlist":"Aucune map installée pour cette playlist",
|
||||
"playlist-is-waiting-to-download":"La playlist est en attente de téléchargment",
|
||||
"download-maps": "Télécharger les maps",
|
||||
"download-missing-maps": "Télécharger les maps manquantes",
|
||||
"playlist-is-downloading":"La playlist est en cours de téléchargement",
|
||||
"some-playlist-maps-are-missing":"Certaines maps de cette playlist sont manquantes",
|
||||
"create-a-playlist": "Créer une playlist",
|
||||
"synchronize-playlists": "Synchroniser les playlists",
|
||||
"export-playlists": "Exporter les playlists",
|
||||
"delete-playlists": "Supprimer les playlists",
|
||||
"choose-image": "Choisir une image",
|
||||
"title": "Titre",
|
||||
"playlist-title": "Titre de la playlist",
|
||||
"description": "Description",
|
||||
"playlist-description": "Description de la playlist",
|
||||
"author": "Auteur",
|
||||
"playlist-author": "Auteur de la playlist",
|
||||
"save": "Enregistrer",
|
||||
"loading": "Chargement...",
|
||||
"installed": "Installée",
|
||||
"no-map-found": "Aucune map trouvée",
|
||||
"edit-playlist-shortcuts": "Maintenez Maj ou Ctrl pour sélectionner plusieurs maps",
|
||||
"add-to-playlist": "Ajouter à la playlist",
|
||||
"remove-from-playlist": "Retirer de la playlist",
|
||||
"playlist-is-empty": "La playlist est vide",
|
||||
"continue": "Continuer",
|
||||
"nb-maps": "Nombre de maps",
|
||||
"nb-mappers": "Nombre de mappers",
|
||||
"duration": "Durée",
|
||||
"nps": "Notes par secondes",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "Date début — Date fin",
|
||||
"all": "Tout",
|
||||
"last-24h": "Dernières 24h",
|
||||
"last-week": "Dernière semaine",
|
||||
"last-month": "Dernier mois",
|
||||
"3-last-month": "3 derniers mois"
|
||||
},
|
||||
"playlists-imported": "Playlists importées",
|
||||
"all-playlists-have-been-successfully-imported": "Toutes les playlists ont été importées avec succès",
|
||||
"no-playlist-found": "Aucune playlist trouvée",
|
||||
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
|
||||
"some-playlists-not-imported": "Certaines playlists non importées",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Certaines playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Certaines playlists ne sont pas lisibles",
|
||||
"unknown": "Certaines playlists n'ont pas pu être importées"
|
||||
},
|
||||
"no-playlists-imported": "Aucune playlist importée",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Les playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
|
||||
"unknown": "Aucune playlist n'a pu être importée"
|
||||
}
|
||||
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["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"],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "拒否する",
|
||||
"apply": "適用",
|
||||
"copy": "コピー",
|
||||
"copied": "コピー済み!"
|
||||
"copied": "コピー済み!",
|
||||
"confirm": "確認"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "バージョンを追加",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "絞り込み",
|
||||
"dropdown": {
|
||||
"export-maps": "マップをエクスポート",
|
||||
"delete-maps": "マップを削除"
|
||||
"delete-maps": "マップを削除",
|
||||
"delete-duplicate-maps": " 重複を削除"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "追加"
|
||||
"drop-down": {
|
||||
"browse-maps": "マップを閲覧",
|
||||
"import-maps": "マップをインポート"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "マップがありません",
|
||||
"button": "マップをダウンロードする"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "マップをインポート",
|
||||
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "プレイリストを閲覧",
|
||||
"create-a-playlist": "プレイリストを作成",
|
||||
"import-playlists": "プレイリストをインポート"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "プレイリストをインポート",
|
||||
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "このバージョンで使用できるMODはまだありません。",
|
||||
"buttons": {
|
||||
"more-infos": "詳細情報",
|
||||
"install-or-update": "インストールとアップデート"
|
||||
"install-or-update": "インストールとアップデート",
|
||||
"reinstall-all": "すべて再インストール"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "全てアンインストールする"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "すでにインストール済みのMOD",
|
||||
"description": "選択したすべてのMODはすでにインストールされています"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "ログアウトすると、次のBeat Saberダウンロード時にアカウントを切り替えることができます。",
|
||||
"logout": "ログアウト",
|
||||
"logout-success": "ログアウト成功",
|
||||
"download-platform": {
|
||||
"title": "デフォルトプラットフォーム",
|
||||
"desc": "Beat Saberのダウンロードに使用するデフォルトのプラットフォームを選択します。",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "インストールフォルダー",
|
||||
"description": "BeatSaberのバージョンとその他の今後の機能を入れるフォルダを変更します。",
|
||||
"description": "BSManager によってダウンロードされたすべてのコンテンツを含むフォルダを変更します。",
|
||||
"choose-folder": "フォルダーを選択"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "バグを報告",
|
||||
"open-logs": "ログを表示"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 チェンジログを覗いてみて!",
|
||||
"not-founds": "😕 あれ、チェンジログが見当たらない!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高度な設定",
|
||||
"description": "BSManagerの高度な設定。",
|
||||
"hardware-acceleration": {
|
||||
"title": "ハードウェアアクセラレーション",
|
||||
"description": "ハードウェアアクセラレーションを有効にしてGPUを使用し、BSManagerのパフォーマンスを向上させます。フレームドロップが発生している場合は、これをオフにしてください。",
|
||||
"modal": {
|
||||
"title": "再起動が必要",
|
||||
"body": "ハードウェアアクセラレーションの設定を変更すると、BSManagerが終了して再起動します。本当に続行しますか?",
|
||||
"confirm-btn": "はい、確かです"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "エラーが発生しました。ハードウェアアクセラレーションを無効にできません。"
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "シンボリックリンクを使用",
|
||||
"description": "フォルダをリンクするためにジャンクションの代わりにシンボリックリンクを使用します。本当に必要な場合にのみこれをオンにしてください。",
|
||||
"modal": {
|
||||
"title": "シンボリックリンクの権限",
|
||||
"body": "シンボリックリンクを作成する際、BSManagerは管理者権限またはシステムで有効になっている開発者モードを必要とします。本当に続行しますか?",
|
||||
"confirm-btn": "はい、確かです"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "エラーが発生しました。シンボリックリンク設定を変更できません。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "稼働中",
|
||||
"no-internet": "インターネット接続がありません"
|
||||
"no-internet": "インターネット接続がありません",
|
||||
"file-not-supported": "ファイルはサポートされていません"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "現在の処理が終了するのを待ってから、もう一度試してください。",
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。"
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。",
|
||||
"file-not-supported": "{types}ファイルのみがサポートされています。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": ".NET 6が必要です!"
|
||||
},
|
||||
"msg": {
|
||||
"401": "SteamがBeat Saberのダウンロードを許可してくれないようだ。😢",
|
||||
"404": "Steamサーバーに接続できません。",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "ライセンスリストを取得できません。",
|
||||
"RateLimitExceeded": "何度も試しているのでしばらくたって、もう一度お試しください。",
|
||||
"TokenRejected": "ログイントークンが拒否されました 😕 もう一度お試しください。",
|
||||
"AccessDenied": "Steamへのアクセスが拒否されました。",
|
||||
"dotnet-required": "Beat Saberをダウンロードするには、.NET 6 Runtimeがインストールされている必要があります。下のボタンをクリックしてダウンロードしてください。"
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": ".NET 6をダウンロード"
|
||||
"AccessDenied": "Steamへのアクセスが拒否されました。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "編集不可",
|
||||
"CantRename": "改名不可能",
|
||||
"VersionAlreadExist": "このバージョンは既に存在しています!",
|
||||
"CantClone": "クローン作成不可"
|
||||
"CantClone": "クローン作成不可",
|
||||
"UnknownError": "不明なエラーが発生しました"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Steam版は編集できませんがクローンを作ることは可能です。"
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "マップのインストール完了",
|
||||
"error": "マップのインストール中にエラーが発生しました"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "重複なし",
|
||||
"msg": "マップは削除されませんでした"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複削除",
|
||||
"msg": "重複が削除されました"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "マップのインポートが完了しました",
|
||||
"error": "マップのインポート中にエラーが発生しました"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "マップが正常にインポートされました。",
|
||||
"some-success": "一部のマップが正常にインポートされました。",
|
||||
"only-accept-zip": "zipファイルのみがサポートされています。",
|
||||
"invalid-zip": "zipファイルにマップが含まれていません。",
|
||||
"unknown": "不明なエラーが発生しました。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +556,15 @@
|
||||
"title": "バックアップを作成しました",
|
||||
"msg": "'UserData'フォルダを共有するとエラーが発生する可能性があります。問題が発生した場合は、フォルダをリンク解除してバックアップを復元してください。"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "フォルダのリンク中にエラーが発生しました",
|
||||
"msg": {
|
||||
"EPERM": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
|
||||
"EACCES": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
|
||||
"ENOSPC": "ディスクがいっぱいです。空き容量を作ってもう一度試してください。",
|
||||
"UNKNOWN_ERROR": "フォルダをリンク中に不明なエラーが発生しました。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -646,6 +730,11 @@
|
||||
"title": "マップを保持は、リンクを解除した後、共有フォルダから現在のバージョンにすべてのマップをコピーします。これを無効をしても共有マップは失われません。"
|
||||
},
|
||||
"valid-btn": "マップのリンクを解除"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "マップを削除しますか?",
|
||||
"desc": "マップ「{map}」のみが重複しています。削除してもよろしいですか?",
|
||||
"desc-plural": "{nb}個の重複したマップが見つかりました。削除してもよろしいですか?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +815,20 @@
|
||||
},
|
||||
"launch-as-admin": "管理者として起動",
|
||||
"not-remind-me": "Больше не напоминать"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "インストールフォルダー",
|
||||
"choose-folder-description": "BSManager によってダウンロードされたすべてのコンテンツ (バージョン、MOD、マップ、プレイリストなど) を含むフォルダを選択してください。",
|
||||
"choose-folder": "フォルダーを選択",
|
||||
"default": "デフォルト",
|
||||
"default-tooltip": "デフォルトでは、ホームフォルダに設定されます"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "尺",
|
||||
"nps" : "秒間ノート数",
|
||||
"njs": "ノートジャンプ速度",
|
||||
"tags": "タグ",
|
||||
"specificities": "一般",
|
||||
"requirements": "要Mod",
|
||||
@@ -741,7 +838,7 @@
|
||||
"accuracy": "正確",
|
||||
"balanced": "バランス",
|
||||
"challenge": "挑戦",
|
||||
"dancestyle": "ダンス",
|
||||
"dance-style": "ダンス",
|
||||
"fitness": "フットネス",
|
||||
"speed": "スピード",
|
||||
"tech": "技術的"
|
||||
@@ -750,39 +847,39 @@
|
||||
"dance": "ダンス",
|
||||
"swing": "スウィング",
|
||||
"nightcore": "ナイトコア",
|
||||
"folk": "フォーク",
|
||||
"family": "ファミリー",
|
||||
"folk-acoustic": "フォーク & アコースティック",
|
||||
"kids-family": "キッズ & ファミリー",
|
||||
"ambient": "アンビエント",
|
||||
"funk": "ファンク",
|
||||
"funk-disco": "ファンク & ディスコ",
|
||||
"jazz": "ジャズ",
|
||||
"soul": "ソウル",
|
||||
"speedcore": "スピードコア ",
|
||||
"speedcore": "スピードコア",
|
||||
"punk": "パンク",
|
||||
"rb": "r&b",
|
||||
"holiday": "休日",
|
||||
"holiday": "ホリデー",
|
||||
"vocaloid": "ボーカロイド",
|
||||
"jrock": "j-rock",
|
||||
"j-rock": "J-ロック",
|
||||
"trance": "トランス",
|
||||
"drumbass": "ドラムンベース",
|
||||
"comedy": "コメディ",
|
||||
"instrumental": "器楽",
|
||||
"drum-and-bass": "ドラム & ベース",
|
||||
"comedy-meme": "コメディ & ミーム",
|
||||
"instrumental": "インストゥルメンタル",
|
||||
"hardcore": "ハードコア",
|
||||
"kpop": "k-pop",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "インディー",
|
||||
"techno": "テクノ",
|
||||
"house": "家",
|
||||
"game": "ビデオゲーム",
|
||||
"film": "映画音楽",
|
||||
"alt": "オルタナティヴ",
|
||||
"house": "ハウス",
|
||||
"video-game-soundtrack": "ゲーム音楽",
|
||||
"tv-movie-soundtrack": "TV & 映画",
|
||||
"alternative": "オルタナティブ",
|
||||
"dubstep": "ダブステップ",
|
||||
"metal": "メタル",
|
||||
"anime": "アニメ",
|
||||
"hiphop": "ヒップポップ",
|
||||
"jpop": "j-pop",
|
||||
"hip-hop-rap": "ヒップホップ & ラップ",
|
||||
"j-pop": "J-ポップ",
|
||||
"rock": "ロック",
|
||||
"pop": "ポップ",
|
||||
"electronic": "エレクトロニック",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"classical-orchestral": "クラシック & オーケストラ"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -809,7 +906,8 @@
|
||||
"bsr-code" : "BSRコード",
|
||||
"download" : "マップをダウンロード",
|
||||
"downloading" :"マップをダウンロード中",
|
||||
"cancel-download" : "ダウンロードをキャンセル"
|
||||
"cancel-download" : "ダウンロードをキャンセル",
|
||||
"hightlight-difficulty": "難易度をハイライト"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,9 +1099,146 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "またはファイルを参照"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "プレイリストの作成エラー",
|
||||
"error-playlist-creation-desc": "プレイリストの作成中にエラーが発生しました。",
|
||||
"playlist-created-title": "プレイリストが作成されました",
|
||||
"playlist-created-desc": "プレイリストが正常に作成されました。今すぐマップを同期できます!",
|
||||
"download-playlist": "プレイリストをダウンロード",
|
||||
"synchronize-playlist": "プレイリストを同期",
|
||||
"synchronize-maps": "マップを同期",
|
||||
"error-playlists-synchronization-title": "プレイリストの同期エラー",
|
||||
"error-playlists-synchronization-desc": "プレイリストの同期中にエラーが発生しました。",
|
||||
"playlists-synchronized-title": "プレイリストが同期されました!",
|
||||
"playlists-synchronized-desc": "プレイリストとそのマップがダウンロードされました。",
|
||||
"playlists-export-error-title": "プレイリストのエクスポートエラー",
|
||||
"playlists-export-error-desc": "プレイリストのエクスポート中にエラーが発生しました。",
|
||||
"playlists-exported-title": "プレイリストがエクスポートされました!",
|
||||
"playlists-exported-desc": "プレイリストが正常にエクスポートされました。",
|
||||
"playlists-with-maps-exported-desc": "プレイリストとそのマップが正常にエクスポートされました。",
|
||||
"playlist-delete-error-title": "プレイリストの削除エラー",
|
||||
"playlist-delete-error-desc": "プレイリストの削除中にエラーが発生しました。",
|
||||
"playlists-deleted-title": "プレイリストが削除されました!",
|
||||
"playlists-deleted-desc": "プレイリストが正常に削除されました。",
|
||||
"edit-playlist": "プレイリストを編集",
|
||||
"playlist-edit-error-title": "プレイリストの編集エラー",
|
||||
"playlist-edit-error-desc": "プレイリストの編集中にエラーが発生しました。",
|
||||
"playlist-edited-title": "プレイリストが編集されました!",
|
||||
"playlist-edited-desc": "プレイリストが正常に変更されました。今すぐマップを同期できます!",
|
||||
"playlists-loading": "プレイリストを読み込み中...",
|
||||
"no-playlists": "プレイリストがありません",
|
||||
"download-playlists": "プレイリストをダウンロード",
|
||||
"created-by": "作成者",
|
||||
"stop-download": "ダウンロードを停止",
|
||||
"cancel-download": "ダウンロードをキャンセル",
|
||||
"open-file": "ファイルを開く",
|
||||
"link-playlists": "プレイリストをリンク",
|
||||
"link-playlist-desc": "プレイリストをリンクすると、すべてのバージョン間でプレイリストを共有できます。リンクすると、このバージョンは共有プレイリストの恩恵を受けます",
|
||||
"link-playlist-info": "プレイリストの追加と削除も共有されます",
|
||||
"keep-playlists": "プレイリストを保持",
|
||||
"keep-playlists-tip": "プレイリストを保持すると、現在のバージョンのプレイリストが共有プレイリストフォルダに移動されます。そうしない場合、それらは失われます",
|
||||
"unlink-playlists": "プレイリストのリンクを解除",
|
||||
"unlink-playlist-desc": "警告:プレイリストのリンクを解除すると、このバージョンでは共有プレイリストを使用できなくなります。",
|
||||
"unlink-keep-playlists-tip": "プレイリストを保持すると、現在のバージョン用に共有プレイリストのコピーが作成されます。そうしない場合、このバージョンのプレイリストは保持されません。",
|
||||
"delete-playlist-ask": "プレイリストを削除しますか?",
|
||||
"delete-playlists-ask": "プレイリストを削除しますか?",
|
||||
"delete-playlist-desc": "プレイリスト「{playlistTitle}」を削除してもよろしいですか?",
|
||||
"delete-playlists-desc": "{nb}個のプレイリストを削除してもよろしいですか?",
|
||||
"delete-maps": "マップを削除",
|
||||
"delete-playlist-maps-tip": "有効にすると、プレイリスト内のすべてのマップが削除されます",
|
||||
"delete-playlists-maps-tip": "有効にすると、プレイリスト内のすべてのマップが削除されます",
|
||||
"export-playlist-ask": "プレイリストをエクスポートしますか?",
|
||||
"export-playlists-ask": "プレイリストをエクスポートしますか?",
|
||||
"export-playlist-desc": "プレイリスト「{playlistTitle}」をエクスポートしてもよろしいですか?",
|
||||
"export-playlists-desc": "{nb}個のプレイリストをエクスポートしてもよろしいですか?",
|
||||
"export-maps": "マップをエクスポート",
|
||||
"export-playlist-maps-tip": "有効にすると、プレイリスト内のすべてのマップもエクスポートされます",
|
||||
"export-playlists-maps-tip": "有効にすると、プレイリスト内のすべてのマップもエクスポートされます",
|
||||
"export": "エクスポート",
|
||||
"need-clone-title": "警告",
|
||||
"need-clone-desc-1": "このプレイリストは外部サイトからダウンロードされ、同期リンクが含まれています。",
|
||||
"need-clone-desc-2": "同期中に変更を失わないようにするため、プレイリストが複製され、同期リンクが削除されます。",
|
||||
"need-clone-desc-3": "その後、必要に応じて元のプレイリストを削除できます。",
|
||||
"understood": "理解しました",
|
||||
"synchronize-playlist-ask": "プレイリストを同期しますか?",
|
||||
"synchronize-playlists-ask": "プレイリストを同期しますか?",
|
||||
"synchronize-playlist-desc": "プレイリスト「{playlistTitle}」を同期してもよろしいですか?",
|
||||
"synchronize-playlists-desc": "{nb}個のプレイリストを同期してもよろしいですか?",
|
||||
"synchronize-playlist-tip": "この操作はプレイリストを更新し、不足しているマップをダウンロードします。数分かかる場合があります。",
|
||||
"synchronize": "同期",
|
||||
"curated": "おすすめ",
|
||||
"verified-mapper": "認証済みマッパー",
|
||||
"empty-playlists": "空のプレイリスト",
|
||||
"search-playlist": "プレイリストを検索",
|
||||
"no-playlists-found": "プレイリストが見つかりません",
|
||||
"error-occur-while-loading-playlists": "プレイリストの読み込み中にエラーが発生しました",
|
||||
"error-occur-while-loading-playlist": "プレイリストの読み込み中にエラーが発生しました",
|
||||
"loading-maps": "マップを読み込み中...",
|
||||
"no-maps-found-for-playlist": "このプレイリストにマップが見つかりません",
|
||||
"playlist-contain-no-maps": "プレイリストにマップが含まれていません",
|
||||
"no-map-installed-for-playlist": "このプレイリストにインストールされたマップがありません",
|
||||
"playlist-is-waiting-to-download": "プレイリストはダウンロード待ちです",
|
||||
"download-maps": "マップをダウンロード",
|
||||
"download-missing-maps": "不足しているマップをダウンロード",
|
||||
"playlist-is-downloading": "プレイリストをダウンロード中です",
|
||||
"some-playlist-maps-are-missing": "このプレイリストの一部のマップが不足しています",
|
||||
"create-a-playlist": "プレイリストを作成",
|
||||
"synchronize-playlists": "プレイリストを同期",
|
||||
"export-playlists": "プレイリストをエクスポート",
|
||||
"delete-playlists": "プレイリストを削除",
|
||||
"choose-image": "画像を選択",
|
||||
"title": "タイトル",
|
||||
"playlist-title": "プレイリストのタイトル",
|
||||
"description": "説明",
|
||||
"playlist-description": "プレイリストの説明",
|
||||
"author": "作者",
|
||||
"playlist-author": "プレイリストの作者",
|
||||
"save": "保存",
|
||||
"loading": "読み込み中...",
|
||||
"installed": "インストール済み",
|
||||
"no-map-found": "マップが見つかりません",
|
||||
"edit-playlist-shortcuts": "ShiftキーまたはCtrlキーを押しながら複数のマップを選択",
|
||||
"add-to-playlist": "プレイリストに追加",
|
||||
"remove-from-playlist": "プレイリストから削除",
|
||||
"playlist-is-empty": "プレイリストが空です",
|
||||
"continue": "続ける",
|
||||
"nb-maps": "マップ数",
|
||||
"nb-mappers": "マッパー数",
|
||||
"duration": "継続時間",
|
||||
"nps": "1秒あたりの音符数",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "開始日 — 終了日",
|
||||
"all": "すべて",
|
||||
"last-24h": "過去24時間",
|
||||
"last-week": "先週",
|
||||
"last-month": "先月",
|
||||
"3-last-month": "過去3ヶ月"
|
||||
},
|
||||
"playlists-imported": "プレイリストがインポートされました",
|
||||
"all-playlists-have-been-successfully-imported": "すべてのプレイリストが正常にインポートされました",
|
||||
"no-playlist-found": "プレイリストが見つかりません",
|
||||
"no-playlist-found-in-selected-files": "選択されたファイルにプレイリストが見つかりません",
|
||||
"some-playlists-not-imported": "一部のプレイリストはインポートされませんでした",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "一部のプレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "一部のプレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "一部のプレイリストは読み取れません",
|
||||
"unknown": "一部のプレイリストをインポートできませんでした"
|
||||
},
|
||||
"no-playlists-imported": "プレイリストはインポートされませんでした",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "プレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "プレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "プレイリストは読み取れません",
|
||||
"unknown": "プレイリストをインポートできませんでした"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["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"],
|
||||
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
|
||||
"dayNames": ["日", "月", "火", "水", "木", "金", "土", "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"],
|
||||
"monthNames": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
|
||||
"timeNames": ["午前", "午後", "午前", "午後", "午前", "午後", "午前", "午後"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "Отказаться",
|
||||
"apply": "Применить",
|
||||
"copy": "Скопировать",
|
||||
"copied": "Скопировано!"
|
||||
"copied": "Скопировано!",
|
||||
"confirm": "Подтвердить"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Добавить версию игры",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "Фильтр",
|
||||
"dropdown": {
|
||||
"export-maps": "Экспорт карт",
|
||||
"delete-maps": "Удалить карты"
|
||||
"delete-maps": "Удалить карты",
|
||||
"delete-duplicate-maps": "Удалить дубликаты"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Добавить"
|
||||
"drop-down": {
|
||||
"browse-maps": "Просмотр карт",
|
||||
"import-maps": "Импорт карт"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Нет карт",
|
||||
"button": "Скачать карты"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте свои карты",
|
||||
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Просмотр плейлистов",
|
||||
"create-a-playlist": "Создать плейлист",
|
||||
"import-playlists": "Импорт плейлистов"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте ваши плейлисты",
|
||||
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "Не найдены моды для этой версии Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "Подробнее",
|
||||
"install-or-update": "Установить или обновить"
|
||||
"install-or-update": "Установить или обновить",
|
||||
"reinstall-all": "Переустановить все"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "Удалить всё"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "Моды уже установлены",
|
||||
"description": "Все выбранные моды уже установлены"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Выход позволит вам сменить учетную запись при следующей загрузке Beat Saber.",
|
||||
"logout": "Выйти",
|
||||
"logout-success": "Выход выполнен успешно",
|
||||
"download-platform": {
|
||||
"title": "Основная платформа",
|
||||
"desc": "Выберите основную платформу, которая будет использоваться для скачивания версий Beat Saber.",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Папка установок",
|
||||
"description": "Измените стандартную папку, где будут версии Beat Saber и прочее.",
|
||||
"description": "Изменить папку, которая будет содержать весь контент, загруженный BSManager.",
|
||||
"choose-folder": "Изменить папку"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "Сообщить о баге",
|
||||
"open-logs": "Открыть логи"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Изучаем Changelog!",
|
||||
"not-founds": "😕 Ой, Changelog нигде не видно!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Дополнительные",
|
||||
"description": "Дополнительные настройки для BSManager.",
|
||||
"hardware-acceleration": {
|
||||
"title": "Аппаратное ускорение",
|
||||
"description": "Включите аппаратное ускорение, чтобы использовать ваш GPU и улучшить производительность BSManager. Отключите эту опцию, если у вас возникают пропуски кадров.",
|
||||
"modal": {
|
||||
"title": "Требуется перезагрузка",
|
||||
"body": "Изменение настройки аппаратного ускорения приведет к завершению и перезапуску BSManager. Вы уверены, что хотите это сделать?",
|
||||
"confirm-btn": "Да, я уверен"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Произошла ошибка, невозможно отключить аппаратное ускорение."
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "Использовать символические ссылки",
|
||||
"description": "Используйте символические ссылки вместо соединений для связывания папок. Включайте эту опцию только если это действительно необходимо.",
|
||||
"modal": {
|
||||
"title": "Разрешения для символических ссылок",
|
||||
"body": "При создании символических ссылок BSManager потребуется права администратора или включенный режим разработчика на вашем устройстве. Вы уверены, что хотите продолжить?",
|
||||
"confirm-btn": "Да, я уверен"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "Произошла ошибка, невозможно изменить настройки символических ссылок."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Операция в процессе",
|
||||
"no-internet": "Нет интернета"
|
||||
"no-internet": "Нет интернета",
|
||||
"file-not-supported": "Файл не поддерживается"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Дождитесь завершения операции, потом попробуйте снова.",
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова."
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова.",
|
||||
"file-not-supported": "Поддерживаются только файлы {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": "Требуется .NET 6"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Похоже Steam не хочет, чтобы мы скачали Beat Saber 😢",
|
||||
"404": "Нет связи с серверами Steam.",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "Не удалось проверить наличие лицензии.",
|
||||
"RateLimitExceeded": "Слишком много попыток, вернитесь позже и попробуйте снова.",
|
||||
"TokenRejected": "Ваш токен для входа был отклонен 😕 Пожалуйста, попробуйте снова.",
|
||||
"AccessDenied": "Доступ к Steam был отклонен.",
|
||||
"dotnet-required": "Среда запуска .NET 6 должна быть установлена, чтобы скачать Beat Saber. Скачайте её с помощью кнопки ниже."
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "Скачать .NET 6"
|
||||
"AccessDenied": "Доступ к Steam был отклонен."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "Не удалось изменить",
|
||||
"CantRename": "Переименование невозможно",
|
||||
"VersionAlreadExist": "Эта версия уже добавлена",
|
||||
"CantClone": "Клонирование невозможно"
|
||||
"CantClone": "Клонирование невозможно",
|
||||
"UnknownError": "Произошла неизвестная ошибка"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Вы не можете изменить версию из Steam, но вы можете её клонировать."
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Карта установлена",
|
||||
"error": "Ошибка установки карты"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Нет дубликатов",
|
||||
"msg": "Ни одна карта не была удалена"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Дубликаты удалены",
|
||||
"msg": "Дубликаты были удалены"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Импорт карт завершен",
|
||||
"error": "Произошла ошибка при импорте карт"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Карты были успешно импортированы.",
|
||||
"some-success": "Некоторые карты успешно импортированы.",
|
||||
"only-accept-zip": "Поддерживаются только zip-файлы.",
|
||||
"invalid-zip": "В zip-файле(ах) нет карт.",
|
||||
"unknown": "Произошла неизвестная ошибка."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +556,15 @@
|
||||
"title": "Бэкап создан",
|
||||
"msg": "Общая папка 'UserData' может создавать ошибки. В случае проблем отвяжите папку, чтобы восстановить бэкап."
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "Ошибка при связывании папки",
|
||||
"msg": {
|
||||
"EPERM": "BSManager не имеет необходимых прав для связывания папки.",
|
||||
"EACCES": "BSManager не имеет необходимых прав для связывания папки.",
|
||||
"ENOSPC": "Диск заполнен, освободите место и попробуйте снова.",
|
||||
"UNKNOWN_ERROR": "Произошла неизвестная ошибка при связывании папки."
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -646,6 +730,11 @@
|
||||
"title": "Общие карты будут скопированы в папку карт этой версии. Карты не будут потеряны, если это не выбрано."
|
||||
},
|
||||
"valid-btn": "Отвязать карты"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Удалить карту?",
|
||||
"desc": "Только карта \"{map}\" является дубликатом. Вы уверены, что хотите ее удалить?",
|
||||
"desc-plural": "Найдено {nb} дубликатов карт. Вы уверены, что хотите их удалить?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -725,12 +814,20 @@
|
||||
"info-3": "Обратите внимание, что не рекомендуется предоставлять права администратора Steam, так как это также влияет на установленные игры и моды, представляя угрозу безопасности."
|
||||
},
|
||||
"launch-as-admin": "Запустить от имени администратора"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "Папка установок",
|
||||
"choose-folder-description": "Выберите папку, которая будет содержать весь контент, загруженный BSManager. (версии, моды, карты, плейлисты и т.д.)",
|
||||
"choose-folder": "Изменить папку",
|
||||
"default": "По умолчанию",
|
||||
"default-tooltip": "По умолчанию в вашей домашней папке"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Длительность",
|
||||
"nps" : "Нот в Секунду",
|
||||
"njs": "Скорость прыжка нот",
|
||||
"tags": "тэги",
|
||||
"specificities": "основное",
|
||||
"requirements": "требуемые моды",
|
||||
@@ -740,7 +837,7 @@
|
||||
"accuracy": "точность",
|
||||
"balanced": "баланс",
|
||||
"challenge": "испытание",
|
||||
"dancestyle": "танец",
|
||||
"dance-style": "танец",
|
||||
"fitness": "фитнес",
|
||||
"speed": "скорость",
|
||||
"tech": "техника"
|
||||
@@ -749,36 +846,36 @@
|
||||
"dance": "танец",
|
||||
"swing": "свинг",
|
||||
"nightcore": "найткор",
|
||||
"folk": "народные",
|
||||
"family": "семейные",
|
||||
"folk-acoustic": "фолк и акустика",
|
||||
"kids-family": "дети и семья",
|
||||
"ambient": "эмбиент",
|
||||
"funk": "фанк",
|
||||
"funk-disco": "фанк и диско",
|
||||
"jazz": "джаз",
|
||||
"soul": "соул",
|
||||
"speedcore": "спидкор",
|
||||
"punk": "панк",
|
||||
"rb": "ар-н-би",
|
||||
"holiday": "праздник",
|
||||
"vocaloid": "вокалоиды",
|
||||
"jrock": "джей-рок",
|
||||
"rb": "r&b",
|
||||
"holiday": "праздники",
|
||||
"vocaloid": "вокалоид",
|
||||
"j-rock": "джей-рок",
|
||||
"trance": "транс",
|
||||
"drumbass": "драм-н-бейс",
|
||||
"comedy": "комедия",
|
||||
"instrumental": "инструментальные",
|
||||
"drum-and-bass": "драм-н-бейс",
|
||||
"comedy-meme": "комедия и мемы",
|
||||
"instrumental": "инструментал",
|
||||
"hardcore": "хардкор",
|
||||
"kpop": "кей-поп",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "инди",
|
||||
"techno": "техно",
|
||||
"house": "хаус",
|
||||
"game": "видеоигры",
|
||||
"film": "фильмы",
|
||||
"alt": "альтернативные",
|
||||
"dubstep": "дапстеп",
|
||||
"video-game-soundtrack": "видеоигра",
|
||||
"tv-movie-soundtrack": "ТВ и кино",
|
||||
"alternative": "альтернатива",
|
||||
"dubstep": "дабстеп",
|
||||
"metal": "метал",
|
||||
"anime": "аниме",
|
||||
"hiphop": "хипхоп",
|
||||
"jpop": "джей-поп",
|
||||
"rock": "прк",
|
||||
"hip-hop-rap": "хип-хоп и рэп",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "рок",
|
||||
"pop": "поп",
|
||||
"electronic": "электроника",
|
||||
"classical-orchestral": "Классика и Оркестр"
|
||||
@@ -808,7 +905,8 @@
|
||||
"bsr-code" : "Код BSR",
|
||||
"download" : "Скачать карту",
|
||||
"downloading" :"Загрузка карты",
|
||||
"cancel-download" : "Отменить загрузку"
|
||||
"cancel-download" : "Отменить загрузку",
|
||||
"hightlight-difficulty": "Выделить сложность"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1000,6 +1098,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Или просмотреть файлы"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Ошибка при создании плейлиста",
|
||||
"error-playlist-creation-desc": "Произошла ошибка при создании плейлиста.",
|
||||
"playlist-created-title": "Плейлист создан",
|
||||
"playlist-created-desc": "Плейлист успешно создан. Теперь вы можете синхронизировать его карты!",
|
||||
"download-playlist": "Скачать плейлист",
|
||||
"synchronize-playlist": "Синхронизировать плейлист",
|
||||
"synchronize-maps": "Синхронизировать карты",
|
||||
"error-playlists-synchronization-title": "Ошибка синхронизации плейлистов",
|
||||
"error-playlists-synchronization-desc": "Произошла ошибка при синхронизации плейлистов.",
|
||||
"playlists-synchronized-title": "Плейлисты синхронизированы!",
|
||||
"playlists-synchronized-desc": "Плейлисты и их карты были загружены.",
|
||||
"playlists-export-error-title": "Ошибка экспорта плейлистов",
|
||||
"playlists-export-error-desc": "Произошла ошибка при экспорте плейлистов.",
|
||||
"playlists-exported-title": "Плейлисты экспортированы!",
|
||||
"playlists-exported-desc": "Плейлисты успешно экспортированы.",
|
||||
"playlists-with-maps-exported-desc": "Плейлисты и их карты успешно экспортированы.",
|
||||
"playlist-delete-error-title": "Ошибка удаления плейлиста",
|
||||
"playlist-delete-error-desc": "Произошла ошибка при удалении плейлиста.",
|
||||
"playlists-deleted-title": "Плейлисты удалены!",
|
||||
"playlists-deleted-desc": "Плейлисты успешно удалены.",
|
||||
"edit-playlist": "Редактировать плейлист",
|
||||
"playlist-edit-error-title": "Ошибка редактирования плейлиста",
|
||||
"playlist-edit-error-desc": "Произошла ошибка при редактировании плейлиста.",
|
||||
"playlist-edited-title": "Плейлист отредактирован!",
|
||||
"playlist-edited-desc": "Плейлист успешно изменен. Теперь вы можете синхронизировать его карты!",
|
||||
"playlists-loading": "Загрузка плейлистов...",
|
||||
"no-playlists": "Нет плейлистов",
|
||||
"download-playlists": "Скачать плейлисты",
|
||||
"created-by": "Создано",
|
||||
"stop-download": "Остановить загрузку",
|
||||
"cancel-download": "Отменить загрузку",
|
||||
"open-file": "Открыть файл",
|
||||
"link-playlists": "Связать плейлисты",
|
||||
"link-playlist-desc": "Связывание плейлистов позволяет делиться плейлистами между всеми версиями. После связывания эта версия будет иметь доступ к общим плейлистам",
|
||||
"link-playlist-info": "Добавление и удаление плейлистов также будет общим",
|
||||
"keep-playlists": "Сохранить плейлисты",
|
||||
"keep-playlists-tip": "Сохранение плейлистов переместит плейлисты из текущей версии в папку общих плейлистов. В противном случае они будут потеряны",
|
||||
"unlink-playlists": "Отвязать плейлисты",
|
||||
"unlink-playlist-desc": "Внимание, отвязка плейлистов больше не позволит использовать общие плейлисты для этой версии.",
|
||||
"unlink-keep-playlists-tip": "Сохранение плейлистов создаст копию общих плейлистов для текущей версии. В противном случае для этой версии не будут сохранены плейлисты.",
|
||||
"delete-playlist-ask": "Удалить плейлист?",
|
||||
"delete-playlists-ask": "Удалить плейлисты?",
|
||||
"delete-playlist-desc": "Вы уверены, что хотите удалить плейлист \"{playlistTitle}\"?",
|
||||
"delete-playlists-desc": "Вы уверены, что хотите удалить {nb} плейлистов?",
|
||||
"delete-maps": "Удалить карты",
|
||||
"delete-playlist-maps-tip": "Если включено, все карты в плейлисте будут удалены",
|
||||
"delete-playlists-maps-tip": "Если включено, все карты в плейлистах будут удалены",
|
||||
"export-playlist-ask": "Экспортировать плейлист?",
|
||||
"export-playlists-ask": "Экспортировать плейлисты?",
|
||||
"export-playlist-desc": "Вы уверены, что хотите экспортировать плейлист \"{playlistTitle}\"?",
|
||||
"export-playlists-desc": "Вы уверены, что хотите экспортировать {nb} плейлистов?",
|
||||
"export-maps": "Экспортировать карты",
|
||||
"export-playlist-maps-tip": "Если включено, все карты в плейлисте также будут экспортированы",
|
||||
"export-playlists-maps-tip": "Если включено, все карты в плейлистах также будут экспортированы",
|
||||
"export": "Экспорт",
|
||||
"need-clone-title": "Предупреждение",
|
||||
"need-clone-desc-1": "Этот плейлист был загружен с внешнего сайта и содержит ссылку для синхронизации.",
|
||||
"need-clone-desc-2": "Чтобы избежать потери ваших изменений во время синхронизации, плейлист будет дублирован, а его ссылка для синхронизации удалена.",
|
||||
"need-clone-desc-3": "Затем вы можете, если хотите, удалить оригинальный плейлист.",
|
||||
"understood": "Я понимаю",
|
||||
"synchronize-playlist-ask": "Синхронизировать плейлист?",
|
||||
"synchronize-playlists-ask": "Синхронизировать плейлисты?",
|
||||
"synchronize-playlist-desc": "Вы уверены, что хотите синхронизировать плейлист \"{playlistTitle}\"?",
|
||||
"synchronize-playlists-desc": "Вы уверены, что хотите синхронизировать {nb} плейлистов?",
|
||||
"synchronize-playlist-tip": "Это действие обновляет плейлисты и загружает отсутствующие карты; это может занять несколько минут.",
|
||||
"synchronize": "Синхронизировать",
|
||||
"curated": "Рекомендованные",
|
||||
"verified-mapper": "Проверенный маппер",
|
||||
"empty-playlists": "Пустые плейлисты",
|
||||
"search-playlist": "Поиск плейлиста",
|
||||
"no-playlists-found": "Плейлисты не найдены",
|
||||
"error-occur-while-loading-playlists": "Произошла ошибка при загрузке плейлистов",
|
||||
"error-occur-while-loading-playlist": "Произошла ошибка при загрузке плейлиста",
|
||||
"loading-maps": "Загрузка карт...",
|
||||
"no-maps-found-for-playlist": "Для этого плейлиста не найдено карт",
|
||||
"playlist-contain-no-maps": "Плейлист не содержит карт",
|
||||
"no-map-installed-for-playlist": "Для этого плейлиста не установлено карт",
|
||||
"playlist-is-waiting-to-download": "Плейлист ожидает загрузки",
|
||||
"download-maps": "Скачать карты",
|
||||
"download-missing-maps": "Скачать отсутствующие карты",
|
||||
"playlist-is-downloading": "Плейлист загружается",
|
||||
"some-playlist-maps-are-missing": "Некоторые карты в этом плейлисте отсутствуют",
|
||||
"create-a-playlist": "Создать плейлист",
|
||||
"synchronize-playlists": "Синхронизировать плейлисты",
|
||||
"export-playlists": "Экспортировать плейлисты",
|
||||
"delete-playlists": "Удалить плейлисты",
|
||||
"choose-image": "Выбрать изображение",
|
||||
"title": "Название",
|
||||
"playlist-title": "Название плейлиста",
|
||||
"description": "Описание",
|
||||
"playlist-description": "Описание плейлиста",
|
||||
"author": "Автор",
|
||||
"playlist-author": "Автор плейлиста",
|
||||
"save": "Сохранить",
|
||||
"loading": "Загрузка...",
|
||||
"installed": "Установлено",
|
||||
"no-map-found": "Карта не найдена",
|
||||
"edit-playlist-shortcuts": "Удерживайте Shift или Ctrl для выбора нескольких карт",
|
||||
"add-to-playlist": "Добавить в плейлист",
|
||||
"remove-from-playlist": "Удалить из плейлиста",
|
||||
"playlist-is-empty": "Плейлист пуст",
|
||||
"continue": "Продолжить",
|
||||
"nb-maps": "Количество карт",
|
||||
"nb-mappers": "Количество мапперов",
|
||||
"duration": "Продолжительность",
|
||||
"nps": "Нот в секунду",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "Дата начала — Дата окончания",
|
||||
"all": "Все",
|
||||
"last-24h": "Последние 24 часа",
|
||||
"last-week": "Последняя неделя",
|
||||
"last-month": "Последний месяц",
|
||||
"3-last-month": "Последние 3 месяца"
|
||||
},
|
||||
"playlists-imported": "Плейлисты импортированы",
|
||||
"all-playlists-have-been-successfully-imported": "Все плейлисты успешно импортированы",
|
||||
"no-playlist-found": "Плейлист не найден",
|
||||
"no-playlist-found-in-selected-files": "Плейлист не найден в выбранных файлах",
|
||||
"some-playlists-not-imported": "Некоторые плейлисты не импортированы",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Некоторые плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Некоторые плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Некоторые плейлисты нечитаемы",
|
||||
"unknown": "Некоторые плейлисты не удалось импортировать"
|
||||
},
|
||||
"no-playlists-imported": "Плейлисты не импортированы",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Плейлисты нечитаемы",
|
||||
"unknown": "Не удалось импортировать плейлисты"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["Вск", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Суб", "Воскресение", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
|
||||
"monthNames": ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авн", "Сен", "Окт", "Ноя", "Дек", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "拒絕",
|
||||
"apply": "應用",
|
||||
"copy": "複製",
|
||||
"copied": "已複製!"
|
||||
"copied": "已複製!",
|
||||
"confirm": "確認"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "新增版本",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "篩選",
|
||||
"dropdown": {
|
||||
"export-maps": "導出譜面",
|
||||
"delete-maps": "刪除譜面"
|
||||
"delete-maps": "刪除譜面",
|
||||
"delete-duplicate-maps": "刪除重複項"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "新增"
|
||||
"drop-down": {
|
||||
"browse-maps": "瀏覽地圖",
|
||||
"import-maps": "匯入地圖"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "無譜面",
|
||||
"button": "下載譜面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "導入您的地圖",
|
||||
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "瀏覽播放列表",
|
||||
"create-a-playlist": "建立播放列表",
|
||||
"import-playlists": "匯入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "匯入您的播放列表",
|
||||
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "該版本 BeatSaber 暫無可用 Mod",
|
||||
"buttons": {
|
||||
"more-infos": "更多資訊",
|
||||
"install-or-update": "安裝或更新"
|
||||
"install-or-update": "安裝或更新",
|
||||
"reinstall-all": "重新安裝全部"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "全部移除"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "模組已安裝",
|
||||
"description": "所有選中的模組已經安裝"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "登出後,您可以在下一次下載 Beat Saber 時切換帳戶。",
|
||||
"logout": "登出",
|
||||
"logout-success": "登出成功",
|
||||
"download-platform": {
|
||||
"title": ",預設平台",
|
||||
"desc": "選擇要下載 BeatSaber 的預設平台。",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "安裝文件夾",
|
||||
"description": "為 BeatSaber 不同版本及未來其他特性修改預設文件夾",
|
||||
"description": "更改將包含 BSManager 下載的所有內容的文件夾。",
|
||||
"choose-folder": "選擇文件夾"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "報告 bug",
|
||||
"open-logs": "打開日誌"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 探索更新日誌!",
|
||||
"not-founds": "😕 噢,找不到更新日誌!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"description": "BSManager的高级设置。",
|
||||
"hardware-acceleration": {
|
||||
"title": "硬體加速",
|
||||
"description": "啟用硬體加速以使用您的GPU並提高BSManager的性能。如果您遇到幀丟失,請關閉此功能。",
|
||||
"modal": {
|
||||
"title": "需要重啟",
|
||||
"body": "更改硬體加速設置將退出並重新啟動BSManager。您確定要這樣做嗎?",
|
||||
"confirm-btn": "是的,我確定"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "發生錯誤,無法禁用硬體加速。"
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "使用符號鏈接",
|
||||
"description": "使用符號鏈接而不是連接來鏈接文件夾。僅在確實需要時才啟用此功能。",
|
||||
"modal": {
|
||||
"title": "符號鏈接權限",
|
||||
"body": "創建符號鏈接時,BSManager將需要管理員權限或啟用開發者模式。您確定要繼續嗎?",
|
||||
"confirm-btn": "是的,我確定"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "發生錯誤,無法更改符號鏈接設置。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作進行中",
|
||||
"no-internet": "無網路"
|
||||
"no-internet": "無網路",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待當前操作完成,然後重試。",
|
||||
"no-internet": "檢查你的連接並重試。"
|
||||
"no-internet": "檢查你的連接並重試。",
|
||||
"file-not-supported": "僅支援{types}檔案。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": "需要 .NET 6"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Steam 似乎不想讓我們下載 Beat Saber😢",
|
||||
"404": "無法聯繫 Steam 伺服器。",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "無法獲取許可證列表。",
|
||||
"RateLimitExceeded": "你已經嘗試了太多次,請稍等片刻,稍後再試。",
|
||||
"TokenRejected": "你的登錄令牌已被拒絕 😕 請重試。",
|
||||
"AccessDenied": "訪問 Steam 被拒絕。",
|
||||
"dotnet-required": "必須安裝 .NET 6 Runtime 才能下載 Beat Saber 版本。單擊下面的按鈕下載它。"
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "下載 .NET 6"
|
||||
"AccessDenied": "訪問 Steam 被拒絕。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "無法編輯",
|
||||
"CantRename": "無法重命名",
|
||||
"VersionAlreadExist": "該版本已存在",
|
||||
"CantClone": "無法複製"
|
||||
"CantClone": "無法複製",
|
||||
"UnknownError": "發生了未知錯誤"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "你不能編輯 Steam 版本。不過你可以複製它。"
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "譜面安裝完成",
|
||||
"error": "安裝譜面時發生錯誤"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "沒有重複",
|
||||
"msg": "沒有刪除地圖"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複已刪除",
|
||||
"msg": "重複已刪除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地圖匯入完成",
|
||||
"error": "地圖匯入時發生錯誤"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地圖已成功匯入。",
|
||||
"some-success": "部分地圖已成功匯入。",
|
||||
"only-accept-zip": "僅支援zip檔案。",
|
||||
"invalid-zip": "zip檔案中沒有地圖。",
|
||||
"unknown": "發生了未知錯誤。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +556,15 @@
|
||||
"title": "備份已創建",
|
||||
"msg": "共享 “UserData” 文件夾可能會產生錯誤,如果出現問題,請取消關聯該文件夾以恢復備份"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "連結文件夾時出錯",
|
||||
"msg": {
|
||||
"EPERM": "BSManager沒有必要的權限來連結文件夾。",
|
||||
"EACCES": "BSManager沒有必要的權限來連結文件夾。",
|
||||
"ENOSPC": "磁碟已滿,請騰出空間後再試。",
|
||||
"UNKNOWN_ERROR": "連結文件夾時發生未知錯誤。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -646,6 +730,11 @@
|
||||
"title": "保留譜面將會在取消關聯後把共享文件夾的所有譜面複製到當前版本。如果此項被禁用,譜面也不會遺失。"
|
||||
},
|
||||
"valid-btn": "取消關聯譜面"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "刪除地圖",
|
||||
"desc": "只有地圖「{map}」是重複的。你確定要刪除它嗎?",
|
||||
"desc-plural": "發現了 {nb} 個重複的地圖。你確定要刪除它們嗎?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +815,20 @@
|
||||
},
|
||||
"launch-as-admin": "以管理員身份啟動",
|
||||
"not-remind-me": "不再提醒我"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "安裝文件夾",
|
||||
"choose-folder-description": "選擇將包含 BSManager 下載的所有內容的文件夾。(版本、mod、地圖、播放列表等)",
|
||||
"choose-folder": "選擇文件夾",
|
||||
"default": "預設",
|
||||
"default-tooltip": "預設為您的主資料夾"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "時長",
|
||||
"nps" : "每秒音符數",
|
||||
"njs": "音符跳躍速度",
|
||||
"tags": "標籤",
|
||||
"specificities": "general",
|
||||
"requirements": "要求",
|
||||
@@ -741,48 +838,48 @@
|
||||
"accuracy": "精確度",
|
||||
"balanced": "平衡",
|
||||
"challenge": "挑戰",
|
||||
"dancestyle": "舞蹈",
|
||||
"dance-style": "舞蹈",
|
||||
"fitness": "健身",
|
||||
"speed": "速度",
|
||||
"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"
|
||||
"dance": "舞蹈",
|
||||
"swing": "搖擺",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民謠 & 原聲",
|
||||
"kids-family": "兒童 & 家庭",
|
||||
"ambient": "氛圍音樂",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "靈魂",
|
||||
"speedcore": "極速核",
|
||||
"punk": "龐克",
|
||||
"rb": "r&b",
|
||||
"holiday": "假日",
|
||||
"vocaloid": "聲庫",
|
||||
"j-rock": "日式搖滾",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 貝斯",
|
||||
"comedy-meme": "喜劇 & 表情包",
|
||||
"instrumental": "純音樂",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "獨立",
|
||||
"techno": "電子舞曲",
|
||||
"house": "浩室音樂",
|
||||
"video-game-soundtrack": "遊戲音樂",
|
||||
"tv-movie-soundtrack": "電視 & 電影",
|
||||
"alternative": "另類",
|
||||
"dubstep": "電音鼓",
|
||||
"metal": "金屬",
|
||||
"anime": "動漫",
|
||||
"hip-hop-rap": "嘻哈 & 說唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "搖滾",
|
||||
"pop": "流行",
|
||||
"electronic": "電子",
|
||||
"classical-orchestral": "古典 & 管弦樂"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -809,7 +906,8 @@
|
||||
"bsr-code" : "BSR代碼",
|
||||
"download" : "下載地圖",
|
||||
"downloading" :"正在下載地圖",
|
||||
"cancel-download" : "取消下載"
|
||||
"cancel-download" : "取消下載",
|
||||
"hightlight-difficulty": "突出顯示難度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,6 +1099,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或瀏覽檔案"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "建立播放清單時發生錯誤",
|
||||
"error-playlist-creation-desc": "建立播放清單時發生錯誤。",
|
||||
"playlist-created-title": "播放清單已建立",
|
||||
"playlist-created-desc": "播放清單已成功建立。您現在可以同步其地圖了!",
|
||||
"download-playlist": "下載播放清單",
|
||||
"synchronize-playlist": "同步播放清單",
|
||||
"synchronize-maps": "同步地圖",
|
||||
"error-playlists-synchronization-title": "同步播放清單時發生錯誤",
|
||||
"error-playlists-synchronization-desc": "同步播放清單時發生錯誤。",
|
||||
"playlists-synchronized-title": "播放清單已同步!",
|
||||
"playlists-synchronized-desc": "播放清單及其地圖已下載。",
|
||||
"playlists-export-error-title": "匯出播放清單時發生錯誤",
|
||||
"playlists-export-error-desc": "匯出播放清單時發生錯誤。",
|
||||
"playlists-exported-title": "播放清單已匯出!",
|
||||
"playlists-exported-desc": "播放清單已成功匯出。",
|
||||
"playlists-with-maps-exported-desc": "播放清單及其地圖已成功匯出。",
|
||||
"playlist-delete-error-title": "刪除播放清單時發生錯誤",
|
||||
"playlist-delete-error-desc": "刪除播放清單時發生錯誤。",
|
||||
"playlists-deleted-title": "播放清單已刪除!",
|
||||
"playlists-deleted-desc": "播放清單已成功刪除。",
|
||||
"edit-playlist": "編輯播放清單",
|
||||
"playlist-edit-error-title": "編輯播放清單時發生錯誤",
|
||||
"playlist-edit-error-desc": "編輯播放清單時發生錯誤。",
|
||||
"playlist-edited-title": "播放清單已編輯!",
|
||||
"playlist-edited-desc": "播放清單已成功修改。您現在可以同步其地圖了!",
|
||||
"playlists-loading": "正在載入播放清單...",
|
||||
"no-playlists": "沒有播放清單",
|
||||
"download-playlists": "下載播放清單",
|
||||
"created-by": "建立者",
|
||||
"stop-download": "停止下載",
|
||||
"cancel-download": "取消下載",
|
||||
"open-file": "開啟檔案",
|
||||
"link-playlists": "連結播放清單",
|
||||
"link-playlist-desc": "連結播放清單允許在所有版本之間共享播放清單。一旦連結,此版本將受益於共享播放清單",
|
||||
"link-playlist-info": "新增和刪除播放清單也將被共享",
|
||||
"keep-playlists": "保留播放清單",
|
||||
"keep-playlists-tip": "保留播放清單將把當前版本的播放清單移動到共享播放清單資料夾。否則,它們將遺失",
|
||||
"unlink-playlists": "取消連結播放清單",
|
||||
"unlink-playlist-desc": "警告,取消連結播放清單將不再允許此版本使用共享播放清單。",
|
||||
"unlink-keep-playlists-tip": "保留播放清單將為當前版本建立共享播放清單的副本。否則,此版本將不保留任何播放清單。",
|
||||
"delete-playlist-ask": "刪除播放清單?",
|
||||
"delete-playlists-ask": "刪除播放清單?",
|
||||
"delete-playlist-desc": "您確定要刪除播放清單 \"{playlistTitle}\" 嗎?",
|
||||
"delete-playlists-desc": "您確定要刪除 {nb} 個播放清單嗎?",
|
||||
"delete-maps": "刪除地圖",
|
||||
"delete-playlist-maps-tip": "如果啟用,播放清單中的所有地圖都將被刪除",
|
||||
"delete-playlists-maps-tip": "如果啟用,播放清單中的所有地圖都將被刪除",
|
||||
"export-playlist-ask": "匯出播放清單?",
|
||||
"export-playlists-ask": "匯出播放清單?",
|
||||
"export-playlist-desc": "您確定要匯出播放清單 \"{playlistTitle}\" 嗎?",
|
||||
"export-playlists-desc": "您確定要匯出 {nb} 個播放清單嗎?",
|
||||
"export-maps": "匯出地圖",
|
||||
"export-playlist-maps-tip": "如果啟用,播放清單中的所有地圖也將被匯出",
|
||||
"export-playlists-maps-tip": "如果啟用,播放清單中的所有地圖也將被匯出",
|
||||
"export": "匯出",
|
||||
"need-clone-title": "警告",
|
||||
"need-clone-desc-1": "此播放清單已從外部網站下載,並包含同步連結。",
|
||||
"need-clone-desc-2": "為避免在同步過程中遺失更改,播放清單將被複製,並刪除其同步連結。",
|
||||
"need-clone-desc-3": "然後,如果您願意,可以刪除原始播放清單。",
|
||||
"understood": "我明白了",
|
||||
"synchronize-playlist-ask": "同步播放清單?",
|
||||
"synchronize-playlists-ask": "同步播放清單?",
|
||||
"synchronize-playlist-desc": "您確定要同步播放清單 \"{playlistTitle}\" 嗎?",
|
||||
"synchronize-playlists-desc": "您確定要同步 {nb} 個播放清單嗎?",
|
||||
"synchronize-playlist-tip": "此操作更新播放清單並下載缺失的地圖;可能需要幾分鐘。",
|
||||
"synchronize": "同步",
|
||||
"curated": "精選",
|
||||
"verified-mapper": "已驗證的製圖者",
|
||||
"empty-playlists": "空播放清單",
|
||||
"search-playlist": "搜尋播放清單",
|
||||
"no-playlists-found": "未找到播放清單",
|
||||
"error-occur-while-loading-playlists": "載入播放清單時發生錯誤",
|
||||
"error-occur-while-loading-playlist": "載入播放清單時發生錯誤",
|
||||
"loading-maps": "正在載入地圖...",
|
||||
"no-maps-found-for-playlist": "未找到此播放清單的地圖",
|
||||
"playlist-contain-no-maps": "播放清單不包含地圖",
|
||||
"no-map-installed-for-playlist": "此播放清單沒有已安裝的地圖",
|
||||
"playlist-is-waiting-to-download": "播放清單正在等待下載",
|
||||
"download-maps": "下載地圖",
|
||||
"download-missing-maps": "下載缺失的地圖",
|
||||
"playlist-is-downloading": "播放清單正在下載",
|
||||
"some-playlist-maps-are-missing": "此播放清單中的一些地圖缺失",
|
||||
"create-a-playlist": "建立播放清單",
|
||||
"synchronize-playlists": "同步播放清單",
|
||||
"export-playlists": "匯出播放清單",
|
||||
"delete-playlists": "刪除播放清單",
|
||||
"choose-image": "選擇圖片",
|
||||
"title": "標題",
|
||||
"playlist-title": "播放清單標題",
|
||||
"description": "描述",
|
||||
"playlist-description": "播放清單描述",
|
||||
"author": "作者",
|
||||
"playlist-author": "播放清單作者",
|
||||
"save": "儲存",
|
||||
"loading": "載入中...",
|
||||
"installed": "已安裝",
|
||||
"no-map-found": "未找到地圖",
|
||||
"edit-playlist-shortcuts": "按住 Shift 或 Ctrl 選擇多個地圖",
|
||||
"add-to-playlist": "新增到播放清單",
|
||||
"remove-from-playlist": "從播放清單中移除",
|
||||
"playlist-is-empty": "播放清單為空",
|
||||
"continue": "繼續",
|
||||
"nb-maps": "地圖數量",
|
||||
"nb-mappers": "製圖者數量",
|
||||
"duration": "持續時間",
|
||||
"nps": "每秒音符數",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "開始日期 — 結束日期",
|
||||
"all": "全部",
|
||||
"last-24h": "最近24小時",
|
||||
"last-week": "上週",
|
||||
"last-month": "上個月",
|
||||
"3-last-month": "最近3個月"
|
||||
},
|
||||
"playlists-imported": "播放清單已匯入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放清單已成功匯入",
|
||||
"no-playlist-found": "未找到播放清單",
|
||||
"no-playlist-found-in-selected-files": "在選定檔案中未找到播放清單",
|
||||
"some-playlists-not-imported": "部分播放清單未匯入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放清單無法解析",
|
||||
"unknown": "某些播放清單無法匯入"
|
||||
},
|
||||
"no-playlists-imported": "未匯入任何播放清單",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放清單無法解析",
|
||||
"unknown": "無法匯入任何播放清單"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["週日", "週一", "週二", "週三", "週四", "週五", "週六", "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
|
||||
"monthNames": ["1 月", "2 月", "3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月", "10 月", "11 月", "12 月", "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"refuse": "拒绝",
|
||||
"apply": "应用",
|
||||
"copy": "复制",
|
||||
"copied": "已复制!"
|
||||
"copied": "已复制!",
|
||||
"confirm": "确认"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "添加版本",
|
||||
@@ -44,14 +45,16 @@
|
||||
"filters-btn": "筛选",
|
||||
"dropdown": {
|
||||
"export-maps": "导出谱面",
|
||||
"delete-maps": "删除谱面"
|
||||
"delete-maps": "删除谱面",
|
||||
"delete-duplicate-maps": "删除重复项"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "添加"
|
||||
"drop-down": {
|
||||
"browse-maps": "浏览地图",
|
||||
"import-maps": "导入地图"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -63,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "无谱面",
|
||||
"button": "下载谱面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的地图",
|
||||
"subtext": "将ZIP文件拖放到此处以导入您的地图"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "浏览播放列表",
|
||||
"create-a-playlist": "创建播放列表",
|
||||
"import-playlists": "导入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的播放列表",
|
||||
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@
|
||||
"mods-not-available": "该版本 BeatSaber 暂无可用 Mod",
|
||||
"buttons": {
|
||||
"more-infos": "更多信息",
|
||||
"install-or-update": "安装或更新"
|
||||
"install-or-update": "安装或更新",
|
||||
"reinstall-all": "重新安装全部"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
@@ -85,6 +104,12 @@
|
||||
"uninstall-all": "全部卸载"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "模组已安装",
|
||||
"description": "所有选中的模组已经安装"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -112,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "登出后,您可以在下一次下载 Beat Saber 时切换帐户。",
|
||||
"logout": "登出",
|
||||
"logout-success": "注销成功",
|
||||
"download-platform": {
|
||||
"title": ",默认平台",
|
||||
"desc": "选择要下载 BeatSaber 的默认平台。",
|
||||
@@ -131,7 +157,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "安装文件夹",
|
||||
"description": "为 BeatSaber 不同版本及未来其他特性修改默认文件夹",
|
||||
"description": "更改将包含 BSManager 下载的所有内容的文件夹。",
|
||||
"choose-folder": "选择文件夹"
|
||||
},
|
||||
"additional-content": {
|
||||
@@ -190,6 +216,38 @@
|
||||
"report-bug": "报告 bug",
|
||||
"open-logs": "打开日志"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 探索更新日志!",
|
||||
"not-founds": "😕 哎呀,找不到更新日志!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"description": "BSManager的高级设置。",
|
||||
"hardware-acceleration": {
|
||||
"title": "硬件加速",
|
||||
"description": "启用硬件加速以使用您的GPU并提高BSManager的性能。如果您遇到帧丢失,请关闭此功能。",
|
||||
"modal": {
|
||||
"title": "需要重启",
|
||||
"body": "更改硬件加速设置将退出并重新启动BSManager。您确定要这样做吗?",
|
||||
"confirm-btn": "是的,我确定"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "发生错误,无法禁用硬件加速。"
|
||||
}
|
||||
},
|
||||
"use-symlinks": {
|
||||
"title": "使用符号链接",
|
||||
"description": "使用符号链接而不是联接来链接文件夹。仅在确实需要时才启用此功能。",
|
||||
"modal": {
|
||||
"title": "符号链接权限",
|
||||
"body": "创建符号链接时,BSManager将需要管理员权限或启用开发者模式。您确定要继续吗?",
|
||||
"confirm-btn": "是的,我确定"
|
||||
},
|
||||
"error-notification": {
|
||||
"message": "发生错误,无法更改符号链接设置。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作进行中",
|
||||
"no-internet": "无网络"
|
||||
"no-internet": "无网络",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待当前操作完成,然后重试。",
|
||||
"no-internet": "检查你的连接并重试。"
|
||||
"no-internet": "检查你的连接并重试。",
|
||||
"file-not-supported": "仅支持{types}文件。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,9 +294,6 @@
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"dotnet-required": "需要 .NET 6"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Steam 似乎不想让我们下载 Beat Saber😢",
|
||||
"404": "无法联系 Steam 服务器。",
|
||||
@@ -259,11 +316,7 @@
|
||||
"LicenceError": "无法获取许可证列表。",
|
||||
"RateLimitExceeded": "你已经尝试了太多次,请稍等片刻,稍后再试。",
|
||||
"TokenRejected": "你的登录令牌已被拒绝 😕 请重试。",
|
||||
"AccessDenied": "访问 Steam 被拒绝。",
|
||||
"dotnet-required": "必须安装 .NET 6 Runtime 才能下载 Beat Saber 版本。单击下面的按钮下载它。"
|
||||
},
|
||||
"actions": {
|
||||
"download-dotnet": "下载 .NET 6"
|
||||
"AccessDenied": "访问 Steam 被拒绝。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,7 +461,8 @@
|
||||
"CantEditSteam": "无法编辑",
|
||||
"CantRename": "无法重命名",
|
||||
"VersionAlreadExist": "该版本已存在",
|
||||
"CantClone": "无法克隆"
|
||||
"CantClone": "无法克隆",
|
||||
"UnknownError": "发生了未知错误"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "你不能编辑 Steam 版本。不过你可以克隆它。"
|
||||
@@ -461,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "谱面安装完成",
|
||||
"error": "安装谱面时发生错误"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "没有重复",
|
||||
"msg": "没有删除地图"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重复已删除",
|
||||
"msg": "重复已删除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地图导入完成",
|
||||
"error": "导入地图时发生错误"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地图已成功导入。",
|
||||
"some-success": "部分地图已成功导入。",
|
||||
"only-accept-zip": "仅支持zip文件。",
|
||||
"invalid-zip": "zip文件中没有地图。",
|
||||
"unknown": "发生了未知错误。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -481,6 +556,15 @@
|
||||
"title": "备份已创建",
|
||||
"msg": "共享 “UserData” 文件夹可能会产生错误,如果出现问题,请取消关联该文件夹以恢复备份"
|
||||
}
|
||||
},
|
||||
"linking-error": {
|
||||
"title": "链接文件夹时出错",
|
||||
"msg": {
|
||||
"EPERM": "BSManager没有必要的权限来链接文件夹。",
|
||||
"EACCES": "BSManager没有必要的权限来链接文件夹。",
|
||||
"ENOSPC": "磁盘已满,请腾出空间后再试。",
|
||||
"UNKNOWN_ERROR": "链接文件夹时发生未知错误。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-launch-shortcut": {
|
||||
@@ -646,6 +730,11 @@
|
||||
"title": "保留谱面将会在取消关联后把共享文件夹的所有谱面复制到当前版本。如果此项被禁用,谱面也不会丢失。"
|
||||
},
|
||||
"valid-btn": "取消关联谱面"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "删除地图",
|
||||
"desc": "只有地图 \"{map}\" 是重复的。你确定要删除它吗?",
|
||||
"desc-plural": "发现了 {nb} 个重复的地图。你确定要删除它们吗?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -726,12 +815,20 @@
|
||||
},
|
||||
"launch-as-admin": "以管理员身份启动",
|
||||
"not-remind-me": "不再提醒我"
|
||||
},
|
||||
"ask-install-path": {
|
||||
"title": "安装文件夹",
|
||||
"choose-folder-description": "选择将包含 BSManager 下载的所有内容的文件夹。(版本、mod、地图、播放列表等)",
|
||||
"choose-folder": "选择文件夹",
|
||||
"default": "默认",
|
||||
"default-tooltip": "默认为您的主文件夹"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "时长",
|
||||
"nps" : "每秒音符数",
|
||||
"njs": "音符跳跃速度",
|
||||
"tags": "标签",
|
||||
"specificities": "general",
|
||||
"requirements": "要求",
|
||||
@@ -741,48 +838,48 @@
|
||||
"accuracy": "精确度",
|
||||
"balanced": "平衡",
|
||||
"challenge": "挑战",
|
||||
"dancestyle": "舞蹈",
|
||||
"dance-style": "舞蹈",
|
||||
"fitness": "健身",
|
||||
"speed": "速度",
|
||||
"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",
|
||||
"dance": "舞蹈",
|
||||
"swing": "摇摆",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民谣 & 原声",
|
||||
"kids-family": "儿童 & 家庭",
|
||||
"ambient": "氛围音乐",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "灵魂",
|
||||
"speedcore": "极速核",
|
||||
"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"
|
||||
"holiday": "假日",
|
||||
"vocaloid": "声库",
|
||||
"j-rock": "日式摇滚",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 贝斯",
|
||||
"comedy-meme": "喜剧 & 表情包",
|
||||
"instrumental": "纯音乐",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "独立",
|
||||
"techno": "电子舞曲",
|
||||
"house": "浩室音乐",
|
||||
"video-game-soundtrack": "游戏音乐",
|
||||
"tv-movie-soundtrack": "电视 & 电影",
|
||||
"alternative": "另类",
|
||||
"dubstep": "电音鼓",
|
||||
"metal": "金属",
|
||||
"anime": "动漫",
|
||||
"hip-hop-rap": "嘻哈 & 说唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "摇滚",
|
||||
"pop": "流行",
|
||||
"electronic": "电子",
|
||||
"classical-orchestral": "古典 & 管弦乐"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -809,7 +906,8 @@
|
||||
"bsr-code" : "BSR代码",
|
||||
"download" : "下载地图",
|
||||
"downloading" :"正在下载地图",
|
||||
"cancel-download" : "取消下载"
|
||||
"cancel-download" : "取消下载",
|
||||
"hightlight-difficulty": "突出显示难度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1001,6 +1099,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或浏览文件"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "创建播放列表时出错",
|
||||
"error-playlist-creation-desc": "创建播放列表时发生错误。",
|
||||
"playlist-created-title": "播放列表已创建",
|
||||
"playlist-created-desc": "播放列表已成功创建。您现在可以同步其地图了!",
|
||||
"download-playlist": "下载播放列表",
|
||||
"synchronize-playlist": "同步播放列表",
|
||||
"synchronize-maps": "同步地图",
|
||||
"error-playlists-synchronization-title": "同步播放列表时出错",
|
||||
"error-playlists-synchronization-desc": "同步播放列表时发生错误。",
|
||||
"playlists-synchronized-title": "播放列表已同步!",
|
||||
"playlists-synchronized-desc": "播放列表及其地图已下载。",
|
||||
"playlists-export-error-title": "导出播放列表时出错",
|
||||
"playlists-export-error-desc": "导出播放列表时发生错误。",
|
||||
"playlists-exported-title": "播放列表已导出!",
|
||||
"playlists-exported-desc": "播放列表已成功导出。",
|
||||
"playlists-with-maps-exported-desc": "播放列表及其地图已成功导出。",
|
||||
"playlist-delete-error-title": "删除播放列表时出错",
|
||||
"playlist-delete-error-desc": "删除播放列表时发生错误。",
|
||||
"playlists-deleted-title": "播放列表已删除!",
|
||||
"playlists-deleted-desc": "播放列表已成功删除。",
|
||||
"edit-playlist": "编辑播放列表",
|
||||
"playlist-edit-error-title": "编辑播放列表时出错",
|
||||
"playlist-edit-error-desc": "编辑播放列表时发生错误。",
|
||||
"playlist-edited-title": "播放列表已编辑!",
|
||||
"playlist-edited-desc": "播放列表已成功修改。您现在可以同步其地图了!",
|
||||
"playlists-loading": "正在加载播放列表...",
|
||||
"no-playlists": "没有播放列表",
|
||||
"download-playlists": "下载播放列表",
|
||||
"created-by": "创建者",
|
||||
"stop-download": "停止下载",
|
||||
"cancel-download": "取消下载",
|
||||
"open-file": "打开文件",
|
||||
"link-playlists": "链接播放列表",
|
||||
"link-playlist-desc": "链接播放列表允许在所有版本之间共享播放列表。一旦链接,此版本将受益于共享播放列表",
|
||||
"link-playlist-info": "添加和删除播放列表也将被共享",
|
||||
"keep-playlists": "保留播放列表",
|
||||
"keep-playlists-tip": "保留播放列表将把当前版本的播放列表移动到共享播放列表文件夹。否则,它们将丢失",
|
||||
"unlink-playlists": "取消链接播放列表",
|
||||
"unlink-playlist-desc": "警告,取消链接播放列表将不再允许此版本使用共享播放列表。",
|
||||
"unlink-keep-playlists-tip": "保留播放列表将为当前版本创建共享播放列表的副本。否则,此版本将不保留任何播放列表。",
|
||||
"delete-playlist-ask": "删除播放列表?",
|
||||
"delete-playlists-ask": "删除播放列表?",
|
||||
"delete-playlist-desc": "您确定要删除播放列表 \"{playlistTitle}\" 吗?",
|
||||
"delete-playlists-desc": "您确定要删除 {nb} 个播放列表吗?",
|
||||
"delete-maps": "删除地图",
|
||||
"delete-playlist-maps-tip": "如果启用,播放列表中的所有地图都将被删除",
|
||||
"delete-playlists-maps-tip": "如果启用,播放列表中的所有地图都将被删除",
|
||||
"export-playlist-ask": "导出播放列表?",
|
||||
"export-playlists-ask": "导出播放列表?",
|
||||
"export-playlist-desc": "您确定要导出播放列表 \"{playlistTitle}\" 吗?",
|
||||
"export-playlists-desc": "您确定要导出 {nb} 个播放列表吗?",
|
||||
"export-maps": "导出地图",
|
||||
"export-playlist-maps-tip": "如果启用,播放列表中的所有地图也将被导出",
|
||||
"export-playlists-maps-tip": "如果启用,播放列表中的所有地图也将被导出",
|
||||
"export": "导出",
|
||||
"need-clone-title": "警告",
|
||||
"need-clone-desc-1": "此播放列表已从外部站点下载,并包含同步链接。",
|
||||
"need-clone-desc-2": "为避免在同步过程中丢失更改,播放列表将被复制,并删除其同步链接。",
|
||||
"need-clone-desc-3": "然后,如果您愿意,可以删除原始播放列表。",
|
||||
"understood": "我明白了",
|
||||
"synchronize-playlist-ask": "同步播放列表?",
|
||||
"synchronize-playlists-ask": "同步播放列表?",
|
||||
"synchronize-playlist-desc": "您确定要同步播放列表 \"{playlistTitle}\" 吗?",
|
||||
"synchronize-playlists-desc": "您确定要同步 {nb} 个播放列表吗?",
|
||||
"synchronize-playlist-tip": "此操作更新播放列表并下载缺失的地图;可能需要几分钟。",
|
||||
"synchronize": "同步",
|
||||
"curated": "推荐",
|
||||
"verified-mapper": "已验证的制图者",
|
||||
"empty-playlists": "空播放列表",
|
||||
"search-playlist": "搜索播放列表",
|
||||
"no-playlists-found": "未找到播放列表",
|
||||
"error-occur-while-loading-playlists": "加载播放列表时发生错误",
|
||||
"error-occur-while-loading-playlist": "加载播放列表时发生错误",
|
||||
"loading-maps": "正在加载地图...",
|
||||
"no-maps-found-for-playlist": "未找到此播放列表的地图",
|
||||
"playlist-contain-no-maps": "播放列表不包含地图",
|
||||
"no-map-installed-for-playlist": "此播放列表没有已安装的地图",
|
||||
"playlist-is-waiting-to-download": "播放列表正在等待下载",
|
||||
"download-maps": "下载地图",
|
||||
"download-missing-maps": "下载缺失的地图",
|
||||
"playlist-is-downloading": "播放列表正在下载",
|
||||
"some-playlist-maps-are-missing": "此播放列表中的一些地图缺失",
|
||||
"create-a-playlist": "创建播放列表",
|
||||
"synchronize-playlists": "同步播放列表",
|
||||
"export-playlists": "导出播放列表",
|
||||
"delete-playlists": "删除播放列表",
|
||||
"choose-image": "选择图片",
|
||||
"title": "标题",
|
||||
"playlist-title": "播放列表标题",
|
||||
"description": "描述",
|
||||
"playlist-description": "播放列表描述",
|
||||
"author": "作者",
|
||||
"playlist-author": "播放列表作者",
|
||||
"save": "保存",
|
||||
"loading": "加载中...",
|
||||
"installed": "已安装",
|
||||
"no-map-found": "未找到地图",
|
||||
"edit-playlist-shortcuts": "按住 Shift 或 Ctrl 选择多个地图",
|
||||
"add-to-playlist": "添加到播放列表",
|
||||
"remove-from-playlist": "从播放列表中移除",
|
||||
"playlist-is-empty": "播放列表为空",
|
||||
"continue": "继续",
|
||||
"nb-maps": "地图数量",
|
||||
"nb-mappers": "制图者数量",
|
||||
"duration": "持续时间",
|
||||
"nps": "每秒音符数",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "开始日期 — 结束日期",
|
||||
"all": "全部",
|
||||
"last-24h": "最近24小时",
|
||||
"last-week": "上周",
|
||||
"last-month": "上个月",
|
||||
"3-last-month": "最近3个月"
|
||||
},
|
||||
"playlists-imported": "播放列表已导入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放列表已成功导入",
|
||||
"no-playlist-found": "未找到播放列表",
|
||||
"no-playlist-found-in-selected-files": "在选定文件中未找到播放列表",
|
||||
"some-playlists-not-imported": "部分播放列表未导入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放列表无法解析",
|
||||
"unknown": "某些播放列表无法导入"
|
||||
},
|
||||
"no-playlists-imported": "未导入任何播放列表",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放列表无法解析",
|
||||
"unknown": "无法导入任何播放列表"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
|
||||
"monthNames": ["1 月", "2 月", "3 月", "4 月", "5 月", "6 月", "7 月", "8 月", "9 月", "10 月", "11 月", "12 月", "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package song_details_cache_v1;
|
||||
|
||||
message SongDetailsCache {
|
||||
repeated SongDetails songs = 1;
|
||||
uint32 lastUpdated = 2;
|
||||
uint32 total = 3;
|
||||
UploadersList uploaders = 4;
|
||||
repeated string difficultyLabels = 5;
|
||||
}
|
||||
|
||||
message UploadersList {
|
||||
repeated string names = 1;
|
||||
repeated uint32 ids = 2;
|
||||
}
|
||||
|
||||
message SongDetails {
|
||||
uint32 idInt = 1;
|
||||
repeated uint32 hashIndices = 2;
|
||||
string name = 3;
|
||||
uint32 duration = 4;
|
||||
UploaderRef uploaderRef = 5;
|
||||
uint32 uploadedAt = 6;
|
||||
repeated MapTag tags = 7;
|
||||
bool ranked = 8;
|
||||
bool qualified = 9;
|
||||
bool curated = 10;
|
||||
bool blRanked = 11;
|
||||
bool blQualified = 12;
|
||||
uint32 upVotes = 13;
|
||||
uint32 downVotes = 14;
|
||||
uint32 downloads = 15;
|
||||
bool automapper = 16;
|
||||
repeated Difficulty difficulties = 17;
|
||||
}
|
||||
|
||||
message Difficulty {
|
||||
DifficultyLabel difficulty = 1;
|
||||
DifficultyCharacteristic characteristic = 2;
|
||||
uint32 labelIndex = 3;
|
||||
uint32 starsT100 = 4;
|
||||
uint32 starsBlT100 = 5;
|
||||
uint32 njsT100 = 6;
|
||||
uint32 npsT100 = 7;
|
||||
int32 offsetT100 = 8;
|
||||
bool chroma = 9;
|
||||
bool cinema = 10;
|
||||
bool me = 11;
|
||||
bool ne = 12;
|
||||
uint32 bombs = 13;
|
||||
uint32 notes = 14;
|
||||
uint32 obstacles = 15;
|
||||
}
|
||||
|
||||
message UploaderRef {
|
||||
uint32 uploader_ref_index = 1;
|
||||
bool verified = 2;
|
||||
}
|
||||
|
||||
enum DifficultyLabel {
|
||||
UNKNOWN_LABEL = 0; // Default value for undefined/unknown labels
|
||||
EASY = 1;
|
||||
NORMAL = 2;
|
||||
HARD = 3;
|
||||
EXPERT = 4;
|
||||
EXPERT_PLUS = 5;
|
||||
}
|
||||
|
||||
enum DifficultyCharacteristic {
|
||||
UNKNOWN_CHARACTERISTIC = 0; // Default value for undefined/unknown characteristics
|
||||
STANDARD = 1;
|
||||
ONE_SABER = 2;
|
||||
NO_ARROWS = 3;
|
||||
LAWLESS = 4;
|
||||
LIGHTSHOW = 5;
|
||||
LEGACY = 6;
|
||||
NINETY_DEGREE = 7;
|
||||
THREESIXTY_DEGREE = 8;
|
||||
}
|
||||
|
||||
enum MapTag {
|
||||
UNKNOWN_TAG = 0; // Default value for undefined/unknown tags
|
||||
DANCE = 1;
|
||||
SWING = 2;
|
||||
NIGHTCORE = 3;
|
||||
FOLK = 4;
|
||||
FAMILY = 5;
|
||||
AMBIENT = 6;
|
||||
FUNK = 7;
|
||||
JAZZ = 8;
|
||||
SOUL = 9;
|
||||
SPEEDCORE = 10;
|
||||
PUNK = 11;
|
||||
RB = 12;
|
||||
HOLIDAY = 13;
|
||||
VOCALOID = 14;
|
||||
J_ROCK = 15;
|
||||
TRANCE = 16;
|
||||
DRUMBASS = 17;
|
||||
COMEDY = 18;
|
||||
INSTRUMENTAL = 19;
|
||||
HARDCORE = 20;
|
||||
K_POP = 21;
|
||||
INDIE = 22;
|
||||
TECHNO = 23;
|
||||
HOUSE = 24;
|
||||
GAME = 25;
|
||||
FILM = 26;
|
||||
ALT = 27;
|
||||
DUBSTEP = 28;
|
||||
METAL = 29;
|
||||
ANIME = 30;
|
||||
HIPHOP = 31;
|
||||
J_POP = 32;
|
||||
ROCK = 33;
|
||||
POP = 34;
|
||||
ELECTRONIC = 35;
|
||||
CLASSICAL_ORCHESTRAL = 36;
|
||||
ACCURACY = 37;
|
||||
BALANCED = 38;
|
||||
CHALLENGE = 39;
|
||||
DANCESTYLE = 40;
|
||||
FITNESS = 41;
|
||||
SPEED = 42;
|
||||
TECH = 43;
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"DepotDownloader/2.5.0": {
|
||||
"dependencies": {
|
||||
"QRCoder": "1.4.3",
|
||||
"SteamKit2": "2.5.0-Beta.1",
|
||||
"protobuf-net": "3.2.16"
|
||||
},
|
||||
"runtime": {
|
||||
"DepotDownloader.dll": {}
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {},
|
||||
"Microsoft.Win32.Registry/5.0.0": {
|
||||
"dependencies": {
|
||||
"System.Security.AccessControl": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"protobuf-net/3.2.16": {
|
||||
"dependencies": {
|
||||
"protobuf-net.Core": "3.2.16"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/protobuf-net.dll": {
|
||||
"assemblyVersion": "3.0.0.0",
|
||||
"fileVersion": "3.2.16.54102"
|
||||
}
|
||||
}
|
||||
},
|
||||
"protobuf-net.Core/3.2.16": {
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "7.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/protobuf-net.Core.dll": {
|
||||
"assemblyVersion": "3.0.0.0",
|
||||
"fileVersion": "3.2.16.54102"
|
||||
}
|
||||
}
|
||||
},
|
||||
"QRCoder/1.4.3": {
|
||||
"runtime": {
|
||||
"lib/net6.0/QRCoder.dll": {
|
||||
"assemblyVersion": "1.4.3.0",
|
||||
"fileVersion": "1.4.3.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SteamKit2/2.5.0-Beta.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.Registry": "5.0.0",
|
||||
"protobuf-net": "3.2.16"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/SteamKit2.dll": {
|
||||
"assemblyVersion": "2.5.0.0",
|
||||
"fileVersion": "2.5.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Collections.Immutable/7.0.0": {
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/System.Collections.Immutable.dll": {
|
||||
"assemblyVersion": "7.0.0.0",
|
||||
"fileVersion": "7.0.22.51805"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
|
||||
"System.Security.AccessControl/5.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Principal.Windows/5.0.0": {}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DepotDownloader/2.5.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.NETCore.Platforms/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==",
|
||||
"path": "microsoft.netcore.platforms/5.0.0",
|
||||
"hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Win32.Registry/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
|
||||
"path": "microsoft.win32.registry/5.0.0",
|
||||
"hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"protobuf-net/3.2.16": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-W6RM2B2f5O7SigBGfkXFYzI/a7Mks+rCHtuGlE/b5O3NMupDon7I0akjCLhrfcnqIfdWDkDafseav/MnIzUSHA==",
|
||||
"path": "protobuf-net/3.2.16",
|
||||
"hashPath": "protobuf-net.3.2.16.nupkg.sha512"
|
||||
},
|
||||
"protobuf-net.Core/3.2.16": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ZFoY0lfMUWGJ4RoyI0oNj2o2l8t08n3k27YyDD0kYWQ+vXj0hTbWEwYWOpGAfCWvyUPkq9Jcc/4uR74GICZL2g==",
|
||||
"path": "protobuf-net.core/3.2.16",
|
||||
"hashPath": "protobuf-net.core.3.2.16.nupkg.sha512"
|
||||
},
|
||||
"QRCoder/1.4.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-fWuFqjm8GTlEb2GqBl3Hi8HZZeZQwBSHxvRPtPjyNbT82H0ff0JwavKRBmMaXCno1Av6McPC8aJzri0Mj2w9Jw==",
|
||||
"path": "qrcoder/1.4.3",
|
||||
"hashPath": "qrcoder.1.4.3.nupkg.sha512"
|
||||
},
|
||||
"SteamKit2/2.5.0-Beta.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-fwwdF0cZZEIdNpcUc1+B9zbP5Q48A3DFOAclPhBh2dMTU03aROaIDktFW+Ww/NN1Z/Emp/LwVqHB+hCM8uxS4Q==",
|
||||
"path": "steamkit2/2.5.0-beta.1",
|
||||
"hashPath": "steamkit2.2.5.0-beta.1.nupkg.sha512"
|
||||
},
|
||||
"System.Collections.Immutable/7.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==",
|
||||
"path": "system.collections.immutable/7.0.0",
|
||||
"hashPath": "system.collections.immutable.7.0.0.nupkg.sha512"
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
|
||||
"path": "system.runtime.compilerservices.unsafe/6.0.0",
|
||||
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
|
||||
},
|
||||
"System.Security.AccessControl/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
|
||||
"path": "system.security.accesscontrol/5.0.0",
|
||||
"hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"System.Security.Principal.Windows/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
|
||||
"path": "system.security.principal.windows/5.0.0",
|
||||
"hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net6.0",
|
||||
"rollForward": "LatestMajor",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "6.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 442 KiB After Width: | Height: | Size: 442 KiB |
@@ -0,0 +1,21 @@
|
||||
If you encounter a keyboard shortcut conflict between AMD Software and Oculus Rift, you can either disable or modify the conflicting shortcut.
|
||||
|
||||
_Special thanks to_ `𝔽𝕪𝕟𝕟 (fynn07)` _for its contribution with information and screenshots_
|
||||
### __Follow these steps to disable the shortcut in AMD Software:__
|
||||
# Step 1 - Access Settings:
|
||||
- Open AMD Software and click on the gear icon in the top right corner to go to settings.
|
||||
|
||||

|
||||
# Step 2 - Modify Keyboard Shortcuts:
|
||||
- In the settings menu, select the "Hotkey" tab.
|
||||
|
||||

|
||||
# Step 3 -Disabling Shortcuts:
|
||||
- Turn off the "Use Hotkeys" option to prevent any conflicts with Oculus Rift.
|
||||
|
||||

|
||||
# Step 3 alt - Modify the Shortcut:
|
||||
- If you prefer to keep using keyboard shortcuts, consider changing the specific conflicting shortcut (e.g., "Ctrl + Shift + I") to another combination that is less likely to interfere.
|
||||
|
||||
ℹ️ _**Keep in mind you can't change the full keybind!**_
|
||||
_**You can only Change the "i" to an different Button!**_ ℹ️
|
||||
@@ -0,0 +1,14 @@
|
||||
Welcome to the bs-manager wiki! (wip c:)
|
||||
|
||||
to get you oculus token check here : https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token
|
||||
|
||||
---
|
||||
|
||||
This Wiki accepts contributions!
|
||||
|
||||
To contribute to the wiki:
|
||||
|
||||
- Fork the project
|
||||
- Add/Edit `.md` files in the `docs/wiki/` folder
|
||||
- Create a Pull Request
|
||||
- Once the PR is merged (after review), it will be present in the repository's wiki
|
||||
@@ -0,0 +1,30 @@
|
||||
### Important
|
||||
|
||||
Your token is a confidential piece of information. Possession of this token allows individuals to download applications, send messages, among other actions, under your identity.
|
||||
|
||||
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
|
||||
- Install the Oculus Rift app
|
||||
|
||||
ℹ️ **If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with BSManager, first claim it from its store page** ℹ️
|
||||
|
||||
## Step 2 - Open developer tools
|
||||
|
||||
- Open Oculus app
|
||||
- Open the developer tools by pressing <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>i</kbd>.
|
||||
|
||||
## Step 3 - Copy your Token
|
||||
In the developer tools :
|
||||
- Open the `Network` tab ***(1)***
|
||||
- Filter for `graph` ***(2)***
|
||||
- Click on the first request ***(3)***
|
||||
- Open the `Headers` tab ***(4)***
|
||||
- Scroll to the bottom to locate your token, it should start with `FRL` ***(5)***
|
||||
- Select the token using your mouse and press <kbd>Ctrl</kbd> + <kbd>c</kbd> to copy it
|
||||
|
||||

|
||||
|
||||
# Known bugs
|
||||
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
|
||||
@@ -0,0 +1,4 @@
|
||||
If nothing opens when you press `Ctrl` + `Shift` + `I`, it's possible that the keyboard shortcut to open the development tools is being used by another application.
|
||||
|
||||
Here's a non-exhaustive list of applications that have caused a problem and their suggested solutions:
|
||||
- [AMD software](https://github.com/Zagrios/bs-manager/wiki/AMD-Sofware-%E2%80%90-Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60I%60)
|
||||
@@ -1,20 +1,26 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"description": "Manage maps, mods and more for Beat Saber",
|
||||
"main": "./.erb/dll/main.bundle.dev.js",
|
||||
"version": "1.5.0-alpha.4",
|
||||
"scripts": {
|
||||
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
|
||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||
"build:dll": "cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||
"build:main": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||
"build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
||||
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts",
|
||||
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
||||
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall",
|
||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
||||
"start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only ./src/main/main.ts",
|
||||
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
|
||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
||||
"start:main": "concurrently -k \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon .\"",
|
||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||
"test": "jest",
|
||||
"publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64"
|
||||
"publish": "npm run build && electron-builder -c.win.certificateSha1=2164d6a7d641ecf6ad57852f665a518ca2bf960f --publish always --win --x64",
|
||||
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
@@ -32,10 +38,9 @@
|
||||
},
|
||||
"build": {
|
||||
"extraResources": [
|
||||
"./assets/favicon.ico",
|
||||
"./assets/jsons/bs-versions.json",
|
||||
"./assets/jsons/patreons.json",
|
||||
"./assets/scripts/**"
|
||||
"./assets/proto/song_details_cache_v1.proto"
|
||||
],
|
||||
"productName": "BSManager",
|
||||
"appId": "org.erb.BSManager",
|
||||
@@ -47,46 +52,28 @@
|
||||
],
|
||||
"afterSign": ".erb/scripts/notarize.js",
|
||||
"afterPack": ".erb/scripts/after-pack.js",
|
||||
"mac": {
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
"type": "distribution",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "assets/entitlements.mac.plist",
|
||||
"entitlementsInherit": "assets/entitlements.mac.plist",
|
||||
"gatekeeperAssess": false
|
||||
},
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"signingHashAlgorithms": ["sha256"],
|
||||
"target": [
|
||||
"nsis",
|
||||
"nsis-web"
|
||||
],
|
||||
"icon": "assets/favicon.ico"
|
||||
"icon": "./build/icons/win/favicon.ico",
|
||||
"extraResources": [
|
||||
"./build/icons/win",
|
||||
"./assets/scripts/*.exe"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
],
|
||||
"category": "Development"
|
||||
"icon": "./build/icons/png",
|
||||
"category": "Utility;Game;",
|
||||
"extraResources": [
|
||||
"./build/icons/png",
|
||||
"./assets/scripts/DepotDownloader"
|
||||
]
|
||||
},
|
||||
"directories": {
|
||||
"app": "release/app",
|
||||
@@ -96,7 +83,15 @@
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "Zagrios"
|
||||
}
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": "bplist",
|
||||
"description": "Beat Saber Playlist (BSManager)",
|
||||
"icon": "./assets/bsm_file.ico",
|
||||
"role": "Viewer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -148,141 +143,148 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.2.13",
|
||||
"@electron/fuses": "^1.7.0",
|
||||
"@electron/notarize": "^2.3.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
|
||||
"@testing-library/jest-dom": "^6.4.1",
|
||||
"@testing-library/react": "^14.2.0",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/got": "^9.6.12",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "20.11.15",
|
||||
"@types/node-fetch": "^2.6.3",
|
||||
"@types/pako": "^2.0.1",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-outside-click-handler": "^1.3.1",
|
||||
"@types/react-test-renderer": "^17.0.2",
|
||||
"@types/react-test-renderer": "^18.0.7",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/recursive-readdir": "^2.2.1",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/to-ico": "^1.1.1",
|
||||
"@types/use-double-click": "^1.0.1",
|
||||
"@types/use-double-click": "^1.0.4",
|
||||
"@types/webpack-bundle-analyzer": "^4.4.2",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.34.0",
|
||||
"@typescript-eslint/parser": "^5.34.0",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"browserslist-config-erb": "^0.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"concurrently": "^7.2.2",
|
||||
"core-js": "^3.24.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"core-js": "^3.36.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.1.0",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron": "^27.1.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"detect-port": "^1.5.1",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-notarize": "^1.2.1",
|
||||
"electronmon": "^2.0.2",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-erb": "^4.0.3",
|
||||
"eslint-import-resolver-typescript": "^2.7.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.2",
|
||||
"eslint-plugin-compat": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.8.7",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"eslint-config-erb": "^4.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-import-resolver-webpack": "^0.13.8",
|
||||
"eslint-plugin-compat": "^4.2.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^27.6.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"lint-staged": "^12.5.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.2.1",
|
||||
"mini-css-extract-plugin": "^2.7.7",
|
||||
"opencollective-postinstall": "^2.0.3",
|
||||
"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",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-loader": "^8.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.54.5",
|
||||
"sass-loader": "^12.6.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.5",
|
||||
"ts-jest": "^27.1.5",
|
||||
"ts-loader": "^9.3.0",
|
||||
"ts-node": "^10.8.2",
|
||||
"typescript": "^4.7.4",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.3.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-bundle-analyzer": "^4.6.1",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.10.0",
|
||||
"webpack-merge": "^5.8.0"
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-merge": "^5.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/fuses": "^1.6.2",
|
||||
"@internationalized/date": "^3.5.4",
|
||||
"@nextui-org/date-picker": "^2.0.7",
|
||||
"@nextui-org/react": "^2.3.6",
|
||||
"@node-steam/vdf": "^2.2.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"archiver": "^6.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"dompurify": "^3.0.9",
|
||||
"dot-prop": "^8.0.2",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-updater": "^6.3.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"framer-motion": "^10.16.16",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.4.2",
|
||||
"history": "^5.3.0",
|
||||
"is-elevated": "^3.0.0",
|
||||
"is-elevated": "^4.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-abi": "^3.47.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-abi": "^3.65.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"ps-list": "^7.2.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-range": "^1.8.14",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.12",
|
||||
"react-window": "^1.8.8",
|
||||
"react-range": "^1.10.0",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.24",
|
||||
"react-window": "^1.8.10",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"rfdc": "^1.3.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"rfdc": "^1.4.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.5.4",
|
||||
"serialize-error": "^8.1.0",
|
||||
"semver": "^7.6.3",
|
||||
"serialize-error": "^11.0.3",
|
||||
"striptags": "^4.0.0-alpha.4",
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"to-ico": "^1.1.5",
|
||||
"use-double-click": "^1.0.5",
|
||||
"use-fit-text": "^2.4.0"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": ">=14.x",
|
||||
"npm": ">=7.x"
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"collective": {
|
||||
"url": "https://www.patreon.com/bsmanager"
|
||||
@@ -312,10 +314,13 @@
|
||||
},
|
||||
"electronmon": {
|
||||
"patterns": [
|
||||
"!src/__tests__/**",
|
||||
"!release/**",
|
||||
"!assets/**"
|
||||
"!**/**",
|
||||
"src/main/**",
|
||||
".erb/dll/**"
|
||||
],
|
||||
"logLevel": "quiet"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.4.8",
|
||||
"version": "1.5.0-alpha.4",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
@@ -9,15 +9,18 @@
|
||||
"url": "https://github.com/Zagrios/bs-manager"
|
||||
},
|
||||
"scripts": {
|
||||
"electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
|
||||
"postinstall": "npm run electron-rebuild && npm run link-modules"
|
||||
"postinstall": "npm run rebuild && npm run link-modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@resvg/resvg-js": "2.6.2",
|
||||
"ps-list": "^7.2.0",
|
||||
"query-process": "^0.0.3",
|
||||
"regedit-rs": "^1.0.2",
|
||||
"sharp": "^0.32.6"
|
||||
"regedit-rs": "^1.0.2"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"volta": {
|
||||
"node": "20.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app } from "electron";
|
||||
import { constants } from "http2";
|
||||
import path from "path";
|
||||
|
||||
export const BS_EXECUTABLE = "Beat Saber.exe";
|
||||
@@ -10,5 +11,8 @@ export const APP_NAME = "BSManager";
|
||||
|
||||
export const STEAMVR_APP_ID = "250820";
|
||||
|
||||
export const IMAGE_CACHE_FOLDER = "imagescache";
|
||||
export const IMAGE_CACHE_PATH = path.join(app.getPath("userData"), IMAGE_CACHE_FOLDER);
|
||||
export const CACHE_PATH = path.join(app.getPath("userData"), "CachedData");
|
||||
|
||||
export const IMAGE_CACHE_PATH = path.join(CACHE_PATH, "imagescache");
|
||||
|
||||
export const HTTP_STATUS_CODES = constants;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyOptions, copy, createReadStream, ensureDir, move, realpath, stat, symlink } from "fs-extra";
|
||||
import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra";
|
||||
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable, concatMap, from } from "rxjs";
|
||||
@@ -7,6 +7,8 @@ import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import crypto from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { ErrorObject } from "serialize-error";
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -78,36 +80,37 @@ export async function getFilesInFolder(folderPath: string): Promise<string[]> {
|
||||
return dirEntries.filter(entry => entry.isFile()).map(file => path.join(folderPath, file.name));
|
||||
}
|
||||
|
||||
export function moveFolderContent(src: string, dest: string): Observable<Progression> {
|
||||
export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable<Progression> {
|
||||
const progress: Progression = { current: 0, total: 0 };
|
||||
return new Observable<Progression>(subscriber => {
|
||||
subscriber.next(progress);
|
||||
(async () => {
|
||||
const srcExist = await pathExist(src);
|
||||
const srcExist = await pathExists(src);
|
||||
|
||||
if (!srcExist) {
|
||||
return subscriber.complete();
|
||||
}
|
||||
|
||||
ensureFolderExist(dest);
|
||||
await ensureFolderExist(dest);
|
||||
|
||||
const files = await readdir(src, { encoding: "utf-8" });
|
||||
const files = await readdir(src, { encoding: "utf-8", withFileTypes: true });
|
||||
progress.total = files.length;
|
||||
|
||||
const promises = files.map(async file => {
|
||||
const srcFullPath = path.join(src, file);
|
||||
const destFullPath = path.join(dest, file);
|
||||
if (await pathExist(destFullPath)) {
|
||||
progress.current++;
|
||||
return subscriber.next(progress);
|
||||
for(const file of files){
|
||||
const srcFullPath = path.join(src, file.name);
|
||||
const destFullPath = path.join(dest, file.name);
|
||||
|
||||
const srcChilds = file.isDirectory() ? await readdir(srcFullPath, { encoding: "utf-8", recursive: true }) : [];
|
||||
const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child)));
|
||||
|
||||
if(file.isFile() || !allChildsAlreadyExist){
|
||||
await move(srcFullPath, destFullPath, option);
|
||||
}
|
||||
await move(srcFullPath, destFullPath);
|
||||
|
||||
progress.current++;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
Promise.allSettled(promises).then(() => subscriber.complete());
|
||||
})();
|
||||
}
|
||||
})().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,7 +154,7 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
const symlinkTarget = await readlink(sourcePath);
|
||||
const relativePath = path.relative(src, symlinkTarget);
|
||||
const newTarget = path.join(dest, relativePath);
|
||||
await symlink(newTarget, destinationPath, "junction");
|
||||
await symlink(newTarget, destinationPath, "junction"); // Only junction to avoid right issues while copying content of BSManager folder
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +178,7 @@ export async function dirSize(dirPath: string): Promise<number>{
|
||||
const realPath = await realpath(fullPath);
|
||||
const stat = await lstat(realPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (stat.isDirectory()) {
|
||||
return dirSize(fullPath);
|
||||
}
|
||||
|
||||
@@ -217,13 +220,27 @@ export function rxCopy(src: string, dest: string, option?: CopyOptions): Observa
|
||||
|
||||
export async function ensurePathNotAlreadyExist(path: string): Promise<string> {
|
||||
let destPath = path;
|
||||
let folderExist = await pathExist(destPath);
|
||||
let folderExist = await pathExists(destPath);
|
||||
let i = 0;
|
||||
|
||||
while (folderExist) {
|
||||
i++;
|
||||
destPath = `${path} (${i})`;
|
||||
folderExist = await pathExist(destPath);
|
||||
folderExist = await pathExists(destPath);
|
||||
}
|
||||
|
||||
return destPath;
|
||||
}
|
||||
|
||||
export function ensurePathNotAlreadyExistSync(path: string): string {
|
||||
let destPath = path;
|
||||
let folderExist = pathExistsSync(destPath);
|
||||
let i = 0;
|
||||
|
||||
while (folderExist) {
|
||||
i++;
|
||||
destPath = `${path} (${i})`;
|
||||
folderExist = pathExistsSync(destPath);
|
||||
}
|
||||
|
||||
return destPath;
|
||||
@@ -244,9 +261,24 @@ export function resolveGUIDPath(guidPath: string): string {
|
||||
return path.join(driveLetter, path.relative(guidVolume, guidPath));
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown> {
|
||||
export function getUniqueFileNamePath(filePath: string): string {
|
||||
const { dir, name, ext } = path.parse(filePath);
|
||||
let i = 0;
|
||||
let newFileName = `${name}${ext}`;
|
||||
|
||||
while (pathExistsSync(path.join(dir, newFileName))) {
|
||||
i++;
|
||||
newFileName = `${name} (${i})${ext}`;
|
||||
}
|
||||
|
||||
return path.join(dir, newFileName);
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown, D = unknown> {
|
||||
total: number;
|
||||
current: number;
|
||||
diff?: number;
|
||||
data?: T;
|
||||
extra?: D;
|
||||
lastError?: ErrorObject;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
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";
|
||||
import log from "electron-log";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
|
||||
ipcMain.on("bsv-search-map", async (event, request: IpcRequest<SearchParams>) => {
|
||||
const utlis = UtilsService.getInstance();
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("bsv-search-map", (args, reply) => {
|
||||
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 });
|
||||
});
|
||||
reply(from(bsvService.searchMaps(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("bsv-get-map-details-from-hashs", async (event, request: IpcRequest<string[]>) => {
|
||||
const utlis = UtilsService.getInstance();
|
||||
ipc.on("bsv-get-map-details-from-hashs", (args, reply) => {
|
||||
const bsvService = BeatSaverService.getInstance();
|
||||
|
||||
bsvService
|
||||
.getMapDetailsFromHashs(request.args)
|
||||
.then(maps => {
|
||||
utlis.ipcSend(request.responceChannel, { success: true, data: maps });
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(e);
|
||||
utlis.ipcSend(request.responceChannel, { success: false, error: e });
|
||||
});
|
||||
reply(from(bsvService.getMapDetailsFromHashs(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("bsv-get-map-details-by-id", async (event, request: IpcRequest<string>) => {
|
||||
const utlis = UtilsService.getInstance();
|
||||
ipc.on("bsv-get-map-details-by-id", (args, reply) => {
|
||||
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 });
|
||||
});
|
||||
reply(from(bsvService.getMapDetailsById(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("bsv-get-playlist-details-by-id", async (event, request: IpcRequest<string>) => {
|
||||
const utlis = UtilsService.getInstance();
|
||||
ipc.on("bsv-search-playlist", (args, reply) => {
|
||||
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 });
|
||||
});
|
||||
reply(from(bsvService.searchPlaylists(args)));
|
||||
});
|
||||
|
||||
ipc.on("bsv-get-playlist-details-by-id", (args, reply) => {
|
||||
const bsvService = BeatSaverService.getInstance();
|
||||
reply(from(bsvService.getPlaylistDetailsById(args.id, args.page)));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { from, of } from "rxjs";
|
||||
|
||||
import { InstallationLocationService } from "main/services/installation-location.service";
|
||||
import { IpcService } from "main/services/ipc.service";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("bs-installer.folder-exists", (_, reply) => {
|
||||
const service = InstallationLocationService.getInstance();
|
||||
reply(of(pathExistsSync(service.installationDirectory())));
|
||||
});
|
||||
|
||||
ipc.on("bs-installer.default-install-path", (_, reply) => {
|
||||
const service = InstallationLocationService.getInstance();
|
||||
reply(of(service.defaultInstallationDirectory()));
|
||||
});
|
||||
|
||||
ipc.on("bs-installer.install-path", (_, reply) => {
|
||||
const service = InstallationLocationService.getInstance();
|
||||
reply(of(service.installationDirectory()));
|
||||
});
|
||||
|
||||
ipc.on("bs-installer.set-install-path", (args, reply) => {
|
||||
const service = InstallationLocationService.getInstance();
|
||||
reply(from(service.setInstallationDirectory(args.path, args.move)));
|
||||
});
|
||||
@@ -1,22 +1,20 @@
|
||||
|
||||
import { LaunchOption } from "shared/models/bs-launch";
|
||||
import { BSLauncherService } from "../services/bs-launcher/bs-launcher.service"
|
||||
import { IpcService } from '../services/ipc.service';
|
||||
import { from } from "rxjs";
|
||||
import { SteamLauncherService } from "../services/bs-launcher/steam-launcher.service";
|
||||
import { OculusLauncherService } from "../services/bs-launcher/oculus-launcher.service";
|
||||
import { SteamService } from "../services/steam.service";
|
||||
import log from "electron-log";
|
||||
import isElevated from "is-elevated";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on<LaunchOption>('bs-launch.launch', (req, reply) => {
|
||||
ipc.on('bs-launch.launch', (args, reply) => {
|
||||
const bsLauncher = BSLauncherService.getInstance();
|
||||
reply(bsLauncher.launch(req.args));
|
||||
reply(bsLauncher.launch(args));
|
||||
});
|
||||
|
||||
ipc.on<boolean>("bs-launch.need-start-as-admin", (_, reply) => {
|
||||
ipc.on("bs-launch.need-start-as-admin", (_, reply) => {
|
||||
const steam = SteamService.getInstance();
|
||||
reply(from(isElevated().then(elevated => {
|
||||
if(elevated){ return false; }
|
||||
@@ -27,13 +25,12 @@ ipc.on<boolean>("bs-launch.need-start-as-admin", (_, reply) => {
|
||||
})));
|
||||
});
|
||||
|
||||
ipc.on<LaunchOption>("create-launch-shortcut", (req, reply) => {
|
||||
ipc.on("create-launch-shortcut", (args, reply) => {
|
||||
const bsLauncher = BSLauncherService.getInstance();
|
||||
reply(from(bsLauncher.createLaunchShortcut(req.args)));
|
||||
reply(from(bsLauncher.createLaunchShortcut(args)));
|
||||
});
|
||||
|
||||
|
||||
ipc.on<void>("bs-launch.restore-steamvr", (_, reply) => {
|
||||
ipc.on("bs-launch.restore-steamvr", (_, reply) => {
|
||||
const steamLauncher = SteamLauncherService.getInstance();
|
||||
reply(from(steamLauncher.restoreSteamVR()));
|
||||
});
|
||||
|
||||
@@ -1,119 +1,93 @@
|
||||
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";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
import log from "electron-log"
|
||||
import { from, of, throwError } from "rxjs";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { SongDetailsCacheService } from "main/services/additional-content/maps/song-details-cache.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("load-version-maps", async (request: IpcRequest<BSVersion>, reply) => {
|
||||
ipc.on("load-version-maps", (args, reply) => {
|
||||
const localMaps = LocalMapsManagerService.getInstance();
|
||||
reply(localMaps.getMaps(request.args));
|
||||
reply(localMaps.getMaps(args));
|
||||
});
|
||||
|
||||
ipc.on("verion-have-maps-linked", async (request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("delete-maps", (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
|
||||
utils.ipcSend<boolean>(request.responceChannel, { success: true, data: await maps.versionIsLinked(request.args) });
|
||||
reply(maps.deleteMaps(args));
|
||||
});
|
||||
|
||||
ipc.on("link-version-maps", async (request: IpcRequest<{ version: BSVersion; keepMaps: boolean }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("export-maps", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
|
||||
maps.linkVersionMaps(request.args.version, request.args.keepMaps)
|
||||
.then(() => {
|
||||
utils.ipcSend<void>(request.responceChannel, { success: true });
|
||||
})
|
||||
.catch(err => {
|
||||
utils.ipcSend<void>(request.responceChannel, { success: true, error: err });
|
||||
});
|
||||
reply(await maps.exportMaps(args.version, args.maps, args.outPath));
|
||||
});
|
||||
|
||||
ipc.on("unlink-version-maps", async (request: IpcRequest<{ version: BSVersion; keepMaps: boolean }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("bs-maps.import-maps", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(maps.importMaps(args.paths, args.version));
|
||||
})
|
||||
|
||||
maps.unlinkVersionMaps(request.args.version, request.args.keepMaps)
|
||||
.then(() => {
|
||||
utils.ipcSend<void>(request.responceChannel, { success: true });
|
||||
})
|
||||
.catch(err => {
|
||||
utils.ipcSend<void>(request.responceChannel, { success: true, error: err });
|
||||
});
|
||||
ipc.on("bs-maps.download-map", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.downloadMap(args.map, args.version)));
|
||||
});
|
||||
|
||||
ipc.on("delete-maps", async (request: IpcRequest<BsmLocalMap[]>, reply) => {
|
||||
ipc.on("last-downloaded-map", (_, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(maps.deleteMaps(request.args));
|
||||
reply(maps.lastDownloadedMap$);
|
||||
})
|
||||
|
||||
ipc.on("one-click-install-map", (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.oneClickDownloadMap(args)))
|
||||
});
|
||||
|
||||
ipc.on("export-maps", async (request: IpcRequest<{ version: BSVersion; maps: BsmLocalMap[]; outPath: string }>, reply) => {
|
||||
ipc.on("register-maps-deep-link", (_, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(await maps.exportMaps(request.args.version, request.args.maps, request.args.outPath));
|
||||
});
|
||||
const { error, result } = tryit(() => maps.enableDeepLinks());
|
||||
|
||||
ipc.on("download-map", async (request: IpcRequest<{ map: BsvMapDetail; version: BSVersion }>, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.downloadMap(request.args.map, request.args.version)));
|
||||
});
|
||||
|
||||
ipc.on("one-click-install-map", async (request: IpcRequest<BsvMapDetail>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
|
||||
maps.oneClickDownloadMap(request.args)
|
||||
.then(() => {
|
||||
utils.ipcSend(request.responceChannel, { success: true });
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
utils.ipcSend(request.responceChannel, { success: false, error: err });
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("register-maps-deep-link", async (request: IpcRequest<void>) => {
|
||||
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 });
|
||||
if(error) {
|
||||
return reply(throwError(() => error));
|
||||
}
|
||||
|
||||
reply(of(result));
|
||||
});
|
||||
|
||||
ipc.on("unregister-maps-deep-link", async (request: IpcRequest<void>) => {
|
||||
ipc.on("unregister-maps-deep-link", (_, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
const utils = UtilsService.getInstance();
|
||||
const { error, result } = tryit(() => maps.disableDeepLinks());
|
||||
|
||||
try {
|
||||
const res = maps.disableDeepLinks();
|
||||
utils.ipcSend(request.responceChannel, { success: true, data: res });
|
||||
} catch (e) {
|
||||
utils.ipcSend(request.responceChannel, { success: false });
|
||||
if(error) {
|
||||
return reply(throwError(() => error));
|
||||
}
|
||||
|
||||
reply(of(result));
|
||||
});
|
||||
|
||||
ipc.on("is-map-deep-links-enabled", async (request: IpcRequest<void>) => {
|
||||
ipc.on("is-map-deep-links-enabled", (_, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
const utils = UtilsService.getInstance();
|
||||
const { error, result } = tryit(() => maps.isDeepLinksEnabled());
|
||||
|
||||
try {
|
||||
const res = maps.isDeepLinksEnabled();
|
||||
utils.ipcSend(request.responceChannel, { success: true, data: res });
|
||||
} catch (e) {
|
||||
utils.ipcSend(request.responceChannel, { success: false });
|
||||
if(error) {
|
||||
return reply(throwError(() => error));
|
||||
}
|
||||
|
||||
reply(of(result));
|
||||
});
|
||||
|
||||
ipc.on("get-version-maps-path", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.getMapsFolderPath(req.args)));
|
||||
});
|
||||
ipc.on("get-maps-info-from-cache", (args, reply) => {
|
||||
const songsCache = SongDetailsCacheService.getInstance();
|
||||
|
||||
const res = (args ?? []).reduce((acc, hash) => {
|
||||
const songDetails = songsCache.getSongDetails(hash);
|
||||
|
||||
if(songDetails){
|
||||
acc.push(songDetails);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SongDetails[]);
|
||||
|
||||
reply(of(res));
|
||||
|
||||
})
|
||||
|
||||
@@ -1,82 +1,45 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { MSModel, MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { LocalModelsManagerService } from "../services/additional-content/local-models-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
|
||||
import { from, of } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("one-click-install-model", async (event, request: IpcRequest<MSModel>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("one-click-install-model", (args, reply) => {
|
||||
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 });
|
||||
});
|
||||
reply(from(models.oneClickDownloadModel(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("register-models-deep-link", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("register-models-deep-link", (_, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(of(models.enableDeepLinks()));
|
||||
});
|
||||
|
||||
ipc.on("unregister-models-deep-link", (_, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(of(models.disableDeepLinks()));
|
||||
});
|
||||
|
||||
ipc.on("is-models-deep-links-enabled", (_, reply) => {
|
||||
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 });
|
||||
}
|
||||
reply(of(maps.isDeepLinksEnabled()));
|
||||
});
|
||||
|
||||
ipcMain.on("unregister-models-deep-link", async (event, request: IpcRequest<void>) => {
|
||||
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<void>) => {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on<ModelDownload>("download-model", async (req, reply) => {
|
||||
ipc.on("download-model", (args, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(models.downloadModel(req.args.model, req.args.version));
|
||||
reply(models.downloadModel(args.model, args.version));
|
||||
});
|
||||
|
||||
ipc.on<{ version: BSVersion; type: MSModelType }>("get-version-models", async (req, reply) => {
|
||||
ipc.on("get-version-models", (args, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
const res = await models.getModels(req.args.type, req.args.version);
|
||||
reply(res);
|
||||
reply(from(models.getModels(args.type, args.version)));
|
||||
});
|
||||
|
||||
ipc.on<{ version: BSVersion; models: BsmLocalModel[]; outPath: string }>("export-models", async (req, reply) => {
|
||||
ipc.on("export-models", (args, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(models.exportModels(req.args.outPath, req.args.version, req.args.models));
|
||||
reply(models.exportModels(args.outPath, args.version, args.models));
|
||||
});
|
||||
|
||||
ipc.on<BsmLocalModel[]>("delete-models", async (req, reply) => {
|
||||
ipc.on("delete-models", (args, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(models.deleteModels(req.args));
|
||||
reply(models.deleteModels(args));
|
||||
});
|
||||
|
||||
@@ -1,67 +1,30 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { InstallModsResult } from "shared/models/mods";
|
||||
import log from "electron-log";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on<BSVersion>("get-available-mods", (req, reply) => {
|
||||
ipc.on("get-available-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.getAvailableMods(req.args)));
|
||||
reply(from(modsManager.getAvailableMods(args)));
|
||||
});
|
||||
|
||||
ipc.on<BSVersion>("get-installed-mods", (req, reply) => {
|
||||
ipc.on("get-installed-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.getInstalledMods(req.args)));
|
||||
reply(from(modsManager.getInstalledMods(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("install-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager
|
||||
.installMods(request.args.mods, request.args.version)
|
||||
.then(nbInstalled => {
|
||||
utils.ipcSend<InstallModsResult>(request.responceChannel, { success: true, data: nbInstalled });
|
||||
})
|
||||
.catch(err => {
|
||||
utils.ipcSend(request.responceChannel, { success: false, error: err });
|
||||
log.error("ipc", "install-mods", err, request);
|
||||
});
|
||||
reply(modsManager.installMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("uninstall-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager
|
||||
.uninstallMods(request.args.mods, request.args.version)
|
||||
.then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, { success: true, data: nbInstalled });
|
||||
})
|
||||
.catch(err => {
|
||||
utils.ipcSend(request.responceChannel, { success: false, error: err });
|
||||
log.error("ipc", "uninstall-mods", err, request);
|
||||
});
|
||||
reply(modsManager.uninstallMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-all-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("uninstall-all-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager
|
||||
.uninstallAllMods(request.args)
|
||||
.then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, { success: true, data: nbInstalled });
|
||||
})
|
||||
.catch(err => {
|
||||
utils.ipcSend(request.responceChannel, { success: false, error: err });
|
||||
log.error("ipc", "uninstall-all-mods", err, request);
|
||||
});
|
||||
reply(modsManager.uninstallAllMods(args));
|
||||
});
|
||||
|
||||
@@ -1,48 +1,76 @@
|
||||
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";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, lastValueFrom, mergeMap, of } from "rxjs";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on<string>("one-click-install-playlist", (req, reply) => {
|
||||
ipc.on("one-click-install-playlist", (args, reply) => {
|
||||
const mapsManager = LocalPlaylistsManagerService.getInstance();
|
||||
reply(mapsManager.oneClickInstallPlaylist(req.args));
|
||||
reply(mapsManager.oneClickInstallPlaylist(args));
|
||||
});
|
||||
|
||||
ipcMain.on("register-playlists-deep-link", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("register-playlists-deep-link", (args, reply) => {
|
||||
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 });
|
||||
}
|
||||
reply(of(maps.enableDeepLinks()));
|
||||
});
|
||||
|
||||
ipcMain.on("unregister-playlists-deep-link", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("unregister-playlists-deep-link", (args, reply) => {
|
||||
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 });
|
||||
}
|
||||
reply(of(maps.disableDeepLinks()));
|
||||
});
|
||||
|
||||
ipcMain.on("is-playlists-deep-links-enabled", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("is-playlists-deep-links-enabled", (args, reply) => {
|
||||
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 });
|
||||
}
|
||||
reply(of(maps.isDeepLinksEnabled()));
|
||||
});
|
||||
|
||||
ipc.on("download-playlist", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
return reply(playlists.downloadPlaylist({
|
||||
bplistSource: args.downloadSource,
|
||||
version: args.version,
|
||||
ignoreSongsHashs: args.ignoreSongsHashs,
|
||||
dest: args.dest
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
ipc.on("get-version-playlists-details", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.getVersionPlaylistsDetails(args));
|
||||
});
|
||||
|
||||
ipc.on("delete-playlist", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(playlists.deletePlaylistFile(args.bpList).pipe(mergeMap(() => {
|
||||
if(args.deleteMaps){
|
||||
return maps.deleteMapsFromHashs(args.version, args.bpList.songs.map(s => s.hash));
|
||||
}
|
||||
return of({ current: 0, total: 0 } as Progression);
|
||||
})));
|
||||
});
|
||||
|
||||
ipc.on("export-playlists", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.exportPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("import-playlists", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.importPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("install-playlist-file", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
const promise = async () => {
|
||||
const playlist = await lastValueFrom(playlists.writeBPListFile({ bpList: args.bplist, version: args.version, dest: args.dest}));
|
||||
return playlists.getLocalBPListDetails(playlist);
|
||||
}
|
||||
|
||||
reply(from(promise()));
|
||||
})
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLocalVersionService } from "../services/bs-local-version.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on<BSVersion>("bs.uninstall", (req, reply) => {
|
||||
ipc.on("bs.uninstall", (args, reply) => {
|
||||
const bsLocalVersionService = BSLocalVersionService.getInstance();
|
||||
|
||||
reply(from(bsLocalVersionService.deleteVersion(req.args)));
|
||||
reply(from(bsLocalVersionService.deleteVersion(args)));
|
||||
});
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
import { BsOculusDownloaderService } from "../../services/bs-version-download/bs-oculus-downloader.service";
|
||||
import { BsSteamDownloaderService, DownloadInfo, DownloadSteamInfo } from "../../services/bs-version-download/bs-steam-downloader.service";
|
||||
import { InstallationLocationService } from "../../services/installation-location.service";
|
||||
import { BsSteamDownloaderService } from "../../services/bs-version-download/bs-steam-downloader.service";
|
||||
import { IpcService } from "../../services/ipc.service";
|
||||
import { from, of } from "rxjs";
|
||||
import { BSLocalVersionService, ImportVersionOptions } from "../../services/bs-local-version.service";
|
||||
import { of } from "rxjs";
|
||||
import { BSLocalVersionService } from "../../services/bs-local-version.service";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on<ImportVersionOptions>("import-version", (req, reply) => {
|
||||
ipc.on("import-version", (args, reply) => {
|
||||
const versionManager = BSLocalVersionService.getInstance();
|
||||
reply(versionManager.importVersion(req.args));
|
||||
reply(versionManager.importVersion(args));
|
||||
});
|
||||
|
||||
// #region Steam
|
||||
// #region Steam
|
||||
|
||||
ipc.on("is-dotnet-6-installed", (_, reply) => {
|
||||
const installer = BsSteamDownloaderService.getInstance();
|
||||
reply(from(installer.isDotNet6Installed()));
|
||||
});
|
||||
|
||||
ipc.on("bs-download.installation-folder", (_, reply) => {
|
||||
const installLocation = InstallationLocationService.getInstance();
|
||||
reply(from(installLocation.installationDirectory()));
|
||||
});
|
||||
|
||||
ipc.on<string>("bs-download.set-installation-folder", (req, reply) => {
|
||||
const installerService = InstallationLocationService.getInstance();
|
||||
reply(from(installerService.setInstallationDirectory(req.args)));
|
||||
});
|
||||
|
||||
ipc.on<DownloadSteamInfo>("auto-download-bs-version", (req, reply) => {
|
||||
ipc.on("auto-download-bs-version", (args, reply) => {
|
||||
const bsInstaller = BsSteamDownloaderService.getInstance();
|
||||
reply(bsInstaller.autoDownloadBsVersion(req.args));
|
||||
reply(bsInstaller.autoDownloadBsVersion(args));
|
||||
});
|
||||
|
||||
ipc.on<DownloadSteamInfo>("download-bs-version", (req, reply) => {
|
||||
ipc.on("download-bs-version", (args, reply) => {
|
||||
const bsInstaller = BsSteamDownloaderService.getInstance();
|
||||
reply(bsInstaller.downloadBsVersion(req.args))
|
||||
reply(bsInstaller.downloadBsVersion(args))
|
||||
});
|
||||
|
||||
ipc.on<DownloadSteamInfo>("download-bs-version-qr", (req, reply) => {
|
||||
ipc.on("download-bs-version-qr", (args, reply) => {
|
||||
const bsInstaller = BsSteamDownloaderService.getInstance();
|
||||
reply(bsInstaller.downloadBsVersionWithQRCode(req.args))
|
||||
reply(bsInstaller.downloadBsVersionWithQRCode(args))
|
||||
});
|
||||
|
||||
ipc.on("stop-download-bs-version", (_, reply) => {
|
||||
@@ -49,23 +33,18 @@ ipc.on("stop-download-bs-version", (_, reply) => {
|
||||
reply(of(bsInstaller.stopDownload()));
|
||||
});
|
||||
|
||||
ipc.on<string>("send-input-bs-download", (req, reply) => {
|
||||
ipc.on("send-input-bs-download", (args, reply) => {
|
||||
const bsInstaller = BsSteamDownloaderService.getInstance();
|
||||
reply(of(bsInstaller.sendInput(req.args)));
|
||||
reply(of(bsInstaller.sendInput(args)));
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Oculus
|
||||
|
||||
ipc.on<DownloadInfo>("bs-oculus-download", async (req, reply) => {
|
||||
ipc.on("bs-oculus-download", async (args, reply) => {
|
||||
const oculusDownloader = BsOculusDownloaderService.getInstance();
|
||||
reply(oculusDownloader.downloadVersion(req.args));
|
||||
});
|
||||
|
||||
ipc.on<DownloadInfo>("bs-oculus-auto-download", async (req, reply) => {
|
||||
const oculusDownloader = BsOculusDownloaderService.getInstance();
|
||||
reply(oculusDownloader.autoDownloadVersion(req.args));
|
||||
reply(oculusDownloader.downloadVersion(args));
|
||||
});
|
||||
|
||||
ipc.on("bs-oculus-stop-download", async (_, reply) => {
|
||||
@@ -73,14 +52,4 @@ ipc.on("bs-oculus-stop-download", async (_, reply) => {
|
||||
reply(of(oculusDownloader.stopDownload()));
|
||||
});
|
||||
|
||||
ipc.on("bs-oculus-has-auth-token", async (_, reply) => {
|
||||
const oculusDownloader = BsOculusDownloaderService.getInstance();
|
||||
reply(from(oculusDownloader.getAuthToken().then(token => !!token)));
|
||||
});
|
||||
|
||||
ipc.on("bs-oculus-clear-auth-token", async (_, reply) => {
|
||||
const oculusDownloader = BsOculusDownloaderService.getInstance();
|
||||
reply(from(oculusDownloader.clearAuthToken()));
|
||||
});
|
||||
|
||||
// #endregion
|
||||
// #endregion
|
||||
|
||||
@@ -1,173 +1,69 @@
|
||||
import { ipcMain, shell } from "electron";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { shell } from "electron";
|
||||
import { BSVersionLibService } from "../services/bs-version-lib.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { BSLocalVersionService } from "../services/bs-local-version.service";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
import path from "path";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService, LinkOptions } from "../services/folder-linker.service";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/local-maps-manager.service";
|
||||
import { readJSON, writeJSON } from "fs-extra";
|
||||
import log from "electron-log";
|
||||
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
|
||||
import { pathExists } from "fs-extra";
|
||||
import { VersionFolderLinkerService } from "../services/version-folder-linker.service";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("bs-version.get-version-dict", (_event, req: IpcRequest<void>) => {
|
||||
BSVersionLibService.getInstance()
|
||||
.getAvailableVersions()
|
||||
.then(versions => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: true, data: versions });
|
||||
})
|
||||
.catch(() => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: false });
|
||||
});
|
||||
ipc.on("bs-version.get-version-dict", (_, reply) => {
|
||||
const versionsLib = BSVersionLibService.getInstance();
|
||||
reply(from(versionsLib.getAvailableVersions()));
|
||||
});
|
||||
|
||||
ipcMain.on("bs-version.installed-versions", async (_event, req: IpcRequest<void>) => {
|
||||
BSLocalVersionService.getInstance()
|
||||
.getInstalledVersions()
|
||||
.then(versions => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: true, data: versions });
|
||||
})
|
||||
.catch(() => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: false });
|
||||
});
|
||||
ipc.on("bs-version.installed-versions", (_, reply) => {
|
||||
const versions = BSLocalVersionService.getInstance();
|
||||
reply(from(versions.getInstalledVersions()));
|
||||
});
|
||||
|
||||
ipcMain.on("bs-version.open-folder", async (_event, req: IpcRequest<BSVersion>) => {
|
||||
const localVersionService = BSLocalVersionService.getInstance();
|
||||
const versionFolder = await localVersionService.getVersionPath(req.args);
|
||||
if (!(await pathExist(versionFolder))) return;
|
||||
shell.openPath(versionFolder);
|
||||
});
|
||||
|
||||
ipcMain.on("bs-version.edit", async (__event, req: IpcRequest<{ version: BSVersion; name: string; color: string }>) => {
|
||||
BSLocalVersionService.getInstance()
|
||||
.editVersion(req.args.version, req.args.name, req.args.color)
|
||||
.then(res => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: !!res, data: res });
|
||||
})
|
||||
.catch((error: BsmException) => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: false, error });
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("bs-version.clone", async (_event, req: IpcRequest<{ version: BSVersion; name: string; color: string }>) => {
|
||||
BSLocalVersionService.getInstance()
|
||||
.cloneVersion(req.args.version, req.args.name, req.args.color)
|
||||
.then(res => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: !!res, data: res });
|
||||
})
|
||||
.catch((error: BsmException) => {
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, { success: false, error });
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("get-version-full-path", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(localVersions.getVersionPath(req.args)));
|
||||
});
|
||||
|
||||
ipc.on("relative-version-path-to-full", async (req: IpcRequest<{ version: BSVersion; relative: string }>, reply) => {
|
||||
path.isAbsolute(req.args.relative) && reply(from(Promise.resolve(req.args.relative)));
|
||||
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
const promise = localVersions
|
||||
.getVersionPath(req.args.version)
|
||||
.catch(() => null)
|
||||
.then(versionPath => {
|
||||
return path.join(versionPath, req.args.relative);
|
||||
});
|
||||
ipc.on("bs-version.open-folder", (args, reply) => {
|
||||
const versions = BSLocalVersionService.getInstance();
|
||||
const promise = versions.getVersionPath(args).then(versionFolder => {
|
||||
if(!versionFolder || !pathExists(versionFolder)) return;
|
||||
shell.openPath(versionFolder);
|
||||
})
|
||||
reply(from(promise));
|
||||
});
|
||||
|
||||
ipc.on("full-version-path-to-relative", async (req: IpcRequest<{ version: BSVersion; fullPath: string }>, reply) => {
|
||||
ipc.on("bs-version.edit", (args, reply) => {
|
||||
const versions = BSLocalVersionService.getInstance();
|
||||
reply(from(versions.editVersion(args.version, args.name, args.color)));
|
||||
});
|
||||
|
||||
ipc.on("bs-version.clone", (args, reply) => {
|
||||
const versions = BSLocalVersionService.getInstance();
|
||||
reply(from(versions.cloneVersion(args.version, args.name, args.color)));
|
||||
});
|
||||
|
||||
ipc.on("get-version-full-path", (args, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
const promise = localVersions
|
||||
.getVersionPath(req.args.version)
|
||||
.catch(() => null)
|
||||
.then(versionPath => {
|
||||
return path.relative(versionPath, req.args.fullPath);
|
||||
});
|
||||
reply(from(promise));
|
||||
reply(from(localVersions.getVersionPath(args)));
|
||||
});
|
||||
|
||||
ipc.on("get-linked-folders", async (req: IpcRequest<{ version: BSVersion; options?: { relative?: boolean } }>, reply) => {
|
||||
ipc.on("full-version-path-to-relative", (args, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(localVersions.getVersionPath(args.version).then(versionPath => path.relative(versionPath, args.fullPath))));
|
||||
});
|
||||
|
||||
ipc.on("get-linked-folders", (args, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.getLinkedFolders(req.args.version, req.args.options)));
|
||||
reply(from(versionLinker.getLinkedFolders(args.version, args.options)));
|
||||
});
|
||||
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string; options?: LinkOptions }>, reply) => {
|
||||
req.args.options ??= {};
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
|
||||
if (req.args.folder.includes(relativeMapsFolder)) {
|
||||
return reply(from(linker.linkFolder(req.args.folder, { keepContents: req.args.options?.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER })));
|
||||
}
|
||||
|
||||
if (req.args.folder.includes("UserData")) {
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
const res = from(linker.linkFolder(req.args.folder, req.args.options));
|
||||
|
||||
const jsonIPAPath = path.join(req.args.folder, "Beat Saber IPA.json");
|
||||
|
||||
if (!(await pathExist(jsonIPAPath))) {
|
||||
return reply(res);
|
||||
}
|
||||
|
||||
await res.toPromise();
|
||||
|
||||
try {
|
||||
const ipaData = (await readJSON(jsonIPAPath)) ?? ({} as any);
|
||||
ipaData.YeetMods = false;
|
||||
await writeJSON(jsonIPAPath, ipaData, { spaces: 4 });
|
||||
} catch (e) {
|
||||
log.error("Disable YeetMods", e);
|
||||
}
|
||||
|
||||
reply(res);
|
||||
});
|
||||
|
||||
ipc.on("link-version-folder-action", async (req: IpcRequest<VersionLinkerAction>, reply) => {
|
||||
ipc.on("link-version-folder-action", (args, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.doAction(req.args)));
|
||||
reply(from(versionLinker.doAction(args)));
|
||||
});
|
||||
|
||||
ipc.on("is-version-folder-linked", async (req: IpcRequest<{ version: BSVersion; relativeFolder: string }>, reply) => {
|
||||
ipc.on("is-version-folder-linked", (args, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.isFolderLinked(req.args.version, req.args.relativeFolder)));
|
||||
reply(from(versionLinker.isFolderLinked(args.version, args.relativeFolder)));
|
||||
});
|
||||
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{ folder: string; options?: LinkOptions }>, reply) => {
|
||||
req.args.options ??= {};
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
|
||||
if (req.args.folder.includes(relativeMapsFolder)) {
|
||||
return reply(from(linker.unlinkFolder(req.args.folder, { ...req.args.options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER })));
|
||||
}
|
||||
|
||||
if (req.args.folder.includes("UserData")) {
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
reply(from(linker.unlinkFolder(req.args.folder, req.args.options)));
|
||||
});
|
||||
|
||||
ipc.on("relink-all-versions-folders", async (req: IpcRequest<void>, reply) => {
|
||||
ipc.on("relink-all-versions-folders", (_, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.relinkAllVersionsFolders()));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./os-controls-ipcs";
|
||||
import "./bs-installer-ipcs.ts";
|
||||
import "./bs-launcher-ipcs";
|
||||
import "./bs-version-ipcs";
|
||||
import "./bs-uninstall-ipcs";
|
||||
@@ -12,3 +13,4 @@ import "./bs-playlist-ipcs";
|
||||
import "./model-saber.ipcs";
|
||||
import "./bs-model-ipcs";
|
||||
import "./bs-version-download/bs-download-ipcs";
|
||||
import "./static-configuration.ipcs";
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { AutoUpdaterService } from "../services/auto-updater.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
import { from, of } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("download-update", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("download-update", (_, reply) => {
|
||||
const updaterService = AutoUpdaterService.getInstance();
|
||||
const utilsService = UtilsService.getInstance();
|
||||
|
||||
updaterService
|
||||
.downloadUpdate()
|
||||
.then(res => utilsService.ipcSend(request.responceChannel, { success: res }))
|
||||
.catch(() => utilsService.ipcSend(request.responceChannel, { success: false }));
|
||||
reply(updaterService.downloadUpdate());
|
||||
});
|
||||
|
||||
ipc.on("check-update", (_, reply) => {
|
||||
@@ -22,7 +14,7 @@ ipc.on("check-update", (_, reply) => {
|
||||
reply(from(updaterService.isUpdateAvailable()));
|
||||
});
|
||||
|
||||
ipcMain.on("install-update", async (event, request: IpcRequest<void>) => {
|
||||
ipc.on("install-update", (_, reply) => {
|
||||
const updaterService = AutoUpdaterService.getInstance();
|
||||
updaterService.quitAndInstall();
|
||||
reply(of(updaterService.quitAndInstall()));
|
||||
});
|
||||
|
||||
@@ -1,26 +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";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { MSGetQuery } from "shared/models/models/model-saber.model";
|
||||
import { from } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest<string | number>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("ms-get-model-by-id", (args, reply) => {
|
||||
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 });
|
||||
});
|
||||
reply(from(ms.getModelById(args)));
|
||||
});
|
||||
|
||||
ipc.on<MSGetQuery>("search-models", async (req, reply) => {
|
||||
ipc.on("search-models", async (args, reply) => {
|
||||
const ms = ModelSaberService.getInstance();
|
||||
reply(ms.searchModels(req.args));
|
||||
reply(ms.searchModels(args));
|
||||
});
|
||||
|
||||
@@ -1,59 +1,79 @@
|
||||
import { ipcMain, shell, dialog, app, BrowserWindow } from "electron";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { SystemNotificationOptions } from "shared/models/notification/system-notification.model";
|
||||
import { shell, dialog, app, BrowserWindow } from "electron";
|
||||
import { NotificationService } from "../services/notification.service";
|
||||
import { SteamService } from "../services/steam.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, of } from "rxjs";
|
||||
import { readFileSync } from "fs-extra";
|
||||
import log from "electron-log";
|
||||
|
||||
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("new-window", async (event, request: IpcRequest<string>) => {
|
||||
shell.openExternal(request.args);
|
||||
ipc.on("new-window", (args, reply) => {
|
||||
reply(from(shell.openExternal(args)));
|
||||
});
|
||||
|
||||
ipc.on<string>("choose-folder", async (req, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: req.args ?? "" })));
|
||||
ipc.on("open-dialog", (args, reply) => {
|
||||
// Use this ipc instead of "choose-folder" or "choose-file" to have more control over the dialog
|
||||
reply(from(dialog.showOpenDialog(args)));
|
||||
})
|
||||
|
||||
ipc.on("choose-folder", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
|
||||
});
|
||||
|
||||
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.setProgressBar(request.args / 100);
|
||||
ipc.on("choose-file", async (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openFile"], defaultPath: args ?? "" })));
|
||||
});
|
||||
|
||||
ipcMain.on("save-file", async (event, request: IpcRequest<{ filename?: string; filters?: Electron.FileFilter[] }>) => {
|
||||
dialog.showSaveDialog({ properties: ["showOverwriteConfirmation"], defaultPath: request.args.filename, filters: request.args.filters }).then(res => {
|
||||
const utils = UtilsService.getInstance();
|
||||
ipc.on("window.progression",(args, reply, sender) => {
|
||||
BrowserWindow.fromWebContents(sender)?.setProgressBar(args / 100);
|
||||
reply(of(undefined));
|
||||
});
|
||||
|
||||
ipc.on("save-file", (args, reply) => {
|
||||
reply(from(dialog.showSaveDialog({ properties: ["showOverwriteConfirmation"], defaultPath: args.filename, filters: args.filters }).then(res => {
|
||||
if (res.canceled || !res.filePath) {
|
||||
utils.ipcSend(request.responceChannel, { success: false });
|
||||
throw new Error("No file path selected");
|
||||
}
|
||||
UtilsService.getInstance().ipcSend(request.responceChannel, { success: true, data: res.filePath });
|
||||
});
|
||||
return res.filePath;
|
||||
})));
|
||||
});
|
||||
|
||||
ipc.on("current-version", (_, reply) => {
|
||||
reply(of(app.getVersion()));
|
||||
});
|
||||
|
||||
ipcMain.on("open-logs", async (event, request: IpcRequest<void>) => {
|
||||
shell.openPath(app.getPath("logs"));
|
||||
ipc.on("open-logs", (_, reply) => {
|
||||
reply(from(shell.openPath(app.getPath("logs"))));
|
||||
});
|
||||
|
||||
ipcMain.on("notify-system", async (event, request: IpcRequest<SystemNotificationOptions>) => {
|
||||
NotificationService.getInstance().notify(request.args);
|
||||
ipc.on("notify-system", (args, reply) => {
|
||||
const systemNotification = NotificationService.getInstance();
|
||||
reply(of(systemNotification.notify(args)));
|
||||
});
|
||||
|
||||
ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
|
||||
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 });
|
||||
});
|
||||
ipc.on("view-path-in-explorer", (args, reply) => {
|
||||
reply(of(shell.showItemInFolder(args)));
|
||||
});
|
||||
|
||||
ipc.on("choose-image", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openFile", "multiSelections"], filters: [{ name: "Images", extensions: ["jpg", "png", "jpeg"] }] }).then(res => {
|
||||
if (res.canceled || !res.filePaths) {
|
||||
return [];
|
||||
}
|
||||
if(args.base64){
|
||||
return res.filePaths.map(path => Buffer.from(readFileSync(path)).toString("base64"));
|
||||
}
|
||||
return res.filePaths;
|
||||
})));
|
||||
});
|
||||
|
||||
ipc.on("restart-app", (_, reply) => {
|
||||
log.info("App was requested to restart");
|
||||
|
||||
reply(of()); // Reply before restarting to avoid any issue
|
||||
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { of } from "rxjs";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { StaticConfigurationService } from "../services/static-configuration.service";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
const staticConfig = StaticConfigurationService.getInstance();
|
||||
|
||||
ipc.on("static-configuration.get", (args, reply) => {
|
||||
reply(of(staticConfig.get(args)));
|
||||
});
|
||||
|
||||
ipc.on("static-configuration.set", (args, reply) => {
|
||||
reply(of(staticConfig.set(args.key, args.value)));
|
||||
});
|
||||
@@ -1,18 +1,10 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { SupportersService } from "../services/supporters.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
|
||||
ipcMain.on("get-supporters", (event, request: IpcRequest<void>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("get-supporters", (_, reply) => {
|
||||
const supportersService = SupportersService.getInstance();
|
||||
|
||||
supportersService
|
||||
.getSupporters()
|
||||
.then(supporters => {
|
||||
utils.ipcSend(request.responceChannel, { success: true, data: supporters });
|
||||
})
|
||||
.catch(() => {
|
||||
utils.ipcSend(request.responceChannel, { success: false });
|
||||
});
|
||||
reply(from(supportersService.getSupporters()));
|
||||
});
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import { WindowManagerService } from "../services/window-manager.service";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { from, of } from "rxjs";
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
// Native windows control, do not pass through IPC service
|
||||
ipcMain.on("close-window", async (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.close();
|
||||
ipc.on("close-window", (_, reply, sender) => {
|
||||
reply(of(BrowserWindow.fromWebContents(sender)?.close()));
|
||||
});
|
||||
|
||||
ipcMain.on("maximise-window", async (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.maximize();
|
||||
ipc.on("maximise-window", (_, reply, sender) => {
|
||||
reply(of(BrowserWindow.fromWebContents(sender)?.maximize()));
|
||||
});
|
||||
|
||||
ipcMain.on("minimise-window", async (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.minimize();
|
||||
ipc.on("minimise-window", (_, reply, sender) => {
|
||||
reply(of(BrowserWindow.fromWebContents(sender)?.minimize()));
|
||||
});
|
||||
|
||||
ipcMain.on("unmaximise-window", async (event) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.unmaximize();
|
||||
ipc.on("unmaximise-window", (_, reply, sender) => {
|
||||
reply(of(BrowserWindow.fromWebContents(sender)?.unmaximize()));
|
||||
});
|
||||
|
||||
|
||||
ipc.on<AppWindow>("open-window-then-close-all", (req, reply) => {
|
||||
ipc.on("open-window-then-close-all", (args, reply) => {
|
||||
const windowManager = WindowManagerService.getInstance();
|
||||
|
||||
const res = windowManager.openWindow(req.args).then(() => {
|
||||
windowManager.closeAllWindows(req.args);
|
||||
const res = windowManager.openWindow(args).then(() => {
|
||||
windowManager.closeAllWindows(args);
|
||||
});
|
||||
|
||||
reply(from(res));
|
||||
});
|
||||
|
||||
ipc.on<AppWindow>("open-window-or-focus", (req, reply) => {
|
||||
ipc.on("open-window-or-focus", (args, reply) => {
|
||||
const windowManager = WindowManagerService.getInstance();
|
||||
reply(from(windowManager.openWindowOrFocus(req.args)));
|
||||
reply(from(windowManager.openWindowOrFocus(args)));
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import "./ipcs";
|
||||
import { WindowManagerService } from "./services/window-manager.service";
|
||||
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 { LocalMapsManagerService } from "./services/additional-content/maps/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";
|
||||
@@ -21,13 +21,17 @@ import { BSLauncherService } from "./services/bs-launcher/bs-launcher.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { LivShortcut } from "./services/liv/liv-shortcut.service";
|
||||
import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service";
|
||||
import { FileAssociationService } from "./services/file-association.service";
|
||||
import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service";
|
||||
import { readdirSync, statSync, unlinkSync } from "fs-extra";
|
||||
import { StaticConfigurationService } from "./services/static-configuration.service";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||
const staticConfig = StaticConfigurationService.getInstance();
|
||||
|
||||
export const filterStrings = new Set<string>();
|
||||
export const filterPatterns = new Set<RegExp>();
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||
|
||||
// Filter all occulus tokens
|
||||
filterPatterns.add(/FRL\S{10,}/g);
|
||||
|
||||
@@ -35,6 +39,13 @@ initLogger();
|
||||
deleteOlestLogs();
|
||||
deleteOldLogs();
|
||||
|
||||
staticConfig.take("disable-hadware-acceleration", disabled => {
|
||||
if(disabled === true){ // strictly check for true
|
||||
log.info("Disabling hardware acceleration");
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const sourceMapSupport = require("source-map-support");
|
||||
@@ -71,6 +82,15 @@ const initServicesMustBeInitialized = () => {
|
||||
LocalModelsManagerService.getInstance();
|
||||
LivShortcut.getInstance();
|
||||
BSLauncherService.getInstance();
|
||||
SongDetailsCacheService.getInstance();
|
||||
}
|
||||
|
||||
const findDeepLinkInArgs = (args: string[]): string => {
|
||||
return args.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
|
||||
}
|
||||
|
||||
const findAssociatedFileInArgs = (args: string[]): string => {
|
||||
return args.find(arg => FileAssociationService.getInstance().isFileAssociated(arg));
|
||||
}
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -79,13 +99,15 @@ if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", (_, argv) => {
|
||||
const deepLink = argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
|
||||
const deepLink = findDeepLinkInArgs(argv);
|
||||
const associatedFile = findAssociatedFileInArgs(argv);
|
||||
|
||||
if (!deepLink) {
|
||||
return;
|
||||
if (deepLink) {
|
||||
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
||||
} else if (associatedFile) {
|
||||
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
|
||||
}
|
||||
|
||||
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
@@ -95,16 +117,21 @@ if (!gotTheLock) {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
||||
|
||||
|
||||
app.setAppUserModelId(APP_NAME);
|
||||
|
||||
initServicesMustBeInitialized();
|
||||
|
||||
const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
|
||||
const deepLink = findDeepLinkInArgs(process.argv);
|
||||
const associatedFile = findAssociatedFileInArgs(process.argv);
|
||||
|
||||
if (!deepLink) {
|
||||
createWindow();
|
||||
} else {
|
||||
if (deepLink) {
|
||||
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
||||
} else if (associatedFile) {
|
||||
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
|
||||
SteamLauncherService.getInstance().restoreSteamVR();
|
||||
|
||||
@@ -36,8 +36,7 @@ export class Archive {
|
||||
|
||||
public addDirectory(path: string, destPath?: string | false): void {
|
||||
this.directories.push(path);
|
||||
destPath = destPath === false ? false : _path.basename(path);
|
||||
this.archive.directory(path, destPath);
|
||||
this.archive.directory(path, destPath ?? _path.basename(path));
|
||||
}
|
||||
|
||||
public addFile(path: string, destPath?: string): void {
|
||||
@@ -46,13 +45,14 @@ export class Archive {
|
||||
this.archive.file(path, { name: destPath });
|
||||
}
|
||||
|
||||
public finalize(): Observable<Progression> {
|
||||
const progress: Progression = {
|
||||
public finalize(): Observable<Progression<string>> {
|
||||
const progress: Progression<string> = {
|
||||
total: 0,
|
||||
current: 0,
|
||||
data: this.output,
|
||||
};
|
||||
|
||||
return new Observable<Progression>(observer => {
|
||||
return new Observable<Progression<string>>(observer => {
|
||||
(async () => {
|
||||
progress.total = await this.loadTotalFiles();
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { pathExistsSync, readFileSync, writeFileSync } from "fs-extra";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import log from "electron-log";
|
||||
import { Subject, debounceTime } from "rxjs";
|
||||
|
||||
export class JsonCache<T = unknown> {
|
||||
|
||||
private _cache: Record<string, T> = {};
|
||||
private readonly setEvent: Subject<void> = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private readonly jsonPath: string,
|
||||
private readonly options: JsonCacheOptions = { autoSave: true, saveDebounce: 1000}
|
||||
){
|
||||
this.load();
|
||||
|
||||
if(this.options.autoSave){
|
||||
this.setEvent = new Subject<void>();
|
||||
this.setEvent.pipe(debounceTime(this.options.saveDebounce ?? 1000)).subscribe(() => this.save());
|
||||
}
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
try {
|
||||
if(pathExistsSync(this.jsonPath)){
|
||||
this._cache = JSON.parse(readFileSync(this.jsonPath).toString());
|
||||
} else {
|
||||
log.warn("File cache not exist yet", this.jsonPath);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Failed to load cache file", this.jsonPath, error);
|
||||
} finally {
|
||||
this._cache ??= {};
|
||||
}
|
||||
}
|
||||
|
||||
public save(): void {
|
||||
const res = tryit(() => writeFileSync(this.jsonPath, JSON.stringify(this._cache), { flush: true }));
|
||||
if(res.error){
|
||||
log.error("Failed to save cache", this.jsonPath, res.error);
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: string): T {
|
||||
return this._cache[key];
|
||||
}
|
||||
|
||||
public set(key: string, value: T): void {
|
||||
this._cache[key] = value;
|
||||
this.setEvent?.next();
|
||||
}
|
||||
|
||||
public delete(key: string): void {
|
||||
delete this._cache[key];
|
||||
this.setEvent?.next();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._cache = {};
|
||||
this.setEvent?.next();
|
||||
}
|
||||
|
||||
public get cache(): Record<string, T> {
|
||||
return this._cache;
|
||||
}
|
||||
}
|
||||
|
||||
export type JsonCacheOptions = {
|
||||
/**
|
||||
* Load the cache immediately after creating the instance
|
||||
* @default true
|
||||
*/
|
||||
loadImmediately?: boolean;
|
||||
/**
|
||||
* Auto save the cache after a change
|
||||
* @default true
|
||||
*/
|
||||
autoSave?: boolean;
|
||||
/**
|
||||
* Time in ms to wait before saving the cache after a change
|
||||
* @default 1000
|
||||
*/
|
||||
saveDebounce?: number;
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent, webUtils } from "electron";
|
||||
import { ProviderPlatform } from "shared/models/provider-platform.enum";
|
||||
|
||||
const sep = process.platform === ProviderPlatform.WINDOWS ? "\\" : "/";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
platform: process.platform,
|
||||
ipcRenderer: {
|
||||
sendMessage(channel: string, args: unknown[]) {
|
||||
ipcRenderer.send(channel, args);
|
||||
@@ -23,15 +24,14 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
},
|
||||
path: {
|
||||
sep,
|
||||
basename: (path: string): string => {
|
||||
return !path ? "" : path.split(sep).at(-1);
|
||||
},
|
||||
join: (...args: string[]): string => {
|
||||
return args.join(sep);
|
||||
}
|
||||
},
|
||||
window: {
|
||||
close: () => { ipcRenderer.send("close-window"); },
|
||||
minimise: () => { ipcRenderer.send("minimise-window"); },
|
||||
maximise: () => { ipcRenderer.send("maximise-window"); },
|
||||
unmaximise: () => { ipcRenderer.send("unmaximise-window"); },
|
||||
webUtils: {
|
||||
getPathForFile: webUtils.getPathForFile
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } 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 } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../deep-link.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { Observable, lastValueFrom, of } from "rxjs";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
import { deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../folder-linker.service";
|
||||
import { allSettled } from "../../../shared/helpers/promise.helpers";
|
||||
import { splitIntoChunk } from "../../../shared/helpers/array.helpers";
|
||||
import { IpcService } from "../ipc.service";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
|
||||
public static getInstance(): LocalMapsManagerService {
|
||||
if (!LocalMapsManagerService.instance) {
|
||||
LocalMapsManagerService.instance = new LocalMapsManagerService();
|
||||
}
|
||||
return LocalMapsManagerService.instance;
|
||||
}
|
||||
|
||||
public static readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
|
||||
public static readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
||||
public static readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
||||
|
||||
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 readonly ipc: IpcService;
|
||||
private readonly linker = FolderLinkerService.getInstance();
|
||||
|
||||
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.linker = FolderLinkerService.getInstance();
|
||||
this.ipc = IpcService.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<string> {
|
||||
if (version) {
|
||||
return path.join(await this.localVersion.getVersionPath(version), LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
}
|
||||
const sharedMapsPath = path.join(await this.installLocation.sharedContentPath(), LocalMapsManagerService.SHARED_MAPS_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
if (!(await pathExist(sharedMapsPath))) {
|
||||
await ensureFolderExist(sharedMapsPath);
|
||||
}
|
||||
return sharedMapsPath;
|
||||
}
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string> {
|
||||
const mapRawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
|
||||
const hashFile = (filePath: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", resolve);
|
||||
});
|
||||
};
|
||||
|
||||
for (const set of mapRawInfo._difficultyBeatmapSets) {
|
||||
for (const diff of set._difficultyBeatmaps) {
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
await hashFile(diffFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return shasum.digest("hex");
|
||||
}
|
||||
|
||||
private async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
const infoFile = files.find(file => path.basename(file).toLowerCase() === "info.dat");
|
||||
|
||||
if (infoFile === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawInfoString = await readFile(infoFile, { encoding: "utf-8" });
|
||||
|
||||
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, path: mapPath };
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
|
||||
const fileName = `${path.basename(zipUrl, ".zip")}-${crypto.randomUUID()}.zip`;
|
||||
const tempPath = this.utils.getTempPath();
|
||||
await ensureFolderExist(this.utils.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = (await lastValueFrom(this.reqService.downloadFile(zipUrl, dest))).data;
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
|
||||
return { zip, zipPath };
|
||||
}
|
||||
|
||||
private openOneClickDownloadMapWindow(mapId: string, isHash = false): void {
|
||||
this.windows.openWindow("oneclick-download-map.html").then(window => {
|
||||
|
||||
this.ipc.once("one-click-map-info", async (_, reply) => {
|
||||
reply(of({ id: mapId, isHash }));
|
||||
}, window.webContents.ipc);
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress> {
|
||||
const progression: BsmLocalMapsProgress = {
|
||||
total: 0,
|
||||
loaded: 0,
|
||||
maps: [],
|
||||
};
|
||||
|
||||
return new Observable<BsmLocalMapsProgress>(observer => {
|
||||
(async () => {
|
||||
const levelsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
if(!(await pathExist(levelsFolder))) {
|
||||
return observer.complete();
|
||||
}
|
||||
|
||||
const levelsPaths = (await getFoldersInFolder(levelsFolder)) ?? [];
|
||||
progression.total = levelsPaths.length;
|
||||
|
||||
const pathChunks = splitIntoChunk(levelsPaths, 50);
|
||||
|
||||
const loadedMaps: BsmLocalMap[] = [];
|
||||
|
||||
for (const chunk of pathChunks) {
|
||||
const mapsInfo = await allSettled(chunk.map(async mapPath => {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
|
||||
if(!mapInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
return mapInfo;
|
||||
}), {removeFalsy: true});
|
||||
|
||||
loadedMaps.push(...(mapsInfo ?? []));
|
||||
}
|
||||
|
||||
progression.maps = loadedMaps;
|
||||
|
||||
observer.next(progression);
|
||||
})().then(() => {
|
||||
observer.complete();
|
||||
}).catch(e=> {
|
||||
observer.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async versionIsLinked(version: BSVersion): Promise<boolean> {
|
||||
const levelsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
const isPathExist = await pathExist(levelsPath);
|
||||
|
||||
if (!isPathExist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lstatSync(levelsPath).isSymbolicLink();
|
||||
}
|
||||
|
||||
public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void> {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
return this.linker.linkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
|
||||
}
|
||||
|
||||
public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void> {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
return this.linker.unlinkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
|
||||
}
|
||||
|
||||
public deleteMaps(maps: BsmLocalMap[]): Observable<DeleteMapsProgress> {
|
||||
const mapsFolders = maps.map(map => map.path);
|
||||
const mapsHashsToDelete = maps.map(map => map.hash);
|
||||
|
||||
return new Observable<DeleteMapsProgress>(observer => {
|
||||
(async () => {
|
||||
const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 };
|
||||
try {
|
||||
for (const folder of mapsFolders) {
|
||||
const detail = await this.loadMapInfoFromPath(folder);
|
||||
if (!mapsHashsToDelete.includes(detail?.hash)) {
|
||||
continue;
|
||||
}
|
||||
await deleteFolder(folder);
|
||||
progress.deleted++;
|
||||
observer.next(progress);
|
||||
}
|
||||
} catch (e) {
|
||||
observer.error(e);
|
||||
}
|
||||
observer.complete();
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
if (!map.versions.at(0).hash) {
|
||||
throw "Cannot download map, no hash found";
|
||||
}
|
||||
|
||||
const zipUrl = map.versions.at(0).downloadURL;
|
||||
const mapFolderName = sanitize(`${map.id}-${map.name}`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const mapPath = path.join(mapsFolder, mapFolderName);
|
||||
|
||||
const installedMap = await pathExists(mapPath).then(exists => {
|
||||
if(!exists){ return null; }
|
||||
return this.loadMapInfoFromPath(mapPath);
|
||||
}).catch(() => null);
|
||||
|
||||
if(map.versions.every(version => version.hash === installedMap?.hash)) {
|
||||
return installedMap;
|
||||
}
|
||||
|
||||
const { zip, zipPath } = await this.downloadMapZip(zipUrl);
|
||||
|
||||
if (!zip) {
|
||||
throw `Cannot download ${zipUrl}`;
|
||||
}
|
||||
|
||||
await ensureFolderExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
await zip.close();
|
||||
await unlink(zipPath);
|
||||
|
||||
const localMap = await this.loadMapInfoFromPath(mapPath);
|
||||
localMap.bsaverInfo = map;
|
||||
|
||||
return localMap;
|
||||
}
|
||||
|
||||
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string) {
|
||||
const archive = new Archive(outPath);
|
||||
|
||||
if (!maps || maps.length === 0) {
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
archive.addDirectory(mapsFolder, false);
|
||||
} else {
|
||||
const mapsFolders = maps.map(map => map.path);
|
||||
mapsFolders.forEach(maps => archive.addDirectory(maps));
|
||||
}
|
||||
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
public async oneClickDownloadMap(map: BsvMapDetail): Promise<void> {
|
||||
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
const downloadedMap = await this.downloadMap(map, versions.pop());
|
||||
const downloadedMapRealFolder = await realpath(path.dirname(downloadedMap.path));
|
||||
|
||||
for (const version of versions) {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
await ensureDir(versionMapsPath);
|
||||
|
||||
if((await realpath(versionMapsPath)) === downloadedMapRealFolder){
|
||||
continue;
|
||||
}
|
||||
|
||||
await copy(downloadedMap.path, path.join(versionMapsPath, path.basename(downloadedMap.path)), { 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.isDeepLinkRegistered(link));
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,13 @@ import sanitize from "sanitize-filename";
|
||||
import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable, Subscription, lastValueFrom, of } from "rxjs";
|
||||
import { Observable, Subscription, lastValueFrom } from "rxjs";
|
||||
import { readdir } from "fs/promises";
|
||||
import md5File from "md5-file";
|
||||
import { allSettled } from "../../../shared/helpers/promise.helpers";
|
||||
import { ModelSaberService } from "../thrid-party/model-saber/model-saber.service";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
import { IpcService } from "../ipc.service";
|
||||
|
||||
export class LocalModelsManagerService {
|
||||
private static instance: LocalModelsManagerService;
|
||||
@@ -40,7 +39,6 @@ export class LocalModelsManagerService {
|
||||
private readonly installPaths: InstallationLocationService;
|
||||
private readonly request: RequestService;
|
||||
private readonly modelSaber: ModelSaberService;
|
||||
private readonly ipc: IpcService;
|
||||
|
||||
private constructor() {
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
@@ -49,26 +47,14 @@ export class LocalModelsManagerService {
|
||||
this.request = RequestService.getInstance();
|
||||
this.installPaths = InstallationLocationService.getInstance();
|
||||
this.modelSaber = ModelSaberService.getInstance();
|
||||
this.ipc = IpcService.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 url = new URL(link);
|
||||
const id = url.pathname.replace("/", "").split("/").at(0);
|
||||
|
||||
this.openOneClickDownloadModelWindow(id, type);
|
||||
});
|
||||
}
|
||||
|
||||
private openOneClickDownloadModelWindow(id: string, type: string) {
|
||||
this.windows.openWindow("oneclick-download-model.html").then(window => {
|
||||
|
||||
this.ipc.once("one-click-model-info", (_, reply) => {
|
||||
reply(of({ id, type }));
|
||||
}, window.webContents.ipc);
|
||||
|
||||
this.windows.openWindow(`oneclick-download-model.html?modelId=${id}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import path from "path";
|
||||
import { Observable, lastValueFrom, tap } from "rxjs";
|
||||
import { Observable, Subject, from, lastValueFrom, takeUntil, tap } 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 { LocalMapsManagerService } from "./local-maps-manager.service";
|
||||
import { LocalMapsManagerService } from "./maps/local-maps-manager.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
|
||||
import { readFileSync } from "fs";
|
||||
import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface";
|
||||
import { readFileSync, Stats } from "fs";
|
||||
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
|
||||
import { copy, copyFile, pathExists, realpath } from "fs-extra";
|
||||
import { Progression, ensureFolderExist, pathExist } from "../../helpers/fs.helpers";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra";
|
||||
import { Progression, getUniqueFileNamePath, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
import { Archive } from "main/models/archive.class";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { findHashInString } from "shared/helpers/string.helpers";
|
||||
import { serializeError } from "serialize-error";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
private static instance: LocalPlaylistsManagerService;
|
||||
@@ -28,96 +42,300 @@ export class LocalPlaylistsManagerService {
|
||||
private readonly DEEP_LINKS = {
|
||||
BeatSaver: "bsplaylist",
|
||||
};
|
||||
private readonly PLAYLIST_FILETYPES = [".bplist", ".json"];
|
||||
|
||||
private readonly versions: BSLocalVersionService;
|
||||
private readonly maps: LocalMapsManagerService;
|
||||
private readonly request: RequestService;
|
||||
private readonly deepLink: DeepLinkService;
|
||||
private readonly fileAssociation: FileAssociationService;
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly bsaver: BeatSaverService;
|
||||
private readonly ipc: IpcService;
|
||||
private readonly songDetails: SongDetailsCacheService;
|
||||
private readonly bsmFs: InstallationLocationService;
|
||||
|
||||
private constructor() {
|
||||
this.maps = LocalMapsManagerService.getInstance();
|
||||
this.versions = BSLocalVersionService.getInstance();
|
||||
this.request = RequestService.getInstance();
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
this.fileAssociation = FileAssociationService.getInstance();
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.bsaver = BeatSaverService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.songDetails = SongDetailsCacheService.getInstance();
|
||||
this.bsmFs = InstallationLocationService.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);
|
||||
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${bplistUrl}`);
|
||||
});
|
||||
|
||||
this.fileAssociation.registerFileAssociation(".bplist", filePath => {
|
||||
log.info("FILE ASSOCIATION RECEIVED", filePath);
|
||||
this.openOneClickDownloadPlaylistWindow(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
private async getPlaylistsFolder(version?: BSVersion) {
|
||||
if (!version) {
|
||||
throw "Playlists are not available to be linked yet";
|
||||
}
|
||||
const rootPath = version ? await this.versions.getVersionPath(version) : this.bsmFs.sharedContentPath();
|
||||
const fullPath = path.join(rootPath, this.PLAYLISTS_FOLDER);
|
||||
|
||||
const versionFolder = await this.versions.getVersionPath(version);
|
||||
await ensureDir(fullPath);
|
||||
|
||||
const folder = path.join(versionFolder, this.PLAYLISTS_FOLDER);
|
||||
|
||||
await ensureFolderExist(folder);
|
||||
|
||||
return folder;
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private async installBPListFile(bslistSource: string, version: BSVersion): Promise<string> {
|
||||
const playlistFolder = await this.getPlaylistsFolder(version);
|
||||
const isLocalFile = await pathExists(bslistSource).catch(e => { log.error(e); return false; });
|
||||
const filename = isLocalFile ? path.basename(bslistSource) : new URL(bslistSource).pathname.split('/').pop()
|
||||
const destFile = path.join(playlistFolder, filename);
|
||||
|
||||
if (isLocalFile) {
|
||||
return copyFile(bslistSource, destFile).then(() => destFile);
|
||||
}
|
||||
return lastValueFrom(this.request.downloadFile(bslistSource, destFile)).then(res => res.data);
|
||||
private acceptPlaylistFiletype(filename: string): boolean {
|
||||
return this.PLAYLIST_FILETYPES.includes(path.extname(filename).toLowerCase());
|
||||
}
|
||||
|
||||
|
||||
private async readPlaylistFile(path: string): Promise<BPList> {
|
||||
if (!(await pathExist(path))) {
|
||||
throw `bplist file not exist at ${path}`;
|
||||
public writeBPListFile(opt: { bpList: BPList, version?: BSVersion, dest?: string }): Observable<LocalBPList> {
|
||||
return new Observable<LocalBPList>(obs => {
|
||||
(async () => {
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}.bplist`);
|
||||
return getUniqueFileNamePath(playlistPath);
|
||||
})();
|
||||
|
||||
writeFileSync(dest, JSON.stringify(opt.bpList, null, 2));
|
||||
|
||||
const localBPList: LocalBPList = { ...opt.bpList, path: dest };
|
||||
|
||||
obs.next(localBPList);
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
private async installBPListFile(opt: {
|
||||
bplistSource: string,
|
||||
version?: BSVersion,
|
||||
dest?: string
|
||||
}): Promise<{path: string, localBPList: LocalBPList}> {
|
||||
const bplist = await this.readPlaylistFromSource(opt.bplistSource);
|
||||
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
|
||||
})();
|
||||
|
||||
writeFileSync(dest, JSON.stringify(bplist, null, 2));
|
||||
|
||||
const localBPList: LocalBPList = { ...bplist, path: dest };
|
||||
|
||||
return { path: dest, localBPList };
|
||||
}
|
||||
|
||||
|
||||
private async readPlaylistFromSource(source: string): Promise<BPList> {
|
||||
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
|
||||
|
||||
if(!isLocalFile && !isValidUrl(source)) {
|
||||
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
|
||||
}
|
||||
|
||||
const rawContent = readFileSync(path).toString();
|
||||
const bpList: BPList = await (async () => {
|
||||
if(isLocalFile){
|
||||
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
|
||||
if(res.error) {
|
||||
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
|
||||
}
|
||||
return res.result;
|
||||
}
|
||||
return this.request.getJSON<BPList>(source);
|
||||
})();
|
||||
|
||||
return JSON.parse(rawContent);
|
||||
if(!bpList?.playlistTitle) {
|
||||
throw new CustomError(`Invalid playlist file (${source})`, "INVALID_PLAYLIST_FILE");
|
||||
}
|
||||
|
||||
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
|
||||
{ ...s, hash: findHashInString(s.hash) ?? s.hash }
|
||||
) : s).filter(Boolean);
|
||||
|
||||
return bpList;
|
||||
}
|
||||
|
||||
private openOneClickDownloadPlaylistWindow(downloadUrl: string): void {
|
||||
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${downloadUrl}`);
|
||||
}
|
||||
|
||||
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
private getLocalBPListsOfFolder(folerPath: string): Observable<Progression<LocalBPList[]>> {
|
||||
return new Observable<Progression<LocalBPList[]>>(obs => {
|
||||
|
||||
const progress: Progression<LocalBPList[]> = { current: 0, total: 0, data: [] };
|
||||
const bpLists: LocalBPList[] = [];
|
||||
|
||||
(async () => {
|
||||
|
||||
if(!pathExistsSync(folerPath)) {
|
||||
throw new Error(`Playlists folder not found ${folerPath}`);
|
||||
}
|
||||
|
||||
const ignoreFunc = (file: string, stats: Stats): boolean => {
|
||||
if(stats.isFile() && !this.acceptPlaylistFiletype(file)) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
const playlistPaths = (await recursiveReadDir(folerPath, [ignoreFunc]));
|
||||
|
||||
progress.total = playlistPaths.length;
|
||||
|
||||
for (const playlistPath of playlistPaths) {
|
||||
const {result: bpList, error} = await tryit(() => this.readPlaylistFromSource(playlistPath));
|
||||
|
||||
if(error) {
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath };
|
||||
bpLists.push(localBpList);
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
progress.data = bpLists;
|
||||
obs.next(progress);
|
||||
})().catch(err => obs.error(err)).finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
private getSongDetailsFromPlaylistSong(song: PlaylistSong): SongDetails | undefined {
|
||||
let songDetails: SongDetails;
|
||||
|
||||
const songHash = findHashInString(song.hash);
|
||||
if(songHash){
|
||||
songDetails = this.songDetails.getSongDetails(song.hash);
|
||||
}
|
||||
|
||||
if(song.key && !songDetails){
|
||||
songDetails = this.songDetails.getSongDetailsById(song.key);
|
||||
}
|
||||
|
||||
const levelIdHash = findHashInString(song.levelid);
|
||||
if(levelIdHash && !songDetails){
|
||||
songDetails = this.songDetails.getSongDetails(levelIdHash);
|
||||
}
|
||||
return songDetails;
|
||||
}
|
||||
|
||||
public getLocalBPListDetails(localBPList: LocalBPList): LocalBPListsDetails {
|
||||
|
||||
const tryExtractPlaylistId = (url: string) => {
|
||||
const regex = /\/id\/(\d+)\/download/;
|
||||
const match = regex.exec(url);
|
||||
return match ? Number(match[1]) : undefined;
|
||||
}
|
||||
|
||||
const bpListDetails: LocalBPListsDetails = {
|
||||
...localBPList,
|
||||
duration: 0,
|
||||
nbMaps: localBPList.songs?.length ?? 0,
|
||||
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
|
||||
}
|
||||
|
||||
const mappers = new Set<number>();
|
||||
|
||||
for(const song of localBPList.songs){
|
||||
const songDetails = this.getSongDetailsFromPlaylistSong(song);
|
||||
|
||||
if(!songDetails) { continue; }
|
||||
|
||||
bpListDetails.duration += songDetails?.duration ? +songDetails.duration : 0;
|
||||
mappers.add(songDetails.uploader?.id);
|
||||
bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
|
||||
song.songDetails = songDetails;
|
||||
}
|
||||
|
||||
bpListDetails.nbMappers = mappers.size;
|
||||
|
||||
return bpListDetails;
|
||||
}
|
||||
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
|
||||
return new Observable<Progression<LocalBPListsDetails[]>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const folder = await this.getPlaylistsFolder(version);
|
||||
const progress$ = this.getLocalBPListsOfFolder(folder);
|
||||
|
||||
const localBPListsRes = (await lastValueFrom(progress$.pipe(tap({ next: progress => obs.next({...progress, data: []}) }))));
|
||||
|
||||
await this.songDetails.waitLoaded(sToMs(15));
|
||||
|
||||
const bpListsDetails: LocalBPListsDetails[] = [];
|
||||
for(const bpList of localBPListsRes.data){
|
||||
bpListsDetails.push(this.getLocalBPListDetails(bpList));
|
||||
}
|
||||
|
||||
obs.next({...localBPListsRes, data: bpListsDetails});
|
||||
|
||||
})().catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylistSongs(localBPList: LocalBPList, ignoreSongsHashs: string[], version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
let destroyed = false;
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const bpListFilePath = await this.installBPListFile(bpListUrl, version);
|
||||
const bpList = await this.readPlaylistFile(bpListFilePath);
|
||||
|
||||
const progress: Progression<DownloadPlaylistProgressionData> = {
|
||||
total: bpList.songs.length,
|
||||
const progress: Progression<DownloadPlaylistProgressionData> = {
|
||||
total: localBPList.songs.length,
|
||||
current: 0,
|
||||
data: {
|
||||
downloadedMaps: [],
|
||||
currentDownload: null,
|
||||
playlistInfos: bpList,
|
||||
playlistPath: bpListFilePath,
|
||||
playlist: this.getLocalBPListDetails(localBPList),
|
||||
}
|
||||
};
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for (const song of bpList.songs) {
|
||||
const [ mapDetail ] = await this.bsaver.getMapDetailsFromHashs([song.hash]);
|
||||
for (const song of localBPList.songs) {
|
||||
|
||||
if(destroyed) { break; }
|
||||
|
||||
if(ignoreSongsHashs.includes(song.hash)) {
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapDetail = await (async () => {
|
||||
let mapDetail: BsvMapDetail;
|
||||
|
||||
const mapHash = findHashInString(song?.hash);
|
||||
if(mapHash) {
|
||||
mapDetail = (await this.bsaver.getMapDetailsFromHashs([findHashInString(mapHash)])).at(0);
|
||||
}
|
||||
|
||||
if(song.key && !mapDetail) {
|
||||
mapDetail = await this.bsaver.getMapDetailsById(song.key);
|
||||
}
|
||||
|
||||
const levelIdHash = findHashInString(song?.levelid);
|
||||
if(levelIdHash && !mapDetail) {
|
||||
mapDetail = (await this.bsaver.getMapDetailsFromHashs([levelIdHash])).at(0);
|
||||
}
|
||||
|
||||
return mapDetail;
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
return undefined as BsvMapDetail;
|
||||
});
|
||||
|
||||
if(!mapDetail) {
|
||||
continue;
|
||||
@@ -134,29 +352,156 @@ export class LocalPlaylistsManagerService {
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
destroyed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylist({ bplistSource, version, ignoreSongsHashs = [], dest }: {
|
||||
bplistSource: string,
|
||||
version?: BSVersion
|
||||
ignoreSongsHashs?: string[]
|
||||
dest?: string
|
||||
}): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
const destroyed$ = new Subject<void>()
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const { localBPList } = await this.installBPListFile({ bplistSource, version, dest });
|
||||
|
||||
await lastValueFrom(this.downloadPlaylistSongs(localBPList, ignoreSongsHashs, version).pipe(
|
||||
tap({ next: p => obs.next(p) }),
|
||||
takeUntil(destroyed$),
|
||||
));
|
||||
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
destroyed$.next();
|
||||
destroyed$.complete();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
public deletePlaylistFile(bpList: LocalBPList): Observable<void>{
|
||||
return from(unlinkPath(bpList.path));
|
||||
}
|
||||
|
||||
public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable<Progression<string>> {
|
||||
|
||||
if(!pathExistsSync(opt.dest)) {
|
||||
throw new CustomError(`Destination folder not found ${opt.dest}`, "DEST_ENOENT");
|
||||
}
|
||||
|
||||
if(opt.bpLists?.length === 0) {
|
||||
throw new CustomError("No playlists to export", "NO_PLAYLISTS");
|
||||
}
|
||||
|
||||
const versionName = opt.version ? opt.version.name ?? opt.version.BSVersion : "Shared";
|
||||
const destName = opt.version ? `${versionName} Playlists` : "Playlists";
|
||||
const zipDest = path.join(opt.dest, `${destName}.zip`);
|
||||
|
||||
const archive = new Archive(zipDest)
|
||||
|
||||
for(const bpList of opt.bpLists) {
|
||||
|
||||
if(!pathExistsSync(bpList.path)) {
|
||||
log.warn(`Playlist file not found for export`, bpList.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
archive.addFile(bpList.path, path.join(this.PLAYLISTS_FOLDER, path.basename(bpList.path)));
|
||||
}
|
||||
|
||||
if(!Array.isArray(opt.playlistsMaps) || opt.playlistsMaps.length === 0){
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
for(const map of opt.playlistsMaps) {
|
||||
|
||||
if(!map?.path || !pathExistsSync(map.path)) {
|
||||
log.warn(`Map file not found for playlist export`, map?.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
archive.addDirectory(
|
||||
map.path,
|
||||
path.join("Maps", path.basename(map.path)) // Dont't know why, but "CustomLevels" not work
|
||||
);
|
||||
}
|
||||
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
public importPlaylists({ version, paths }: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
|
||||
return new Observable<Progression<LocalBPListsDetails>>(obs => {
|
||||
|
||||
let canceled = false;
|
||||
|
||||
(async () => {
|
||||
const bplistPaths = paths.filter(p => this.acceptPlaylistFiletype(p));
|
||||
const progress: Progression<LocalBPListsDetails> = { current: 0, total: bplistPaths.length, data: null };
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for(const playlistPath of bplistPaths) {
|
||||
if(canceled) {
|
||||
log.info("Playlist import canceled");
|
||||
return;
|
||||
}
|
||||
|
||||
const { result: localBPList, error } = await tryit(() => this.installBPListFile({ bplistSource: playlistPath, version }));
|
||||
|
||||
if(error) {
|
||||
progress.lastError = serializeError(CustomError.fromError(error));
|
||||
obs.next(progress);
|
||||
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bpListDetails = this.getLocalBPListDetails(localBPList.localBPList);
|
||||
|
||||
progress.current += 1;
|
||||
progress.data = bpListDetails;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bplistUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
const versions = await this.versions.getInstalledVersions();
|
||||
|
||||
const download$ = this.downloadPlaylist(bpListUrl, versions.pop()).pipe(tap({
|
||||
const download$ = this.downloadPlaylist({ bplistSource: bplistUrl, version: versions.pop() }).pipe(tap({
|
||||
next: progress => obs.next(progress),
|
||||
error: err => obs.error(err),
|
||||
}));
|
||||
|
||||
const { data: {downloadedMaps, playlistPath} } = await lastValueFrom(download$);
|
||||
const { data: {downloadedMaps, playlist} } = await lastValueFrom(download$);
|
||||
|
||||
if(downloadedMaps?.length === 0 || !playlistPath) { return; }
|
||||
if(downloadedMaps?.length === 0 || !playlist.path) { return; }
|
||||
|
||||
const realSourceMapsFolder = await realpath(path.dirname(downloadedMaps[0].path));
|
||||
|
||||
for (const version of versions) {
|
||||
await this.installBPListFile(playlistPath, version);
|
||||
await this.installBPListFile({ bplistSource: playlist.path, version});
|
||||
|
||||
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
|
||||
const realDestMapsFolder = await realpath(versionMapsFolder).catch(e => {
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } 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, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../../window-manager.service";
|
||||
import { Observable, Subject, lastValueFrom } from "rxjs";
|
||||
import { Archive } from "../../../models/archive.class";
|
||||
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../../folder-linker.service";
|
||||
import { allSettled } from "../../../../shared/helpers/promise.helpers";
|
||||
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
|
||||
import { SongDetailsCacheService } from "./song-details-cache.service";
|
||||
import { SongCacheService } from "./song-cache.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
import { FieldRequired } from "shared/helpers/type.helpers";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
|
||||
public static getInstance(): LocalMapsManagerService {
|
||||
if (!LocalMapsManagerService.instance) {
|
||||
LocalMapsManagerService.instance = new LocalMapsManagerService();
|
||||
}
|
||||
return LocalMapsManagerService.instance;
|
||||
}
|
||||
|
||||
public static readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
|
||||
public static readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
||||
public static readonly RELATIVE_MAPS_FOLDER = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
public static readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
||||
|
||||
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 readonly linker: FolderLinkerService;
|
||||
private readonly songDetailsCache: SongDetailsCacheService;
|
||||
private readonly songCache: SongCacheService;
|
||||
|
||||
private readonly _lastDownloadedMap = new Subject<{ map: BsmLocalMap, version?: BSVersion }>();
|
||||
|
||||
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.linker = FolderLinkerService.getInstance();
|
||||
this.songDetailsCache = SongDetailsCacheService.getInstance();
|
||||
this.songCache = SongCacheService.getInstance();
|
||||
|
||||
const handleOneClick = (mapId: string, isHash = false) => {
|
||||
this.windows.openWindow(`oneclick-download-map.html?mapId=${mapId}&isHash=${isHash}`);
|
||||
}
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
|
||||
handleOneClick(new URL(link).host);
|
||||
});
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ScoreSaber, link);
|
||||
handleOneClick(new URL(link).host, true);
|
||||
});
|
||||
}
|
||||
|
||||
public async getMapsFolderPath(version?: BSVersion): Promise<string> {
|
||||
if (version) {
|
||||
return path.join(await this.localVersion.getVersionPath(version), LocalMapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
}
|
||||
const sharedMapsPath = path.join(this.installLocation.sharedContentPath(), LocalMapsManagerService.SHARED_MAPS_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
if (!(await pathExist(sharedMapsPath))) {
|
||||
await ensureFolderExist(sharedMapsPath);
|
||||
}
|
||||
return sharedMapsPath;
|
||||
}
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string> {
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if(!mapInfo || error) {
|
||||
log.error(`Unable to cumpute hash, cannot parse map info at ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Unable to cumpute hash, cannot parse map info at ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
|
||||
const hashFile = (filePath: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data as BinaryLike));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", resolve);
|
||||
});
|
||||
};
|
||||
|
||||
for (const diff of mapInfo.difficulties) {
|
||||
if(diff.beatmapFilename){
|
||||
const diffFilePath = path.join(mapPath, diff.beatmapFilename);
|
||||
await hashFile(diffFilePath);
|
||||
}
|
||||
|
||||
if(diff.lightshowDataFilename) {
|
||||
const lightshowFilePath = path.join(mapPath, diff.lightshowDataFilename);
|
||||
await hashFile(lightshowFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return shasum.digest("hex");
|
||||
}
|
||||
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
||||
return { mapInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) };
|
||||
};
|
||||
|
||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
|
||||
if (cachedMapInfos) {
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||
}
|
||||
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
const infoFile = files.find(file => path.basename(file).toLowerCase() === "info.dat");
|
||||
|
||||
if (infoFile === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawInfoString = await readFile(infoFile, { encoding: "utf-8" });
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if (error) {
|
||||
log.error(`Cannot parse map info.dat. Map path: ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Cannot read map info.dat. Map path: ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
|
||||
const fileName = `${path.basename(zipUrl, ".zip")}-${crypto.randomUUID()}.zip`;
|
||||
const tempPath = this.utils.getTempPath();
|
||||
await ensureFolderExist(this.utils.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
|
||||
const zipPath = (await lastValueFrom(this.reqService.downloadFile(zipUrl, dest))).data;
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
|
||||
return { zip, zipPath };
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress> {
|
||||
const progression: BsmLocalMapsProgress = {
|
||||
total: 0,
|
||||
loaded: 0,
|
||||
maps: [],
|
||||
};
|
||||
|
||||
return new Observable<BsmLocalMapsProgress>(observer => {
|
||||
(async () => {
|
||||
await this.songDetailsCache.waitLoaded(sToMs(30));
|
||||
|
||||
const levelsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
if(!(await pathExist(levelsFolder))) {
|
||||
return observer.complete();
|
||||
}
|
||||
|
||||
const levelsPaths = (await getFoldersInFolder(levelsFolder)) ?? [];
|
||||
progression.total = levelsPaths.length;
|
||||
|
||||
const pathChunks = splitIntoChunk(levelsPaths, 50);
|
||||
|
||||
const loadedMaps: BsmLocalMap[] = [];
|
||||
|
||||
for (const chunk of pathChunks) {
|
||||
const mapsInfo = await allSettled(chunk.map(async mapPath => {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
|
||||
if(!mapInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { mapInfo: mapInfo.mapInfo, hash: mapInfo.hash });
|
||||
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
return mapInfo;
|
||||
}), {removeFalsy: true});
|
||||
|
||||
loadedMaps.push(...(mapsInfo ?? []));
|
||||
}
|
||||
|
||||
progression.maps = loadedMaps;
|
||||
|
||||
observer.next(progression);
|
||||
})().then(() => {
|
||||
observer.complete();
|
||||
}).catch(e=> {
|
||||
observer.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async versionIsLinked(version: BSVersion): Promise<boolean> {
|
||||
const levelsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
const isPathExist = await pathExist(levelsPath);
|
||||
|
||||
if (!isPathExist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lstatSync(levelsPath).isSymbolicLink();
|
||||
}
|
||||
|
||||
public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void> {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
return this.linker.linkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
|
||||
}
|
||||
|
||||
public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void> {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
return this.linker.unlinkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
|
||||
}
|
||||
|
||||
public deleteMaps(maps: FieldRequired<BsmLocalMap, "path">[]): Observable<DeleteMapsProgress> {
|
||||
return new Observable<DeleteMapsProgress>(observer => {
|
||||
const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 };
|
||||
|
||||
(async () => {
|
||||
for (const map of maps) {
|
||||
const mapPath = map.path;
|
||||
|
||||
if (pathExistsSync(mapPath)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.deleted++;
|
||||
}
|
||||
|
||||
observer.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(e => observer.error(e))
|
||||
.finally(() => observer.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public deleteMapsFromHashs(version: BSVersion, hashs: string[]): Observable<Progression> {
|
||||
return new Observable<Progression>(observer => {
|
||||
const progress: Progression = { total: hashs.length, current: 0 };
|
||||
|
||||
(async () => {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo && hashs.includes(mapInfo.hash)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.current++;
|
||||
}
|
||||
|
||||
observer.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(e => observer.error(e))
|
||||
.finally(() => observer.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public async getMapInfoFromHash(hash: string, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
const mapInfo = this.songCache.getMapInfoFromHash(hash);
|
||||
|
||||
const cachedMapPath = (versionMapsPath && mapInfo?.dirname) && path.join(versionMapsPath, mapInfo.dirname);
|
||||
|
||||
if(cachedMapPath && pathExistsSync(cachedMapPath)){
|
||||
return this.loadMapInfoFromPath(cachedMapPath);
|
||||
}
|
||||
|
||||
// if not in cache, search in the folder
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo?.hash === hash) {
|
||||
return mapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
let unsubscribed = false;
|
||||
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
|
||||
let nbImportedMaps = 0;
|
||||
|
||||
(async () => {
|
||||
const mapsPath = await this.getMapsFolderPath(version);
|
||||
for(const zipPath of zipPaths) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
progress = { total: 0, current: 0 };
|
||||
obs.next(progress); // reset progress for each zip
|
||||
|
||||
if(!pathExistsSync(zipPath)) { continue; }
|
||||
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const { result: zipEntries, error } = await tryit(() => zip.entries());
|
||||
|
||||
if(error) {
|
||||
const res = await tryit(() => zip.close());
|
||||
log.error("Could not read zip entries", zipPath, error, res?.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const zipEntriesValues = Object.values(zipEntries);
|
||||
|
||||
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
|
||||
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
|
||||
acc.push(path.dirname(entry.name));
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(mapsFolders.length === 0) {
|
||||
log.warn("No maps \"info.dat\" found in zip", zipPath);
|
||||
}
|
||||
|
||||
progress.total = mapsFolders.length;
|
||||
obs.next(progress);
|
||||
|
||||
for(const folder of mapsFolders) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
await zip.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isRoot = folder === ".";
|
||||
const dest = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : path.join(mapsPath, folder);
|
||||
|
||||
let extract: () => Promise<BsmLocalMap>;
|
||||
|
||||
if(isRoot){
|
||||
const entries = zipEntriesValues.filter(entry => entry.isFile && path.dirname(entry.name) === ".");
|
||||
extract = async () => {
|
||||
await Promise.all(entries.map(entry => {
|
||||
log.info("Extracting", `"${entry.name}"`, "from", `"${zipPath}"`, "into", `"${path.join(dest, path.basename(entry.name))}"`);
|
||||
return zip.extract(entry.name, path.join(dest, path.basename(entry.name)));
|
||||
}));
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
};
|
||||
} else {
|
||||
extract = async () => {
|
||||
log.info("Extracting", `"${folder}"`, "from", `"${zipPath}"`, "into", `"${mapsPath}"`);
|
||||
await zip.extract(folder, dest);
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
const { result: bsmMap, error } = await tryit(extract);
|
||||
|
||||
if(error) {
|
||||
log.error("Could not extract map", zipPath, folder, mapsPath, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
nbImportedMaps++;
|
||||
progress.current++;
|
||||
progress.data = bsmMap;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
await zip.close();
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
if(!nbImportedMaps){
|
||||
throw new CustomError("No \"Info.dat\" file located in any of the zip files", "invalid-zip");
|
||||
}
|
||||
return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips");
|
||||
})
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
unsubscribed = true;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
if (!map.versions.at(0).hash) {
|
||||
throw new Error("Cannot download map, no hash found");
|
||||
}
|
||||
|
||||
log.info("Downloading map", map.name, map.id);
|
||||
|
||||
const zipUrl = map.versions.at(0).downloadURL;
|
||||
const mapFolderName = sanitize(`${map.id}-${map.name}`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const mapPath = path.join(mapsFolder, mapFolderName);
|
||||
|
||||
const installedMap = await pathExists(mapPath).then(exists => {
|
||||
if(!exists){ return null; }
|
||||
return this.loadMapInfoFromPath(mapPath);
|
||||
}).catch(() => null);
|
||||
|
||||
if(map.versions.every(version => version.hash === installedMap?.hash)) {
|
||||
return installedMap;
|
||||
}
|
||||
|
||||
const { zip, zipPath } = await this.downloadMapZip(zipUrl);
|
||||
|
||||
if (!zip) {
|
||||
throw new Error(`Cannot download ${zipUrl}`);
|
||||
}
|
||||
|
||||
await ensureFolderExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
await zip.close();
|
||||
await unlink(zipPath);
|
||||
|
||||
const localMap = await this.loadMapInfoFromPath(mapPath);
|
||||
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
|
||||
|
||||
this._lastDownloadedMap.next({ map: localMap, version });
|
||||
|
||||
return localMap;
|
||||
}
|
||||
|
||||
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string): Promise<Observable<Progression>> {
|
||||
const archive = new Archive(outPath);
|
||||
|
||||
if (!maps || maps.length === 0) {
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
archive.addDirectory(mapsFolder, false);
|
||||
} else {
|
||||
const mapsFolders = maps.map(map => map.path);
|
||||
mapsFolders.forEach(maps => archive.addDirectory(maps));
|
||||
}
|
||||
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
public async oneClickDownloadMap(map: BsvMapDetail): Promise<void> {
|
||||
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
const downloadedMap = await this.downloadMap(map, versions.pop());
|
||||
const downloadedMapRealFolder = await realpath(path.dirname(downloadedMap.path));
|
||||
|
||||
for (const version of versions) {
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
await ensureDir(versionMapsPath);
|
||||
|
||||
if((await realpath(versionMapsPath)) === downloadedMapRealFolder){
|
||||
continue;
|
||||
}
|
||||
|
||||
await copy(downloadedMap.path, path.join(versionMapsPath, path.basename(downloadedMap.path)), { 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.isDeepLinkRegistered(link));
|
||||
}
|
||||
|
||||
public get lastDownloadedMap$(): Observable<{ map: BsmLocalMap, version?: BSVersion }> {
|
||||
return this._lastDownloadedMap.asObservable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CACHE_PATH } from "main/constants";
|
||||
import { JsonCache } from "main/models/json-cache.class";
|
||||
import path from "path";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export class SongCacheService {
|
||||
|
||||
private static instance: SongCacheService;
|
||||
|
||||
public static getInstance(): SongCacheService {
|
||||
if (!SongCacheService.instance) {
|
||||
SongCacheService.instance = new SongCacheService();
|
||||
}
|
||||
return SongCacheService.instance;
|
||||
}
|
||||
|
||||
private readonly MAPS_INFO_CACHE_PATH = path.join(CACHE_PATH, "map-info-cache.json");
|
||||
|
||||
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
|
||||
|
||||
private constructor(){
|
||||
console.log(this.MAPS_INFO_CACHE_PATH);
|
||||
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
|
||||
}
|
||||
|
||||
public getMapInfoFromDirname(dirname: string): CachedMapInfoWithHash {
|
||||
return this.mapsInfoCache.get(dirname);
|
||||
}
|
||||
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedMapInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.mapsInfoCache.cache).find(([, info]) => info.hash === hash);
|
||||
return res ? { dirname: res[0], info: res[1] } : undefined;
|
||||
}
|
||||
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedMapInfoWithHash): void {
|
||||
this.mapsInfoCache.set(dirname, info);
|
||||
}
|
||||
|
||||
public deleteMapInfoFromDirname(dirname: string): void {
|
||||
this.mapsInfoCache.delete(dirname);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CachedMapInfoWithHash = {
|
||||
hash: string;
|
||||
mapInfo: MapInfo;
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
import path from "path";
|
||||
import { ensureDirSync, existsSync, readFile, writeFile } from "fs-extra";
|
||||
import { BehaviorSubject, Observable, catchError, filter, lastValueFrom, of, take, timeout } from "rxjs";
|
||||
import { RequestService } from "../../request.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { CACHE_PATH, HTTP_STATUS_CODES } from "main/constants";
|
||||
import log from "electron-log";
|
||||
import protobuf from "protobufjs";
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import { SongDetails } from "shared/models/maps/song-details-cache/song-details-cache.model";
|
||||
import { inflate } from "pako";
|
||||
import { RawSongDetailsCache } from "shared/models/maps/song-details-cache/raw-song-details-cache.model";
|
||||
import { RawSongDetailsDeserializer } from "shared/models/maps/song-details-cache/raw-song-details-deserializer.class";
|
||||
import { StaticConfigurationService } from "main/services/static-configuration.service";
|
||||
|
||||
export class SongDetailsCacheService {
|
||||
|
||||
private static instance: SongDetailsCacheService;
|
||||
|
||||
public static getInstance(): SongDetailsCacheService {
|
||||
if (!SongDetailsCacheService.instance) {
|
||||
SongDetailsCacheService.instance = new SongDetailsCacheService();
|
||||
}
|
||||
return SongDetailsCacheService.instance;
|
||||
}
|
||||
|
||||
private readonly dataSource = [
|
||||
"https://raw.githubusercontent.com/Zagrios/beat-saber-scraped-maps/master/song_details_cache_v1.gz",
|
||||
"https://cdn.jsdelivr.net/gh/Zagrios/beat-saber-scraped-maps@master/song_details_cache_v1.gz",
|
||||
]
|
||||
|
||||
private readonly PROTO_CACHE_PATH = path.join(CACHE_PATH, "song-details-cache");
|
||||
private readonly etagKey = "song-details-cache-etag";
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private readonly request: RequestService;
|
||||
private readonly utils: UtilsService;
|
||||
|
||||
private songDetailsCache: Record<string, SongDetails> = {};
|
||||
private songDetailsIdIndex: Record<string, SongDetails> = {};
|
||||
private readonly _loaded$ = new BehaviorSubject<boolean>(null);
|
||||
|
||||
private constructor(){
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.request = RequestService.getInstance();
|
||||
this.utils = UtilsService.getInstance();
|
||||
this.loadCache()
|
||||
}
|
||||
|
||||
private async loadCache(): Promise<void> {
|
||||
const protoCacheExists = existsSync(this.PROTO_CACHE_PATH);
|
||||
const etag = protoCacheExists ? this.staticConfig.get(this.etagKey) : null;
|
||||
|
||||
await this.downloadCacheFile(etag).then(etag => {
|
||||
this.staticConfig.set(this.etagKey, etag);
|
||||
log.info("SongDetailsCache downloaded");
|
||||
this.songDetailsIdIndex = this.createIdIndex(this.songDetailsCache);
|
||||
log.info("SongDetailsIdIndex created");
|
||||
}).catch(err => {
|
||||
log.error("Unable to download cache file", err);
|
||||
});
|
||||
|
||||
this.readProtoMessageCacheFile(this.PROTO_CACHE_PATH).then(cache => {
|
||||
this.songDetailsCache = cache;
|
||||
log.info("SongDetailsCache loaded");
|
||||
}).catch(err => {
|
||||
log.error("Failed to read cache file", this.PROTO_CACHE_PATH, err);
|
||||
}).finally(() => {
|
||||
this._loaded$.next(true);
|
||||
})
|
||||
}
|
||||
|
||||
private createIdIndex(songDetailsCache: Record<string, SongDetails>): Record<string, SongDetails> {
|
||||
const res: Record<string, SongDetails> = {};
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for(const hash in songDetailsCache){
|
||||
res[songDetailsCache[hash].id] = songDetailsCache[hash];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private async readProtoMessageCacheFile(filePath: string): Promise<Record<string, SongDetails>> {
|
||||
const protobufRoot = protobuf.loadSync(this.getProtoShemaPath());
|
||||
const cacheMessage = protobufRoot.lookupType("SongDetailsCache");
|
||||
|
||||
const buffer = await readFile(filePath);
|
||||
|
||||
const messageBuffer = cacheMessage.decode(buffer);
|
||||
const messageObj = cacheMessage.toObject(messageBuffer) as RawSongDetailsCache;
|
||||
|
||||
const res: Record<string, SongDetails> = {};
|
||||
|
||||
RawSongDetailsDeserializer.setUploadersList(messageObj.uploaders);
|
||||
RawSongDetailsDeserializer.setDifficultyLabels(messageObj.difficultyLabels);
|
||||
|
||||
for(const rawSong of messageObj.songs){
|
||||
const deserialized = RawSongDetailsDeserializer.deserialize(rawSong);
|
||||
res[deserialized.hash.toLowerCase()] = deserialized;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the GZipped Proto file and write it to the cache destination
|
||||
* @param etag
|
||||
* @returns {string} new etag or the same if the file is the same
|
||||
*/
|
||||
private async downloadCacheFile(etag?: string): Promise<string> {
|
||||
const { buffer, etag: newEtag } = await this.downloadGZCacheFile(etag);
|
||||
|
||||
if(!buffer) { return etag; }
|
||||
|
||||
ensureDirSync(path.dirname(this.PROTO_CACHE_PATH));
|
||||
|
||||
await writeFile(this.PROTO_CACHE_PATH, inflate(buffer), { encoding: "binary" });
|
||||
|
||||
return newEtag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the GZipped Proto file from the sources
|
||||
* @returns {Promise<{ buffer: Buffer, etag: string }>} {\
|
||||
* buffer: GZipper Buffer, will be empty if etag is the same\
|
||||
* etag: ETag of the file, should be never empty\
|
||||
* }
|
||||
*/
|
||||
private async downloadGZCacheFile(etag?: string): Promise<{ buffer: Buffer, etag: string }> {
|
||||
|
||||
let lastError: Error;
|
||||
|
||||
for(const sourceUrl of this.dataSource){
|
||||
|
||||
const { result, error } = await tryit(() => {
|
||||
return lastValueFrom(this.request.downloadBuffer(sourceUrl, {
|
||||
headers: etag ? { "If-None-Match": etag } : {},
|
||||
decompress: false
|
||||
})).then(res => ({ buffer: res.data, request: res.extra}));
|
||||
});
|
||||
|
||||
|
||||
if(error) {
|
||||
lastError = error;
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Downloaded SongDetailCache file from source:", sourceUrl, "ETAG:", result.request.headers.etag, result.request.statusCode);
|
||||
|
||||
return {
|
||||
buffer: result.request.statusCode === HTTP_STATUS_CODES.HTTP_STATUS_NOT_MODIFIED ? null : result.buffer,
|
||||
etag: result.request.headers.etag
|
||||
}
|
||||
}
|
||||
|
||||
log.error("Failed to download SongDetailCache file", etag, lastError);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
private getProtoShemaPath(): string {
|
||||
return this.utils.getAssetsPath(path.join("proto", "song_details_cache_v1.proto"))
|
||||
}
|
||||
|
||||
public get loaded$(): Observable<boolean> {
|
||||
return this._loaded$.pipe(filter(val => typeof val === "boolean"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise that resolves when the cache is loaded (loaded does not mean cache contains data, just the all load process is done)
|
||||
* @param timeoutMs in milliseconds
|
||||
* @throws {TimeoutError} if the cache is not ready after the provided timeout
|
||||
*/
|
||||
public waitLoaded(timeoutMs: number): Promise<boolean> {
|
||||
|
||||
const obs = this.loaded$.pipe(take(1));
|
||||
|
||||
return lastValueFrom(obs.pipe(timeout(timeoutMs), catchError((err => {
|
||||
log.error("Wait loaded SongDetailsCache timed out", err);
|
||||
return of(false);
|
||||
}))));
|
||||
}
|
||||
|
||||
public getSongDetails(hash: string): SongDetails | undefined {
|
||||
return this.songDetailsCache[hash.toLowerCase()];
|
||||
}
|
||||
|
||||
public getSongDetailsById(id: string): SongDetails | undefined {
|
||||
return this.songDetailsIdIndex[id.toLowerCase()];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
|
||||
import log from "electron-log";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { gt } from "semver";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export class AutoUpdaterService {
|
||||
private static instance: AutoUpdaterService;
|
||||
|
||||
private readonly utilsService: UtilsService;
|
||||
|
||||
public static getInstance(): AutoUpdaterService {
|
||||
if (!AutoUpdaterService.instance) {
|
||||
AutoUpdaterService.instance = new AutoUpdaterService();
|
||||
@@ -18,37 +17,46 @@ export class AutoUpdaterService {
|
||||
constructor() {
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
public isUpdateAvailable(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then(info => {
|
||||
const needUpdate = (() => {
|
||||
if (!info?.updateInfo) {
|
||||
return false;
|
||||
}
|
||||
return gt(info.updateInfo.version, autoUpdater.currentVersion.version);
|
||||
})();
|
||||
resolve(needUpdate);
|
||||
})
|
||||
.catch(() => resolve(false));
|
||||
});
|
||||
return autoUpdater.checkForUpdates().then(info => {
|
||||
return !!info?.updateInfo && gt(info.updateInfo.version, autoUpdater.currentVersion.version);
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
public downloadUpdate(): Promise<boolean> {
|
||||
autoUpdater.removeAllListeners("download-progress");
|
||||
autoUpdater.addListener("download-progress", info => {
|
||||
this.utilsService.ipcSend("update-download-progress", { success: true, data: info.percent });
|
||||
});
|
||||
public downloadUpdate(): Observable<Progression> {
|
||||
return new Observable<Progression>(observer => {
|
||||
|
||||
return autoUpdater.downloadUpdate().then(res => !!res && !!res.length);
|
||||
observer.next({ current: 0, total: 0 });
|
||||
|
||||
const progressListener = (progress: ProgressInfo) => {
|
||||
observer.next({ current: progress.transferred, total: progress.total });
|
||||
};
|
||||
|
||||
const downloadedListener = () => {
|
||||
observer.next({ current: 100, total: 100 });
|
||||
};
|
||||
|
||||
autoUpdater.addListener("download-progress", progressListener);
|
||||
autoUpdater.addListener("update-downloaded", downloadedListener);
|
||||
|
||||
const cancelToken = new CancellationToken();
|
||||
|
||||
autoUpdater.downloadUpdate(cancelToken)
|
||||
.catch(err => observer.error(err))
|
||||
.finally(() => observer.complete());
|
||||
|
||||
return () => {
|
||||
cancelToken.cancel();
|
||||
autoUpdater.removeListener("download-progress", progressListener);
|
||||
autoUpdater.removeListener("update-downloaded", downloadedListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public quitAndInstall() {
|
||||
autoUpdater.quitAndInstall();
|
||||
log.info("Quit and install");
|
||||
return autoUpdater.quitAndInstall();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process";
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
|
||||
export abstract class AbstractLauncherService {
|
||||
|
||||
@@ -46,20 +47,47 @@ export abstract class AbstractLauncherService {
|
||||
|
||||
}
|
||||
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBSProcess(bsExePath, args, options);
|
||||
|
||||
let timoutId: NodeJS.Timeout;
|
||||
|
||||
const exit = new Promise<number>((resolve, reject) => {
|
||||
// Don't remove, useful for debugging!
|
||||
// process.stdout.on("data", (data) => {
|
||||
// log.info(`BS stdout: ${data}`);
|
||||
// });
|
||||
// process.stderr.on("data", (data) => {
|
||||
// log.error(`BS stderr: ${data}`);
|
||||
// });
|
||||
|
||||
process.on("error", (err) => {
|
||||
log.error(`Error while launching BS`, err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
process.on("exit", (code) => {
|
||||
log.info(`BS process exit with code ${code}`);
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
const unrefAfter = options?.unrefAfter ?? sToMs(10);
|
||||
|
||||
timoutId = setTimeout(() => {
|
||||
log.error("BS process unref after timeout", unrefAfter);
|
||||
process.unref();
|
||||
process.removeAllListeners();
|
||||
resolve(-1);
|
||||
}, unrefAfter);
|
||||
|
||||
}).finally(() => {
|
||||
clearTimeout(timoutId);
|
||||
});
|
||||
|
||||
return { process, exit };
|
||||
}
|
||||
}
|
||||
|
||||
export type SpawnBsProcessOptions = {
|
||||
unrefAfter?: number;
|
||||
} & SpawnOptionsWithoutStdio;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { WindowManagerService } from "../window-manager.service";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { BSVersionLibService } from "../bs-version-lib.service";
|
||||
import { execOnOs } from "../../helpers/env.helpers";
|
||||
import sharp from "sharp";
|
||||
import { Resvg } from "@resvg/resvg-js";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { SteamLauncherService } from "./steam-launcher.service";
|
||||
import { OculusLauncherService } from "./oculus-launcher.service";
|
||||
@@ -127,22 +127,24 @@ export class BSLauncherService {
|
||||
return res;
|
||||
}
|
||||
|
||||
private createShortcutPngBuffer(color: Color): Promise<Buffer>{
|
||||
|
||||
const svgBuffer = Buffer.from(`
|
||||
private createShortcutPngBuffer(color: Color): Buffer{
|
||||
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.4 406.4" height="406.4" width="406.4">
|
||||
<rect rx="69.453" height="406.4" width="406.4" fill="${color.hex()}"/>
|
||||
<path d="M65.467 60.6H336.4v33.867L200.933 162.2 65.467 94.467z" fill="#fff"/>
|
||||
</svg>
|
||||
`);
|
||||
`;
|
||||
|
||||
return sharp(svgBuffer).resize(256).png().toBuffer();
|
||||
return new Resvg(svg, {
|
||||
fitTo: { mode: "width", value: 256 }
|
||||
}).render().asPng();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .png file for the shortcut with the given color
|
||||
* @param {Color} color
|
||||
* @returns {Promise<string>} Path of the icon
|
||||
* @param {Color} color
|
||||
* @returns {Promise<string>} Path of the icon
|
||||
*/
|
||||
private async createShortcutPng(color: Color): Promise<string>{
|
||||
const pngBuffer = this.createShortcutPngBuffer(color);
|
||||
@@ -211,25 +213,25 @@ export class BSLauncherService {
|
||||
const bsPath: string = await (async () => {
|
||||
const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version);
|
||||
return bsPath ?? this.localVersionService.getVersionPath(launchOption.version);
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
launchOption.version = (await this.localVersionService.getVersionOfBSFolder(bsPath, {
|
||||
steam: launchOption.version.steam,
|
||||
oculus: launchOption.version.oculus,
|
||||
oculus: launchOption.version.oculus,
|
||||
})) ?? launchOption.version;
|
||||
|
||||
launchOption.version = {...(await this.remoteVersion.getVersionDetails(launchOption.version.BSVersion)), ...launchOption.version};
|
||||
|
||||
|
||||
this.ipc.once("shortcut-launch-options", (_data, reply) => {
|
||||
reply(of(launchOption));
|
||||
});
|
||||
|
||||
this.windows.openWindow("shortcut-launch.html");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
type ShortcutParams = {
|
||||
@@ -246,9 +248,9 @@ type ShortcutParams = {
|
||||
|
||||
/**
|
||||
* Create .desktop file for url shortcut (only for linux)
|
||||
* @param {string} shortcutPath
|
||||
* @param options
|
||||
* @returns
|
||||
* @param {string} shortcutPath
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
function createDesktopUrlShortcut(shortcutPath: string, options?: {
|
||||
url: string
|
||||
|
||||
@@ -8,9 +8,9 @@ import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import isElevated from "is-elevated";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { exec } from "child_process";
|
||||
import fs from 'fs';
|
||||
|
||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||
|
||||
@@ -46,13 +46,6 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
});
|
||||
}
|
||||
|
||||
private needStartBsAsAdmin(): Promise<boolean> {
|
||||
return isElevated().then(elevated => {
|
||||
if(elevated){ return false; }
|
||||
return this.steam.isElevated();
|
||||
})
|
||||
}
|
||||
|
||||
private getStartBsAsAdminExePath(): string {
|
||||
return path.join(this.util.getAssetsScriptsPath(), "start_beat_saber_admin.exe");
|
||||
}
|
||||
@@ -75,7 +68,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
return new Observable<BSLaunchEventData>(obs => {(async () => {
|
||||
|
||||
const bsFolderPath = await this.localVersions.getInstalledVersionPath(launchOptions.version);
|
||||
const exePath = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
let exePath = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
|
||||
if(!(await pathExists(exePath))){
|
||||
throw CustomError.fromError(new Error(`Path not exist : ${exePath}`), BSLaunchError.BS_NOT_FOUND);
|
||||
@@ -102,15 +95,68 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
await this.restoreSteamVR().catch(log.error);
|
||||
}
|
||||
|
||||
const launchArgs = this.buildBsLaunchArgs(launchOptions);
|
||||
let launchArgs = this.buildBsLaunchArgs(launchOptions);
|
||||
const steamPath = await this.steam.getSteamPath();
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
};
|
||||
|
||||
// Linux setup
|
||||
if (process.platform === "linux") {
|
||||
if (launchOptions.admin) {
|
||||
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
|
||||
launchOptions.admin = false;
|
||||
}
|
||||
|
||||
// Create the compat data path if it doesn't exist.
|
||||
// If the user never ran Beat Saber through steam before
|
||||
// using bsmanager, it won't exist, and proton will fail
|
||||
// to launch the game.
|
||||
const compatDataPath = `${steamPath}/steamapps/compatdata/${BS_APP_ID}`;
|
||||
if (!fs.existsSync(compatDataPath)) {
|
||||
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
|
||||
fs.mkdirSync(compatDataPath);
|
||||
}
|
||||
|
||||
// proton run BeatSaber.exe
|
||||
launchArgs = [
|
||||
"run",
|
||||
`${exePath}`,
|
||||
...launchArgs,
|
||||
];
|
||||
exePath = launchOptions.protonPath;
|
||||
if (!exePath) {
|
||||
throw CustomError.fromError(new Error("Proton path not set"), BSLaunchError.PROTON_NOT_SET);
|
||||
}
|
||||
|
||||
// Setup Proton environment variables
|
||||
Object.assign(env, {
|
||||
"WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work
|
||||
"STEAM_COMPAT_DATA_PATH": compatDataPath,
|
||||
"STEAM_COMPAT_INSTALL_PATH": bsFolderPath,
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
|
||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||
// Run game in steam environment; fixes #585 for unicode song titles
|
||||
"SteamEnv": "1",
|
||||
// Uncomment these to create a proton log file in the Beat Saber install directory.
|
||||
// "PROTON_LOG": 1,
|
||||
// "PROTON_LOG_DIR": bsFolderPath,
|
||||
});
|
||||
}
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
const spawnOpts = { env, cwd: bsFolderPath };
|
||||
|
||||
const launchPromise = !launchOptions.admin ? (
|
||||
this.launchBs(exePath, launchArgs, { env: {...process.env, "SteamAppId": BS_APP_ID} }).exit
|
||||
this.launchBs(exePath, launchArgs, spawnOpts).exit
|
||||
) : (
|
||||
new Promise<number>(resolve => {
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, { env: {...process.env, "SteamAppId": BS_APP_ID} });
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, spawnOpts);
|
||||
adminProcess.on("error", err => {
|
||||
log.error("Error while starting BS as Admin", err);
|
||||
resolve(-1)
|
||||
|
||||
@@ -6,14 +6,13 @@ import { BS_APP_ID, OCULUS_BS_BACKUP_DIR, OCULUS_BS_DIR } from "../constants";
|
||||
import path from "path";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { lstat, rename } from "fs/promises";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
import { ReadStream, createReadStream, pathExists, readFile, writeFile } from "fs-extra";
|
||||
import { ReadStream, createReadStream, pathExists, pathExistsSync, readFile, writeFile } from "fs-extra";
|
||||
import readline from "readline";
|
||||
import { Observable, Subject, catchError, finalize, from, map, switchMap, throwError } from "rxjs";
|
||||
import { BsStore } from "../../shared/models/bs-store.enum";
|
||||
@@ -92,7 +91,7 @@ export class BSLocalVersionService {
|
||||
}
|
||||
|
||||
public async getVersionOfBSFolder(
|
||||
bsPath: string,
|
||||
bsPath: string,
|
||||
options?: {
|
||||
steam?: boolean;
|
||||
oculus?: boolean;
|
||||
@@ -126,7 +125,7 @@ export class BSLocalVersionService {
|
||||
|
||||
// Will be removed in future version. It just to prepare future features
|
||||
if(!metadata?.id){
|
||||
metadata = await this.initVersionMetadata(folderVersion, metadata ?? { store: BsStore.STEAM });
|
||||
metadata = await this.initVersionMetadata(folderVersion, metadata ?? { store: BsStore.STEAM });
|
||||
}
|
||||
folderVersion.metadata = metadata;
|
||||
|
||||
@@ -182,8 +181,8 @@ export class BSLocalVersionService {
|
||||
|
||||
|
||||
/**
|
||||
* Return path of a version even if it's not installed.
|
||||
* @param {BSVersion} version
|
||||
* Return path of a version even if it's not installed.
|
||||
* @param {BSVersion} version
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getVersionPath(version: BSVersion): Promise<string>{
|
||||
@@ -191,25 +190,25 @@ export class BSLocalVersionService {
|
||||
if(version.oculus){ return this.oculusService.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]); }
|
||||
|
||||
return path.join(
|
||||
await this.installLocationService.versionsDirectory(),
|
||||
this.installLocationService.versionsDirectory(),
|
||||
this.getVersionFolder(version)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return path of an installed version. Returns null if not found.
|
||||
* @param {BSVersion} version
|
||||
* @param {BSVersion} version
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getInstalledVersionPath(version: BSVersion): Promise<string>{
|
||||
const versionPath = await this.getVersionPath(version);
|
||||
if(await pathExists(versionPath)){ return versionPath; }
|
||||
|
||||
const versionFolders = await getFoldersInFolder(await this.installLocationService.versionsDirectory());
|
||||
const versionFolders = await getFoldersInFolder(this.installLocationService.versionsDirectory());
|
||||
|
||||
for(const folder of versionFolders){
|
||||
const stats = await lstat(folder);
|
||||
if(stats.ino === version.ino){
|
||||
if(stats.ino === version.ino){
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
@@ -270,11 +269,11 @@ export class BSLocalVersionService {
|
||||
versions.push(oculusVersion);
|
||||
}
|
||||
|
||||
if (!(await pathExists(await this.installLocationService.versionsDirectory()))) {
|
||||
if (!(await pathExists(this.installLocationService.versionsDirectory()))) {
|
||||
return versions;
|
||||
}
|
||||
|
||||
const folderInInstallation = await getFoldersInFolder(await this.installLocationService.versionsDirectory());
|
||||
const folderInInstallation = await getFoldersInFolder(this.installLocationService.versionsDirectory());
|
||||
|
||||
log.info("Finded versions folders", folderInInstallation);
|
||||
|
||||
@@ -306,54 +305,58 @@ export class BSLocalVersionService {
|
||||
.catch(() => { return false; })
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
if(version.steam || version.oculus){ throw {title: "CantEditSteam", message: "CantEditSteam"} as BsmException; }
|
||||
const oldPath = await this.getVersionPath(version);
|
||||
const editedVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color}
|
||||
: {...version, name: sanitize(name), color};
|
||||
const newPath = await this.getVersionPath(editedVersion);
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
if(version.steam || version.oculus){ throw new CustomError("Do not edit official Beat Saber versions", "CantEditSteam") }
|
||||
const oldPath = await this.getVersionPath(version);
|
||||
const editedVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color}
|
||||
: {...version, name: sanitize(name), color};
|
||||
const newPath = await this.getVersionPath(editedVersion);
|
||||
|
||||
if(oldPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}
|
||||
if(oldPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}
|
||||
|
||||
if((await pathExists(newPath)) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if(pathExistsSync(newPath)){
|
||||
throw new CustomError("Unable to edit the version, path already exist", "VersionAlreadExist");
|
||||
}
|
||||
|
||||
return rename(oldPath, newPath).then(() => {
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("edit version error", err, version, name, color);
|
||||
throw {title: "CantRename", ...err} as BsmException;
|
||||
});
|
||||
}
|
||||
return rename(oldPath, newPath).then(() => {
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("edit version error", err, version, name, color);
|
||||
throw CustomError.fromError(err, "CantRename");
|
||||
});
|
||||
}
|
||||
|
||||
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
const originPath = await this.getVersionPath(version);
|
||||
const cloneVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color, steam: false, oculus: false}
|
||||
: {...version, name: sanitize(name), color, steam: false, oculus: false};
|
||||
const newPath = await this.getVersionPath(cloneVersion);
|
||||
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
const originPath = await this.getVersionPath(version);
|
||||
const cloneVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color, steam: false, oculus: false}
|
||||
: {...version, name: sanitize(name), color, steam: false, oculus: false};
|
||||
const newPath = await this.getVersionPath(cloneVersion);
|
||||
|
||||
if(originPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(cloneVersion);
|
||||
}
|
||||
if(pathExistsSync(newPath)){
|
||||
throw new CustomError("Unable to clone the version, path already exist", "VersionAlreadExist");
|
||||
}
|
||||
|
||||
if(await pathExists(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if(originPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(cloneVersion);
|
||||
}
|
||||
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("clone version error", err, version, name, color);
|
||||
throw {title: "CantClone", ...err} as BsmException
|
||||
})
|
||||
}
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("Error occured while cloning the version", err, version, name, color);
|
||||
throw CustomError.fromError(err, "CantClone");
|
||||
})
|
||||
}
|
||||
|
||||
public importVersion(opt: ImportVersionOptions): Observable<Progression<BSVersion>>{
|
||||
const { fromPath, store } = opt;
|
||||
|
||||