mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2427865cf0 | |||
| a118ec2013 | |||
| 8733a1f6c7 | |||
| 5ed8574a4f | |||
| 5aa491135d | |||
| 06a738a9b7 | |||
| 0a0639225a | |||
| ecb9b9e097 | |||
| 1591e32b4f | |||
| 57ca031e70 | |||
| 49ef8203c8 | |||
| d9a68e91ca | |||
| 2a4e775cec | |||
| 58529a2ea2 | |||
| ff028c77a1 | |||
| 8edfea66f2 | |||
| 378fa1150e | |||
| 92a1232f85 | |||
| 25047b6c0a | |||
| 2512165898 | |||
| bf1d9408d8 | |||
| b076850f63 | |||
| ff8932ea99 | |||
| 6d19d21116 | |||
| 58f8d8e243 | |||
| 06bb2fe416 | |||
| 58211271e6 | |||
| e3dc280d34 | |||
| 1cd3aa762a | |||
| 94f682b6a2 | |||
| ecb1b57c9b | |||
| 21fdea9c35 | |||
| 2073d8ffa4 | |||
| c9c24479d3 | |||
| cf0921051b | |||
| 526a3c3ee8 | |||
| 2003507713 | |||
| 674aab3b4d | |||
| 09c9f19dcb | |||
| 1cb3cc09a9 | |||
| e327074459 | |||
| 56f80f14b2 | |||
| a8fc1e1a08 | |||
| d09ca56eeb | |||
| 1df7045683 | |||
| fd2795a1f7 | |||
| 47ff527f4d | |||
| c6a3a1249c | |||
| 3f1e3e27ba | |||
| 459b7797d0 | |||
| cd89072c4c | |||
| 092f10a6ab | |||
| 4f43f3b833 | |||
| 4a90ac466a | |||
| 5ff8473a85 | |||
| a5784e9b6e | |||
| e62b79222b | |||
| f3359cb43d | |||
| 390167f0b5 | |||
| 9964940020 | |||
| 0df4cef2a6 | |||
| 846e1f55be | |||
| 4dbc552ca9 | |||
| 066ced228d | |||
| 40ebf89389 | |||
| 59892160c3 | |||
| 7f6e87bdfa | |||
| cde12371d7 | |||
| d9b4259b97 | |||
| 9c20640fa7 | |||
| c7c28d6461 | |||
| 3859e04756 | |||
| d210c0f38d | |||
| 10f8685b68 | |||
| a2052d6e0f | |||
| 5b5a4a0817 | |||
| a372707ebd | |||
| b1d9a29da9 | |||
| 46556cf3cc | |||
| 06c242eb01 | |||
| c6500f7cf6 | |||
| b00d41f98f | |||
| e1b55e4019 | |||
| feb6ea3f87 | |||
| 9d945cdcfb | |||
| 5ea12c2c3a | |||
| 0a30867a12 | |||
| f46fee7ee7 | |||
| ed5f8fb073 | |||
| dfe63e525e | |||
| 207d3c782e | |||
| 56b462ad8a | |||
| 2e85c62c02 | |||
| 7fcef774a8 | |||
| 5337c4a61f | |||
| ed158f55c5 | |||
| 568ad7d051 | |||
| e1ca8dbf99 | |||
| 9bbc57008a | |||
| 34dbfe4cf8 | |||
| bd5d29f5db | |||
| faf6c0f121 | |||
| 79be1a842c | |||
| 266feae483 | |||
| cc4061fd38 | |||
| 2714a125f2 | |||
| 2d1dc21942 | |||
| e842cfa7eb | |||
| b2357cd57f | |||
| 7ffeb5e9a6 | |||
| b2f855bec0 | |||
| 3a4ddc13c1 | |||
| f8a62c3e4e | |||
| f380522eac | |||
| f81aa82b30 | |||
| edc4f1cbf4 | |||
| c058b29175 | |||
| 63205b9eaf | |||
| 64abf3ae82 | |||
| 135bff8192 | |||
| e41737decf | |||
| d552d0a011 | |||
| cb32765c50 | |||
| baa1ec60a1 | |||
| 188aa68b86 | |||
| c763e0fefc | |||
| 5ff28cac90 | |||
| 4bd471f51c | |||
| aba9a7de6d | |||
| ba0b40e0ac | |||
| 273d9532a2 | |||
| 6b07fdebe0 | |||
| 02ef273722 | |||
| 4435df2fd7 | |||
| 011e9d1b04 | |||
| 0a067303a2 | |||
| 6ab5b4f1a1 | |||
| 3c818598bf | |||
| ab68d5a0e0 | |||
| 93e12af0c4 | |||
| caf79898cd | |||
| c4a703c214 | |||
| 409267b105 | |||
| 8f1a7f86ac | |||
| 6ddf62c81e | |||
| f69f937197 | |||
| c00051e8a9 | |||
| 7c1b71cd0b | |||
| 7a86016e8e | |||
| abfd0cf4dd | |||
| ea7527d952 | |||
| 3f10d5917f | |||
| aa8ba4ffc2 | |||
| 5dd80efa8c | |||
| 2637726f5a | |||
| e7b3ca4fdf | |||
| a74be4ba9d | |||
| 4615a4a45a | |||
| 092f2af065 | |||
| 2c9e02fd2d | |||
| 14f079b50c | |||
| fa2f69f800 | |||
| c0aea5bdc3 | |||
| b525cac250 | |||
| 941cd96801 | |||
| f800118d10 | |||
| e8c7315bc3 | |||
| df8c7f2376 | |||
| 0594b7eca0 | |||
| 5f46faf6ee | |||
| a7d00f22c5 | |||
| 4f18ba3a42 | |||
| 770a797f6f | |||
| 8c21a0f3f2 | |||
| c4a56d7036 | |||
| 701b3bf141 | |||
| 60af42fe1d | |||
| b38b1e87d3 | |||
| 2c38965d86 | |||
| 8b9b433f64 | |||
| 307bd9f205 | |||
| ed3a09e4a8 | |||
| d01dd2f34a | |||
| 21762a9d94 | |||
| b0a5e806e3 | |||
| 7b0bcf5041 | |||
| c3c3be0823 | |||
| 2af84326c1 | |||
| 41628d4e6e | |||
| 51b4329cfd | |||
| c2b288089b | |||
| b831560b52 | |||
| 3e61a14ca3 | |||
| 6be5c776fa | |||
| 0130334a74 | |||
| 2536b0c4d3 | |||
| 933b642985 | |||
| b97484ace0 | |||
| 7fafbb316e | |||
| a54809ffb5 | |||
| 3a2c92614c | |||
| 187174aa5e | |||
| 37538f60b5 | |||
| 35772768af | |||
| 6fc3fed6dd | |||
| ff3d82238f | |||
| 28407c5cdd | |||
| af6e2053e1 | |||
| 07f2455cce | |||
| 1e7dde613e | |||
| 258931dddd | |||
| 28c71be051 | |||
| 9225f2cd77 |
@@ -0,0 +1,170 @@
|
||||
import fetch from "node-fetch";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const API_BASE = "https://www.patreon.com/api/oauth2/v2";
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env var: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function mapTierTitleToType(tierTitle) {
|
||||
if (!tierTitle) return undefined;
|
||||
const normalized = String(tierTitle).toLowerCase();
|
||||
if (normalized.includes("diamond")) return "diamond";
|
||||
if (normalized.includes("gold")) return "gold";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractPreferredLink(userAttributes) {
|
||||
if (!userAttributes || !userAttributes.social_connections) return undefined;
|
||||
const sc = userAttributes.social_connections;
|
||||
// Prefer Twitch, then YouTube, then Twitter/X
|
||||
const providers = ["twitch", "youtube", "twitter"];
|
||||
for (const provider of providers) {
|
||||
const entry = sc[provider];
|
||||
if (entry && entry.url) return entry.url;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchAllMembers(accessToken, campaignId) {
|
||||
let url =
|
||||
`${API_BASE}/campaigns/${encodeURIComponent(
|
||||
campaignId
|
||||
)}/members` +
|
||||
"?include=user,currently_entitled_tiers" +
|
||||
"&fields[member]=patron_status,full_name,pledge_relationship_start,last_charge_date" +
|
||||
"&fields[user]=full_name,vanity,url,social_connections" +
|
||||
"&fields[tier]=title" +
|
||||
"&page[count]=100";
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": process.env.PATREON_USER_AGENT || "BSManager - Patreon Sync",
|
||||
};
|
||||
|
||||
const allMembers = [];
|
||||
const usersById = new Map();
|
||||
const tiersById = new Map();
|
||||
|
||||
while (url) {
|
||||
const res = await fetch(url, { headers });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Failed to fetch members (${res.status}): ${text}`);
|
||||
}
|
||||
const json = await res.json();
|
||||
|
||||
if (Array.isArray(json.included)) {
|
||||
for (const inc of json.included) {
|
||||
if (inc.type === "user") {
|
||||
usersById.set(inc.id, inc);
|
||||
} else if (inc.type === "tier") {
|
||||
tiersById.set(inc.id, inc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(json.data)) {
|
||||
allMembers.push(...json.data);
|
||||
}
|
||||
|
||||
url = json.links && json.links.next ? json.links.next : undefined;
|
||||
}
|
||||
|
||||
return { members: allMembers, usersById, tiersById };
|
||||
}
|
||||
|
||||
function buildPatreonsList({ members, usersById, tiersById, existingLinkByUsername }) {
|
||||
const uniqueByUsername = new Map();
|
||||
|
||||
for (const member of members) {
|
||||
const attrs = member.attributes || {};
|
||||
if (attrs.patron_status !== "active_patron") continue;
|
||||
|
||||
const userRel = member.relationships && member.relationships.user && member.relationships.user.data;
|
||||
const user = userRel ? usersById.get(userRel.id) : undefined;
|
||||
const userAttrs = (user && user.attributes) || {};
|
||||
|
||||
const tiersRel =
|
||||
member.relationships &&
|
||||
member.relationships.currently_entitled_tiers &&
|
||||
member.relationships.currently_entitled_tiers.data;
|
||||
let type;
|
||||
if (Array.isArray(tiersRel) && tiersRel.length > 0) {
|
||||
// Use first tier title match to determine type
|
||||
const tier = tiersById.get(tiersRel[0].id);
|
||||
type = mapTierTitleToType(tier && tier.attributes && tier.attributes.title);
|
||||
}
|
||||
|
||||
const username = (userAttrs.vanity || userAttrs.full_name || user?.id || "[ ]").trim();
|
||||
const link = type === "diamond" ? extractPreferredLink(userAttrs) : undefined;
|
||||
|
||||
// Determine first payment/relationship start date (fallback to last_charge_date)
|
||||
const firstDateStr = attrs.pledge_relationship_start || attrs.last_charge_date || null;
|
||||
let ts = Number.MAX_SAFE_INTEGER;
|
||||
if (firstDateStr) {
|
||||
const parsed = Date.parse(firstDateStr);
|
||||
ts = Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
|
||||
}
|
||||
|
||||
const entry = { username };
|
||||
if (type) entry.type = type;
|
||||
// Preserve link from existing JSON if present; otherwise use new link
|
||||
const preservedLink = existingLinkByUsername && existingLinkByUsername.get(username);
|
||||
if (preservedLink) entry.link = preservedLink;
|
||||
else if (link) entry.link = link;
|
||||
|
||||
// Ensure uniqueness by username
|
||||
if (!uniqueByUsername.has(username)) {
|
||||
uniqueByUsername.set(username, { entry, ts });
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueByUsername.values())
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
.map((x) => x.entry);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ACCESS_TOKEN = requireEnv("PATREON_ACCESS_TOKEN");
|
||||
const CAMPAIGN_ID = requireEnv("PATREON_CAMPAIGN_ID");
|
||||
|
||||
const jsonPath = path.resolve(process.cwd(), "assets", "jsons", "patreons.json");
|
||||
let existing = [];
|
||||
try {
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const raw = fs.readFileSync(jsonPath, "utf8");
|
||||
existing = JSON.parse(raw);
|
||||
}
|
||||
} catch (_) {
|
||||
existing = [];
|
||||
}
|
||||
const existingLinkByUsername = new Map();
|
||||
if (Array.isArray(existing)) {
|
||||
for (const e of existing) {
|
||||
if (e && e.username && e.link) existingLinkByUsername.set(e.username, e.link);
|
||||
}
|
||||
}
|
||||
|
||||
const { members, usersById, tiersById } = await fetchAllMembers(ACCESS_TOKEN, CAMPAIGN_ID);
|
||||
const patreons = buildPatreonsList({ members, usersById, tiersById, existingLinkByUsername });
|
||||
const output = `${JSON.stringify(patreons, null, "\t")}\n`;
|
||||
fs.writeFileSync(jsonPath, output, "utf8");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Updated ${jsonPath} with ${patreons.length} active patrons.`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 24.11.1
|
||||
cache: "npm"
|
||||
|
||||
# Update package lists
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 24.11.1
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 24.11.1
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 24.11.1
|
||||
cache: "npm"
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Update Patreon supporters
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Every day at 00:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-patreons:
|
||||
if: github.repository == 'Zagrios/bs-manager' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Patreon update script
|
||||
env:
|
||||
PATREON_ACCESS_TOKEN: ${{ secrets.PATREON_ACCESS_TOKEN }}
|
||||
PATREON_CAMPAIGN_ID: ${{ secrets.PATREON_CAMPAIGN_ID }}
|
||||
PATREON_USER_AGENT: BSManager - Patreon Sync (GitHub Action)
|
||||
run: npx ts-node ./.erb/scripts/update-patreon.js
|
||||
|
||||
- name: Create Pull Request (only if changes)
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
commit-message: "[chore] update patreons.json"
|
||||
title: "[chore] update patreons.json"
|
||||
body: |
|
||||
Automated daily update of Patreon supporters.
|
||||
branch: chore/patreon-update
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
assets/jsons/patreons.json
|
||||
+83
-17
@@ -1,8 +1,74 @@
|
||||
## Not released yet ~
|
||||
[PR Merged since the last release](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-18)\
|
||||
[PR Merged since the last alpha](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-11+)
|
||||
[PR Merged since the last release](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-02-03)\
|
||||
[PR Merged since the last alpha](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-02-03)
|
||||
|
||||
## [1.4.18](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.18) (Jan 18, 2025)
|
||||
## [1.5.2](https://github.com/Zagrios/bs-manager/releases/v1.5.2) (Feb 3, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where BSManager was unable to load mods #786
|
||||
|
||||
## [1.5.1](https://github.com/Zagrios/bs-manager/releases/v1.5.1) (Jan 31, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where playlist files were wiped when linking the playlist folder #769 (Thanks to @silentrald)
|
||||
- Fixed missing translations when deleting duplicate maps #774
|
||||
|
||||
### Other changes
|
||||
- Added more logs for file and folder manipulations #772 (Thanks to @silentrald)
|
||||
|
||||
## [1.5.0](https://github.com/Zagrios/bs-manager/releases/v1.5.0) (Jan 30, 2025)
|
||||
|
||||
### Features
|
||||
- A notification for outdated mods now appears when navigating to a Beat Saber version #755
|
||||
- Added Brazilian Portuguese translation #751 (Thanks to @arthurmluz)
|
||||
- Advanced launch arguments now accept environment variables #749 (Thanks to @silentrald)
|
||||
- Added the ability to create shortcuts in the Steam library to launch different versions of Beat Saber #731
|
||||
- Added Italian translation #721 (Thanks to @Davitekk)
|
||||
- Added the ability to log in directly with Meta within BSManager to download Beat Saber #716
|
||||
- Added an advanced setting on Windows to use the system proxy #704 (Thanks to @GoldJohnKing)
|
||||
- Added an advanced launch option to skip starting Steam before launching Beat Saber #664 (Thanks to @LiamillionSS)
|
||||
- You can now filter maps by leaderboard (Beatleader/ScoreSaber/All) #623 (Thanks to @silentrald)
|
||||
- BSManager now goes to the latest launched version page on startup #603 (Thanks to @silentrald)
|
||||
- Drag and drop your mods to install them #575 (Thanks to @silentrald)
|
||||
- Added Korean translation #641 (Thanks to @izunya)
|
||||
- Drag and drop your maps to import them #575 (Thanks to @silentrald)
|
||||
- Added playlist support #503
|
||||
- Added Linux support (Thanks to @silentrald and @Insprill)
|
||||
- Added reset button to reset values when editing a Beat Saber version #438 (Thanks to @Liborsaf)
|
||||
- You can now choose to install only needed mods or reinstall all mods #540
|
||||
- The installation location for contents downloaded by BSManager is now asked after the installation of BSManager or when the previous location can no longer be accessed #566 (Thanks to @silentrald)
|
||||
- Added an advanced setting to disable hardware acceleration for BSManager #572
|
||||
- Added an advanced setting to use symlinks instead of junctions #572
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where, under certain conditions, BSManager could crash if an error occurred while downloading Beat Saber from Steam #722 (Thanks to @silentrald)
|
||||
- Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version #472
|
||||
- Fixed an issue where the launch window from a shortcut would never close #485
|
||||
- Advanced launch arguments are used even when the input field is closed #496
|
||||
- Fixed an issue where having many maps caused some maps to not have their information loaded #503
|
||||
- Fixed an issue where downloading maps sometimes resulted in a timeout error #524
|
||||
- Fixed an issue where the duration of maps could be wrong in some cases #522
|
||||
- Fixed missing styles on the "add" and "setting" buttons in the nav bar #530
|
||||
- Fixed an issue where switching between two Beat Saber versions didn't clear the selected content of the previous version #557
|
||||
- Fixed a potential crash when BeatMods cloud contained invalid mod dependencies #558
|
||||
- Fixed an issue where the folder location for the contents downloaded by BSManager could not be changed if the previous location was unreachable #566 (Thanks to @silentrald)
|
||||
|
||||
### Other changes
|
||||
- A new log file is now created each time BSManager is launched #750 (Thanks to @silentrald)
|
||||
- The advanced launch panel has been completely reworked #709
|
||||
- Link and Unlink modals have been reworked to provide more detailed information to users #699
|
||||
- Some folders are now blacklisted to prevent issues caused by linking incorrect folders #691 (Thanks to @silentrald)
|
||||
- .NET Framework is no longer required #489
|
||||
- Clear English translations for the Steam credentials popup #476 (Thanks to @Aeywoo)
|
||||
- NSFW models are now blurred when browsing for models #493
|
||||
- If map stars are unavailable, NPS or NJS of the map is now used to indicate its difficulty #553
|
||||
- When no mods are available for a Beat Saber version, the version number is now shown #507
|
||||
- After a first full maps loading, maps loads almost instantly #503
|
||||
- Improved performance in lists with lot of contents #503
|
||||
- Improved German translation #559 (Thanks to @kgabriel-dev)
|
||||
- Updated the icon for the button to un-maximize the BSManager window #562 (Thanks to @joriskleiber)
|
||||
|
||||
## [1.4.18](https://github.com/Zagrios/bs-manager/releases/v1.4.18) (Jan 18, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where enabling Oculus sideloading was not working if BSManager installation path contained spaces #735
|
||||
@@ -12,20 +78,20 @@
|
||||
### Other changes
|
||||
- Events occurring in `bs-admin-start.exe` and `oculus-allow-dev-sideloaded.exe` are now logged #735
|
||||
|
||||
## [1.4.17](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.17) (Jan 2, 2025)
|
||||
## [1.4.17](https://github.com/Zagrios/bs-manager/releases/v1.4.17) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where original Steam installations of Beat Saber did not have any mods available
|
||||
- Mod descriptions should no longer contain HTML tags
|
||||
|
||||
## [1.4.16](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.16) (Jan 2, 2025)
|
||||
## [1.4.16](https://github.com/Zagrios/bs-manager/releases/v1.4.16) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Fixed infinite mod loading when a mod file could not be found on BeatMods #725 (Thanks to @silentrald)
|
||||
- Fixed an issue where no mods were shown as installed when a mod dependency could not be found #726 (Thanks to @silentrald)
|
||||
- Fixed minor UI issues
|
||||
|
||||
## [1.4.15](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.15) (Jan 2, 2025)
|
||||
## [1.4.15](https://github.com/Zagrios/bs-manager/releases/v1.4.15) (Jan 2, 2025)
|
||||
|
||||
### Fixes
|
||||
- Use the sideloading feature of Oculus to prevent errors when launching Oculus versions of Beat Saber #720
|
||||
@@ -36,14 +102,14 @@
|
||||
- Switched to the new BeatMods API for loading mods
|
||||
|
||||
|
||||
## [1.4.14](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.14) (Dec 2, 2024)
|
||||
## [1.4.14](https://github.com/Zagrios/bs-manager/releases/v1.4.14) (Dec 2, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue where downloading private playlists through OneClick was always resulting in an error #679 (Thanks to @Top-Cat)
|
||||
- Beat Saber file verification was broken due to an error in DepotDownloader #677
|
||||
|
||||
|
||||
## [1.4.13](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.13) (Nov 26, 2024)
|
||||
## [1.4.13](https://github.com/Zagrios/bs-manager/releases/v1.4.13) (Nov 26, 2024)
|
||||
|
||||
### Features
|
||||
- Added the possibility to unselect all mods from the mods panel #666
|
||||
@@ -61,13 +127,13 @@
|
||||
- Added warnings for outdated versions in the versions view #673
|
||||
|
||||
|
||||
## [1.4.12](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.12) (Nov 6, 2024)
|
||||
## [1.4.12](https://github.com/Zagrios/bs-manager/releases/v1.4.12) (Nov 6, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed OneClick playlist download that has been broken in the previous release #645
|
||||
- Fixed an issue where downloading playlists could fail if the playlist's file contained special characters #645
|
||||
|
||||
## [1.4.11](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.11) (Nov 4, 2024)
|
||||
## [1.4.11](https://github.com/Zagrios/bs-manager/releases/v1.4.11) (Nov 4, 2024)
|
||||
|
||||
### Fixes
|
||||
- BSManager was not working if the path to BSM's installation folder contained URL special characters #636
|
||||
@@ -78,7 +144,7 @@
|
||||
- The "Broken Models" notification has been removed #640
|
||||
|
||||
|
||||
## [1.4.10](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.10) (Oct 27, 2024)
|
||||
## [1.4.10](https://github.com/Zagrios/bs-manager/releases/v1.4.10) (Oct 27, 2024)
|
||||
|
||||
### Fixes
|
||||
- Mods using a manifest file were sometimes not detected as installed #564
|
||||
@@ -92,7 +158,7 @@
|
||||
- The naming scheme for downloaded maps and playlists has been updated to prevent duplicates when downloading maps or playlists from Beat Saber or other tools
|
||||
|
||||
|
||||
## [1.4.9](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.9) (Oct 15, 2024)
|
||||
## [1.4.9](https://github.com/Zagrios/bs-manager/releases/v1.4.9) (Oct 15, 2024)
|
||||
|
||||
### Fixes
|
||||
- Updated DepotDownloader dependency to fix connection errors with Steam
|
||||
@@ -101,14 +167,14 @@
|
||||
- Added support for `info.dat` v4
|
||||
|
||||
|
||||
## [1.4.8](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.8) (Aug 1, 2024)
|
||||
## [1.4.8](https://github.com/Zagrios/bs-manager/releases/v1.4.8) (Aug 1, 2024)
|
||||
|
||||
### Other changes
|
||||
- Added checks to avoid logging sensitive data in log files
|
||||
- The oldest log files are deleted to retain a maximum of 5 log files
|
||||
|
||||
|
||||
## [1.4.7](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.7) (Mar 13, 2024)
|
||||
## [1.4.7](https://github.com/Zagrios/bs-manager/releases/v1.4.7) (Mar 13, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed being unable to download models with BSManager #436
|
||||
@@ -116,7 +182,7 @@
|
||||
### Other changes
|
||||
- Fixed a typo in german translation (Thanks to @fllppi)
|
||||
|
||||
## [1.4.6](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.6) (Mar 6, 2024)
|
||||
## [1.4.6](https://github.com/Zagrios/bs-manager/releases/v1.4.6) (Mar 6, 2024)
|
||||
|
||||
### Features
|
||||
- It's now possible to exclude already installed maps when downloading maps from BSManager #426 (Thanks to @Liborsaf)
|
||||
@@ -125,14 +191,14 @@
|
||||
- Fixed an issue where BSManager couldn't get path to user's documents under certain conditions, preventing any action from the user #431
|
||||
- To prevent the last launched Beat Saber version getting wiped by Oculus auto-updates, the symlink created to launch the version is now deleted after the game stops #432
|
||||
|
||||
## [1.4.5](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.5) (Feb 12, 2024)
|
||||
## [1.4.5](https://github.com/Zagrios/bs-manager/releases/v1.4.5) (Feb 12, 2024)
|
||||
|
||||
### Fixes
|
||||
- Fixed an issue preventing launching downgraded Steam versions under certain conditions #415
|
||||
- Fixed an issue preventing launching downgraded Oculus versions under certain conditions #421
|
||||
- Fixed an issue where SteamVR was not properly restored after launching Beat Saber from a shortcut with FPFC mode #417 (Thanks to @slinkstr)
|
||||
|
||||
## [1.4.4](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.4) (Feb 1, 2024)
|
||||
## [1.4.4](https://github.com/Zagrios/bs-manager/releases/v1.4.4) (Feb 1, 2024)
|
||||
|
||||
### Fixes
|
||||
- When launching an Oculus Beat Saber version, the Oculus app is also started if needed to avoid crashes #405
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+">Report
|
||||
href="https://github.com/Zagrios/bs-manager/issues/new?template=1-bug-report.yaml">Report
|
||||
Bug</a>
|
||||
·
|
||||
<a
|
||||
href="https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+">Request
|
||||
href="https://github.com/Zagrios/bs-manager/issues/new?template=2-feature-request.yaml">Request
|
||||
Feature</a>
|
||||
·
|
||||
<a href="https://github.com/Zagrios/bs-manager/security/policy">Report a security vulnerability</a>
|
||||
@@ -472,7 +472,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</a> - Lead Developer & Founder. (Mathieu Gries)</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>
|
||||
|
||||
@@ -802,8 +802,7 @@
|
||||
"OculusBinaryId": "8189133517853022",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4451340304252928024",
|
||||
"ReleaseDate": "1732107748",
|
||||
"year": "2024",
|
||||
"recommended": true
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.0",
|
||||
@@ -831,5 +830,139 @@
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/04ad25256ffc2686f4ea19830e70df6b2752f728.png",
|
||||
"ReleaseDate": "1738951837",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.3",
|
||||
"BSManifest": "2468336483467342711",
|
||||
"OculusBinaryId": "8747607938672241",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510699509433500685",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/648716b53c17412cd269567ce67d3867514b6acd.png",
|
||||
"ReleaseDate": "1740672033",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.4",
|
||||
"BSManifest": "3323243802033113443",
|
||||
"OculusBinaryId": "8889412401158460",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510702041542166484",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/c27dac7f96e53672d020d97838cec1d5d28e3180.png",
|
||||
"ReleaseDate": "1743515622",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.5",
|
||||
"BSManifest": "2447038720841508806",
|
||||
"OculusBinaryId": "9120095564756808",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/502825275922317447",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/2b5845893d915902106aaf30b6d1a95f68a4b226.png",
|
||||
"ReleaseDate": "1747321305",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.6",
|
||||
"BSManifest": "3050696685250334115",
|
||||
"OculusBinaryId": "9315734585192904",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510708375093248769",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/0f658ed39f480446e0dc5807b1591e8a18aef2f5.png",
|
||||
"ReleaseDate": "1749135144",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.7",
|
||||
"BSManifest": "7263483117834945201",
|
||||
"OculusBinaryId": "9533840436715650",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510710285788516821",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images//32055887/c34cf1f62f18898280e2bf905518ba71cce0cffd.png",
|
||||
"ReleaseDate": "1752159922",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.8",
|
||||
"BSManifest": "8437413909225671968",
|
||||
"OculusBinaryId": "9637940462972313",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510712822654566578",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images//32055887/5e32755428ba8f737bf7e9cb7cfc58465b72818c.png",
|
||||
"ReleaseDate": "1753369967",
|
||||
"year": "2025",
|
||||
"recommended": true
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.9",
|
||||
"BSManifest": "3458990238328802301",
|
||||
"OculusBinaryId": "9907754402657583",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510715359276302413",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/2c6d12b6791daf434c74a5b7ab94a236789c6836.png",
|
||||
"ReleaseDate": "1755776928",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.10",
|
||||
"BSManifest": "6631675572109548834",
|
||||
"OculusBinaryId": "10067683139998041",
|
||||
"ReleaseDate": "1756386614",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.11",
|
||||
"BSManifest": "2631032361926028583",
|
||||
"OculusBinaryId": "23961660520173735",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/539991294395549709",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/a626005dae1a96ad8aafa4e65941e26b7918e073.png",
|
||||
"ReleaseDate": "1758552138",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.12",
|
||||
"BSManifest": "1671670480231783169",
|
||||
"OculusBinaryId": "24069417846064668",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/539991928250303115",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/2a5b01ea0613ac1674cb19e18547d0649ccd7464.png",
|
||||
"ReleaseDate": "1759152049",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.40.13",
|
||||
"BSManifest": "5625229280277602839",
|
||||
"OculusBinaryId": "24341751555497961",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/637948480724140077",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/90da819ce7dd3dbf1600472e0fea4be3c9a28694.png",
|
||||
"ReleaseDate": "1761838104",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.41.1",
|
||||
"BSManifest": "1904061226749256371",
|
||||
"OculusBinaryId": "24664294529910327",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/542249438751490862",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/6760ebbe87eafaf0215ddfac2858b3716844bc9d.png",
|
||||
"ReleaseDate": "1764692794",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.42.0",
|
||||
"BSManifest": "3610593956417791952",
|
||||
"OculusBinaryId": "24766479246358521",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/542251341653738360",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/9b9e66ee161deafcf4d9548ccf08dde9e4ae726a.png",
|
||||
"ReleaseDate": "1766073892",
|
||||
"year": "2025"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.42.1",
|
||||
"BSManifest": "4753635509173254286",
|
||||
"OculusBinaryId": "25154695844203524",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/534373847137255841",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/b5b3f993a2a00861db9fdfb2eed68018e5d012ba.png",
|
||||
"ReleaseDate": "1769692187",
|
||||
"year": "2026"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.42.2",
|
||||
"BSManifest": "9067042658303247735",
|
||||
"OculusBinaryId": "25217144241292017",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/536626281230369168",
|
||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/1161f8316767615d2fbe863673cace231c6d2b6f.png",
|
||||
"ReleaseDate": "1770303705",
|
||||
"year": "2026"
|
||||
}
|
||||
]
|
||||
+38
-102
@@ -1,146 +1,82 @@
|
||||
[
|
||||
{
|
||||
"username": "Shidorien"
|
||||
},
|
||||
{
|
||||
"username": "iPixelGalaxy"
|
||||
},
|
||||
{
|
||||
"username": "Emyte",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "GoodOldNervy"
|
||||
},
|
||||
{
|
||||
"username": "Naysy"
|
||||
},
|
||||
{
|
||||
"username": "Falmil"
|
||||
},
|
||||
{
|
||||
"username": "Anonymously42",
|
||||
"type": "diamond",
|
||||
"link": "https://www.twitch.tv/anonymously42tv"
|
||||
},
|
||||
{
|
||||
"username": "Burt",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Phil",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Protocrush",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Karlito",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": ".sharkey"
|
||||
},
|
||||
{
|
||||
"username": "Z3t4"
|
||||
},
|
||||
{
|
||||
"username": "blot455",
|
||||
"type": "diamond"
|
||||
},
|
||||
{
|
||||
"username": "Lumberjack462",
|
||||
"type": "diamond",
|
||||
"link": "https://www.youtube.com/@lumberjack462"
|
||||
},
|
||||
{
|
||||
"username": "Xero"
|
||||
},
|
||||
{
|
||||
"username": "Minescence"
|
||||
},
|
||||
{
|
||||
"username": "mereknom"
|
||||
},
|
||||
{
|
||||
"username": "Jascha"
|
||||
},
|
||||
{
|
||||
"username": "Celldweller",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "rhythmshade"
|
||||
},
|
||||
{
|
||||
"username": "liborsaf"
|
||||
"username": "Rhythm Shade"
|
||||
},
|
||||
{
|
||||
"username": "aatame3",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Joshua"
|
||||
"username": "Joshua Knick"
|
||||
},
|
||||
{
|
||||
"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": "Austin Bauman"
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
},
|
||||
{
|
||||
"username": "Reflected Chop",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Furiouspupa"
|
||||
},
|
||||
{
|
||||
"username": "Maximilian"
|
||||
},
|
||||
{
|
||||
"username": "Paul Smith"
|
||||
},
|
||||
{
|
||||
"username": "MunityVR",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Sk4venger"
|
||||
},
|
||||
{
|
||||
"username": "Alexander Herman",
|
||||
{
|
||||
"username": "Jesper Norsted"
|
||||
},
|
||||
{
|
||||
"username": "bosspie",
|
||||
"type": "diamond"
|
||||
},
|
||||
{
|
||||
"username": "Camryn Jackson"
|
||||
},
|
||||
{
|
||||
"username": "bones",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Arts Rimuro Suraimu",
|
||||
{
|
||||
"username": "Weed"
|
||||
},
|
||||
{
|
||||
"username": "minefox54"
|
||||
},
|
||||
{
|
||||
"username": "Elysee Buthidi",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Tindux",
|
||||
"type": "diamond"
|
||||
},
|
||||
{
|
||||
"username": "Einherjar",
|
||||
"type": "diamond"
|
||||
},
|
||||
{
|
||||
"username": "Daniel"
|
||||
},
|
||||
{
|
||||
"username": "lethal_loops"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatare",
|
||||
"sabers": "Säbel",
|
||||
"platforms": "Plattformen",
|
||||
"blocks": "Blöcke",
|
||||
"bloqs": "Blöcke",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"accept": "Akzeptieren",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Weiter",
|
||||
"shared": "Geteilt",
|
||||
"pin": "Anheften",
|
||||
"unpin": "Lösen"
|
||||
"unpin": "Lösen",
|
||||
"this-guide": "dieser Leitfaden",
|
||||
"create": "Erstellen",
|
||||
"edit": "Bearbeiten"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "veraltet"
|
||||
"outdated": "veraltet",
|
||||
"update-text": "BSManager {version} ist verfügbar!",
|
||||
"update-button": "Aktualisieren und neu starten",
|
||||
"see-changelog": "Änderungen ansehen"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Version hinzufügen",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Diese Version ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht wie erwartet. Es wird empfohlen, die empfohlene Version ({recommendedVersion}) von Beat Saber zu verwenden, um die neuesten Funktionen und Fehlerbehebungen zu genießen.",
|
||||
"advanced-launch": {
|
||||
"button": "Startoptionen",
|
||||
"placeholder": "Startoptionen, bspw: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Startoptionen, bspw: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Eine Startoption erstellen"
|
||||
},
|
||||
"skipsteam": "Steam überspringen",
|
||||
"skipsteam-description": "Verhindert, dass Steam automatisch mit Beat Saber geöffnet wird. Aktivieren Sie dies, wenn Sie eine andere VR-Laufzeit wie WiVRn oder Monado verwenden, mit der SteamVR interferieren könnte.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Dauer",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Hochladedatum"
|
||||
"date-uploaded": "Hochladedatum",
|
||||
"added-date": "Hinzugefügt am"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Lade Mods...",
|
||||
"no-internet": "Kein Internet",
|
||||
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar.",
|
||||
"status": {
|
||||
"no-wineprefix": "Konnte den BSManager WINEPREFIX-Pfad nicht finden. Bitte starte Beat Saber zuerst in BSManager.",
|
||||
"beatmods-down": "Beatmods ist derzeit nicht erreichbar. Bitte versuchen Sie es später noch einmal. Wenn das Problem weiterhin besteht, informieren Sie uns auf {links}.",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Mehr Infos",
|
||||
"install-or-update": "Installieren oder aktualisieren",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Name",
|
||||
"size": "Größe",
|
||||
"installed": "Installiert",
|
||||
"latest": "Neueste",
|
||||
"description": "Beschreibung",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Alle deinstallieren",
|
||||
"unselect-all": "Alle abwählen"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "Das ist ein großer Mod!",
|
||||
"this-is-a-very-large-mod": "Das ist ein sehr großer Mod!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Englisch, Vereinigtes Königreich",
|
||||
"en-US": "Englisch, USA",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Traditionelles Chinesisch",
|
||||
"ja-JP": "Japanisch",
|
||||
"ko-KR": "Koreanisch",
|
||||
"uk-UA": "Ukrainisch"
|
||||
"uk-UA": "Ukrainisch",
|
||||
"tl-PH": "Filipino"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Es ist ein Fehler aufgetreten, die Systemproxyeinstellungen können nicht geändert werden."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Automatische Aktualisierung",
|
||||
"description": "BSManager wird beim Start der Anwendung automatisch aktualisiert.",
|
||||
"error-notification": {
|
||||
"message": "Ein Fehler ist aufgetreten, die Einstellungen für die automatische Aktualisierung können nicht geändert werden."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Es ist nicht möglich, das Manifest für diese Version herunterzuladen. Dein Anmelde-Token ist möglicherweise ungültig.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Es ist nicht möglich, das Manifest für diese Version herunterzuladen. Dein Anmelde-Token ist möglicherweise ungültig oder du besitzt die Desktop-Version von Beat Saber nicht.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Unmöglich, das Manifest für diese Version zu finden.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Beim Lesen des Manifests ist ein Fehler aufgetreten.",
|
||||
"ALREADY_DOWNLOADING": "Eine Version wird bereits heruntergeladen.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Unmöglich, das für den Download erforderliche Manifest zu erhalten.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Unmöglich, das für den Download erforderliche Manifest zu erhalten. Dein Anmelde-Token ist möglicherweise ungültig oder du besitzt die Desktop-Version von Beat Saber nicht.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Beim Überprüfen der Dateien ist ein Fehler aufgetreten.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Einige Dateien konnten nicht heruntergeladen werden.",
|
||||
"META_LOGIN_TIMED_OUT": "Es hat zu lange gedauert, das Anmelde-Token abzurufen.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Das Meta-Anmeldefenster wurde geschlossen.",
|
||||
"NO_META_AUTH_TOKEN": "Unmöglich, das für den Download erforderliche Meta-Anmelde-Token zu erhalten.",
|
||||
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Fordere deine Desktop-Version an"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Alle Mods wurden entfernt.",
|
||||
"errors": {
|
||||
"no-mods": "Keine Mods sind in dieser Version installiert 😑"
|
||||
"no-mods": "Keine Mods sind in dieser Version installiert 😑",
|
||||
"no-wineprefix": "Konnte den BSManager WINEPREFIX-Pfad nicht finden. Bitte starte Beat Saber zuerst in BSManager."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Nicht mehr daran erinnern",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Version kann nicht deinstalliert werden",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager kann die originale Beat Saber Version nicht deinstallieren",
|
||||
"VersionNotFound": "Version nicht gefunden.",
|
||||
"delete-folder": "Version kann nicht gelöscht werden. Stelle sicher, dass Beat Saber nicht läuft."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Dein Meta-Verbindungstoken wird benötigt, um Beat Saber herunterzuladen.",
|
||||
"need-cookie-enabled": "Beim Einloggen in Meta wird ein Anmeldefenster geöffnet und du kannst dann den Anmeldevorgang beginnen. Bitte stelle sicher, dass Cookies akzeptiert werden, sonst können wir deinen Token zum Starten des Downloads nicht abrufen.",
|
||||
"enter-token-manually": "Mein Anmelde-Token manuell eingeben",
|
||||
"enter-token-manually-tooltip": "Dies ermöglicht es dir, deinen Anmelde-Token einzugeben, ohne dich über Meta anzumelden."
|
||||
"enter-token-manually-tooltip": "Dies ermöglicht es dir, deinen Anmelde-Token einzugeben, ohne dich über Meta anzumelden.",
|
||||
"must-own-desktop-version": "Du musst die Desktop-Version von Beat Saber besitzen. Wenn du die eigenständige Version gekauft hast, folge {link}, um sie zu beanspruchen."
|
||||
},
|
||||
"stay": "Angemeldet bleiben",
|
||||
"connect-to-meta": "Mit Meta verbinden"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "Die Version {outdatedVersion} ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht mehr wie erwartet. Bitte lade die neueste empfohlene Version ({recommendedVersion}) von Beat Saber herunter, um die neuesten Funktionen und Fehlerbehebungen zu nutzen."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Benutzerdefinierte Startoption",
|
||||
"title-placeholder": "Meine Option",
|
||||
"desc": "Erstellen Sie eine wiederverwendbare Startoption, um Beat Saber mit einem bestimmten Befehl zu starten.",
|
||||
"name": "Name",
|
||||
"launch-option": "Startoption",
|
||||
"create": "Erstellen"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatars",
|
||||
"sabers": "Sabers",
|
||||
"platforms": "Platforms",
|
||||
"blocks": "Blocks",
|
||||
"bloqs": "Blocks",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"accept": "Accept",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Continue",
|
||||
"shared": "Shared",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin"
|
||||
"unpin": "Unpin",
|
||||
"this-guide": "this guide",
|
||||
"create": "Create",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "outdated"
|
||||
"outdated": "outdated",
|
||||
"update-text": "BSManager {version} is available!",
|
||||
"update-button": "Update and Restart",
|
||||
"see-changelog": "See changelog"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Add a version",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "This version is outdated, and some mods or features may no longer work as expected. Prefer using the recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes.",
|
||||
"advanced-launch": {
|
||||
"button": "Launch options",
|
||||
"placeholder": "Launch options ex: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Launch options ex: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Create a launch option"
|
||||
},
|
||||
"skipsteam": "Skip Steam",
|
||||
"skipsteam-description": "Stops Steam from opening automatically with Beat Saber, enable if you are using a different VR runtime like WiVRn or Monado that SteamVR may interfere with.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Duration",
|
||||
"likes": "Likes",
|
||||
"date-uploaded": "Date Uploaded"
|
||||
"date-uploaded": "Date Uploaded",
|
||||
"added-date": "Added Date"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Loading mods...",
|
||||
"no-internet": "No internet",
|
||||
"mods-not-available": "No mods are available yet for this version of Beat Saber",
|
||||
"status": {
|
||||
"no-wineprefix": "Could not find BSManager WINEPREFIX path. Please launch Beat Saber in BSManager first.",
|
||||
"beatmods-down": "Beatmods is currently unreachable. Please retry later. If the issue persists, inform us on {links}.",
|
||||
"unknown": "An unknown error occurred ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "More info",
|
||||
"install-or-update": "Install or update",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"installed": "Installed",
|
||||
"latest": "Latest",
|
||||
"description": "Description",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Uninstall all",
|
||||
"unselect-all": "Unselect all"
|
||||
}
|
||||
},
|
||||
"mod-item":{
|
||||
"this-is-a-large-mod": "This is a large mod!",
|
||||
"this-is-a-very-large-mod": "This is a very large mod!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "English, United Kingdom",
|
||||
"en-US": "English, USA",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Chinese (Traditional)",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean",
|
||||
"uk-UA": "Ukrainian"
|
||||
"uk-UA": "Ukrainian",
|
||||
"tl-PH": "Filipino"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "An error occurred, unable to change system proxy settings."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Auto Update",
|
||||
"description": "BSManager will automatically update when you launch the application.",
|
||||
"error-notification": {
|
||||
"message": "An error occurred, unable to change auto update settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Unable to download the manifest for this version; your login token may be invalid.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Unable to download the manifest. Your login token may be invalid, or you don't own the desktop version of Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Unable to find the manifest for this version.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "An error occurred while reading the manifest.",
|
||||
"ALREADY_DOWNLOADING": "A version is already being downloaded.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Unable to obtain the manifest required for download.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Unable to obtain the manifest required for download. Your login token may be invalid, or you don't own the desktop version of Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "An error occurred during file verification.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Some files failed to download.",
|
||||
"META_LOGIN_TIMED_OUT": "The login token took too long to retrieve.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "The Meta login window was closed.",
|
||||
"NO_META_AUTH_TOKEN": "Unable to retrieve the Meta login token required for download.",
|
||||
"UNKNOWN_ERROR": "An unknown error occurred."
|
||||
},
|
||||
"actions":{
|
||||
"claim-your-desktop-version": "Claim your desktop version"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "All mods have been uninstalled.",
|
||||
"errors": {
|
||||
"no-mods": "No mods are installed in this version 😑"
|
||||
"no-mods": "No mods are installed in this version 😑",
|
||||
"no-wineprefix": "Could not find BSManager WINEPREFIX path. Please launch Beat Saber in BSManager first."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Do not remind me",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Unable to uninstall version",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager cannot uninstall the original Beat Saber version",
|
||||
"VersionNotFound": "Version not found.",
|
||||
"delete-folder": "Unable to delete the version. Ensure Beat Saber is not running."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Your Meta connection token is required to download Beat Saber.",
|
||||
"need-cookie-enabled": "By logging into Meta, a login window will open and you can then start the login process. Please ensure to accept cookies, otherwise, we might not be able to retrieve your token to start the download.",
|
||||
"enter-token-manually": "Enter my login token manually",
|
||||
"enter-token-manually-tooltip": "This will allow you to enter your login token without going through Meta login."
|
||||
"enter-token-manually-tooltip": "This will allow you to enter your login token without going through Meta login.",
|
||||
"must-own-desktop-version": "You must own the desktop version of Beat Saber. If you bought the standalone version, follow {link} to claim it."
|
||||
},
|
||||
"stay": "Remember me",
|
||||
"connect-to-meta": "Connect to Meta"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "The version {outdatedVersion} is outdated, and some mods or features may no longer work as expected. Please download the latest recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Custom launch option",
|
||||
"title-placeholder": "My option",
|
||||
"desc": "Create a re-usable launch option to launch Beat Saber with a specific command.",
|
||||
"name": "Name",
|
||||
"launch-option": "Launch option",
|
||||
"create": "Create"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatares",
|
||||
"sabers": "Sables",
|
||||
"platforms": "Plataformas",
|
||||
"blocks": "Bloques",
|
||||
"bloqs": "Bloques",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Borrar",
|
||||
"accept": "Aceptar",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Continuar",
|
||||
"shared": "Compartido",
|
||||
"pin": "Fijar",
|
||||
"unpin": "Desfijar"
|
||||
"unpin": "Desfijar",
|
||||
"this-guide": "esta guía",
|
||||
"create": "Crear",
|
||||
"edit": "Editar"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsoleto"
|
||||
"outdated": "obsoleto",
|
||||
"update-text": "¡BSManager {version} está disponible!",
|
||||
"update-button": "Actualizar y reiniciar",
|
||||
"see-changelog": "Ver los cambios"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Agregar una versión",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Esta versión está desactualizada, y algunos mods o funciones pueden no funcionar como se espera. Es mejor usar la versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas características y correcciones.",
|
||||
"advanced-launch": {
|
||||
"button": "Opciones de lanzamiento",
|
||||
"placeholder": "Opciones de lanzamiento ej: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Opciones de lanzamiento ej: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Crear una opción de lanzamiento"
|
||||
},
|
||||
"skipsteam": "Saltar Steam",
|
||||
"skipsteam-description": "Evita que Steam se abra automáticamente con Beat Saber, habilita esto si estás usando un runtime de VR diferente como WiVRn o Monado que SteamVR podría interferir.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Duración",
|
||||
"likes": "Me gusta",
|
||||
"date-uploaded": "Fecha de subida"
|
||||
"date-uploaded": "Fecha de subida",
|
||||
"added-date": "Fecha de adición"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Cargando mods...",
|
||||
"no-internet": "Sin internet",
|
||||
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
|
||||
"status": {
|
||||
"no-wineprefix": "No se pudo encontrar la ruta del WINEPREFIX de BSManager. Por favor, inicia Beat Saber en BSManager primero.",
|
||||
"beatmods-down": "Beatmods no está disponible en este momento. Por favor, inténtalo más tarde. Si el problema persiste, infórmanos en {links}.",
|
||||
"unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Más información",
|
||||
"install-or-update": "Instalar o actualizar",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Nombre",
|
||||
"size": "Tamaño",
|
||||
"installed": "Instalado",
|
||||
"latest": "Último",
|
||||
"description": "Descripción",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Desinstalar todos",
|
||||
"unselect-all": "Deseleccionar todo"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "¡Este es un mod grande!",
|
||||
"this-is-a-very-large-mod": "¡Este es un mod muy grande!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Inglés, Reino Unido",
|
||||
"en-US": "Inglés, Estados Unidos",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Chino tradicional",
|
||||
"ja-JP": "Japonés",
|
||||
"ko-KR": "Coreano",
|
||||
"uk-UA": "Ucraniano"
|
||||
"uk-UA": "Ucraniano",
|
||||
"tl-PH": "Filipino"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Ocurrió un error, no se pueden cambiar los ajustes del proxy del sistema."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Actualización automática",
|
||||
"description": "BSManager se actualizará automáticamente al iniciar la aplicación.",
|
||||
"error-notification": {
|
||||
"message": "Ocurrió un error, no se pudo cambiar la configuración de actualización automática."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "No se puede descargar el manifiesto de esta versión; su token de inicio de sesión podría ser inválido.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "No se puede descargar el manifiesto de esta versión. Su token de inicio de sesión podría ser inválido o que no poseas la versión de escritorio de Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Imposible encontrar el manifiesto de esta versión.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Se produjo un error al leer el manifiesto.",
|
||||
"ALREADY_DOWNLOADING": "Ya se está descargando una versión.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Imposible obtener el manifiesto necesario para la descarga.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Imposible obtener el manifiesto necesario para la descarga. Su token de inicio de sesión podría ser inválido o que no poseas la versión de escritorio de Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Se produjo un error durante la verificación de archivos.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Algunos archivos no pudieron ser descargados.",
|
||||
"META_LOGIN_TIMED_OUT": "El token de inicio de sesión tardó demasiado en recuperarse.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La ventana de inicio de sesión de Meta fue cerrada.",
|
||||
"NO_META_AUTH_TOKEN": "Imposible recuperar el token de inicio de sesión de Meta necesario para la descarga.",
|
||||
"UNKNOWN_ERROR": "Se produjo un error desconocido."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Reclama tu versión de escritorio"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Todos los mods han sido desinstalados.",
|
||||
"errors": {
|
||||
"no-mods": "No hay mods instalados en esta versión 😑"
|
||||
"no-mods": "No hay mods instalados en esta versión 😑",
|
||||
"no-wineprefix": "No se pudo encontrar la ruta del WINEPREFIX de BSManager. Por favor, inicia Beat Saber en BSManager primero."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "No volver a recordármelo",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "No se puede desinstalar la versión",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager no puede desinstalar la versión original de Beat Saber",
|
||||
"VersionNotFound": "Versión no encontrada.",
|
||||
"delete-folder": "No se puede eliminar la versión. Asegúrate de que Beat Saber no esté en marcha."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Tu token de conexión a Meta es necesario para descargar Beat Saber.",
|
||||
"need-cookie-enabled": "Al conectarte a Meta, se abrirá una ventana de inicio de sesión y entonces podrás empezar el proceso de conexión. Asegúrate de aceptar las cookies, de lo contrario, podríamos no poder recuperar tu token para iniciar la descarga.",
|
||||
"enter-token-manually": "Ingresar mi token de inicio de sesión manualmente",
|
||||
"enter-token-manually-tooltip": "Esto te permitirá ingresar tu token de inicio de sesión sin pasar por el inicio de sesión de Meta."
|
||||
"enter-token-manually-tooltip": "Esto te permitirá ingresar tu token de inicio de sesión sin pasar por el inicio de sesión de Meta.",
|
||||
"must-own-desktop-version": "Debes poseer la versión de escritorio de Beat Saber. Si compraste la versión autónoma, sigue {link} para reclamarla."
|
||||
},
|
||||
"stay": "Recuérdame",
|
||||
"connect-to-meta": "Conectarse a Meta"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "La versión {outdatedVersion} está obsoleta, y algunos mods o funciones pueden no funcionar como se espera. Por favor, descarga la última versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas funciones y correcciones de errores."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Opción de lanzamiento personalizada",
|
||||
"title-placeholder": "Mi opción",
|
||||
"desc": "Crea una opción de lanzamiento reutilizable para lanzar Beat Saber con un comando específico.",
|
||||
"name": "Nombre",
|
||||
"launch-option": "Opción de lanzamiento",
|
||||
"create": "Crear"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
+153
-103
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatars",
|
||||
"sabers": "Sabres",
|
||||
"platforms": "Plateformes",
|
||||
"blocks": "Blocs",
|
||||
"bloqs": "Blocs",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
"accept": "Accepter",
|
||||
@@ -26,15 +26,21 @@
|
||||
"continue": "Continuer",
|
||||
"shared": "Partagé",
|
||||
"pin": "Épingler",
|
||||
"unpin": "Désépingler"
|
||||
"unpin": "Désépingler",
|
||||
"this-guide": "ce guide",
|
||||
"create": "Créer",
|
||||
"edit": "Modifier"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
"parse": "Impossible d'analyser correctement la chaîne de variable d'environnement."
|
||||
"parse": "Impossible de lire les variables d'environnement."
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsolète"
|
||||
"outdated": "obsolète",
|
||||
"update-text": "BSManager {version} est disponible!",
|
||||
"update-button": "Mettre à jour et redémarrer",
|
||||
"see-changelog": "Voir les changements"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Ajouter une version",
|
||||
@@ -48,22 +54,23 @@
|
||||
"version-viewer": {
|
||||
"launch-mods": {
|
||||
"oculus": "Mode Oculus",
|
||||
"oculus-description": "Si vous utilisez Beat Saber via Steam, cela vous permet d'utiliser le compositeur VR d'Oculus (sans passer par SteamVR pour gagner en performances). Ceci n'est pas nécessaire pour utiliser les casques Oculus.",
|
||||
"oculus-description": "Si vous utilisez Beat Saber via Steam, cela permet d'utiliser l'environnement Oculus sans passer par SteamVR, pour un gain en performances potentiel. Ceci n'est pas obligatoire pour utiliser les casques Oculus.",
|
||||
"desktop": "Mode FPFC",
|
||||
"desktop-description": "Cela vous permet d'utiliser WASD et la souris pour naviguer dans le menu en jeu. Cela rend les tests beaucoup plus faciles, car vous n'avez pas à mettre votre casque!",
|
||||
"desktop-description": "Ce mode vous permet d'utiliser votre clavier (WASD) et votre souris pour naviguer en jeu. Cela rend les tests beaucoup plus faciles, car vous n'avez pas à mettre de casque VR !",
|
||||
"debug": "Mode Debug",
|
||||
"debug-description": "Active la fenêtre de log pour IPA. Cela affichera la console de débogage utilisée par les mods.",
|
||||
"outdated-tippy": "Cette version est obsolète et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Préférez utiliser la version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et correctifs.",
|
||||
"advanced-launch": {
|
||||
"button": "Options de lancement",
|
||||
"placeholder": "Options de lancement ex: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Options de lancement. Exemple : KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Créer une option de lancement"
|
||||
},
|
||||
"skipsteam": "Ignorer Steam",
|
||||
"skipsteam-description": "Empêche Steam de s'ouvrir automatiquement avec Beat Saber, activez-le si vous utilisez un autre runtime VR comme WiVRn ou Monado avec lequel SteamVR pourrait interférer.",
|
||||
"skipsteam-description": "Empêche Steam de s'ouvrir automatiquement avec Beat Saber, activez-le si vous utilisez un autre environnement VR comme WiVRn ou Monado avec lequel SteamVR pourrait interférer.",
|
||||
"map-editor": "Éditeur de map",
|
||||
"map-editor-description": "Lance l'éditeur officiel de map de Beat Saber au lieu du jeu.",
|
||||
"proton-logs": "Journaux de Proton",
|
||||
"proton-logs-description": "Active l'enregistrement des journaux Proton pour cette installation de Beat Saber sur \"{versionPath}\"."
|
||||
"proton-logs": "Logs de Proton",
|
||||
"proton-logs-description": "Active l'enregistrement des logs Proton pour cette installation de Beat Saber sur \"{versionPath}\"."
|
||||
},
|
||||
"maps": {
|
||||
"search-bar": {
|
||||
@@ -100,11 +107,12 @@
|
||||
"sort": {
|
||||
"name": "Nom",
|
||||
"song-author": "Auteur de la chanson",
|
||||
"map-author": "Auteur de la carte",
|
||||
"map-author": "Auteur de la map",
|
||||
"bpm": "BPM",
|
||||
"duration": "Durée",
|
||||
"likes": "J'aime",
|
||||
"date-uploaded": "Date de téléchargement"
|
||||
"date-uploaded": "Date de publication",
|
||||
"added-date": "Date d'ajout"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -120,7 +128,7 @@
|
||||
"sort": {
|
||||
"title": "Titre",
|
||||
"author": "Auteur",
|
||||
"number-of-maps": "Nombre de cartes",
|
||||
"number-of-maps": "Nombre de maps",
|
||||
"duration": "Durée",
|
||||
"notes-per-second": "NPS"
|
||||
}
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Chargement des mods...",
|
||||
"no-internet": "Pas d'accès Internet",
|
||||
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
|
||||
"status": {
|
||||
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber depuis BSManager.",
|
||||
"beatmods-down": "BeatMods est actuellement inaccessible. Veuillez réessayer plus tard. Si le problème persiste, informez-nous sur {links}.",
|
||||
"unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Plus d'infos",
|
||||
"install-or-update": "Installer ou mettre à jour",
|
||||
@@ -139,20 +152,25 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Nom",
|
||||
"installed": "Installé",
|
||||
"latest": "Récent",
|
||||
"size": "Taille",
|
||||
"installed": "Installée",
|
||||
"latest": "Dernière",
|
||||
"description": "Description",
|
||||
"dropdown": {
|
||||
"import-mods": "Importer des mods",
|
||||
"uninstall-all": "Tout désinstaller",
|
||||
"unselect-all": "Tout désélectionner"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "C'est un mod volumineux !",
|
||||
"this-is-a-very-large-mod": "C'est un mod très volumineux !"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"all-mods-already-installed": {
|
||||
"title": "Mods déjà installées",
|
||||
"description": "Tous les mods séléctionnées sont déjà installées"
|
||||
"title": "Mods déjà installés",
|
||||
"description": "Tous les mods sélectionnés sont déjà installés"
|
||||
},
|
||||
"outdated-mods": {
|
||||
"title": "Mod obsolète",
|
||||
@@ -192,17 +210,17 @@
|
||||
"steam-and-oculus": {
|
||||
"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": "Se déconnecter",
|
||||
"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.",
|
||||
"desc": "Choisissez la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
|
||||
"always-ask": "Toujours demander"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Apparence",
|
||||
"description": "Choisis les deux couleurs principales de BSManager.",
|
||||
"description": "Choisissez les deux couleurs principales de BSManager.",
|
||||
"reset": "Réinitialiser",
|
||||
"sub-title": "Thème",
|
||||
"themes": {
|
||||
@@ -213,7 +231,7 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Dossier d'installation",
|
||||
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager."
|
||||
"description": "Changez le dossier qui contiendra tout le contenu téléchargé par BSManager."
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Dossier Proton",
|
||||
@@ -231,8 +249,8 @@
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"title": "Langage",
|
||||
"description": "Sélectionne un langage.",
|
||||
"title": "Langue",
|
||||
"description": "Sélectionnez une langue.",
|
||||
"languages": {
|
||||
"en-EN": "English, UK",
|
||||
"en-US": "English, US",
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Anglais, Royaume-Uni",
|
||||
"en-US": "Anglais, États-Unis",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Chinois traditionnel",
|
||||
"ja-JP": "Japonais",
|
||||
"ko-KR": "Coréen",
|
||||
"uk-UA": "Ukrainien"
|
||||
"uk-UA": "Ukrainien",
|
||||
"tl-PH": "Philippin"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -293,7 +313,7 @@
|
||||
"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.",
|
||||
"description": "Activez l'accélération matérielle pour utiliser votre carte graphique et améliorer les performances de BSManager. Désactivez cette option si vous rencontrez des problèmes de performance.",
|
||||
"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 ?",
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Une erreur est survenue, impossible de modifier les paramètres du proxy système."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Mise à jour automatique",
|
||||
"description": "BSManager se mettra automatiquement à jour au lancement de l'application.",
|
||||
"error-notification": {
|
||||
"message": "Une erreur est survenue, impossible de modifier les paramètres de mise à jour automatique."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,8 +371,8 @@
|
||||
"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.",
|
||||
"operation-running": "Veuillez attendre la fin de l'opération en cours puis recommencez.",
|
||||
"no-internet": "Vérifiez votre connexion Internet et réessayez.",
|
||||
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
||||
}
|
||||
}
|
||||
@@ -361,9 +388,9 @@
|
||||
"warnings": {
|
||||
"msg": {
|
||||
"ManifestChecksum": "Le manifeste précédemment téléchargé ne correspond pas au nouveau 🤔",
|
||||
"ConnectionTimeout": "Ta connexion internet semble instable 🥶",
|
||||
"ConnectionLost": "La connexion a été perdue, essayez à nouveau...",
|
||||
"ConnectionError": "Impossible de se connecter à Steam, essayez à nouveau...",
|
||||
"ConnectionTimeout": "Votre connexion internet semble instable 🥶",
|
||||
"ConnectionLost": "La connexion a été perdue, réessayez...",
|
||||
"ConnectionError": "Impossible de se connecter à Steam, réessayez...",
|
||||
"Unknown": "Quelque chose d'étrange s'est produit 🤔 Votre connexion est probablement instable."
|
||||
}
|
||||
},
|
||||
@@ -371,26 +398,26 @@
|
||||
"msg": {
|
||||
"401": "Steam ne semble pas vouloir nous laisser télécharger Beat Saber 😢",
|
||||
"404": "Impossible de contacter les serveurs de Steam.",
|
||||
"ExeNotFoundWindows": "\"DepotDownloader.exe\" est manquant. Veuillez vérifier si l'exécutable est mis en quarantaine par votre antivirus.",
|
||||
"ExeNotFoundLinux": "L'exécutable \"DepotDownloader\" est manquanto.",
|
||||
"ExeNotFoundWindows": "\"DepotDownloader.exe\" est manquant. Vérifiez que votre antivirus n'a pas mis le fichier en quarantaine.",
|
||||
"ExeNotFoundLinux": "L'exécutable \"DepotDownloader\" est manquant.",
|
||||
"Password": "Le mot de passe est invalide.",
|
||||
"InvalidCredentials": "Identifiants de connexion invalides, connexion non approuvée, ou trop de tentatives de connexion.",
|
||||
"NoManifest": "Aucun manifest n'a été trouvé.",
|
||||
"NoManifest": "Aucun manifeste n'a été trouvé.",
|
||||
"DirectoryCreate": "Impossible d'installer les dossiers nécessaires.",
|
||||
"NotAvailableApp": "Tu essayes de télécharger Beat Saber alors que tu ne l'as pas ? 🤣",
|
||||
"DepotNotFound": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||
"DepotNotFound": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
||||
"NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯",
|
||||
"InvalidManifest": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||
"NoValidKey": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||
"NoManifestCode": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||
"InvalidManifest": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
||||
"NoValidKey": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
||||
"NoManifestCode": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
||||
"Unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯",
|
||||
"NoServer": "Impossible de contacter les serveurs de Steam.",
|
||||
"NotAllowed": "Apparemment tu n'es pas autorisé à télécharger Beat Saber 🥱",
|
||||
"ConnectionTimeout": "Impossible de se connecter à Steam 😕",
|
||||
"SteamLib": "Si tu as cette erreur, signale le bug sur GitHub avec les logs stp.",
|
||||
"SteamLib": "Si tu vois cette erreur, signale le bug sur GitHub avec les logs de BSManager.",
|
||||
"ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯",
|
||||
"LicenceError": "Impossible d'obtenir la liste des licences.",
|
||||
"RateLimitExceeded": "Tu as essayé trop de fois attends un peu et recommence plus tard.",
|
||||
"RateLimitExceeded": "Tu as essayé trop de fois, attends un peu et réessaye plus tard.",
|
||||
"TokenRejected": "Votre token de connexion a été rejeté 😕 Veuillez réessayer.",
|
||||
"AccessDenied": "L'accès à Steam a été refusé."
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Impossible de télécharger le manifest de cette version; vote token de connexion est peut-être invalide.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Impossible de trouver le manifest de cette version.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Une erreur c'est produite lors de la lecture du manifest.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Impossible de télécharger le manifeste de cette version. Votre token de connexion est peut-être invalide, ou vous ne possédez pas la version PC de Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Impossible de trouver le manifeste de cette version.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Une erreur s'est produite lors de la lecture du manifeste.",
|
||||
"ALREADY_DOWNLOADING": "Une version est déjà en cours de téléchargement.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Impossible d'obtenir le manifest nécessaire au téléchargement.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Une erreur c'est produite lors de la vérification des fichiers.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Impossible d'obtenir le manifeste nécessaire au téléchargement. Votre token de connexion est peut-être invalide, ou vous ne possédez pas la version PC de Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Une erreur s'est produite lors de la vérification des fichiers.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Certains fichiers n'ont pas pu être téléchargés.",
|
||||
"META_LOGIN_TIMED_OUT": "Le Token de connexion à mis trop de temps à être récupéré.",
|
||||
"META_LOGIN_TIMED_OUT": "Le token de connexion à mis trop de temps à être récupéré.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La fenêtre de connexion à Meta a été fermée.",
|
||||
"NO_META_AUTH_TOKEN": "Impossible de récupérer le token de connexion à Meta nécessaire au téléchargement.",
|
||||
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Réclamez votre version PC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +456,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"import-error": {
|
||||
"desc": "Vérifier que le dossier sélectionné est une installation de Beat Saber."
|
||||
"desc": "Vérifie que le dossier sélectionné est une installation de Beat Saber."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -480,7 +510,7 @@
|
||||
}
|
||||
},
|
||||
"check-all-enabled": {
|
||||
"title": "OneClick désactivée(s)",
|
||||
"title": "OneClick désactivé",
|
||||
"description": "Une ou plusieurs installations OneClick sont désactivées. Rendez-vous dans les paramètres pour les activer.",
|
||||
"actions": {
|
||||
"settings": "Paramètres",
|
||||
@@ -495,12 +525,12 @@
|
||||
"titles": {
|
||||
"BS_LAUNCHING": "Lancement...🚀",
|
||||
"STEAM_LAUNCHING": "Steam se lance !",
|
||||
"SKIPPING_STEAM_LAUNCH": "Sauter le lancement de Steam"
|
||||
"SKIPPING_STEAM_LAUNCH": "Contourner le lancement de Steam"
|
||||
},
|
||||
"msg": {
|
||||
"BS_LAUNCHING": "N'oublie pas de t'échauffer 😉",
|
||||
"STEAM_LAUNCHING": "Beat Saber se lancera automatiquement après Steam.",
|
||||
"SKIPPING_STEAM_LAUNCH": "J'espère que vous savez ce que vous faites :)"
|
||||
"SKIPPING_STEAM_LAUNCH": "J'espère que tu sais ce que tu fais :)"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -549,7 +579,7 @@
|
||||
"UnknownError": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Tu ne peux pas modifier la version Steam, cependant tu peux la cloner."
|
||||
"CantEditSteam": "Vous ne pouvez pas modifier la version Steam, vous pouvez cependant la cloner."
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Tous les mods ont été désinstallés.",
|
||||
"errors": {
|
||||
"no-mods": "Aucun mod n'est installé dans dans cette version 😑"
|
||||
"no-mods": "Aucun mod n'est installé dans dans cette version 😑",
|
||||
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber dans BSManager."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -612,8 +643,8 @@
|
||||
"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"
|
||||
"title": "Aucun doublon",
|
||||
"msg": "Aucune map n'a été supprimée"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Doublons supprimés",
|
||||
@@ -649,7 +680,7 @@
|
||||
"info": {
|
||||
"userdata-backup-created": {
|
||||
"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"
|
||||
"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": {
|
||||
@@ -681,9 +712,19 @@
|
||||
"title": "Version obsolète",
|
||||
"msg": "Cette version de Beat Saber est obsolète. Utilisez la version recommandée pour profiter des dernières fonctionnalités et correctifs.",
|
||||
"actions": {
|
||||
"do-not-remind": "Ne plus me rappeler",
|
||||
"do-not-remind": "Ne plus me le rappeler",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Impossible de désinstaller la version",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager ne peut pas désinstaller la version originale de Beat Saber",
|
||||
"VersionNotFound": "La version n'a pas été trouvée.",
|
||||
"delete-folder": "Impossible de supprimer la version. Vérifiez que Beat Saber n'est pas en cours d'exécution."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -724,7 +765,7 @@
|
||||
"note": {
|
||||
"use-the": "Utilisez l'",
|
||||
"steam-mobile-app": "application mobile Steam",
|
||||
"to-connect-with-qr": "pour vous connecter avec un QR code."
|
||||
"to-connect-with-qr": "pour vous connecter avec un code QR."
|
||||
}
|
||||
},
|
||||
"stay": "Se souvenir de moi"
|
||||
@@ -744,20 +785,20 @@
|
||||
"steam-credentials": {
|
||||
"title": "Steam Credentials",
|
||||
"p-1": "Les informations d'identification ne sont utilisées que pour télécharger le jeu, car Steam doit vérifier que vous avez payé le jeu afin d'être autorisé à le télécharger. Ils ne sont pas sauvegardés et transmis directement à DepotDownloader. Si vous ne voulez pas entrer vos informations d'identification, vous pouvez suivre ce tutoriel :",
|
||||
"p-2": "puis cliquez sur l'icône de l'engrenage dans le coin supérieur droit et sélectionnez \"Importer une version\", sélectionnez le dossier où beat saber a été téléchargé (Si vous suivez le tutoriel ci-dessus, vous devriez avoir le bon emplacement)."
|
||||
"p-2": "puis cliquez sur l'icône de l'engrenage dans le coin supérieur droit et sélectionnez \"Importer une version\", vous pouvez ensuite sélectionner le dossier où Beat Saber a été téléchargé (Si vous suivez le tutoriel ci-dessus, vous devriez avoir le bon emplacement)."
|
||||
},
|
||||
"bs-import-version": {
|
||||
"title": "Importer une version",
|
||||
"description": "Importe une version de Beat Saber pour profiter des fonctionnalités de BSManager. L'importation copiera le dossier d'installation de Beat Saber sélectionné dans le dossier de versions de BSManager.",
|
||||
"oculus-version": "Version Oculus",
|
||||
"oculus-version-tooltip": "Cocher si c'est une version Oculus",
|
||||
"oculus-version-tooltip": "Cochez si l'installation provient du magasin Oculus",
|
||||
"buttons": {
|
||||
"submit": "Importer une version"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"title": "Désinstaller",
|
||||
"description": "Es-tu sûr de vouloir désinstaller Beat Saber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||
"description": "Êtes-vous sûr de vouloir désinstaller Beat Saber {version} ? Vous allez devoir la retélécharger pour y jouer.",
|
||||
"buttons": {
|
||||
"submit": "Désinstaller"
|
||||
}
|
||||
@@ -793,12 +834,12 @@
|
||||
},
|
||||
"uninstall-mod": {
|
||||
"title": "Désinstaller",
|
||||
"description": "Es-tu sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
|
||||
"description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
|
||||
"description": "Êtes-vous sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
|
||||
"description-bsipa": "Êtes-vous sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
|
||||
},
|
||||
"uninstall-all-mods": {
|
||||
"title": "Désinstaller les mods",
|
||||
"description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
|
||||
"description": "Êtes-vous sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
|
||||
},
|
||||
"maps-actions": {
|
||||
"delete-maps": {
|
||||
@@ -807,8 +848,8 @@
|
||||
"multiple": "Supprimer les maps ?"
|
||||
},
|
||||
"desc": {
|
||||
"single": "Es-tu sur de vouloir supprimer la map {name} ?",
|
||||
"multiple": "Es-tu sur de vouloir supprimer {nb} maps ?"
|
||||
"single": "Êtes-vous sûr de vouloir supprimer la map {name} ?",
|
||||
"multiple": "Êtes-vous sûr de vouloir supprimer {nb} maps ?"
|
||||
},
|
||||
"info": {
|
||||
"desc": {
|
||||
@@ -823,8 +864,8 @@
|
||||
},
|
||||
"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 ?"
|
||||
"desc": "Seule la map \"{map}\" est un doublon. Êtes-vous sûr de vouloir la supprimer ?",
|
||||
"desc-plural": "{nb} maps en double ont été trouvées. Êtes-vous sûr de vouloir les supprimer ?"
|
||||
}
|
||||
},
|
||||
"link-contents": {
|
||||
@@ -848,7 +889,7 @@
|
||||
"search-btn": "Rechercher",
|
||||
"loading-maps": "Chargement des maps...",
|
||||
"no-maps-found": "Aucune map trouvée",
|
||||
"no-internet": "Pas d'internet"
|
||||
"no-internet": "Pas d'accès Internet"
|
||||
},
|
||||
"mods-disclaimer": {
|
||||
"title": "avis de non-responsabilité",
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Votre token de connexion à Meta est nécessaire pour télécharger Beat Saber.",
|
||||
"need-cookie-enabled": "En vous connectant à Meta, une fenêtre de connexion s'ouvrira et vous pourrez alors débuter le processus de connexion. Veillez à bien accepter les cookies sinon quoi, il se pourrait que l'on n'arrive pas à récupérer votre token pour démarrer le téléchargement.",
|
||||
"enter-token-manually": "Entrer mon token de connexion manuellement",
|
||||
"enter-token-manually-tooltip": "Cela vous permettra d'entrer votre token de connexion sans passer par la connexion Meta."
|
||||
"enter-token-manually-tooltip": "Cela vous permettra d'entrer votre token de connexion sans passer par la connexion Meta.",
|
||||
"must-own-desktop-version": "Vous devez posséder la version PC de Beat Saber. Si vous avez acheté la version autonome, suivez {link} pour la réclamer."
|
||||
},
|
||||
"stay": "Se souvenir de moi",
|
||||
"connect-to-meta": "Se connecter à Meta"
|
||||
@@ -910,7 +952,7 @@
|
||||
"token-is-invalid": "Le Token est invalide.",
|
||||
"save-my-token": "Sauvegarder mon token",
|
||||
"have-token-saved": "J'ai déjà un token sauvegardé",
|
||||
"save-token-info": "Ceci enregistrera votre token pour le réutiliser plus facilement. Vous devrez créer un mot de passe pour crypter votre token à des fins de stockage. Si jamais vous oubliez votre mot de passe, il vous suffit de fournir à nouveau votre token.",
|
||||
"save-token-info": "Ceci enregistrera votre token pour le réutiliser plus facilement. Vous devrez créer un mot de passe pour chiffrer votre token à des fins de stockage. Si jamais vous oubliez votre mot de passe, il vous suffira de fournir à nouveau votre token.",
|
||||
"password": "Mot de passe",
|
||||
"password-too-short": "Mot de passe trop court",
|
||||
"info-enter-password": "Pour télécharger Beat Saber, votre token de connexion à Oculus est nécessaire. Entrez le mot de passe utilisé pour sauvegarder votre token Oculus.",
|
||||
@@ -931,7 +973,7 @@
|
||||
},
|
||||
"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-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, maps, playlists, etc.)",
|
||||
"default": "Par défaut",
|
||||
"default-tooltip": "Par défaut, dans votre dossier personnel"
|
||||
},
|
||||
@@ -943,18 +985,26 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "La version {outdatedVersion} est obsolète, et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Veuillez télécharger la dernière version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et corrections de bugs."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Option de lancement personnalisée",
|
||||
"title-placeholder": "Mon option",
|
||||
"desc": "Créer une option de lancement réutilisable pour lancer Beat Saber avec une commande spécifique.",
|
||||
"name": "Nom",
|
||||
"launch-option": "Option de lancement",
|
||||
"create": "Créer"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Durée",
|
||||
"nps": "Notes Par Seconde",
|
||||
"nps": "Notes par seconde",
|
||||
"njs": "Vitesse de saut des notes",
|
||||
"tags": "tags",
|
||||
"specificities": "général",
|
||||
"requirements": "requis",
|
||||
"exclude": "exclure",
|
||||
"leaderboard": "tableau des leaders"
|
||||
"leaderboard": "classement"
|
||||
},
|
||||
"map-types": {
|
||||
"accuracy": "précision",
|
||||
@@ -1007,11 +1057,11 @@
|
||||
"automapper": "IA",
|
||||
"curated": "recommandée",
|
||||
"verified": "vérifiée",
|
||||
"fullSpread": "panel complet"
|
||||
"fullSpread": "toutes difficultés"
|
||||
},
|
||||
"map-leaderboard": {
|
||||
"All": "Tout",
|
||||
"Ranked": "Classée",
|
||||
"Ranked": "Classé",
|
||||
"BeatLeader": "BeatLeader",
|
||||
"ScoreSaber": "ScoreSaber"
|
||||
},
|
||||
@@ -1029,10 +1079,10 @@
|
||||
"by": "Par {songAutor}",
|
||||
"mapped-by": "mappée par",
|
||||
"delete": "Supprimer",
|
||||
"preview": "Aperçu de la carte",
|
||||
"preview": "Aperçu de la map",
|
||||
"bsr-code": "Code BSR",
|
||||
"download": "Télécharger la carte",
|
||||
"downloading": "Téléchargement de la carte",
|
||||
"download": "Télécharger la map",
|
||||
"downloading": "Téléchargement de la map",
|
||||
"cancel-download": "Annuler le téléchargement",
|
||||
"hightlight-difficulty": "Surligner la difficulté"
|
||||
}
|
||||
@@ -1076,12 +1126,12 @@
|
||||
"modals": {
|
||||
"delete-model": {
|
||||
"title": "Supprimer le modèle",
|
||||
"desc": "Es-tu sûr de vouloir supprimer le modèle {modelName} ?",
|
||||
"desc": "Êtes-vous sûr de vouloir supprimer le modèle {modelName} ?",
|
||||
"linked-annotation": "Ce modèle sera supprimé de toutes les versions liées."
|
||||
},
|
||||
"delete-models": {
|
||||
"title": "Supprimer les modèles",
|
||||
"desc": "Es-tu sûr de vouloir supprimer {nb} modèles ?",
|
||||
"desc": "Êtes-vous sûr de vouloir supprimer {nb} modèles ?",
|
||||
"linked-annotation": "Ces modèles seront supprimés de toutes les versions liées."
|
||||
},
|
||||
"download-models": {
|
||||
@@ -1097,11 +1147,11 @@
|
||||
"tag-desc": "Afficher seulement les modèles avec le tag spécifié.",
|
||||
"name-desc": "Afficher seulement les modèles avec le nom spécifié.",
|
||||
"discordid-desc": "Afficher seulement les modèles de l'utilisateur discord spécifié.",
|
||||
"status-desc": "Afficher seulement les modèles avec le status spécifié. (profil seulement, et seulement pour l'auteur)"
|
||||
"status-desc": "Afficher seulement les modèles avec le statut spécifié. (profil seulement, et seulement pour l'auteur)"
|
||||
},
|
||||
"no-models": "Aucun modèles trouvés.",
|
||||
"no-models": "Aucun modèle trouvé.",
|
||||
"no-internet": "Pas de connexion internet.",
|
||||
"error-occured": "Une erreur est survenue, réessaye plus tard."
|
||||
"error-occured": "Une erreur est survenue, réessayez plus tard."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -1112,16 +1162,16 @@
|
||||
"not-remind": "Ne plus me rappeler"
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Export terminé 🎉"
|
||||
"title": "Exportation terminée 🎉"
|
||||
}
|
||||
}
|
||||
},
|
||||
"beat-saver": {
|
||||
"maps-sorts": {
|
||||
"Latest": "Dernière",
|
||||
"Latest": "Récent",
|
||||
"Relevance": "Pertinence",
|
||||
"Rating": "Notes",
|
||||
"Curated": "Recommandée"
|
||||
"Rating": "Évaluation",
|
||||
"Curated": "Recommandé"
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
@@ -1149,7 +1199,7 @@
|
||||
"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 !",
|
||||
"playlist-created-desc": "La playlist a été créée avec succès. Vous pouvez maintenant synchroniser ses maps !",
|
||||
"download-playlist": "Télécharger la playlist",
|
||||
"synchronize-playlist": "Synchroniser la playlist",
|
||||
"synchronize-maps": "Synchroniser les maps",
|
||||
@@ -1170,8 +1220,8 @@
|
||||
"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...",
|
||||
"playlist-edited-desc": "La playlist a été modifiée avec succès. Vous pouvez maintenant synchroniser ses maps !",
|
||||
"playlists-loading": "Chargement des playlists...",
|
||||
"no-playlists": "Aucune playlist",
|
||||
"download-playlists": "Télécharger des playlists",
|
||||
"created-by": "Créée par",
|
||||
@@ -1180,15 +1230,15 @@
|
||||
"open-file": "Ouvrir le fichier",
|
||||
"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-playlist-desc": "Êtes-vous sûr de vouloir supprimer la playlist \"{playlistTitle}\" ?",
|
||||
"delete-playlists-desc": "Êtes-vous sûr 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-playlist-desc": "Êtes-vous sûr de vouloir exporter la playlist \"{playlistTitle}\" ?",
|
||||
"export-playlists-desc": "Êtes-vous sûr de vouloir exporter {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",
|
||||
@@ -1200,15 +1250,15 @@
|
||||
"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-desc": "Êtes-vous sûr de vouloir synchroniser la playlist \"{playlistTitle}\" ?",
|
||||
"synchronize-playlists-desc": "Êtes-vous sûr de vouloir synchroniser {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",
|
||||
"no-playlists-found": "Aucune playlist trouvée",
|
||||
"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...",
|
||||
@@ -1235,7 +1285,7 @@
|
||||
"loading": "Chargement...",
|
||||
"installed": "Installée",
|
||||
"no-map-found": "Aucune map trouvée",
|
||||
"edit-playlist-shortcuts": "Maintenez Maj ou Ctrl pour sélectionner plusieurs maps",
|
||||
"edit-playlist-shortcuts": "Maintenir 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",
|
||||
@@ -1245,7 +1295,7 @@
|
||||
"duration": "Durée",
|
||||
"nps": "Notes par secondes",
|
||||
"date-picker": {
|
||||
"start-date-end-date": "Date début — Date fin",
|
||||
"start-date-end-date": "Date de début — Date de fin",
|
||||
"all": "Tout",
|
||||
"last-24h": "Dernières 24h",
|
||||
"last-week": "Dernière semaine",
|
||||
@@ -1256,18 +1306,18 @@
|
||||
"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-not-imported": "Certaines playlists n'ont pas été 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",
|
||||
"INVALID_PLAYLIST_FILE": "Certaines playlists sont invalides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Certaines playlists sont illisibles",
|
||||
"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",
|
||||
"INVALID_PLAYLIST_FILE": "Les playlists sont invalides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Les playlists sont illisibles",
|
||||
"unknown": "Aucune playlist n'a pu être importée"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatar",
|
||||
"sabers": "Spade",
|
||||
"platforms": "Piattaforme",
|
||||
"blocks": "Blocchi",
|
||||
"bloqs": "Blocchi",
|
||||
"cancel": "Cancella",
|
||||
"delete": "Elimina",
|
||||
"accept": "Accetta",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Continua",
|
||||
"shared": "Condivisi",
|
||||
"pin": "Fissa",
|
||||
"unpin": "Rimuovi Fissaggio"
|
||||
"unpin": "Rimuovi Fissaggio",
|
||||
"this-guide": "questa guida",
|
||||
"create": "Crea",
|
||||
"edit": "Modifica"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "obsoleta"
|
||||
"outdated": "obsoleta",
|
||||
"update-text": "BSManager {version} è disponibile!",
|
||||
"update-button": "Aggiorna e riavvia",
|
||||
"see-changelog": "Vedi i cambiamenti"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Aggiungi una versione",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Questa versione è obsoleta, e alcune mod o funzioni non potrebbero più funzionare correttamente. Consigliamo di usare la versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix.",
|
||||
"advanced-launch": {
|
||||
"button": "Opzioni di Lancio",
|
||||
"placeholder": "Opzioni di Lancio es: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Opzioni di Lancio es: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Crea un'opzione di lancio"
|
||||
},
|
||||
"skipsteam": "Salta Steam",
|
||||
"skipsteam-description": "Ferma Steam da aprirsi automaticamente con Beat Saber, abilitalo se stai usando un VR runtime differente come WiVRn o Monado con cui SteamVR potrebbe interferire.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Durata",
|
||||
"likes": "Mi piace",
|
||||
"date-uploaded": "Data di caricamento"
|
||||
"date-uploaded": "Data di caricamento",
|
||||
"added-date": "Data di aggiunta"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Caricamento delle mod in corso...",
|
||||
"no-internet": "Internet non disponibile",
|
||||
"mods-not-available": "Nessuna mod è ancora disponibile per questa versione di Beat Saber",
|
||||
"status": {
|
||||
"no-wineprefix": "Impossibile trovare il percorso del WINEPREFIX di BSManager. Per favore avvia prima Beat Saber in BSManager.",
|
||||
"beatmods-down": "Beatmods non è attualmente raggiungibile. Riprova più tardi. Se il problema persiste, informaci su {links}.",
|
||||
"unknown": "È accaduto un errore sconosciuto ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Più info",
|
||||
"install-or-update": "Installa o aggiorna",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Nome",
|
||||
"size": "Dimensione",
|
||||
"installed": "Installata",
|
||||
"latest": "Ultima Versione",
|
||||
"description": "Descrizione",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Disinstalla tutto",
|
||||
"unselect-all": "Deseleziona tutto"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "Questo è un mod grande!",
|
||||
"this-is-a-very-large-mod": "Questo è un mod molto grande!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Inglese, Regno Unito",
|
||||
"en-US": "Inglese, Stati Uniti d'America",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Cinese (Tradizionale)",
|
||||
"ja-JP": "Giapponese",
|
||||
"ko-KR": "Coreano",
|
||||
"uk-UA": "Ucraino"
|
||||
"uk-UA": "Ucraino",
|
||||
"tl-PH": "Filippino"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "C'è stato un errore, impossibile cambiare le impostazioni del proxy di sistema."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Aggiornamento automatico",
|
||||
"description": "BSManager si aggiornerà automaticamente all'avvio dell'applicazione.",
|
||||
"error-notification": {
|
||||
"message": "Si è verificato un errore, impossibile modificare le impostazioni di aggiornamento automatico."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Impossibile scaricare il manifest per questa versione; il tuo token di accesso potrebbe essere invalido.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Impossibile scaricare il manifesto. Il tuo token di accesso potrebbe non essere valido o potresti non possedere la versione desktop di Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Impossibile trovare il manifest per questa versione.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "C'è stato un errore durante la lettura del manifest.",
|
||||
"ALREADY_DOWNLOADING": "Una versione sta già venendo scaricata.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Impossibile ottenere il manifest richiesto per il download.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Impossibile ottenere il manifesto necessario per il download. Il tuo token di accesso potrebbe non essere valido o potresti non possedere la versione desktop di Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "C'è stato un errore durante la verifica dei file.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Alcuni file non sono stati scaricati con successo.",
|
||||
"META_LOGIN_TIMED_OUT": "Il token di accesso ha impiegato troppo tempo per essere recuperato.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La finestra di login di Meta è stata chiusa.",
|
||||
"NO_META_AUTH_TOKEN": "Impossibile recuperare il token di accesso Meta necessario per il download.",
|
||||
"UNKNOWN_ERROR": "C'è stato un errore sconosciuto."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Richiedi la tua versione desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Tutte le mod sono state installate.",
|
||||
"errors": {
|
||||
"no-mods": "Non ci sono mod installate su questa versione 😑"
|
||||
"no-mods": "Non ci sono mod installate su questa versione 😑",
|
||||
"no-wineprefix": "Impossibile trovare il percorso del WINEPREFIX di BSManager. Per favore avvia prima Beat Saber in BSManager."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Non ricordarmelo più",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Impossibile disinstallare la versione",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager non può disinstallare la versione originale di Beat Saber",
|
||||
"VersionNotFound": "Versione non trovata.",
|
||||
"delete-folder": "Impossibile eliminare la versione. Assicurati che Beat Saber non sia in esecuzione."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Il tuo token di connessione Meta è necessario per scaricare Beat Saber.",
|
||||
"need-cookie-enabled": "Accedendo a Meta, si aprirà una finestra di login e potrai quindi avviare il processo di login. Assicurati di accettare i cookie, altrimenti potremmo non essere in grado di recuperare il tuo token per avviare il download.",
|
||||
"enter-token-manually": "Inserisci manualmente il mio token di login",
|
||||
"enter-token-manually-tooltip": "Questo ti permetterà di inserire il tuo token di accesso senza passare per il login di Meta."
|
||||
"enter-token-manually-tooltip": "Questo ti permetterà di inserire il tuo token di accesso senza passare per il login di Meta.",
|
||||
"must-own-desktop-version": "Devi possedere la versione desktop di Beat Saber. Se hai acquistato la versione autonoma, segui {link} per richiederla."
|
||||
},
|
||||
"stay": "Ricordami",
|
||||
"connect-to-meta": "Connettiti a Meta"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "La versione {outdatedVersion} è obsoleta, e alcune mod o funzioni potrebbero non funzionare più come previsto. Per favore scarica l'ultima versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Opzione di lancio personalizzata",
|
||||
"title-placeholder": "La mia opzione",
|
||||
"desc": "Crea un'opzione di lancio rilocabile per lanciare Beat Saber con un comando specifico.",
|
||||
"name": "Nome",
|
||||
"launch-option": "Opzione di lancio",
|
||||
"create": "Crea"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "アバター",
|
||||
"sabers": "セイバー",
|
||||
"platforms": "プラットフォーム",
|
||||
"blocks": "ブロック",
|
||||
"bloqs": "ブロック",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"accept": "同意する",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "続ける",
|
||||
"shared": "共有",
|
||||
"pin": "固定",
|
||||
"unpin": "固定解除"
|
||||
"unpin": "固定解除",
|
||||
"this-guide": "このガイド",
|
||||
"create": "作成",
|
||||
"edit": "編集"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "時代遅れ"
|
||||
"outdated": "時代遅れ",
|
||||
"update-text": "BSManager {version} が利用可能です!",
|
||||
"update-button": "更新して再起動",
|
||||
"see-changelog": "変更点を確認"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "バージョンを追加",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。",
|
||||
"advanced-launch": {
|
||||
"button": "起動オプション",
|
||||
"placeholder": "起動オプション 例:KEY=VALUE %command% fpfc"
|
||||
"placeholder": "起動オプション 例:KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "起動オプションを作成"
|
||||
},
|
||||
"skipsteam": "Steamをスキップ",
|
||||
"skipsteam-description": "Beat Saberと一緒にSteamが自動的に開くのを防ぎます。SteamVRが干渉する可能性のあるWiVRnやMonadoなど、別のVRランタイムを使用している場合は有効にしてください。",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "持続時間",
|
||||
"likes": "いいね",
|
||||
"date-uploaded": "アップロード日"
|
||||
"date-uploaded": "アップロード日",
|
||||
"added-date": "追加日"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Modを読み込み中...",
|
||||
"no-internet": "インターネット接続がありません!",
|
||||
"mods-not-available": "このバージョンで使用できるMODはまだありません。",
|
||||
"status": {
|
||||
"no-wineprefix": "BSManager WINEPREFIXのパスが見つかりませんでした。まずBSManagerでBeat Saberを起動してください。",
|
||||
"beatmods-down": "現在、Beatmodsにアクセスできません。後でもう一度お試しください。問題が解決しない場合は、{links}でお知らせください。",
|
||||
"unknown": "不明なエラーが発生しました (´・ω・`)"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "詳細情報",
|
||||
"install-or-update": "インストールとアップデート",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "名前",
|
||||
"size": "サイズ",
|
||||
"installed": "インストール済み",
|
||||
"latest": "最新",
|
||||
"description": "說明",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "全てアンインストールする",
|
||||
"unselect-all": "すべて選択解除"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "これは大きなModです!",
|
||||
"this-is-a-very-large-mod": "これは非常に大きなModです!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "イギリス英語",
|
||||
"en-US": "アメリカ英語",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "繁体字中国",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "韓国語",
|
||||
"uk-UA": "ウクライナ語"
|
||||
"uk-UA": "ウクライナ語",
|
||||
"tl-PH": "フィリピン人"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "エラーが発生しました。システムプロキシ設定を変更できません。"
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "自動更新",
|
||||
"description": "アプリケーション起動時にBSManagerが自動的に更新されます。",
|
||||
"error-notification": {
|
||||
"message": "エラーが発生し、自動更新の設定を変更できませんでした。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "このバージョンのマニフェストをダウンロードできません。ログイントークンが無効な可能性があります。",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "このバージョンのマニフェストをダウンロードできません。ログイントークンが無効であるか、Beat Saberのデスクトップ版を所有していない可能性があります。",
|
||||
"MANIFEST_FILE_NOT_FOUND": "このバージョンのマニフェストが見つかりません。",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "マニフェストの読み取り中にエラーが発生しました。",
|
||||
"ALREADY_DOWNLOADING": "バージョンはすでにダウンロードされています。",
|
||||
"UNABLE_TO_GET_MANIFEST": "ダウンロードに必要なマニフェストを取得できません。",
|
||||
"UNABLE_TO_GET_MANIFEST": "ダウンロードに必要なマニフェストを取得できません。ログイントークンが無効であるか、Beat Saberのデスクトップ版を所有していない可能性があります。",
|
||||
"VERIFY_INTEGRITY_FAILED": "ファイルの検証中にエラーが発生しました。",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "一部のファイルのダウンロードに失敗しました。",
|
||||
"META_LOGIN_TIMED_OUT": "ログイントークンの取得に時間がかかりすぎました。",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "メタ・ログイン・ウィンドウが閉じられた。",
|
||||
"NO_META_AUTH_TOKEN": "ダウンロードに必要な Meta ログイン トークンを取得できません。",
|
||||
"UNKNOWN_ERROR": "不明なエラーが発生しました。"
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "デスクトップ版を取得"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "すべてのModをアンインストールしました。",
|
||||
"errors": {
|
||||
"no-mods": "このバージョンのModはインストールされていません 😑"
|
||||
"no-mods": "このバージョンのModはインストールされていません 😑",
|
||||
"no-wineprefix": "BSManager WINEPREFIXのパスが見つかりませんでした。まずBSManagerでBeat Saberを起動してください。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "再度通知しない",
|
||||
"ok": "OK"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "バージョンをアンインストールできません",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManagerはBeat Saberのオリジナルバージョンをアンインストールできません",
|
||||
"VersionNotFound": "バージョンが見つかりません。",
|
||||
"delete-folder": "バージョンを削除できません。Beat Saberが実行中でないことを確認してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Beat SaberのダウンロードにはMetaのログイントークンが必要です。",
|
||||
"need-cookie-enabled": "Metaにログインするとログインウィンドウが開き、ログインプロセスを開始することができます。Cookieを受け入れるようにしてください。そうしないとダウンロードを開始するためのトークンを取得できない場合があります。",
|
||||
"enter-token-manually": "ログイントークンを手動で入力する",
|
||||
"enter-token-manually-tooltip": "これにより、Metaログインを経由せずにログイントークンを入力できるようになります。"
|
||||
"enter-token-manually-tooltip": "これにより、Metaログインを経由せずにログイントークンを入力できるようになります。",
|
||||
"must-own-desktop-version": "Beat Saberのデスクトップ版を所有している必要があります。スタンドアロン版を購入した場合は、{link} を確認して取得してください。"
|
||||
},
|
||||
"stay": "記憶する",
|
||||
"connect-to-meta": "Metaに接続する"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "バージョン {outdatedVersion} は古いため、一部のモッドや機能が期待通りに動作しない場合があります。最新の機能やバグ修正を楽しむために、推奨される最新バージョンの Beat Saber ({recommendedVersion}) をダウンロードしてください。"
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "カスタム起動オプション",
|
||||
"title-placeholder": "私のオプション",
|
||||
"desc": "特定のコマンドでBeat Saberを起動するための再利用可能な起動オプションを作成します。",
|
||||
"name": "名前",
|
||||
"launch-option": "起動オプション",
|
||||
"create": "作成"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "아바타",
|
||||
"sabers": "세이버",
|
||||
"platforms": "플랫폼",
|
||||
"blocks": "블록",
|
||||
"bloqs": "블록",
|
||||
"cancel": "취소",
|
||||
"delete": "삭제",
|
||||
"accept": "동의",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "계속",
|
||||
"shared": "공유",
|
||||
"pin": "고정",
|
||||
"unpin": "고정 해제"
|
||||
"unpin": "고정 해제",
|
||||
"this-guide": "이 가이드",
|
||||
"create": "만들기",
|
||||
"edit": "수정"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "구식"
|
||||
"outdated": "구식",
|
||||
"update-text": "BSManager {version} 가 사용 가능합니다!",
|
||||
"update-button": "업데이트 후 재시작",
|
||||
"see-changelog": "변경 사항 보기"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "버전 추가",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "이 버전은 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정을 사용하려면 권장 버전({recommendedVersion})의 Beat Saber를 사용하는 것이 좋습니다.",
|
||||
"advanced-launch": {
|
||||
"button": "실행 옵션",
|
||||
"placeholder": "실행 옵션 예: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "실행 옵션 예: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "실행 옵션 만들기"
|
||||
},
|
||||
"skipsteam": "Steam 실행 건너뛰기",
|
||||
"skipsteam-description": "Beat Saber와 함께 Steam이 자동으로 열리는 것을 방지합니다. SteamVR이 간섭할 수 있는 WiVRn 또는 Monado와 같은 다른 VR 런타임을 사용하는 경우 활성화하세요.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "지속 시간",
|
||||
"likes": "좋아요",
|
||||
"date-uploaded": "업로드 날짜"
|
||||
"date-uploaded": "업로드 날짜",
|
||||
"added-date": "추가 날짜"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "모드를 불러오는 중...",
|
||||
"no-internet": "인터넷 연결이 없습니다!",
|
||||
"mods-not-available": "이 버전에서 사용할 수 있는 모드가 아직 없습니다.",
|
||||
"status": {
|
||||
"no-wineprefix": "BSManager WINEPREFIX 경로를 찾을 수 없습니다. 먼저 BSManager에서 Beat Saber를 실행하세요.",
|
||||
"beatmods-down": "현재 Beatmods에 접근할 수 없습니다. 나중에 다시 시도해 주세요. 문제가 계속되면 {links}에서 알려주세요.",
|
||||
"unknown": "알 수 없는 오류가 발생했습니다 (´・ω・`)"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "자세히 보기",
|
||||
"install-or-update": "설치 및 업데이트",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "이름",
|
||||
"size": "크기",
|
||||
"installed": "설치됨",
|
||||
"latest": "최신",
|
||||
"description": "설명",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "모두 제거",
|
||||
"unselect-all": "모두 선택 해제"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "이것은 큰 모드입니다!",
|
||||
"this-is-a-very-large-mod": "이것은 매우 큰 모드입니다!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "영어(영국)",
|
||||
"en-US": "영어(미국)",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "중국어 (번체)",
|
||||
"ja-JP": "일본어",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "우크라이나어"
|
||||
"uk-UA": "우크라이나어",
|
||||
"tl-PH": "필리핀인"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "오류가 발생했습니다. 시스템 프록시 설정을 변경할 수 없습니다."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "자동 업데이트",
|
||||
"description": "애플리케이션을 실행할 때 BSManager가 자동으로 업데이트됩니다.",
|
||||
"error-notification": {
|
||||
"message": "오류가 발생하여 자동 업데이트 설정을 변경할 수 없습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "이 버전의 매니페스트를 다운로드할 수 없습니다. 로그인 토큰이 무효일 수 있습니다.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "매니페스트를 다운로드할 수 없습니다. 로그인 토큰이 유효하지 않거나 Beat Saber 데스크톱 버전을 소유하고 있지 않을 수 있습니다.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "이 버전의 매니페스트를 찾을 수 없습니다.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "매니페스트를 읽는 중 오류가 발생했습니다.",
|
||||
"ALREADY_DOWNLOADING": "버전이 이미 다운로드 중입니다.",
|
||||
"UNABLE_TO_GET_MANIFEST": "다운로드에 필요한 매니페스트를 가져올 수 없습니다.",
|
||||
"UNABLE_TO_GET_MANIFEST": "다운로드에 필요한 매니페스트를 가져올 수 없습니다. 로그인 토큰이 유효하지 않거나 Beat Saber 데스크톱 버전을 소유하고 있지 않을 수 있습니다.",
|
||||
"VERIFY_INTEGRITY_FAILED": "파일 검증 중 오류가 발생했습니다.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "일부 파일을 다운로드하지 못했습니다.",
|
||||
"META_LOGIN_TIMED_OUT": "로그인 토큰을 얻는 데 시간이 너무 오래 걸렸습니다.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 로그인 창이 닫혔습니다.",
|
||||
"NO_META_AUTH_TOKEN": "다운로드에 필요한 Meta 로그인 토큰을 얻을 수 없습니다.",
|
||||
"UNKNOWN_ERROR": "알 수 없는 오류가 발생했습니다."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "데스크톱 버전 받기"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "모든 Mod가 제거되었습니다.",
|
||||
"errors": {
|
||||
"no-mods": "이 버전에 설치된 Mod가 없습니다 😑"
|
||||
"no-mods": "이 버전에 설치된 Mod가 없습니다 😑",
|
||||
"no-wineprefix": "BSManager WINEPREFIX 경로를 찾을 수 없습니다. 먼저 BSManager에서 Beat Saber를 실행하세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "다시 알리지 않기",
|
||||
"ok": "확인"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "버전을 제거할 수 없습니다",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager는 원본 Beat Saber 버전을 제거할 수 없습니다",
|
||||
"VersionNotFound": "버전을 찾을 수 없습니다.",
|
||||
"delete-folder": "버전을 삭제할 수 없습니다. Beat Saber가 실행 중이지 않은지 확인하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Beat Saber를 다운로드하려면 Meta 로그인 토큰이 필요합니다.",
|
||||
"need-cookie-enabled": "Meta에 로그인하면 로그인 창이 열리며 로그인 프로세스를 시작할 수 있습니다. 쿠키를 허용해 주세요. 그렇지 않으면 다운로드를 시작하기 위한 토큰을 얻지 못할 수 있습니다.",
|
||||
"enter-token-manually": "로그인 토큰을 수동으로 입력하기",
|
||||
"enter-token-manually-tooltip": "이를 통해 Meta 로그인을 거치지 않고 로그인 토큰을 입력할 수 있습니다."
|
||||
"enter-token-manually-tooltip": "이를 통해 Meta 로그인을 거치지 않고 로그인 토큰을 입력할 수 있습니다.",
|
||||
"must-own-desktop-version": "Beat Saber의 데스크톱 버전을 보유해야 합니다. 독립형 버전을 구매했다면 {link}를 따라가서 받으세요."
|
||||
},
|
||||
"stay": "기억하기",
|
||||
"connect-to-meta": "Meta에 연결하기"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "버전 {outdatedVersion}은(는) 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정 사항을 즐기기 위해 Beat Saber의 최신 권장 버전 ({recommendedVersion})을 다운로드하십시오."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "사용자 정의 실행 옵션",
|
||||
"title-placeholder": "내 옵션",
|
||||
"desc": "특정 명령으로 Beat Saber를 실행하기 위한 재사용 가능한 실행 옵션을 만듭니다.",
|
||||
"name": "이름",
|
||||
"launch-option": "실행 옵션",
|
||||
"create": "만들기"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Avatares",
|
||||
"sabers": "Sabres",
|
||||
"platforms": "Platformas",
|
||||
"blocks": "Blocos",
|
||||
"bloqs": "Blocos",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Deletar",
|
||||
"accept": "Aceitar",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Continuar",
|
||||
"shared": "Compartilhados",
|
||||
"pin": "Fixar",
|
||||
"unpin": "Desafixar"
|
||||
"unpin": "Desafixar",
|
||||
"this-guide": "este guia",
|
||||
"create": "Criar",
|
||||
"edit": "Editar"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "Desatualizado"
|
||||
"outdated": "Desatualizado",
|
||||
"update-text": "BSManager {version} está disponível!",
|
||||
"update-button": "Atualizar e reiniciar",
|
||||
"see-changelog": "Ver os changelogs"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Adicionar uma versão",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Essa versão está desatualizada, e alguns mods ou funções podem não funcionar mais como esperado. Prefira utilizar a versão recomendada ({recommendedVersion}) de Beat Saber para aproveitar todas as últimas novidades e correções de erros.",
|
||||
"advanced-launch": {
|
||||
"button": "Opções de Inicialização",
|
||||
"placeholder": "Opções de lançamento, ex: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Opções de lançamento, ex: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Criar uma opção de lançamento"
|
||||
},
|
||||
"skipsteam": "Pular Steam",
|
||||
"skipsteam-description": "Faz com que a Steam pare de abrir automaticamente com Beat Saber, ative se você está utilizando algum outro executável de VR como WiVRn ou Monado, que pode interferir com o SteamVR.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Duração",
|
||||
"likes": "Gostei",
|
||||
"date-uploaded": "Data de envio"
|
||||
"date-uploaded": "Data de envio",
|
||||
"added-date": "Data de adição"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Carregando mods...",
|
||||
"no-internet": "Sem internet",
|
||||
"mods-not-available": "Nenhum mod está disponível para essa versão de Beat Saber ainda",
|
||||
"status": {
|
||||
"no-wineprefix": "Não foi possível encontrar o caminho WINEPREFIX do BSManager. Por favor, inicie o Beat Saber no BSManager primeiro.",
|
||||
"beatmods-down": "Beatmods está atualmente inacessível. Tente novamente mais tarde. Se o problema persistir, nos avise no {links}.",
|
||||
"unknown": "Um erro desconhecido aconteceu ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Mais informações",
|
||||
"install-or-update": "Instalar ou Atualizar",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Nome",
|
||||
"size": "tamanho",
|
||||
"installed": "Instalado",
|
||||
"latest": "Mais recente",
|
||||
"description": "Descrição",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Desinstalar todos",
|
||||
"unselect-all": "Desmarcar todos"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "Este é um mod grande!",
|
||||
"this-is-a-very-large-mod": "Este é um mod muito grande!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Inglês, Reino Unido",
|
||||
"en-US": "Inglês, Estados Unidos",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Chinês (Tradicional)",
|
||||
"ja-JP": "Japonês",
|
||||
"ko-KR": "Coreano",
|
||||
"uk-UA": "Ucraniano"
|
||||
"uk-UA": "Ucraniano",
|
||||
"tl-PH": "Filipino"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Um erro aconteceu, não foi possível alterar a configuração de proxy de sistema."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Atualização automática",
|
||||
"description": "O BSManager será atualizado automaticamente ao iniciar o aplicativo.",
|
||||
"error-notification": {
|
||||
"message": "Ocorreu um erro, não foi possível alterar as configurações de atualização automática."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Não foi possível baixar o manifesto para essa versão; seu token de login pode estár inválido.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Não foi possível baixar o manifesto. Seu token de login pode ser inválido ou você pode não possuir a versão para desktop do Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Não foi possível encontrar o manifesto para essa versão.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Um erro aconteceu enquanto o manifesto estava sendo lido.",
|
||||
"ALREADY_DOWNLOADING": "Uma versão já está sendo baixada.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Não foi possível obter o manifesto necessário para baixar.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Não foi possível obter o manifesto necessário para o download. Seu token de login pode ser inválido ou você pode não possuir a versão para desktop do Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Um erro aconteceu durante a verificação de arquivos.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Alguns arquivos falharam na hora de serem baixados.",
|
||||
"META_LOGIN_TIMED_OUT": "O token de login demorou tempo de mais para chegar.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "A tela de login da Meta foi fechada.",
|
||||
"NO_META_AUTH_TOKEN": "Não foi possível recuperar o token de login da Meta necessário para baixar.",
|
||||
"UNKNOWN_ERROR": "Um erro desconhecido aconteceu."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Resgatar sua versão para desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Todos os mods foram desinstalados.",
|
||||
"errors": {
|
||||
"no-mods": "Nenhum mod está instalado nessa versão 😑"
|
||||
"no-mods": "Nenhum mod está instalado nessa versão 😑",
|
||||
"no-wineprefix": "Não foi possível encontrar o caminho WINEPREFIX do BSManager. Por favor, inicie o Beat Saber no BSManager primeiro."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Não me lembrar novamente",
|
||||
"ok": "Ok"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Não é possível desinstalar a versão",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "O BSManager não pode desinstalar a versão original do Beat Saber",
|
||||
"VersionNotFound": "Versão não encontrada.",
|
||||
"delete-folder": "Não foi possível deletar a versão. Certifique-se de que Beat Saber não está em execução."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Seu token de conexão com a Meta é necessário para baixar Beat Saber.",
|
||||
"need-cookie-enabled": "Ao entrar na Meta, uma janela de login irá aparecer e então você pode iniciar o processo de login. Por favor, lembre de aceitar os cookies, caso contrário, podemos não conseguir recuperar seu token para começar o download.",
|
||||
"enter-token-manually": "Digitar meu token de login manualmente",
|
||||
"enter-token-manually-tooltip": "Isso irá lhe permitir digitar seu token de login sem ter que passar pelo login da Meta."
|
||||
"enter-token-manually-tooltip": "Isso irá lhe permitir digitar seu token de login sem ter que passar pelo login da Meta.",
|
||||
"must-own-desktop-version": "Você deve possuir a versão para desktop do Beat Saber. Se comprou a versão autônoma, siga {link} para resgatá-la."
|
||||
},
|
||||
"stay": "Lembre-se de mim",
|
||||
"connect-to-meta": "Conectar a Meta"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "Essa versão {outdatedVersion} está desatualizada, e alguns mods ou funções podem não funcionar como esperado. Por favor baixe a versão mais recente ({recommendedVersion}) de Beat Saber para aproveitar as últimas novidades e correções."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Opção de lançamento personalizada",
|
||||
"title-placeholder": "Minha opção",
|
||||
"desc": "Crie uma opção de lançamento reutilizável para lançar Beat Saber com um comando específico.",
|
||||
"name": "Nome",
|
||||
"launch-option": "Opção de lançamento",
|
||||
"create": "Criar"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "Аватары",
|
||||
"sabers": "Сабли",
|
||||
"platforms": "Платформы",
|
||||
"blocks": "Блоки",
|
||||
"bloqs": "Блоки",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"accept": "Принять",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Продолжить",
|
||||
"shared": "Общий",
|
||||
"pin": "Закрепить",
|
||||
"unpin": "Открепить"
|
||||
"unpin": "Открепить",
|
||||
"this-guide": "это руководство",
|
||||
"create": "Создать",
|
||||
"edit": "Редактировать"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "устаревший"
|
||||
"outdated": "устаревший",
|
||||
"update-text": "BSManager {version} доступен!",
|
||||
"update-button": "Обновить и перезапустить",
|
||||
"see-changelog": "Смотреть изменения"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Добавить версию игры",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.",
|
||||
"advanced-launch": {
|
||||
"button": "Параметры запуска",
|
||||
"placeholder": "Параметры запуска, например: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Параметры запуска, например: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Создать параметр запуска"
|
||||
},
|
||||
"skipsteam": "Пропустить Steam",
|
||||
"skipsteam-description": "Предотвращает автоматическое открытие Steam с Beat Saber, включите, если вы используете другую VR-среду, такую как WiVRn или Monado, с которой SteamVR может мешать.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Длительность",
|
||||
"likes": "Лайки",
|
||||
"date-uploaded": "Дата загрузки"
|
||||
"date-uploaded": "Дата загрузки",
|
||||
"added-date": "Дата добавления"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Загрузка модов...",
|
||||
"no-internet": "Нет интернета",
|
||||
"mods-not-available": "Не найдены моды для этой версии Beat Saber",
|
||||
"status": {
|
||||
"no-wineprefix": "Не удалось найти путь WINEPREFIX BSManager. Пожалуйста, сначала запустите Beat Saber в BSManager.",
|
||||
"beatmods-down": "Сейчас Beatmods недоступен. Пожалуйста, попробуйте позже. Если проблема сохраняется, сообщите нам в {links}.",
|
||||
"unknown": "Неизвестная ошибка ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Подробнее",
|
||||
"install-or-update": "Установить или обновить",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Имя",
|
||||
"size": "Размер",
|
||||
"installed": "Установлено",
|
||||
"latest": "Latest",
|
||||
"description": "Описание",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Удалить всё",
|
||||
"unselect-all": "Снять все выделения"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "Это большой мод!",
|
||||
"this-is-a-very-large-mod": "Это очень большой мод!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Английский, Великобритания",
|
||||
"en-US": "Английский, США",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Китайский (традиционный)",
|
||||
"ja-JP": "Японский",
|
||||
"ko-KR": "Корейский",
|
||||
"uk-UA": "Украинский"
|
||||
"uk-UA": "Украинский",
|
||||
"tl-PH": "Филиппинец"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Произошла ошибка, не удалось изменить настройки системного прокси."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Автообновление",
|
||||
"description": "BSManager будет автоматически обновляться при запуске приложения.",
|
||||
"error-notification": {
|
||||
"message": "Произошла ошибка, не удалось изменить настройки автообновления."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Невозможно загрузить манифест этой версии; ваш токен входа, возможно, недействителен.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Невозможно загрузить манифест этой версии. Ваш токен входа, возможно, недействителен, или у вас нет настольной версии Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Невозможно найти манифест этой версии.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "При чтении манифеста произошла ошибка.",
|
||||
"ALREADY_DOWNLOADING": "Версия уже загружается.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Невозможно получить манифест, необходимый для загрузки.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Невозможно получить манифест, необходимый для загрузки. Ваш токен входа, возможно, недействителен, или у вас нет настольной версии Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "При проверке файлов произошла ошибка.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Некоторые файлы не удалось загрузить.",
|
||||
"META_LOGIN_TIMED_OUT": "Получение токена входа заняло слишком много времени.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Окно входа в Meta было закрыто.",
|
||||
"NO_META_AUTH_TOKEN": "Невозможно получить токен входа в Meta, необходимый для загрузки.",
|
||||
"UNKNOWN_ERROR": "Произошла неизвестная ошибка."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Получить настольную версию"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Все моды были удалены.",
|
||||
"errors": {
|
||||
"no-mods": "Для этой версии не было установлено модов 😑"
|
||||
"no-mods": "Для этой версии не было установлено модов 😑",
|
||||
"no-wineprefix": "Не удалось найти путь WINEPREFIX BSManager. Пожалуйста, сначала запустите Beat Saber в BSManager."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Больше не напоминать",
|
||||
"ok": "Ок"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Невозможно удалить версию",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager не может удалить оригинальную версию Beat Saber",
|
||||
"VersionNotFound": "Версия не найдена.",
|
||||
"delete-folder": "Не удалось удалить версию. Убедитесь, что Beat Saber не запущен."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "Для скачивания Beat Saber необходим ваш токен подключения к Meta.",
|
||||
"need-cookie-enabled": "При входе в Meta откроется окно входа, и вы сможете начать процесс входа. Пожалуйста, убедитесь, что вы приняли cookies, иначе мы можем не суметь получить ваш токен для начала загрузки.",
|
||||
"enter-token-manually": "Ввести мой токен входа вручную",
|
||||
"enter-token-manually-tooltip": "Это позволит вам ввести ваш токен входа без прохождения через вход в Meta."
|
||||
"enter-token-manually-tooltip": "Это позволит вам ввести ваш токен входа без прохождения через вход в Meta.",
|
||||
"must-own-desktop-version": "Вы должны владеть настольной версией Beat Saber. Если вы купили автономную версию, следуйте {link}, чтобы получить её."
|
||||
},
|
||||
"stay": "Запомнить меня",
|
||||
"connect-to-meta": "Подключиться к Meta"
|
||||
@@ -942,6 +984,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "Версия {outdatedVersion} устарела, и некоторые моды или функции могут работать некорректно. Пожалуйста, скачайте последнюю рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться новейшими функциями и исправлениями ошибок."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Опция запуска",
|
||||
"title-placeholder": "Моя опция",
|
||||
"desc": "Создайте повторно используемую опцию запуска для запуска Beat Saber с конкретной командой.",
|
||||
"name": "Имя",
|
||||
"launch-option": "Опция запуска",
|
||||
"create": "Создать"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"avatars": "Аватари",
|
||||
"sabers": "Шпаги",
|
||||
"platforms": "Платформи",
|
||||
"blocks": "Блоки",
|
||||
"bloqs": "Блоки",
|
||||
"cancel": "Скасувати",
|
||||
"delete": "Видалити",
|
||||
"accept": "Прийняти",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "Продовжити",
|
||||
"shared": "Спільне",
|
||||
"pin": "Закріпити",
|
||||
"unpin": "Відкріпити"
|
||||
"unpin": "Відкріпити",
|
||||
"this-guide": "цей посібник",
|
||||
"create": "Створити",
|
||||
"edit": "Редагувати"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "застарілий"
|
||||
"outdated": "застарілий",
|
||||
"update-text": "BSManager {version} доступний!",
|
||||
"update-button": "Оновити та перезапустити",
|
||||
"see-changelog": "Переглянути зміни"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Додати версію",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "Ця версія застаріла і деякі моди і/або функції можуть працювати некоректно. Рекомендуємо використовувати версію ({recommendedVersion}), щоб скористатися останніми функціями і виправленнями.",
|
||||
"advanced-launch": {
|
||||
"button": "Параметри запуску",
|
||||
"placeholder": "Параметри запуску, наприклад: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "Параметри запуску, наприклад: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "Створити параметр запуску"
|
||||
},
|
||||
"skipsteam": "Пропустити Steam",
|
||||
"skipsteam-description": "Запобігає автоматичному відкриттю Steam. Увімкніть це, якщо ви використовуєте інше серидовище VR (WiVRn/Monado), з яким Steam праює некоректно.",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "Довжина",
|
||||
"likes": "Лайки",
|
||||
"date-uploaded": "Дата завантаження"
|
||||
"date-uploaded": "Дата завантаження",
|
||||
"added-date": "Дата додавання"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Завантаження модів...",
|
||||
"no-internet": "Немає інтернету",
|
||||
"mods-not-available": "Для цієї версії Beat Saber поки що немає модів",
|
||||
"status": {
|
||||
"no-wineprefix": "Не вдалося знайти шлях до WINEPREFIX BSManager. Будь ласка, спочатку запустіть Beat Saber у BSManager.",
|
||||
"beatmods-down": "Beatmods зараз недоступний. Будь ласка, спробуйте пізніше. Якщо проблема не зникне, повідомте нас на {links}.",
|
||||
"unknown": "Сталася невідома помилка ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "Більше інформації",
|
||||
"install-or-update": "Встановити або оновити",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Назва",
|
||||
"size": "розмір",
|
||||
"installed": "Встановлено",
|
||||
"latest": "Остання",
|
||||
"description": "Опис",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "Видалити всі",
|
||||
"unselect-all": "Зняти виділення з всіх"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "Це великий мод!",
|
||||
"this-is-a-very-large-mod": "Це дуже великий мод!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "Англійська, Велика Британія",
|
||||
"en-US": "Англійська, США",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "Китайська (традиційна)",
|
||||
"ja-JP": "Японська",
|
||||
"ko-KR": "Корейська",
|
||||
"uk-UA": "Українська"
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "філіппінська"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "Сталася помилка, неможливо змінити налаштування системного проксі."
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "Автоматичне оновлення (Avtomatychne onovlennya)",
|
||||
"description": "BSManager автоматично оновлюватиметься, коли ви запустите програму.",
|
||||
"error-notification": {
|
||||
"message": "Сталася помилка, неможливо змінити налаштування автоматичного оновлення."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Не вдалося завантажити маніфест для цієї версії; ваш токен входу може бути недійсним.",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "Не вдалося завантажити маніфест. Ваш токен входу може бути недійсним, або у вас немає настільної версії Beat Saber.",
|
||||
"MANIFEST_FILE_NOT_FOUND": "Не вдалося знайти маніфест для цієї версії.",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "Сталася помилка під час читання маніфесту.",
|
||||
"ALREADY_DOWNLOADING": "Версія вже завантажується.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Не вдалося отримати маніфест, необхідний для завантаження.",
|
||||
"UNABLE_TO_GET_MANIFEST": "Не вдалося отримати маніфест, необхідний для завантаження. Ваш токен входу може бути недійсним, або у вас немає настільної версії Beat Saber.",
|
||||
"VERIFY_INTEGRITY_FAILED": "Сталася помилка під час перевірки цілісності файлів.",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "Деякі файли не вдалося завантажити.",
|
||||
"META_LOGIN_TIMED_OUT": "Токен входу занадто довго отримувався.",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Вікно входу в Meta було закрите користувачем.",
|
||||
"NO_META_AUTH_TOKEN": "Не вдалося отримати токен входу Meta, необхідний для завантаження.",
|
||||
"UNKNOWN_ERROR": "Сталася невідома помилка."
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "Отримати настільну версію"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "Усі моди були видалені.",
|
||||
"errors": {
|
||||
"no-mods": "У цій версії не встановлено жодних модів 😑"
|
||||
"no-mods": "У цій версії не встановлено жодних модів 😑",
|
||||
"no-wineprefix": "Не вдалося знайти шлях до WINEPREFIX BSManager. Будь ласка, спочатку запустіть Beat Saber у BSManager."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "Не нагадувати",
|
||||
"ok": "Гаразд"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "Неможливо видалити версію",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager не може видалити оригінальну версію Beat Saber",
|
||||
"VersionNotFound": "Версія не знайдена.",
|
||||
"delete-folder": "Не вдалося видалити версію. Переконайтеся, що Beat Saber не запущений."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -883,7 +924,8 @@
|
||||
"token-needed": "Для завантаження Beat Saber потрібен ваш токен підключення Meta.",
|
||||
"need-cookie-enabled": "При вході в Meta відкриється вікно входу, і ви зможете розпочати процес входу. Будь ласка, прийміть куки, інакше ми не зможемо отримати ваш токен для початку завантаження.",
|
||||
"enter-token-manually": "Ввести мій токен входу вручну",
|
||||
"enter-token-manually-tooltip": "Це дозволить вам ввести ваш токен входу без проходження входу через Meta."
|
||||
"enter-token-manually-tooltip": "Це дозволить вам ввести ваш токен входу без проходження входу через Meta.",
|
||||
"must-own-desktop-version": "Ви повинні мати настільну версію Beat Saber. Якщо ви придбали автономну версію, перейдіть за посиланням {link}, щоб отримати її."
|
||||
},
|
||||
"stay": "Запам'ятати мене",
|
||||
"connect-to-meta": "Підключитися до Meta"
|
||||
@@ -938,6 +980,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "Версія {outdatedVersion} застаріла, і деякі моди чи функції можуть більше не працювати належним чином. Будь ласка, завантажте останню рекомендовану версію ({recommendedVersion}) Beat Saber, щоб насолоджуватися останніми функціями та виправленнями помилок."
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "Опція запуску",
|
||||
"title-placeholder": "Моя опція",
|
||||
"desc": "Створіть повторно використовувану опцію запуску для запуску Beat Saber з конкретною командою.",
|
||||
"name": "Ім'я",
|
||||
"launch-option": "Опція запуску",
|
||||
"create": "Створити"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "頭像",
|
||||
"sabers": "光劍",
|
||||
"platforms": "平台",
|
||||
"blocks": "方塊",
|
||||
"bloqs": "方塊",
|
||||
"cancel": "取消",
|
||||
"delete": "刪除",
|
||||
"accept": "接受",
|
||||
@@ -26,7 +26,10 @@
|
||||
"continue": "繼續",
|
||||
"shared": "共享",
|
||||
"pin": "固定",
|
||||
"unpin": "取消固定"
|
||||
"unpin": "取消固定",
|
||||
"this-guide": "本指南",
|
||||
"create": "新增",
|
||||
"edit": "編輯"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "過時"
|
||||
"outdated": "過時",
|
||||
"update-text": "BSManager {version} 可用!",
|
||||
"update-button": "更新並重新啟動",
|
||||
"see-changelog": "查看變更"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "新增版本",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。",
|
||||
"advanced-launch": {
|
||||
"button": "啟動選項",
|
||||
"placeholder": "啟動選項,例如: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "啟動選項,例如: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "新增啟動選項"
|
||||
},
|
||||
"skipsteam": "跳過 Steam",
|
||||
"skipsteam-description": "防止 Steam 與 Beat Saber 自動打開,如果您使用的是其他 VR 執行時,如 WiVRn 或 Monado,SteamVR 可能會干擾,請啟用此選項。",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "持續時間",
|
||||
"likes": "喜歡",
|
||||
"date-uploaded": "上傳日期"
|
||||
"date-uploaded": "上傳日期",
|
||||
"added-date": "新增日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Mod 載入中...",
|
||||
"no-internet": "無網路",
|
||||
"mods-not-available": "該版本 BeatSaber 暫無可用 Mod",
|
||||
"status": {
|
||||
"no-wineprefix": "找不到 BSManager WINEPREFIX 路徑。請先在 BSManager 中啟動 Beat Saber。",
|
||||
"beatmods-down": "Beatmods 目前無法訪問。請稍後再試。如果問題持續,請在 {links} 上告訴我們。",
|
||||
"unknown": "發生了一個錯誤 ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "更多資訊",
|
||||
"install-or-update": "安裝或更新",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "名稱",
|
||||
"size": "大小",
|
||||
"installed": "已安裝",
|
||||
"latest": "最新",
|
||||
"description": "描述",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "全部移除",
|
||||
"unselect-all": "取消全選"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "這是一個大型Mod!",
|
||||
"this-is-a-very-large-mod": "這是一個非常大型的Mod!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -247,6 +265,7 @@
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "英式英語",
|
||||
"en-US": "美式英語",
|
||||
@@ -260,7 +279,8 @@
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日語",
|
||||
"ko-KR": "韓語",
|
||||
"uk-UA": "烏克蘭"
|
||||
"uk-UA": "烏克蘭",
|
||||
"tl-PH": "菲律賓人"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -321,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "發生錯誤,無法更改系統代理設置。"
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "自動更新",
|
||||
"description": "啟動應用程式時,BSManager 將自動更新。",
|
||||
"error-notification": {
|
||||
"message": "發生錯誤,無法更改自動更新設定。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "無法下載此版本的清單;你的登錄令牌可能無效。",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "無法下載清單。您的登入令牌可能無效,或者您未擁有 Beat Saber 的桌面版本。",
|
||||
"MANIFEST_FILE_NOT_FOUND": "找不到此版本的清單。",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "讀取清單時發生錯誤。",
|
||||
"ALREADY_DOWNLOADING": "已經有一個版本在下載中。",
|
||||
"UNABLE_TO_GET_MANIFEST": "無法獲取下載所需的清單。",
|
||||
"UNABLE_TO_GET_MANIFEST": "無法獲取下載所需的清單。您的登入令牌可能無效,或者您未擁有 Beat Saber 的桌面版本。",
|
||||
"VERIFY_INTEGRITY_FAILED": "文件驗證過程中發生錯誤。",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "一些文件下載失敗。",
|
||||
"META_LOGIN_TIMED_OUT": "獲取登錄令牌用時過長。",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 登錄窗口被關閉。",
|
||||
"NO_META_AUTH_TOKEN": "無法獲取下載所需的 Meta 登錄令牌。",
|
||||
"UNKNOWN_ERROR": "發生未知錯誤。"
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "領取您的桌面版本"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "所有 Mod 已被移除。",
|
||||
"errors": {
|
||||
"no-mods": "該版本中沒有安裝 Mod 😑"
|
||||
"no-mods": "該版本中沒有安裝 Mod 😑",
|
||||
"no-wineprefix": "找不到 BSManager WINEPREFIX 路徑。請先在 BSManager 中啟動 Beat Saber。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -684,6 +715,16 @@
|
||||
"do-not-remind": "不要再提醒我",
|
||||
"ok": "確定"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "無法卸載版本",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager無法卸載Beat Saber的原始版本",
|
||||
"VersionNotFound": "找不到版本。",
|
||||
"delete-folder": "無法刪除版本。確保Beat Saber未運行。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -888,7 +929,8 @@
|
||||
"token-needed": "需要 Meta 登錄令牌用於下載 Beat Saber。",
|
||||
"need-cookie-enabled": "通過登錄 Meta,將打開一個登錄窗口,然後你可以開始登錄過程。請確保接受 cookie,否則我們可能無法檢索你的令牌以開始下載。",
|
||||
"enter-token-manually": "手動輸入我的登錄令牌",
|
||||
"enter-token-manually-tooltip": "這將允許你在不通過 Meta 登錄的情況下輸入你的登錄令牌。"
|
||||
"enter-token-manually-tooltip": "這將允許你在不通過 Meta 登錄的情況下輸入你的登錄令牌。",
|
||||
"must-own-desktop-version": "您必須擁有 Beat Saber 的桌面版本。如果您購買了獨立版本,請按照 {link} 領取。"
|
||||
},
|
||||
"stay": "記住我",
|
||||
"connect-to-meta": "連接到 Meta"
|
||||
@@ -943,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "版本 {outdatedVersion} 已過時,某些模組或功能可能無法正常運作。請下載最新推薦的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修正。"
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "自訂啟動選項",
|
||||
"title-placeholder": "我的選項",
|
||||
"desc": "建立一個可重用的啟動選項,以特定命令啟動 Beat Saber。",
|
||||
"name": "名稱",
|
||||
"launch-option": "啟動選項",
|
||||
"create": "建立"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"avatars": "头像",
|
||||
"sabers": "光剑",
|
||||
"platforms": "平台",
|
||||
"blocks": "方块",
|
||||
"bloqs": "方块",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"accept": "接受",
|
||||
@@ -21,12 +21,15 @@
|
||||
"copied": "已复制!",
|
||||
"confirm": "确认",
|
||||
"choose-folder": "选择文件夹",
|
||||
"Unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
|
||||
"unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
|
||||
"warning": "警告",
|
||||
"continue": "继续",
|
||||
"shared": "共享",
|
||||
"pin": "固定",
|
||||
"unpin": "取消固定"
|
||||
"unpin": "取消固定",
|
||||
"this-guide": "本指南",
|
||||
"create": "创建",
|
||||
"edit": "编辑"
|
||||
},
|
||||
"generic": {
|
||||
"env": {
|
||||
@@ -34,7 +37,10 @@
|
||||
}
|
||||
},
|
||||
"title-bar": {
|
||||
"outdated": "过时"
|
||||
"outdated": "过时",
|
||||
"update-text": "BSManager {version} 可用!",
|
||||
"update-button": "更新并重新启动",
|
||||
"see-changelog": "查看变更"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "添加版本",
|
||||
@@ -56,7 +62,8 @@
|
||||
"outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。",
|
||||
"advanced-launch": {
|
||||
"button": "启动选项",
|
||||
"placeholder": "启动选项,例如: KEY=VALUE %command% fpfc"
|
||||
"placeholder": "启动选项,例如: KEY=VALUE %command% fpfc",
|
||||
"create-launch-option": "创建启动选项"
|
||||
},
|
||||
"skipsteam": "跳过 Steam",
|
||||
"skipsteam-description": "防止 Steam 与 Beat Saber 自动打开,如果您使用的是其他 VR 运行时,如 WiVRn 或 Monado,SteamVR 可能会干扰,请启用此选项。",
|
||||
@@ -104,7 +111,8 @@
|
||||
"bpm": "BPM",
|
||||
"duration": "持续时间",
|
||||
"likes": "喜欢",
|
||||
"date-uploaded": "上传日期"
|
||||
"date-uploaded": "上传日期",
|
||||
"added-date": "添加日期"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -131,6 +139,11 @@
|
||||
"loading-mods": "Mod 加载中...",
|
||||
"no-internet": "无网络",
|
||||
"mods-not-available": "该版本 BeatSaber 暂无可用 Mod",
|
||||
"status": {
|
||||
"no-wineprefix": "找不到 BSManager WINEPREFIX 路径。请先在 BSManager 中启动 Beat Saber。",
|
||||
"beatmods-down": "Beatmods 目前无法访问。请稍后再试。如果问题持续,请在 {links} 上告知我们。",
|
||||
"unknown": "发生了一个错误 ¯\\_(ツ)_/¯"
|
||||
},
|
||||
"buttons": {
|
||||
"more-infos": "更多信息",
|
||||
"install-or-update": "安装或更新",
|
||||
@@ -139,6 +152,7 @@
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "名称",
|
||||
"size": "大小",
|
||||
"installed": "已安装",
|
||||
"latest": "最新",
|
||||
"description": "描述",
|
||||
@@ -147,6 +161,10 @@
|
||||
"uninstall-all": "全部卸载",
|
||||
"unselect-all": "取消全选"
|
||||
}
|
||||
},
|
||||
"mod-item": {
|
||||
"this-is-a-large-mod": "这是一个大型Mod!",
|
||||
"this-is-a-very-large-mod": "这是一个非常大型的Mod!"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@@ -240,12 +258,14 @@
|
||||
"es-ES": "Español",
|
||||
"it-IT": "Italiano",
|
||||
"de-DE": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"uk-UA": "Українська",
|
||||
"tl-PH": "Filipino",
|
||||
"translated": {
|
||||
"en-EN": "英式英语",
|
||||
"en-US": "美式英语",
|
||||
@@ -259,7 +279,8 @@
|
||||
"zh-TW": "正体中文",
|
||||
"ja-JP": "日语",
|
||||
"ko-KR": "韩语",
|
||||
"uk-UA": "烏克蘭"
|
||||
"uk-UA": "烏克蘭",
|
||||
"tl-PH": "菲律宾人"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -320,6 +341,13 @@
|
||||
"error-notification": {
|
||||
"message": "发生错误,无法启用系统代理。"
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"title": "自动更新",
|
||||
"description": "启动应用程序时,BSManager 将自动更新。",
|
||||
"error-notification": {
|
||||
"message": "发生错误,无法更改自动更新设置。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,17 +426,20 @@
|
||||
"oculus-download": {
|
||||
"errors": {
|
||||
"msg": {
|
||||
"DOWNLOAD_MANIFEST_FAILED": "无法下载此版本的清单;你的登录令牌可能无效。",
|
||||
"DOWNLOAD_MANIFEST_FAILED": "无法下载清单。您的登录令牌可能无效,或者您未拥有 Beat Saber 的桌面版本。",
|
||||
"MANIFEST_FILE_NOT_FOUND": "找不到此版本的清单。",
|
||||
"PARSE_MANIFEST_FILE_FAILED": "读取清单时发生错误。",
|
||||
"ALREADY_DOWNLOADING": "已经有一个版本在下载中。",
|
||||
"UNABLE_TO_GET_MANIFEST": "无法获取下载所需的清单。",
|
||||
"UNABLE_TO_GET_MANIFEST": "无法获取下载所需的清单。您的登录令牌可能无效,或者您未拥有 Beat Saber 的桌面版本。",
|
||||
"VERIFY_INTEGRITY_FAILED": "文件验证过程中发生错误。",
|
||||
"SOME_FILES_FAILED_TO_DOWNLOAD": "一些文件下载失败。",
|
||||
"META_LOGIN_TIMED_OUT": "获取登录令牌用时过长。",
|
||||
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 登录窗口被关闭。",
|
||||
"NO_META_AUTH_TOKEN": "无法获取下载所需的 Meta 登录令牌。",
|
||||
"UNKNOWN_ERROR": "发生未知错误。"
|
||||
},
|
||||
"actions": {
|
||||
"claim-your-desktop-version": "领取您的桌面版本"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,7 +620,8 @@
|
||||
"msg": {
|
||||
"success": "所有 Mod 已被卸载。",
|
||||
"errors": {
|
||||
"no-mods": "该版本中没有安装 Mod 😑"
|
||||
"no-mods": "该版本中没有安装 Mod 😑",
|
||||
"no-wineprefix": "找不到 BSManager WINEPREFIX 路径。请先在 BSManager 中启动 Beat Saber。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -683,6 +715,16 @@
|
||||
"do-not-remind": "不再提醒",
|
||||
"ok": "确定"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"errors": {
|
||||
"title": "无法卸载版本",
|
||||
"desc": {
|
||||
"CantDeleteOfficialVersion": "BSManager无法卸载Beat Saber的原始版本",
|
||||
"VersionNotFound": "未找到版本。",
|
||||
"delete-folder": "无法删除版本。确保Beat Saber未运行。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -887,7 +929,8 @@
|
||||
"token-needed": "需要 Meta 登录令牌用于下载 Beat Saber。",
|
||||
"need-cookie-enabled": "通过登录 Meta,将打开一个登录窗口,然后你可以开始登录过程。请确保接受 cookie,否则我们可能无法检索你的令牌以开始下载。",
|
||||
"enter-token-manually": "手动输入我的登录令牌",
|
||||
"enter-token-manually-tooltip": "这将允许你在不通过 Meta 登录的情况下输入你的登录令牌。"
|
||||
"enter-token-manually-tooltip": "这将允许你在不通过 Meta 登录的情况下输入你的登录令牌。",
|
||||
"must-own-desktop-version": "您必须拥有 Beat Saber 的桌面版本。如果您购买了独立版本,请按照 {link} 领取。"
|
||||
},
|
||||
"stay": "记住我",
|
||||
"connect-to-meta": "连接到 Meta"
|
||||
@@ -942,6 +985,14 @@
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "版本 {outdatedVersion} 已过时,某些模组或功能可能无法正常工作。请下载最新推荐的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修复。"
|
||||
},
|
||||
"custom-launch-option": {
|
||||
"title": "自定义启动选项",
|
||||
"title-placeholder": "我的选项",
|
||||
"desc": "创建一个可重用的启动选项,以特定命令启动 Beat Saber。",
|
||||
"name": "名称",
|
||||
"launch-option": "启动选项",
|
||||
"create": "创建"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+27
-4
@@ -5,6 +5,7 @@
|
||||
- [PPA Repository](#ppa-repository)
|
||||
- [dpkg Install](#dpkg-install)
|
||||
- [Arch (AUR)](#arch-aur)
|
||||
- [NixOS/Nix](#nixosnix)
|
||||
- [Universal (flatpak)](#universal-flatpak)
|
||||
- [Proton Setup](#proton-setup)
|
||||
|
||||
@@ -40,10 +41,10 @@ Download the `.deb` file in the releases and run the following command:
|
||||
dpkg -i ./bsmanager.deb
|
||||
```
|
||||
|
||||
**NOTE:** When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
|
||||
|
||||
- Download the latest `.deb` file on the Releases page; or
|
||||
- Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
|
||||
> [!NOTE]
|
||||
> When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
|
||||
> - Download the latest `.deb` file on the Releases page; or
|
||||
> - Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
|
||||
|
||||
### Arch (AUR)
|
||||
|
||||
@@ -51,6 +52,28 @@ Refer to [bs-manager-git](https://aur.archlinux.org/packages/bs-manager-git).
|
||||
|
||||
To install AUR packages, you need to install [yay](https://github.com/Jguer/yay).
|
||||
|
||||
### NixOS/Nix
|
||||
|
||||
Refer to [bs-manager](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/bs/bs-manager/package.nix).
|
||||
|
||||
Available in nixpkgs on `24.11` and `unstable` branches
|
||||
|
||||
> [!NOTE]
|
||||
> bs-manager will not be automatically updated in the client. You must update your channel or `flake.lock` to get the latest version in nixpkgs.
|
||||
> Additionally updates to the nix package will lag behind main due to nixpkgs being run by volunteers
|
||||
|
||||
#### Installation
|
||||
|
||||
BSManager can be installed via your configuration or [home-manager](https://github.com/nix-community/home-manager).
|
||||
|
||||
To try out bs-manager without installing, please run:
|
||||
```bash
|
||||
nix-shell -p bs-manager --run bs-manager
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> bs-manager has been untested on non-nixos systems running the nix package manager; if you encounter issues, please contact the nix package maintainer
|
||||
|
||||
### Universal (flatpak)
|
||||
|
||||
This should work on any linux distribution. You are only required to have `flatpak` installed in your system. If it is not installed, then go to [flatpak](https://flatpak.org/setup/) to look for a guide on how to install it on your distro.
|
||||
|
||||
+7
-6
@@ -18,7 +18,7 @@
|
||||
2. Install the Oculus Rift app on your computer.
|
||||
|
||||
> [!NOTE]
|
||||
>__If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with [BSManager](https://www.bsmanager.io), first claim it from its store page.__
|
||||
>__If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with [BSManager](https://www.bsmanager.io), first follow [this guide](https://github.com/Zagrios/bs-manager/wiki/how-to-claim-oculus-desktop-version) claim it from its store page.__
|
||||
|
||||
## Step 2 - Open developer tools
|
||||
|
||||
@@ -30,11 +30,12 @@
|
||||
In the developer tools:
|
||||
|
||||
1. Open the __`Network`__ tab.
|
||||
2. Filter for __`graph`__.
|
||||
3. Click on the first request that appears.
|
||||
4. Open the __`Payload`__ tab.
|
||||
5. Scroll to the bottom to locate your token. It should start with __`FRL`__.
|
||||
6. Select the token using your mouse and press `Ctrl` + `C` to copy it.
|
||||
2. Press CTRL+R.
|
||||
3. Filter for __`graph`__.
|
||||
4. Click on the first request that appears.
|
||||
5. Open the __`Payload`__ tab.
|
||||
6. Scroll to the bottom to locate your token. It should start with __`FRL`__.
|
||||
7. Select the token using your mouse and press `Ctrl` + `C` to copy it.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Login/How-to-obtain-your-Oculus-Token/GetYourOculusToken.png" alt="GetYourOculusToken.png" />
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 428 KiB |
+21
@@ -0,0 +1,21 @@
|
||||
## Table of Contents
|
||||
|
||||
- [Step 1 - Install and log into the Oculus Rift app](#step-1---install-and-log-into-the-oculus-rift-app)
|
||||
- [Step 2 - Claim your desktop version of Beat Saber](#step-2---claim-your-desktop-version-of-beat-saber)
|
||||
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
|
||||
1. Download the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/).
|
||||
2. Install the Oculus Rift app on your computer.
|
||||
|
||||
## Step 2 - Claim your desktop version of Beat Saber
|
||||
|
||||
1. Open the Oculus app.
|
||||
2. Go to the __`Store`__ tab.
|
||||
3. Search for __`Beat Saber`__.
|
||||
4. Click on the __`Get`__ button to claim the game.
|
||||
|
||||
<div align="center">
|
||||
<img src="../wiki/Guides/Login/how-to-claim-oculus-desktop-version/claimOculusDesktopVersion.png" alt="claimOculusDesktopVersion.png" />
|
||||
</div>
|
||||
@@ -19,6 +19,7 @@
|
||||
<summary>🔑 Login</summary>
|
||||
<ul>
|
||||
<li><a href="How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a></li>
|
||||
<li><a href="how-to-claim-oculus-desktop-version">How to claim your Oculus Desktop version of Beat Saber</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ const config = {
|
||||
afterSign: ".erb/scripts/notarize.js",
|
||||
afterPack: ".erb/scripts/after-pack.js",
|
||||
win: {
|
||||
signingHashAlgorithms: ["sha256"],
|
||||
signtoolOptions: {
|
||||
signingHashAlgorithms: ["sha256"],
|
||||
certificateSha1: "d55f8cda15bd9cba76ea796b9504860b16c7f46e",
|
||||
},
|
||||
target: [
|
||||
"nsis",
|
||||
"nsis-web"
|
||||
|
||||
+2
-1
@@ -12,8 +12,9 @@ const config: Config = {
|
||||
},
|
||||
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
|
||||
moduleDirectories: ["node_modules", "src"],
|
||||
testPathIgnorePatterns: ["release/app/dist"],
|
||||
testPathIgnorePatterns: ["<rootDir>/release/app"],
|
||||
setupFiles: ["./.erb/scripts/check-build-exists.ts"],
|
||||
modulePathIgnorePatterns: ["<rootDir>/release/app"]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Generated
+4392
-718
File diff suppressed because it is too large
Load Diff
+31
-25
@@ -2,27 +2,29 @@
|
||||
"name": "bs-manager",
|
||||
"description": "Manage maps, mods and more for Beat Saber",
|
||||
"main": "./.erb/dll/main.bundle.dev.js",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.6",
|
||||
"scripts": {
|
||||
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
|
||||
"build-rust-scripts": "tsx ./.erb/scripts/build-rust-scripts.js",
|
||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||
"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",
|
||||
"postinstall": "tsx .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 --config electron-builder.config.js && npm run build:dll",
|
||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
||||
"package": "tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --config electron-builder.config.js && npm run build:dll",
|
||||
"start": "tsx ./.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",
|
||||
"test": "jest ./src/__tests__/**/*.test.ts",
|
||||
"test:unit": "jest ./src/__tests__/unit",
|
||||
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --win --x64",
|
||||
"build:win": "npm run build && electron-builder --config electron-builder.config.js --publish never --win --x64",
|
||||
"publish": "npm run build && electron-builder --config electron-builder.config.js --publish always --win --x64",
|
||||
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64",
|
||||
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak"
|
||||
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak",
|
||||
"update:patreon": "tsx ./.erb/scripts/update-patreon.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
@@ -102,16 +104,16 @@
|
||||
"autoprefixer": "^10.4.17",
|
||||
"browserslist-config-erb": "^0.0.3",
|
||||
"chalk": "^4.1.2",
|
||||
"concurrently": "^8.2.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"core-js": "^3.36.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"detect-port": "^1.5.1",
|
||||
"electron": "^33.3.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"detect-port": "^2.1.0",
|
||||
"electron": "39.2.7",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electronmon": "^2.0.2",
|
||||
"electronmon": "^2.0.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-erb": "^4.1.0",
|
||||
@@ -144,6 +146,8 @@
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.3.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.90.3",
|
||||
@@ -153,6 +157,7 @@
|
||||
"webpack-merge": "^5.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/is-prop-valid": "1.4.0",
|
||||
"@internationalized/date": "^3.5.4",
|
||||
"@nextui-org/date-picker": "^2.0.7",
|
||||
"@nextui-org/react": "^2.3.6",
|
||||
@@ -160,27 +165,27 @@
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"archiver": "^7.0.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^4.2.3",
|
||||
"color": "^5.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"dot-prop": "^8.0.2",
|
||||
"dot-prop": "^9.0.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"framer-motion": "12.23.26",
|
||||
"fs-extra": "^11.3.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.5",
|
||||
"got": "^14.4.7",
|
||||
"history": "^5.3.0",
|
||||
"is-elevated": "^4.0.0",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-abi": "^3.65.0",
|
||||
"node-abi": "^4.2.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"protobufjs": "^8.0.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
@@ -193,15 +198,16 @@
|
||||
"react-window": "^1.8.11",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"rfdc": "^1.4.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.6.3",
|
||||
"semver": "7.7.3",
|
||||
"serialize-error": "^12.0.0",
|
||||
"striptags": "^4.0.0-alpha.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"to-ico": "^1.1.5",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"use-double-click": "^1.0.5",
|
||||
"use-fit-text": "^2.4.0",
|
||||
"yauzl": "^3.2.0"
|
||||
@@ -244,6 +250,6 @@
|
||||
"logLevel": "quiet"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+334
-12
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@resvg/resvg-js": "2.6.2",
|
||||
"ps-list": "^7.2.0",
|
||||
"query-process": "^0.0.3",
|
||||
"regedit-rs": "^1.0.2"
|
||||
"regedit-rs": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js": {
|
||||
@@ -38,6 +38,118 @@
|
||||
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-android-arm-eabi": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
|
||||
"integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-android-arm64": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
|
||||
"integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-darwin-arm64": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
|
||||
"integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-darwin-x64": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
|
||||
"integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
|
||||
"integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
|
||||
"integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
|
||||
"integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
|
||||
@@ -54,6 +166,54 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-linux-x64-musl": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
|
||||
"integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
|
||||
"integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
|
||||
"integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
|
||||
@@ -99,6 +259,134 @@
|
||||
"query-process-win32-x64-msvc": "0.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-android-arm-eabi": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-android-arm-eabi/-/query-process-android-arm-eabi-0.0.3.tgz",
|
||||
"integrity": "sha512-NB+9T+/poBcygDiG+7d2lJSeioP5ZyLT0HwUBdnLgNWvNCpzbJxydup1SLpnt5G2NF6nlTgbUZlH4hvxY7t/bw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-android-arm64": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-android-arm64/-/query-process-android-arm64-0.0.3.tgz",
|
||||
"integrity": "sha512-TiAw2yO62vQzM0s02471LToHyUs/PRkLGGlA715z10BPIM5NckzIe245AI45CDyH6tE/3nVHUd0LX/dfjQtTcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-linux-arm-gnueabihf": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-linux-arm-gnueabihf/-/query-process-linux-arm-gnueabihf-0.0.3.tgz",
|
||||
"integrity": "sha512-w7NLHBKt7qselCDoh/PVWyeuEb2NiiDDf1A9UB4K3Ns/H24hK3NbfMMBm8mCFxUnFUqoVX8O+VMs7dD/FNlTxA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-linux-arm64-gnu": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-linux-arm64-gnu/-/query-process-linux-arm64-gnu-0.0.3.tgz",
|
||||
"integrity": "sha512-nrPHjnSqCBuuN7IZkZWOnBpiJjyKOjfpT0Xt2EjSGp+I7C0CHbnhCL/vG1hBzaWZn0mxJ5nf0c5N0Fja7krnpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-linux-arm64-musl": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-linux-arm64-musl/-/query-process-linux-arm64-musl-0.0.3.tgz",
|
||||
"integrity": "sha512-vk8xMYTnS1PRKhwJgqLKyBAGIFP2CYZW2BsaKuNgQKUrDv7NnBaG7yukZT8niHNO1ycSrFpR/xqeJ8R23hO07A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-linux-x64-gnu": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-linux-x64-gnu/-/query-process-linux-x64-gnu-0.0.3.tgz",
|
||||
"integrity": "sha512-j1tGcNnGyVCABJ1PHZD1gKuyuomNzsPow3NDuZzUGjPqvKu67RsmKJJJOhMyT1SvUkcqdLFdtS1KTD20I4GS5Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-linux-x64-musl": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-linux-x64-musl/-/query-process-linux-x64-musl-0.0.3.tgz",
|
||||
"integrity": "sha512-AOGrrI/Qcb30iKFstiW60zNG/HQevCY0Q38eMHGd7LrBZ5sEN7NzZOjzKp43KeNHejuZBDMxf0gdGAqDznOAng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-win32-arm64-msvc": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-win32-arm64-msvc/-/query-process-win32-arm64-msvc-0.0.3.tgz",
|
||||
"integrity": "sha512-LHvxbzMFwPPUXHe4bDCAqs1bYbp8j0bMW8XyJGPZnIYrnwHz6g8VE5WQEtXQxSznsbfVxAKJXUtn088EjDVazw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/query-process-win32-x64-msvc": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/query-process-win32-x64-msvc/-/query-process-win32-x64-msvc-0.0.3.tgz",
|
||||
@@ -115,25 +403,59 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regedit-rs": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs/-/regedit-rs-1.0.2.tgz",
|
||||
"integrity": "sha512-4vEgiZNO1FCG8z/Zx3v/6PU1+eZ+ELe6R0ca+VB96Vw+Mi3M0IVHAjtMFbl97lUSX11dJqpyousX/wY8QcI1lA==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs/-/regedit-rs-1.0.4.tgz",
|
||||
"integrity": "sha512-G+gESK8PvuqUec8LT4TrkbGvOOw4EzGd+kRn4zzmnnrWu0mgBD6P0zQPj+9EUIYW8L4BPETWiSkkyAFamc4JEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"regedit-rs-win32-arm64-msvc": "1.0.2",
|
||||
"regedit-rs-win32-ia32-msvc": "1.0.2",
|
||||
"regedit-rs-win32-x64-msvc": "1.0.2"
|
||||
"regedit-rs-win32-arm64-msvc": "1.0.4",
|
||||
"regedit-rs-win32-ia32-msvc": "1.0.4",
|
||||
"regedit-rs-win32-x64-msvc": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/regedit-rs-win32-arm64-msvc": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs-win32-arm64-msvc/-/regedit-rs-win32-arm64-msvc-1.0.4.tgz",
|
||||
"integrity": "sha512-4oBCk+r8BnXT/SHJ+b6cKlhSy53oX++XdyqCCBOfrvy8hP7EX7rsp02vPgLErwCzQ4E42QLPIO0FoLRmGzTztg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/regedit-rs-win32-ia32-msvc": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs-win32-ia32-msvc/-/regedit-rs-win32-ia32-msvc-1.0.4.tgz",
|
||||
"integrity": "sha512-hhzZ7QCtQqUiM73H1NfqeRDU6eos778Kynk5SaV0IMHiNOgMgj2Mb0xQdWRovJgpquRgm+x0DS3cn2MbNZwuWQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/regedit-rs-win32-x64-msvc": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs-win32-x64-msvc/-/regedit-rs-win32-x64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-ccCSyd5vWBKVWftBKLKzegqwwPMWcQtIW0ub66dCFFuv2s+x2EcZZWGdD9dVXX2Z6V9DU2JRPKgWUNjVPaj6Xg==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/regedit-rs-win32-x64-msvc/-/regedit-rs-win32-x64-msvc-1.0.4.tgz",
|
||||
"integrity": "sha512-kb+965y6NKQijtCYYSvvE8puKAT3uyrHpr6HmZdGDZQb/iKNY2GmUX1/YrlYf9Kavdr5IJ9rkat6pokND+Npzw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.6",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
@@ -9,18 +9,18 @@
|
||||
"url": "https://github.com/Zagrios/bs-manager"
|
||||
},
|
||||
"scripts": {
|
||||
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
|
||||
"rebuild": "tsx ../../.erb/scripts/electron-rebuild.js",
|
||||
"link-modules": "tsx ../../.erb/scripts/link-modules.ts",
|
||||
"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"
|
||||
"regedit-rs": "1.0.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"volta": {
|
||||
"node": "20.11.0"
|
||||
"node": "24.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,59 +3,89 @@ import { parseEnvString } from "main/helpers/env.helpers";
|
||||
describe("Test parseEnvString", () => {
|
||||
|
||||
it("Empty", () => {
|
||||
const envVars = parseEnvString("");
|
||||
expect(envVars).toEqual({});
|
||||
const { env, command } = parseEnvString("");
|
||||
expect(env).toEqual({});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Single test; no quotes", () => {
|
||||
const envString = "HELLO=World!";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual({
|
||||
HELLO: "World!",
|
||||
});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Single test; single quotes", () => {
|
||||
const envString = "SINGLE_QOUTE='Single quote with spaces'";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual({
|
||||
SINGLE_QOUTE: "Single quote with spaces",
|
||||
});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Single test; double quotes", () => {
|
||||
const envString = 'DOUBLE_QOUTE="Some random quote."';
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual({
|
||||
DOUBLE_QOUTE: "Some random quote.",
|
||||
});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Single test; empty value", () => {
|
||||
const envString = "EMPTY=";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual({
|
||||
EMPTY: "",
|
||||
});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Multiple test; combined", () => {
|
||||
const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual(expect.objectContaining({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
HELLO: "World!",
|
||||
DOUBLE_QUOTE: "Two Words",
|
||||
SINGLE_QUOTE: "",
|
||||
EMPTY: ""
|
||||
}));
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Key with numbers and lower case", () => {
|
||||
const envString = "H3ll0=world";
|
||||
const envVars = parseEnvString(envString);
|
||||
expect(envVars).toEqual({
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual({
|
||||
H3ll0: "world",
|
||||
});
|
||||
expect(command).toEqual("");
|
||||
});
|
||||
|
||||
it("Simple command", () => {
|
||||
const { env, command } = parseEnvString("some-command");
|
||||
expect(env).toEqual({});
|
||||
expect(command).toBe("some-command");
|
||||
});
|
||||
|
||||
it("Env with command", () => {
|
||||
const { env, command } = parseEnvString("SAMPLE=value some-command");
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
SAMPLE: "value"
|
||||
}));
|
||||
expect(command).toBe("some-command");
|
||||
});
|
||||
|
||||
it("Complex with %command%", () => {
|
||||
const envString = "KEY=value gamescope -h 720 -H 1440 -S integer -- %command% ";
|
||||
const { env, command } = parseEnvString(envString);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
KEY: "value"
|
||||
}));
|
||||
expect(command).toBe("gamescope -h 720 -H 1440 -S integer -- %command%");
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
||||
|
||||
const SAMPLE_EXE = `"Beat Saber.exe"`;
|
||||
const PROTON_EXE = `"proton" run ${SAMPLE_EXE}`;
|
||||
|
||||
describe("Test parseLaunchOptions", () => {
|
||||
|
||||
it("Empty", () => {
|
||||
const {
|
||||
env, cmdlet, args
|
||||
} = parseLaunchOptions("", { commandReplacement: SAMPLE_EXE });
|
||||
expect(env).toEqual({});
|
||||
expect(cmdlet).toBe(SAMPLE_EXE);
|
||||
expect(args).toBe("");
|
||||
});
|
||||
|
||||
it("Envs", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
`HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`,
|
||||
{ commandReplacement: SAMPLE_EXE }
|
||||
);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
HELLO: "World!",
|
||||
DOUBLE_QUOTE: "Two Words",
|
||||
SINGLE_QUOTE: "",
|
||||
EMPTY: ""
|
||||
}));
|
||||
expect(cmdlet).toEqual(SAMPLE_EXE);
|
||||
expect(args).toEqual("");
|
||||
});
|
||||
|
||||
it("Env with %command%", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
`TEST=TEST %command%`,
|
||||
{ commandReplacement: SAMPLE_EXE }
|
||||
);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
TEST: "TEST",
|
||||
}));
|
||||
expect(cmdlet).toEqual(SAMPLE_EXE);
|
||||
expect(args).toEqual("");
|
||||
});
|
||||
|
||||
it("Envs with arguments", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
`HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY= %command% --vr-mode`,
|
||||
{ commandReplacement: SAMPLE_EXE }
|
||||
);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
HELLO: "World!",
|
||||
DOUBLE_QUOTE: "Two Words",
|
||||
SINGLE_QUOTE: "",
|
||||
EMPTY: ""
|
||||
}));
|
||||
expect(cmdlet).toEqual(SAMPLE_EXE);
|
||||
expect(args).toEqual("--vr-mode");
|
||||
});
|
||||
|
||||
it("Linux Command 1", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
"gamemoderun %command%",
|
||||
{ commandReplacement: PROTON_EXE }
|
||||
);
|
||||
expect(env).toEqual({});
|
||||
expect(cmdlet).toBe("gamemoderun");
|
||||
expect(args).toBe(PROTON_EXE);
|
||||
});
|
||||
|
||||
it("Linux Command 2", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
"mangohud %command%",
|
||||
{ commandReplacement: PROTON_EXE }
|
||||
);
|
||||
expect(env).toEqual({});
|
||||
expect(cmdlet).toBe("mangohud");
|
||||
expect(args).toBe(PROTON_EXE);
|
||||
});
|
||||
|
||||
it("Linux Command 3", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
"gamescope -h 720 -H 1440 -S integer -- %command%",
|
||||
{ commandReplacement: PROTON_EXE }
|
||||
);
|
||||
expect(env).toEqual({});
|
||||
expect(cmdlet).toBe("gamescope");
|
||||
expect(args).toBe(`-h 720 -H 1440 -S integer -- ${PROTON_EXE}`);
|
||||
});
|
||||
|
||||
it("Linux Command 4", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
`LD_PRELOAD="" gamescope --hdr-enabled -f -h 1440 -r 144 --force-grab-cursor --framerate-limit 144 --mangoapp -- %command%`,
|
||||
{ commandReplacement: PROTON_EXE }
|
||||
);
|
||||
expect(env).toEqual({
|
||||
LD_PRELOAD: ""
|
||||
});
|
||||
expect(cmdlet).toBe("gamescope");
|
||||
expect(args).toBe(`--hdr-enabled -f -h 1440 -r 144 --force-grab-cursor --framerate-limit 144 --mangoapp -- ${PROTON_EXE}`);
|
||||
});
|
||||
|
||||
it("Complex Linux Command", () => {
|
||||
const { env, cmdlet, args } = parseLaunchOptions(
|
||||
"WINEPREFIX=some-path HELLO=World gamescope -h 720 -H 1440 -S integer -- %command% --debug",
|
||||
{ commandReplacement: PROTON_EXE }
|
||||
);
|
||||
expect(env).toEqual(expect.objectContaining({
|
||||
WINEPREFIX: "some-path",
|
||||
HELLO: "World",
|
||||
}));
|
||||
expect(cmdlet).toBe("gamescope");
|
||||
expect(args).toBe(`-h 720 -H 1440 -S integer -- ${PROTON_EXE} --debug`);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { parseMetadata } from "main/helpers/mods.helpers";
|
||||
import path from "path";
|
||||
|
||||
const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "dll");
|
||||
|
||||
describe("Test mods.helpers.test", () => {
|
||||
|
||||
test("parseMetadata of BSML.dll", async () => {
|
||||
const filepath = path.join(TEST_FOLDER, "BSML.dll");
|
||||
const data = await parseMetadata(filepath);
|
||||
|
||||
expect(data).toBeTruthy();
|
||||
|
||||
expect(data.name).toBe("BeatSaberMarkupLanguage");
|
||||
expect(data.id).toBe("BeatSaberMarkupLanguage");
|
||||
expect(data.author).toBeTruthy();
|
||||
expect(data.version).toBeTruthy();
|
||||
expect(data.gameVersion).toBeTruthy();
|
||||
});
|
||||
|
||||
test("parseMetadata of Bugsmash.dll", async () => {
|
||||
const filepath = path.join(TEST_FOLDER, "Bugsmash.dll");
|
||||
const data = await parseMetadata(filepath);
|
||||
|
||||
expect(data).toBeTruthy();
|
||||
|
||||
expect(data.name).toBe("Bugsmash");
|
||||
expect(data.id).toBe("Bugsmash");
|
||||
expect(data.author).toBeTruthy();
|
||||
expect(data.version).toBeTruthy();
|
||||
expect(data.gameVersion).toBeTruthy();
|
||||
});
|
||||
|
||||
test("parseMetadata of PlaylistManager.dll", async () => {
|
||||
const filepath = path.join(TEST_FOLDER, "PlaylistManager.dll");
|
||||
const data = await parseMetadata(filepath);
|
||||
|
||||
expect(data).toBeTruthy();
|
||||
|
||||
expect(data.name).toBe("PlaylistManager");
|
||||
expect(data.id).toBe("PlaylistManager");
|
||||
expect(data.author).toBeTruthy();
|
||||
expect(data.version).toBeTruthy();
|
||||
expect(data.gameVersion).toBeTruthy();
|
||||
|
||||
});
|
||||
|
||||
test("parseMetadata of SongCore.dll", async () => {
|
||||
const filepath = path.join(TEST_FOLDER, "SongCore.dll");
|
||||
const data = await parseMetadata(filepath);
|
||||
|
||||
expect(data).toBeTruthy();
|
||||
|
||||
expect(data.name).toBe("SongCore");
|
||||
expect(data.id).toBe("SongCore");
|
||||
expect(data.author).toBeTruthy();
|
||||
expect(data.version).toBeTruthy();
|
||||
expect(data.gameVersion).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ jest.mock("electron-log", () => ({
|
||||
|
||||
jest.mock("ps-list", () => (): unknown[] => []);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const IS_LINUX = process.platform === "linux";
|
||||
|
||||
describe("Test os.helpers bsmSpawn", () => {
|
||||
@@ -51,6 +50,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
STEAM_COMPAT_CLIENT_INSTALL_PATH: "/steam",
|
||||
STEAM_COMPAT_APP_ID: BS_APP_ID,
|
||||
SteamEnv: "1",
|
||||
OXR_PARALLEL_VIEWS: "1",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -89,14 +89,11 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
it("Complex spawn command call (Mods install)", () => {
|
||||
bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, {
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./wine64"` },
|
||||
});
|
||||
|
||||
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(spawnSpy).toHaveBeenCalledWith(
|
||||
process.platform === "win32"
|
||||
? `"./BSIPA.exe" "./Beat Saber.exe" -n`
|
||||
: `"./wine64" "./BSIPA.exe" "./Beat Saber.exe" -n`,
|
||||
`"./BSIPA.exe" "./Beat Saber.exe" -n`,
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
@@ -112,14 +109,11 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
env: BS_ENV,
|
||||
},
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./proton" run` },
|
||||
});
|
||||
|
||||
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(spawnSpy).toHaveBeenCalledWith(
|
||||
IS_WINDOWS
|
||||
? `"./Beat Saber.exe" --no-yeet fpfc`
|
||||
: `"./proton" run "./Beat Saber.exe" --no-yeet fpfc`,
|
||||
`"./Beat Saber.exe" --no-yeet fpfc`,
|
||||
expect.objectContaining({
|
||||
cwd: "/",
|
||||
detached: true,
|
||||
@@ -140,14 +134,15 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
"STEAM_COMPAT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_APP_ID",
|
||||
"SteamEnv"
|
||||
"SteamEnv",
|
||||
"OXR_PARALLEL_VIEWS"
|
||||
];
|
||||
const newEnv = {
|
||||
...BS_ENV,
|
||||
something: "else",
|
||||
more: "tests",
|
||||
};
|
||||
bsmSpawn(`"./Beat Saber.exe"`, {
|
||||
bsmSpawn(`"./proton" run "./Beat Saber.exe"`, {
|
||||
args: ["--no-yeet", "fpfc"],
|
||||
options: {
|
||||
cwd: "/",
|
||||
@@ -155,7 +150,6 @@ describe("Test os.helpers bsmSpawn", () => {
|
||||
env: newEnv,
|
||||
},
|
||||
log: BsmShellLog.Command,
|
||||
linux: { prefix: `"./proton" run` },
|
||||
flatpak: {
|
||||
host: true,
|
||||
env: flatpakEnv,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import log from "electron-log";
|
||||
import { isPromise } from "../../shared/helpers/promise.helpers";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
|
||||
type LogOptions = {
|
||||
logInput?: boolean;
|
||||
logOutput?: boolean;
|
||||
logArgs?: boolean;
|
||||
};
|
||||
|
||||
const stringifyArgs = (args: unknown[]) => {
|
||||
return args?.map(a => JSON.stringify(a)).join(', ');
|
||||
};
|
||||
|
||||
const logInfo = (message: string, propertyKey: string, args: unknown[], logArgs: boolean, result: unknown) => {
|
||||
log.info(`[${message}] ${propertyKey}(${logArgs ? stringifyArgs(args) : ''})`, result);
|
||||
};
|
||||
|
||||
const logError = (propertyKey: string, args: unknown[], logArgs: boolean, error: unknown) => {
|
||||
log.error(`[ERROR] ${propertyKey}(${logArgs ? stringifyArgs(args) : ''})`, error);
|
||||
throw error;
|
||||
};
|
||||
|
||||
export function Log(options?: LogOptions){
|
||||
|
||||
const logInput = options?.logInput ?? false;
|
||||
const logOutput = options?.logOutput ?? true;
|
||||
const logArgs = options?.logArgs ?? true;
|
||||
|
||||
return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function(...args: unknown[]) {
|
||||
|
||||
if (logInput) {
|
||||
logInfo('INPUT', propertyKey, args, logArgs, null);
|
||||
}
|
||||
|
||||
const outcome = tryit(() => originalMethod.apply(this, args));
|
||||
|
||||
if (isPromise(outcome)) {
|
||||
return outcome.then(({ result, error }) => {
|
||||
|
||||
if (error) {
|
||||
logError(propertyKey, args, logArgs, error);
|
||||
}
|
||||
if (logOutput) {
|
||||
logInfo('OUTPUT', propertyKey, args, logArgs, result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
const { result, error } = outcome;
|
||||
|
||||
if (error) {
|
||||
logError(propertyKey, args, logArgs, error);
|
||||
}
|
||||
|
||||
if (logOutput) {
|
||||
logInfo('OUTPUT', propertyKey, args, logArgs, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ enum EnvParserState {
|
||||
QUOTE_VALUE,
|
||||
DQUOTE_VALUE,
|
||||
SPACE,
|
||||
EXIT,
|
||||
ERROR,
|
||||
};
|
||||
|
||||
@@ -28,7 +29,19 @@ const isAlphaCharacter = (c: string) =>
|
||||
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
||||
const isNumber = (c: string) => c >= "0" && c <= "9";
|
||||
|
||||
export function parseEnvString(envString: string): Record<string, string> {
|
||||
/**
|
||||
* Parses the env values from an envString command
|
||||
*
|
||||
* @params envString
|
||||
* @returns ({
|
||||
* env - parsed environment variables
|
||||
* command - part of the env string which is the command
|
||||
* })
|
||||
*/
|
||||
export function parseEnvString(envString: string): {
|
||||
env: Record<string, string>;
|
||||
command: string;
|
||||
} {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
let state: EnvParserState = EnvParserState.NAME_START;
|
||||
@@ -39,13 +52,13 @@ export function parseEnvString(envString: string): Record<string, string> {
|
||||
|
||||
switch (state) {
|
||||
case EnvParserState.NAME_START:
|
||||
index = pos;
|
||||
if (isAlphaCharacter(c) || c === "_") {
|
||||
state = EnvParserState.NAME;
|
||||
index = pos;
|
||||
} else if (c !== " ") {
|
||||
state = EnvParserState.ERROR;
|
||||
state = EnvParserState.EXIT;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case EnvParserState.NAME:
|
||||
if (c === "=") {
|
||||
@@ -53,57 +66,65 @@ export function parseEnvString(envString: string): Record<string, string> {
|
||||
newName = envString.substring(index, pos);
|
||||
index = pos + 1;
|
||||
} else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") {
|
||||
state = EnvParserState.ERROR;
|
||||
state = EnvParserState.EXIT;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case EnvParserState.VALUE_START:
|
||||
if (c === "'") {
|
||||
++index;
|
||||
state = EnvParserState.QUOTE_VALUE;
|
||||
} else if (c === '"') {
|
||||
++index;
|
||||
state = EnvParserState.DQUOTE_VALUE;
|
||||
} else if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = "";
|
||||
} else {
|
||||
state = EnvParserState.VALUE;
|
||||
}
|
||||
break;
|
||||
++index;
|
||||
state = EnvParserState.QUOTE_VALUE;
|
||||
} else if (c === '"') {
|
||||
++index;
|
||||
state = EnvParserState.DQUOTE_VALUE;
|
||||
} else if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = "";
|
||||
} else {
|
||||
state = EnvParserState.VALUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.VALUE:
|
||||
if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
state = EnvParserState.NAME_START;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.QUOTE_VALUE:
|
||||
if (c === "'") {
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.DQUOTE_VALUE:
|
||||
if (c === '"') {
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
state = EnvParserState.SPACE;
|
||||
envVars[newName] = envString.substring(index, pos);
|
||||
}
|
||||
break;
|
||||
|
||||
case EnvParserState.SPACE:
|
||||
if (c === " ") {
|
||||
state = EnvParserState.NAME_START;
|
||||
} else {
|
||||
state = EnvParserState.ERROR;
|
||||
}
|
||||
break;
|
||||
state = EnvParserState.NAME_START;
|
||||
} else {
|
||||
state = EnvParserState.ERROR;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
// Early exit
|
||||
if (state === EnvParserState.EXIT) {
|
||||
return {
|
||||
env: envVars,
|
||||
command: envString.substring(index).trim()
|
||||
};
|
||||
}
|
||||
|
||||
if (state === EnvParserState.ERROR) {
|
||||
throw new CustomError(
|
||||
`parseEnvString failed: invalid character at position ${pos}`,
|
||||
@@ -114,15 +135,15 @@ export function parseEnvString(envString: string): Record<string, string> {
|
||||
|
||||
if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) {
|
||||
envVars[newName] = envString.substring(index);
|
||||
return envVars;
|
||||
return { env: envVars, command: "" };
|
||||
}
|
||||
|
||||
if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) {
|
||||
return envVars;
|
||||
return { env: envVars, command: "" };
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
"parseEnvString failed: invalid ending state",
|
||||
"generic.env.parse"
|
||||
);
|
||||
return {
|
||||
env: envVars,
|
||||
command: envString.substring(index + 1).trim(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { CopyOptions, MoveOptions, RmOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, rm, stat, symlink, unlink, unlinkSync } from "fs-extra";
|
||||
import { CopyOptions, MoveOptions, RmOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, rm, stat, symlink, unlink, unlinkSync, rmSync } from "fs-extra";
|
||||
import { access, mkdir, readdir, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable, concatMap, from } from "rxjs";
|
||||
import log from "electron-log";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import crypto from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
@@ -33,7 +32,7 @@ export async function deleteFile(filepath: string) {
|
||||
log.info("Deleting file", `"${filepath}"`);
|
||||
await unlink(filepath);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete file", `"${filepath}"`);
|
||||
log.error("Could not delete file", `"${filepath}"`, error);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||
}
|
||||
}
|
||||
@@ -43,7 +42,7 @@ export function deleteFileSync(filepath: string) {
|
||||
log.info("Deleting file", `"${filepath}"`);
|
||||
unlinkSync(filepath);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete file", `"${filepath}"`);
|
||||
log.error("Could not delete file", `"${filepath}"`, error);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||
}
|
||||
}
|
||||
@@ -71,6 +70,7 @@ export function deleteFolderSync(folderPath: string, options?: RmOptions) {
|
||||
try {
|
||||
options = options || { recursive: true, force: true };
|
||||
log.info("Deleting folder", `"${folderPath}"`, options);
|
||||
rmSync(folderPath, options);
|
||||
} catch (error: any) {
|
||||
log.error("Could not delete folder", `"${folderPath}"`);
|
||||
throw CustomError.fromError(error, "generic.fs.delete-folder");
|
||||
@@ -180,7 +180,7 @@ export function isSubdirectory(parent: string, child: string): boolean {
|
||||
|
||||
export async function copyDirectoryWithJunctions(src: string, dest: string, options?: CopyOptions): Promise<void> {
|
||||
if (isSubdirectory(src, dest)) {
|
||||
throw { message: `Cannot copy directory '${src}' into itself '${dest}'.`, code: "COPY_TO_SUBPATH" } as BsmException;
|
||||
throw new CustomError(`Cannot copy directory '${src}' into itself '${dest}'.`, "COPY_TO_SUBPATH");
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { parseEnvString } from "./env.helpers";
|
||||
|
||||
const COMMAND_KEYWORD = "%command%";
|
||||
|
||||
/**
|
||||
* Parses the launch options command into parts to be used for bsmSpawn
|
||||
*
|
||||
* @params command
|
||||
* @params options.commandReplacement - Replaces the %command% string
|
||||
* @params options.linux - If the application is running under linux. Can be toggled in testing to check if the logic works.
|
||||
* @returns {
|
||||
* env - environment variables
|
||||
* cmdlet - BS.exe or a binary executable like gamemoderun and gamescope
|
||||
* args - Arguments for the cmdlet.
|
||||
* }
|
||||
*/
|
||||
export function parseLaunchOptions(launchOption: string, options: {
|
||||
commandReplacement: string;
|
||||
}): {
|
||||
env: Record<string, string>;
|
||||
cmdlet: string;
|
||||
args: string;
|
||||
} {
|
||||
if (!launchOption) {
|
||||
return { env: {}, cmdlet: options.commandReplacement, args: "" };
|
||||
}
|
||||
|
||||
const parsed = parseEnvString(launchOption);
|
||||
const { env } = parsed;
|
||||
|
||||
// If launch options only contains env strings
|
||||
if (!parsed.command) {
|
||||
return { env, cmdlet: options.commandReplacement, args: "" };
|
||||
}
|
||||
|
||||
const command = parsed.command.indexOf(COMMAND_KEYWORD) === -1
|
||||
? `${options.commandReplacement} ${parsed.command}`
|
||||
: parsed.command.replace(COMMAND_KEYWORD, options.commandReplacement);
|
||||
|
||||
// Offset if it starts with a " or '
|
||||
let offset = 0;
|
||||
if (command.startsWith('"')) {
|
||||
offset = command.indexOf('"', 1);
|
||||
} else if (command.startsWith("'")) {
|
||||
offset = command.indexOf("'", 1);
|
||||
}
|
||||
|
||||
// First word/token is the cmdlet, the rest are the arguments
|
||||
const index = command.indexOf(" ", offset);
|
||||
if (index === -1) {
|
||||
return { env, cmdlet: command.trim(), args: "" };
|
||||
}
|
||||
|
||||
return {
|
||||
env, cmdlet: command.substring(0, index),
|
||||
args: command.substring(index + 1).trim(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readFile } from "fs-extra";
|
||||
import { ModMetadata } from "shared/models/mods/mod.interface";
|
||||
|
||||
// NOTE: getMetadata is slow since its using a search string and object parsing.
|
||||
// Ways to optimize:
|
||||
// - Find a way to locate the metadata string within the pdb/dll file
|
||||
// - Correct object sizing, since the first byte does not match sometimes with the size somehow
|
||||
|
||||
// ref: https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json
|
||||
const SEARCH_ARRAY = [
|
||||
0x22, 0x24, 0x73, 0x63, 0x68,
|
||||
0x65, 0x6D, 0x61, 0x22
|
||||
].reverse(); // '"$schema"'
|
||||
|
||||
export async function parseMetadata(filepath: string): Promise<ModMetadata | null> {
|
||||
const data = await readFile(filepath);
|
||||
|
||||
// Find the word SEARCH_ARRAY from end to start of the buffer data
|
||||
// NOTE: Inefficient algorithm
|
||||
let start = data.length - 1;
|
||||
for (let i = 0; start >= 0; --start) {
|
||||
if (data[start] !== SEARCH_ARRAY[i]) {
|
||||
i = 0;
|
||||
} else if (++i === SEARCH_ARRAY.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search for the starting left curly brace '{'
|
||||
while (data[--start] !== 0x7B);
|
||||
|
||||
// Find the ending right curly brace '}'
|
||||
// NOTE: Inefficient algorithm
|
||||
let braces = 1;
|
||||
let end = start + 1;
|
||||
for (; end < data.length && braces > 0; ++end) {
|
||||
if (data[end] === 0x7B) { // {
|
||||
++braces;
|
||||
} else if (data[end] === 0x7D) { // }
|
||||
--braces;
|
||||
}
|
||||
}
|
||||
|
||||
// Does not work with every dll (eg. Bugsmash.dll)
|
||||
// const length = (data[start - 1] << 24)
|
||||
// + (data[start - 2] << 16)
|
||||
// + (data[start - 3] << 8)
|
||||
// + (data[start - 4]);
|
||||
// const end = start + length;
|
||||
|
||||
return JSON.parse(data.toString(undefined, start, end)) as ModMetadata;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,6 @@ import log from "electron-log";
|
||||
import psList from "ps-list";
|
||||
import { IS_FLATPAK } from "main/constants";
|
||||
|
||||
type LinuxOptions = {
|
||||
// Add the prefix to the command
|
||||
// eg. command - "./Beat Saber.exe" --no-yeet, prefix - "path/to/proton" run
|
||||
// = "path/to/proton" run "./Beat Saber.exe" --no-yeet
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
// Only applied if package as flatpak
|
||||
type FlatpakOptions = {
|
||||
// Force to use "flatpak-spawn --host" to run commands outside of the sandbox
|
||||
@@ -24,11 +17,10 @@ export enum BsmShellLog {
|
||||
};
|
||||
|
||||
interface BsmShellOptions<OptionsType> {
|
||||
args?: string[];
|
||||
args?: string[] | string;
|
||||
options?: OptionsType;
|
||||
// Look into BsmShellLog values
|
||||
log?: number;
|
||||
linux?: LinuxOptions;
|
||||
flatpak?: FlatpakOptions;
|
||||
};
|
||||
|
||||
@@ -37,7 +29,9 @@ export type BsmExecOptions = BsmShellOptions<cp.ExecOptions>;
|
||||
|
||||
function updateCommand(command: string, options: BsmSpawnOptions) {
|
||||
if (options?.args) {
|
||||
command += ` ${options.args.join(" ")}`;
|
||||
command += typeof(options.args) === "string"
|
||||
? ` ${options.args}`
|
||||
: ` ${options.args.join(" ")}`;
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
@@ -45,10 +39,6 @@ function updateCommand(command: string, options: BsmSpawnOptions) {
|
||||
// All distros should support "bash" by default
|
||||
options.options.shell = "bash";
|
||||
|
||||
if (options.linux?.prefix) {
|
||||
command = `${options.linux.prefix} ${command}`;
|
||||
}
|
||||
|
||||
if (options?.flatpak?.host) {
|
||||
const envArgs = (options?.flatpak?.env && options?.options?.env)
|
||||
&& options.flatpak.env
|
||||
|
||||
@@ -51,7 +51,15 @@ async function enableWindowsProxy(enable: boolean): Promise<void> {
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
const httpProxyUrl = `http://${await getProxyServer().catch(err => log.error(err))}`
|
||||
const httpProxyServer = await getProxyServer().catch(err => log.error(err));
|
||||
let httpProxyUrl: string | null = null;
|
||||
if (httpProxyServer) {
|
||||
// Prepend the http protocol
|
||||
httpProxyUrl = httpProxyServer.startsWith("http://")
|
||||
? httpProxyServer
|
||||
: `http://${httpProxyServer}`;
|
||||
}
|
||||
|
||||
globalProxyAgent.HTTP_PROXY = httpProxyUrl;
|
||||
globalProxyAgent.HTTPS_PROXY = httpProxyUrl;
|
||||
globalProxyAgent.NO_PROXY = `${await getProxyOverride().catch(err => log.error(err))}`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from } from "rxjs";
|
||||
import { BeatModsApiService } from "main/services/mods/beat-mods-api.service";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -33,3 +34,8 @@ ipc.on("bs-mods.uninstall-all-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(modsManager.uninstallAllMods(args));
|
||||
});
|
||||
|
||||
ipc.on("bs-mods.beatmods-up", (_, reply) => {
|
||||
const beatMods = BeatModsApiService.getInstance();
|
||||
reply(from(beatMods.isUp()));
|
||||
})
|
||||
|
||||
@@ -14,6 +14,11 @@ ipc.on("check-update", (_, reply) => {
|
||||
reply(from(updaterService.isUpdateAvailable()));
|
||||
});
|
||||
|
||||
ipc.on("get-available-update", (_, reply) => {
|
||||
const updaterService = AutoUpdaterService.getInstance();
|
||||
reply(from(updaterService.getAvailableUpdate()));
|
||||
});
|
||||
|
||||
ipc.on("install-update", (_, reply) => {
|
||||
const updaterService = AutoUpdaterService.getInstance();
|
||||
reply(of(updaterService.quitAndInstall()));
|
||||
|
||||
@@ -9,3 +9,8 @@ ipc.on("linux.verify-proton-folder", (_, reply) => {
|
||||
reply(of(linuxService.verifyProtonPath()));
|
||||
});
|
||||
|
||||
ipc.on("linux.get-wine-prefix-path", (_, reply) => {
|
||||
const linuxService = LinuxService.getInstance();
|
||||
reply(of(linuxService.getWinePrefixPath()));
|
||||
});
|
||||
|
||||
|
||||
+28
-8
@@ -28,6 +28,7 @@ import { StaticConfigurationService } from "./services/static-configuration.serv
|
||||
import { configureProxy } from './helpers/proxy.helpers';
|
||||
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { AutoUpdate } from "shared/models/config";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||
const staticConfig = StaticConfigurationService.getInstance();
|
||||
@@ -99,6 +100,10 @@ const findAssociatedFileInArgs = (args: string[]): string => {
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
const init = () => {
|
||||
initServicesMustBeInitialized();
|
||||
}
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
@@ -121,11 +126,8 @@ if (!gotTheLock) {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
||||
|
||||
|
||||
app.setAppUserModelId(APP_NAME);
|
||||
|
||||
initServicesMustBeInitialized();
|
||||
init();
|
||||
|
||||
const deepLink = findDeepLinkInArgs(process.argv);
|
||||
const associatedFile = findAssociatedFileInArgs(process.argv);
|
||||
@@ -134,10 +136,18 @@ if (!gotTheLock) {
|
||||
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
||||
} else if (associatedFile) {
|
||||
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
|
||||
} else if (process.platform === "linux") {
|
||||
createWindow("index.html");
|
||||
} else {
|
||||
createWindow(process.platform === "linux"
|
||||
? "index.html" : "launcher.html"
|
||||
);
|
||||
const configService = StaticConfigurationService.getInstance();
|
||||
const autoUpdate = configService.get("auto-update", AutoUpdate.ALWAYS);
|
||||
const update = autoUpdate !== AutoUpdate.NEVER;
|
||||
if (autoUpdate === AutoUpdate.ONCE) {
|
||||
configService.set("auto-update", AutoUpdate.NEVER);
|
||||
}
|
||||
|
||||
// Skip launcher only if autoUpdate is strictly false
|
||||
createWindow(update ? "launcher.html" : "index.html");
|
||||
}
|
||||
|
||||
SteamLauncherService.getInstance().restoreSteamVR();
|
||||
@@ -164,6 +174,16 @@ function convertDateToDateString(date: Date): string {
|
||||
return `${date.getFullYear()}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function getReadableTime(date: Date): string {
|
||||
return `${
|
||||
date.getHours().toString().padStart(2, "0")
|
||||
}-${
|
||||
date.getMinutes().toString().padStart(2, "0")
|
||||
}-${
|
||||
date.getSeconds().toString().padStart(2, "0")
|
||||
}`;
|
||||
}
|
||||
|
||||
function initLogger(){
|
||||
log.transports.file.level = "info";
|
||||
|
||||
@@ -178,7 +198,7 @@ function initLogger(){
|
||||
|
||||
filepath = path.join(
|
||||
app.getPath("logs"), nowString,
|
||||
`${now.getTime()}-v${app.getVersion()}.log`
|
||||
`${nowString}_${getReadableTime(now)}_v${app.getVersion()}.log`
|
||||
);
|
||||
currentDateString = nowString;
|
||||
return filepath;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import log from "electron-log";
|
||||
import fetch from "node-fetch";
|
||||
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
|
||||
import { mkdirs, createWriteStream, pathExists, WriteStream } from "fs-extra";
|
||||
@@ -87,7 +88,10 @@ export class OculusDownloader {
|
||||
sub.next(progress);
|
||||
}
|
||||
|
||||
})().catch(err => sub.error(err)).finally(() => {
|
||||
})()
|
||||
.then(() => {
|
||||
log.info("Downloaded file to", `"${destination}"`);
|
||||
}).catch(err => sub.error(err)).finally(() => {
|
||||
sub.complete();
|
||||
writeStream?.end();
|
||||
});
|
||||
@@ -100,12 +104,25 @@ export class OculusDownloader {
|
||||
}
|
||||
|
||||
private async isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise<boolean> {
|
||||
const [fileName, fileData] = file;
|
||||
const destination = path.join(folder, fileName);
|
||||
|
||||
const [filename, fileData] = file;
|
||||
const destination = path.join(
|
||||
folder,
|
||||
process.platform === "win32"
|
||||
? filename
|
||||
: filename.replaceAll("\\", "/")
|
||||
);
|
||||
|
||||
return pathExists(destination).then(exists => {
|
||||
if(!exists){ return false; }
|
||||
return hashFile(destination, "sha256").then(hash => hash === fileData.sha256);
|
||||
if(!exists){
|
||||
log.info("File not found", `"${destination}"`);
|
||||
return false;
|
||||
}
|
||||
return hashFile(destination, "sha256").then(hash => {
|
||||
const isValid = hash === fileData.sha256;
|
||||
log.info("File integrity", isValid ? "VALID" : "INVALID", `"${destination}"`);
|
||||
return isValid;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,6 +141,7 @@ export class OculusDownloader {
|
||||
if(canceled){ return; }
|
||||
|
||||
if(!(await this.isFileIntegrityValid(oculusFile, folder))){
|
||||
log.info("File integrity invalid", `"${oculusFile[0]}"`);
|
||||
wrongFiles.push(oculusFile);
|
||||
}
|
||||
|
||||
@@ -176,7 +194,13 @@ export class OculusDownloader {
|
||||
return from([res])
|
||||
}
|
||||
|
||||
const target = path.join(options.destination, filename);
|
||||
const target = path.join(
|
||||
options.destination,
|
||||
process.platform === "win32"
|
||||
? filename
|
||||
: filename.replaceAll("\\", "/")
|
||||
);
|
||||
|
||||
return this.downloadManifestFile(file, target).pipe(
|
||||
catchError(err => {
|
||||
this.options.logger?.error(err);
|
||||
|
||||
@@ -359,7 +359,18 @@ export class LocalPlaylistsManagerService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const downloadedMap = await this.maps.downloadMap(mapDetail, version);
|
||||
const downloadedMap = await this.maps.downloadMap(mapDetail, version)
|
||||
.catch(error => {
|
||||
log.error(
|
||||
"Could not download map for playlist",
|
||||
`[${mapDetail.id}] "${mapDetail.name}"`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if (!downloadedMap) {
|
||||
continue;
|
||||
}
|
||||
|
||||
progress.data.downloadedMaps.push(downloadedMap);
|
||||
progress.data.currentDownload = mapDetail;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { BsmLocalMap, BsmLocalMapMetadata, 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 } from "fs-extra";
|
||||
import { copy, createReadStream, ensureDir, existsSync, pathExists, pathExistsSync, readJson, realpath, writeJson } from "fs-extra";
|
||||
import { RequestService } from "../../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
@@ -31,6 +31,7 @@ import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
import { escapeRegExp } from "../../../../shared/helpers/string.helpers";
|
||||
import dateFormat from "dateformat";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -46,6 +47,7 @@ export class LocalMapsManagerService {
|
||||
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";
|
||||
public static readonly METADATA_FILE = "metadata.json";
|
||||
|
||||
private readonly DEEP_LINKS = {
|
||||
BeatSaver: "beatsaver",
|
||||
@@ -138,16 +140,21 @@ export class LocalMapsManagerService {
|
||||
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string, metadata: BsmLocalMapMetadata): 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) };
|
||||
return {
|
||||
mapInfo, coverUrl, songUrl, hash, path: mapPath,
|
||||
songDetails: this.songDetailsCache.getSongDetails(hash),
|
||||
metadata,
|
||||
};
|
||||
};
|
||||
|
||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
|
||||
if (cachedMapInfos) {
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||
const metadata = await this.getMetadata(mapPath);
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath, metadata);
|
||||
}
|
||||
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
@@ -166,8 +173,9 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||
const metadata = await this.getMetadata(mapPath);
|
||||
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath, metadata);
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<string> {
|
||||
@@ -369,10 +377,12 @@ export class LocalMapsManagerService {
|
||||
for (const folder of mapsFolders) {
|
||||
log.info(">", folder);
|
||||
|
||||
const regex = new RegExp(`^${escapeRegExp(folder)}\\/`);
|
||||
const entriesNames = isRoot
|
||||
? null
|
||||
: [ new RegExp(`^${escapeRegExp(folder)}\\/`) ];
|
||||
|
||||
const exported = await zip.extract(destination, {
|
||||
entriesNames: [regex],
|
||||
entriesNames,
|
||||
abortToken: abortController
|
||||
});
|
||||
|
||||
@@ -428,9 +438,10 @@ export class LocalMapsManagerService {
|
||||
throw new Error("Cannot download map, no hash found");
|
||||
}
|
||||
|
||||
log.info("Downloading map", map.name, map.id);
|
||||
|
||||
const zipUrl = map.versions.at(0).downloadURL;
|
||||
|
||||
log.info("Downloading map", map.name, map.id, zipUrl);
|
||||
|
||||
const mapFolderName = sanitize(`${map.id} (${map.metadata.songName} - ${map.metadata.levelAuthorName})`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
@@ -459,6 +470,21 @@ export class LocalMapsManagerService {
|
||||
return localMap;
|
||||
}
|
||||
|
||||
private async getMetadata(mapPath: string): Promise<BsmLocalMapMetadata> {
|
||||
const metadataPath = path.join(mapPath, LocalMapsManagerService.METADATA_FILE);
|
||||
if (existsSync(metadataPath)) {
|
||||
return await readJson(metadataPath) as BsmLocalMapMetadata;
|
||||
}
|
||||
|
||||
// Create the metadata then return it to the user
|
||||
const metadata: BsmLocalMapMetadata = {
|
||||
addedDate: dateFormat(new Date(), "yyyy-mm-dd'T'HH:MM:ss.l"),
|
||||
};
|
||||
|
||||
await writeJson(metadataPath, metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string): Promise<Observable<Progression>> {
|
||||
const archive = new Archive(outPath);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export class SongCacheService {
|
||||
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
|
||||
|
||||
private constructor(){
|
||||
console.log(this.MAPS_INFO_CACHE_PATH);
|
||||
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
|
||||
import { autoUpdater, CancellationToken, ProgressInfo, UpdateInfo } from "electron-updater";
|
||||
import log from "electron-log";
|
||||
import { gt } from "semver";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable } from "rxjs";
|
||||
import { safeGt } from "shared/helpers/semver.helpers";
|
||||
|
||||
export class AutoUpdaterService {
|
||||
private static instance: AutoUpdaterService;
|
||||
@@ -31,6 +32,18 @@ export class AutoUpdaterService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getAvailableUpdate(): Promise<UpdateInfo | null> {
|
||||
return autoUpdater.checkForUpdates().then(info => {
|
||||
if (info?.updateInfo && safeGt(info.updateInfo.version, autoUpdater.currentVersion.version)) {
|
||||
return info.updateInfo;
|
||||
}
|
||||
return null;
|
||||
}).catch((error: Error): UpdateInfo | null => {
|
||||
log.error("Could not get update", error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public downloadUpdate(): Observable<Progression> {
|
||||
return new Observable<Progression>(observer => {
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { LaunchOption } from "shared/models/bs-launch";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "child_process";
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { IS_FLATPAK } from "main/constants";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { parseEnvString } from "main/helpers/env.helpers";
|
||||
|
||||
export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
||||
const launchArgs = [];
|
||||
@@ -30,10 +28,6 @@ export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
||||
launchArgs.push("editor");
|
||||
}
|
||||
|
||||
if (launchOptions.command) {
|
||||
launchArgs.push(launchOptions.command);
|
||||
}
|
||||
|
||||
return Array.from(new Set(launchArgs).values());
|
||||
}
|
||||
|
||||
@@ -47,20 +41,20 @@ export abstract class AbstractLauncherService {
|
||||
this.localVersions = BSLocalVersionService.getInstance();
|
||||
}
|
||||
|
||||
private readonly COMMAND_FORMAT = "%command%";
|
||||
protected launchBeatSaberProcess(options: LaunchBeatSaberOptions): ChildProcessWithoutNullStreams {
|
||||
const spawnOptions: SpawnOptionsWithoutStdio = {
|
||||
detached: true,
|
||||
cwd: options.beatSaberFolderPath,
|
||||
env: options.env,
|
||||
};
|
||||
|
||||
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams {
|
||||
|
||||
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) };
|
||||
|
||||
if(args.includes("--verbose")){
|
||||
if (options.args?.includes("--verbose")){
|
||||
spawnOptions.windowsVerbatimArguments = true;
|
||||
}
|
||||
|
||||
spawnOptions.shell = true; // For windows to spawn properly
|
||||
return bsmSpawn(`"${bsExePath}"`, {
|
||||
args, options: spawnOptions, log: BsmShellLog.Command,
|
||||
linux: { prefix: options?.protonPrefix || "" },
|
||||
return bsmSpawn(options.cmdlet, {
|
||||
args: options.args, options: spawnOptions, log: BsmShellLog.Command,
|
||||
flatpak: {
|
||||
host: IS_FLATPAK,
|
||||
env: [
|
||||
@@ -73,6 +67,7 @@ export abstract class AbstractLauncherService {
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||
"STEAM_COMPAT_APP_ID",
|
||||
"SteamEnv",
|
||||
"OXR_PARALLEL_VIEWS",
|
||||
"PROTON_LOG",
|
||||
"PROTON_LOG_DIR",
|
||||
],
|
||||
@@ -80,8 +75,8 @@ export abstract class AbstractLauncherService {
|
||||
});
|
||||
}
|
||||
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBSProcess(bsExePath, args, options);
|
||||
protected launchBeatSaber(options: LaunchBeatSaberOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBeatSaberProcess(options);
|
||||
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
@@ -120,38 +115,35 @@ export abstract class AbstractLauncherService {
|
||||
return { process, exit };
|
||||
}
|
||||
|
||||
protected injectAdditionalArgsEnvs(
|
||||
launchOptions: LaunchOption,
|
||||
env: Record<string, string>
|
||||
) {
|
||||
if (!launchOptions.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { command } = launchOptions;
|
||||
const index = command.indexOf(this.COMMAND_FORMAT);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envString = command.substring(0, index);
|
||||
log.info("Parsing env string ", `"${envString}"`)
|
||||
for (const [ key, value ] of Object.entries(parseEnvString(envString))) {
|
||||
if (key in env) {
|
||||
log.warn("Ignoring", `${key}=${value}`, "already set env launch command");
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Injecting", `${key}="${value}"`, "to the env launch command");
|
||||
// Launch option helper function
|
||||
protected mergeEnvVariables(
|
||||
originalEnv: Record<string, string>,
|
||||
newEnv: Record<string, string>
|
||||
): Record<string, string> {
|
||||
const env = { ...originalEnv };
|
||||
for (const [ key, value ] of Object.entries(newEnv)) {
|
||||
log.info(
|
||||
key in env ? "Overriding" : "Injecting",
|
||||
`${key}="${value}"`,
|
||||
"to the env launch command"
|
||||
);
|
||||
env[key] = value;
|
||||
}
|
||||
|
||||
launchOptions.command = command.substring(index + this.COMMAND_FORMAT.length);
|
||||
return env;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type SpawnBsProcessOptions = {
|
||||
protonPrefix?: string;
|
||||
export type LaunchBeatSaberOptions = {
|
||||
// To be passed to the bsmSpawn helper function
|
||||
// Can be the Beat Saber exe or wrapper exe (for linux)
|
||||
cmdlet: string;
|
||||
env: Record<string, string>;
|
||||
beatSaberFolderPath: string;
|
||||
|
||||
args?: string[]; // Appended to the cmdlet string
|
||||
|
||||
// Timeout value (in ms) to unref the Beat Saber process to BSM
|
||||
unrefAfter?: number;
|
||||
} & SpawnOptionsWithoutStdio;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { pathExists } from "fs-extra";
|
||||
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
||||
import { isProcessRunning } from "../../helpers/os.helpers";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -47,21 +48,31 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
}
|
||||
|
||||
// Make sure Oculus is running
|
||||
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err));
|
||||
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err)).catch(err => {
|
||||
log.warn("Unable to start Oculus client. Force launch.", err);
|
||||
});
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||
let env: Record<string, string> = {
|
||||
...process.env,
|
||||
};
|
||||
const {
|
||||
env: parsedEnv,
|
||||
cmdlet, args,
|
||||
} = parseLaunchOptions(launchOptions.command, {
|
||||
commandReplacement: `"${exePath}"`,
|
||||
});
|
||||
env = this.mergeEnvVariables(env, parsedEnv);
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
// Launch Beat Saber
|
||||
const process = this.launchBs(
|
||||
exePath,
|
||||
buildBsLaunchArgs(launchOptions),
|
||||
{ env }
|
||||
);
|
||||
const bsProcess = this.launchBeatSaber({
|
||||
env, cmdlet,
|
||||
beatSaberFolderPath: bsPath,
|
||||
args: [ args, ...buildBsLaunchArgs(launchOptions) ]
|
||||
});
|
||||
|
||||
return process.exit.catch(err => {
|
||||
return bsProcess.exit.catch(err => {
|
||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import { SteamService } from "../steam.service";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
||||
import { AbstractLauncherService, buildBsLaunchArgs, LaunchBeatSaberOptions } from "./abstract-launcher.service";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { exec } from "child_process";
|
||||
import { exec, ChildProcessWithoutNullStreams } from "child_process";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { app } from "electron";
|
||||
import { app, Event } from "electron";
|
||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
||||
|
||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||
|
||||
@@ -64,6 +65,48 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
});
|
||||
}
|
||||
|
||||
protected launchBeatSaber(options: LaunchBeatSaberOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBeatSaberProcess(options);
|
||||
|
||||
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}`);
|
||||
// });
|
||||
|
||||
const onWillQuitHandler = async (event: Event) => {
|
||||
app.removeListener('will-quit', onWillQuitHandler);
|
||||
if (!process.killed) {
|
||||
event.preventDefault();
|
||||
log.info(`Unref'ing BS process ${process.pid} on app will-quit`);
|
||||
process.unref();
|
||||
await this.restoreSteamVR().catch(log.error);
|
||||
resolve(-1);
|
||||
app.quit();
|
||||
}
|
||||
};
|
||||
|
||||
process.on("error", (err) => {
|
||||
log.error(`Error while launching BS`, err);
|
||||
reject(err);
|
||||
app.removeListener('will-quit', onWillQuitHandler);
|
||||
});
|
||||
|
||||
process.on("exit", (code) => {
|
||||
log.info(`BS process exit with code ${code}`);
|
||||
resolve(code);
|
||||
app.removeListener('will-quit', onWillQuitHandler);
|
||||
});
|
||||
|
||||
app.on('will-quit', onWillQuitHandler);
|
||||
});
|
||||
|
||||
return { process, exit };
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
|
||||
|
||||
return new Observable<BSLaunchEventData>(obs => {(async () => {
|
||||
@@ -104,24 +147,35 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
|
||||
const steamPath = await this.steam.getSteamPath();
|
||||
|
||||
const env = {
|
||||
let env: Record<string, string> = {
|
||||
...process.env,
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
};
|
||||
|
||||
let protonPrefix = "";
|
||||
// Linux setup
|
||||
if (process.platform === "linux") {
|
||||
const linuxSetup = await this.linux.setupLaunch(
|
||||
if (launchOptions.admin) {
|
||||
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
|
||||
launchOptions.admin = false;
|
||||
}
|
||||
|
||||
Object.assign(env, await this.linux.buildEnvVariables(
|
||||
launchOptions, steamPath, bsFolderPath
|
||||
);
|
||||
protonPrefix = linuxSetup.protonPrefix;
|
||||
Object.assign(env, linuxSetup.env);
|
||||
));
|
||||
}
|
||||
|
||||
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||
const {
|
||||
env: parsedEnv,
|
||||
cmdlet, args
|
||||
} = parseLaunchOptions(launchOptions.command, {
|
||||
commandReplacement: process.platform === "win32"
|
||||
? `"${bsExePath}"`
|
||||
: `${await this.linux.getProtonPrefix()} "${bsExePath}"`,
|
||||
});
|
||||
env = this.mergeEnvVariables(env, parsedEnv);
|
||||
|
||||
const launchArgs = buildBsLaunchArgs(launchOptions);
|
||||
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
@@ -129,9 +183,12 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
const spawnOpts = { env, cwd: bsFolderPath };
|
||||
|
||||
const launchPromise = !launchOptions.admin ? (
|
||||
this.launchBs(bsExePath, launchArgs, {
|
||||
...spawnOpts,
|
||||
protonPrefix
|
||||
this.launchBeatSaber({
|
||||
env, cmdlet,
|
||||
args: args
|
||||
? [ args, ...launchArgs ]
|
||||
: launchArgs,
|
||||
beatSaberFolderPath: bsFolderPath,
|
||||
}).exit
|
||||
) : (
|
||||
new Promise<number>(resolve => {
|
||||
|
||||
@@ -61,7 +61,7 @@ export class BSLocalVersionService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versionsDict = await this.remoteVersionService.getAvailableVersions();
|
||||
const versionsDict = (await this.remoteVersionService.getAvailableVersions()).reverse();
|
||||
|
||||
let stream: ReadStream;
|
||||
|
||||
@@ -244,8 +244,12 @@ export class BSLocalVersionService {
|
||||
private async getSteamVersion(): Promise<BSVersion> {
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
||||
|
||||
if (!steamBsFolder || !(await pathExists(steamBsFolder))) {
|
||||
return null;
|
||||
if (!steamBsFolder) {
|
||||
throw new Error("No Beat Saber Steam version found");
|
||||
}
|
||||
|
||||
if (!(await pathExists(steamBsFolder))) {
|
||||
throw new Error(`Beat Saber Steam version not found in "${steamBsFolder}"`);
|
||||
}
|
||||
|
||||
return this.getVersionOfBSFolder(steamBsFolder, { steam: true });
|
||||
@@ -265,7 +269,8 @@ export class BSLocalVersionService {
|
||||
const versions: BSVersion[] = [];
|
||||
|
||||
const steamVersion = await this.getSteamVersion().catch(e => {
|
||||
log.error("unable to get original Steam version", e);
|
||||
log.error("Unable to get original Steam version", e);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (steamVersion) {
|
||||
@@ -306,14 +311,12 @@ export class BSLocalVersionService {
|
||||
return versions;
|
||||
}
|
||||
|
||||
public async deleteVersion(version: BSVersion): Promise<boolean>{
|
||||
if(version.steam || version.oculus){ return false; }
|
||||
public async deleteVersion(version: BSVersion): Promise<void>{
|
||||
if(version.steam || version.oculus){ throw new CustomError("BSManager is not able to delete official Beat Saber versions", "CantDeleteOfficialVersion"); }
|
||||
const versionFolder = await this.getVersionPath(version);
|
||||
if(!(await pathExists(versionFolder))){ return true; }
|
||||
if(!(await pathExists(versionFolder))){ throw new CustomError("Version not found", "VersionNotFound"); }
|
||||
|
||||
return deleteFolder(versionFolder)
|
||||
.then(() => { return true; })
|
||||
.catch(() => { return false; })
|
||||
return deleteFolder(versionFolder);
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
|
||||
@@ -107,7 +107,7 @@ export class BsOculusDownloaderService {
|
||||
let downloadVersion: BSVersion
|
||||
|
||||
return of(downloadInfo.token).pipe(
|
||||
switchMap(token => {
|
||||
switchMap(async token => {
|
||||
isOculusTokenValid(token, log.info); // Log token validity
|
||||
if(!downloadInfo.isVerification){
|
||||
return this.createDownloadVersion(downloadInfo.bsVersion).then(({version, dest}) => ({token, version, dest}))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import log from "electron-log";
|
||||
import { deleteFile, deleteFileSync, deleteFolder, deleteFolderSync, ensureFolderExist, moveFolderContent, pathExist } from "../helpers/fs.helpers";
|
||||
import { deleteFileSync, deleteFolder, deleteFolderSync, ensureFolderExist, moveFolderContent, pathExist } from "../helpers/fs.helpers";
|
||||
import { lstat, symlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { copy, mkdirSync, readlink, symlinkSync } from "fs-extra";
|
||||
@@ -113,7 +113,7 @@ export class FolderLinkerService {
|
||||
if (isTargetedToSharedPath) {
|
||||
return;
|
||||
}
|
||||
await deleteFile(folderPath);
|
||||
await deleteFolder(folderPath);
|
||||
|
||||
log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`);
|
||||
return symlink(sharedPath, folderPath, this.getLinkingType());
|
||||
@@ -141,7 +141,7 @@ export class FolderLinkerService {
|
||||
if (!(await this.isFolderSymlink(folderPath))) {
|
||||
return;
|
||||
}
|
||||
await deleteFile(folderPath);
|
||||
await deleteFolder(folderPath);
|
||||
|
||||
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export class InstallationLocationService {
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private readonly updateListeners: Set<Listener> = new Set();
|
||||
|
||||
private readonly installPath: string;
|
||||
private _installationDirectory: string;
|
||||
|
||||
private constructor() {
|
||||
@@ -34,6 +35,13 @@ export class InstallationLocationService {
|
||||
this.staticConfig.$watch(this.STORE_INSTALLATION_PATH_KEY).subscribe(() => {
|
||||
this.triggerListeners();
|
||||
});
|
||||
|
||||
if (process.platform === "linux") {
|
||||
this.installPath = process.env.XDG_DATA_HOME
|
||||
|| path.join(process.env.HOME, ".local", "share");
|
||||
} else {
|
||||
this.installPath = app.getPath("home");
|
||||
}
|
||||
}
|
||||
|
||||
private triggerListeners(): void {
|
||||
@@ -65,7 +73,7 @@ export class InstallationLocationService {
|
||||
|
||||
public defaultInstallationDirectory(): string {
|
||||
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
|
||||
const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : app.getPath("home");
|
||||
const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : this.installPath;
|
||||
return path.join(installationDirectory, this.INSTALLATION_FOLDER);
|
||||
}
|
||||
|
||||
@@ -85,7 +93,7 @@ export class InstallationLocationService {
|
||||
return app.getPath("documents");
|
||||
}
|
||||
|
||||
return app.getPath("home");
|
||||
return this.installPath;
|
||||
};
|
||||
|
||||
this._installationDirectory = installParentPath();
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BsmShellLog, bsmExec } from "main/helpers/os.helpers";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { SteamShortcutData } from "shared/models/steam/shortcut.model";
|
||||
import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service";
|
||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
@@ -38,26 +39,11 @@ export class LinuxService {
|
||||
return path.resolve(sharedFolder, "compatdata");
|
||||
}
|
||||
|
||||
public async setupLaunch(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
): Promise<{
|
||||
protonPrefix: string;
|
||||
env: Record<string, string>;
|
||||
}> {
|
||||
if (launchOptions.admin) {
|
||||
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
|
||||
launchOptions.admin = false;
|
||||
}
|
||||
|
||||
public async getProtonPrefix() {
|
||||
const protonPath = await this.getProtonPath();
|
||||
return {
|
||||
protonPrefix: await this.isNixOS()
|
||||
? `steam-run "${protonPath}" run`
|
||||
: `"${protonPath}" run`,
|
||||
env: await this.buildEnvVariables(launchOptions, steamPath, bsFolderPath)
|
||||
};
|
||||
return await this.isNixOS()
|
||||
? `steam-run "${protonPath}" run`
|
||||
: `"${protonPath}" run`;
|
||||
}
|
||||
|
||||
private async getProtonPath(): Promise<string> {
|
||||
@@ -81,7 +67,7 @@ export class LinuxService {
|
||||
return protonPath;
|
||||
}
|
||||
|
||||
private async buildEnvVariables(
|
||||
public async buildEnvVariables(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
@@ -105,6 +91,8 @@ export class LinuxService {
|
||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||
// Run game in steam environment; fixes #585 for unicode song titles
|
||||
"SteamEnv": "1",
|
||||
// Fix reflections in Monado
|
||||
"OXR_PARALLEL_VIEWS": "1",
|
||||
};
|
||||
|
||||
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
|
||||
@@ -174,18 +162,42 @@ export class LinuxService {
|
||||
|
||||
// === Shortcuts === //
|
||||
|
||||
private getCommand(
|
||||
protonPrefix: string,
|
||||
bsFolderPath: string,
|
||||
env: Record<string, string>,
|
||||
launchOptions: LaunchOption
|
||||
): string {
|
||||
private async getCommand(
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
beatSaberFolderPath: string
|
||||
): Promise<string> {
|
||||
const protonPrefix = await this.getProtonPrefix();
|
||||
const launchEnv = await this.buildEnvVariables(
|
||||
launchOptions, steamPath, beatSaberFolderPath
|
||||
);
|
||||
|
||||
const beatSaberExePath = path.join(beatSaberFolderPath, BS_EXECUTABLE);
|
||||
|
||||
const {
|
||||
env: parsedEnv,
|
||||
args: parsedArgs,
|
||||
cmdlet,
|
||||
} = parseLaunchOptions(launchOptions.command, {
|
||||
commandReplacement: `${protonPrefix} ${beatSaberExePath}`,
|
||||
});
|
||||
|
||||
const args = buildBsLaunchArgs(launchOptions);
|
||||
log.debug("Launch arguments:", args, "Parsed arguments:", parsedArgs);
|
||||
if (parsedArgs) {
|
||||
args.unshift(parsedArgs);
|
||||
}
|
||||
|
||||
const env = {
|
||||
...launchEnv, ...parsedEnv,
|
||||
SteamAppId: BS_APP_ID,
|
||||
SteamOverlayGameId: BS_APP_ID,
|
||||
SteamGameId: BS_APP_ID,
|
||||
};
|
||||
const envString = Object.entries(env)
|
||||
.map(([ key, value ]) => `${key}="${value}"`)
|
||||
.join(" ");
|
||||
const bsExe = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||
const args = buildBsLaunchArgs(launchOptions).join(" ");
|
||||
return `${envString} ${protonPrefix} "${bsExe}" ${args}`;
|
||||
return `${envString} ${cmdlet} ${args.join(" ")}`;
|
||||
}
|
||||
|
||||
public async createDesktopShortcut(
|
||||
@@ -194,22 +206,11 @@ export class LinuxService {
|
||||
icon: string,
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
beatSaberFolderPath: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const {
|
||||
protonPrefix, env
|
||||
} = await this.setupLaunch(launchOptions, steamPath, bsFolderPath);
|
||||
|
||||
Object.assign(env, {
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
});
|
||||
|
||||
const command = this.getCommand(
|
||||
protonPrefix, bsFolderPath,
|
||||
env, launchOptions
|
||||
const command = await this.getCommand(
|
||||
launchOptions, steamPath, beatSaberFolderPath
|
||||
);
|
||||
|
||||
const desktopEntry = [
|
||||
@@ -217,7 +218,7 @@ export class LinuxService {
|
||||
"Type=Application",
|
||||
`Name=${name}`,
|
||||
`Icon=${icon}`,
|
||||
`Path=${bsFolderPath}`,
|
||||
`Path=${beatSaberFolderPath}`,
|
||||
`Exec=${command}`
|
||||
].join("\n");
|
||||
|
||||
@@ -235,31 +236,20 @@ export class LinuxService {
|
||||
icon: string,
|
||||
launchOptions: LaunchOption,
|
||||
steamPath: string,
|
||||
bsFolderPath: string
|
||||
beatSaberFolderPath: string
|
||||
): Promise<SteamShortcutData> {
|
||||
const env = await this.buildEnvVariables(
|
||||
launchOptions, steamPath, bsFolderPath
|
||||
const protonPath = await this.getProtonPath();
|
||||
const command = await this.getCommand(
|
||||
launchOptions, steamPath, beatSaberFolderPath
|
||||
);
|
||||
Object.assign(env, {
|
||||
"SteamAppId": BS_APP_ID,
|
||||
"SteamOverlayGameId": BS_APP_ID,
|
||||
"SteamGameId": BS_APP_ID,
|
||||
});
|
||||
|
||||
const protonPrefix = await this.isNixOS()
|
||||
? "steam-run %command% run"
|
||||
: "%command% run";
|
||||
|
||||
return {
|
||||
AppName: shortcutName,
|
||||
Exe: await this.getProtonPath(),
|
||||
StartDir: bsFolderPath,
|
||||
Exe: protonPath,
|
||||
StartDir: beatSaberFolderPath,
|
||||
icon,
|
||||
OpenVR: "\x01",
|
||||
LaunchOptions: this.getCommand(
|
||||
protonPrefix, bsFolderPath,
|
||||
env, launchOptions
|
||||
)
|
||||
LaunchOptions: command
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,17 @@ export class BeatModsApiService {
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
public async isUp(): Promise<boolean> {
|
||||
try {
|
||||
// The data in status can be dropped
|
||||
await this.requestService.getJSON<{}>(`${this.MODS_REPO_API_URL}/status`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("Could not connect to beatmods", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string {
|
||||
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
|
||||
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
|
||||
@@ -59,17 +70,16 @@ export class BeatModsApiService {
|
||||
});
|
||||
}
|
||||
|
||||
public getModByHash(hash: string): Promise<BbmModVersion|undefined> {
|
||||
public async getModByHash(hash: string): Promise<BbmModVersion> {
|
||||
if (this.modsHashCache.has(hash)) {
|
||||
return Promise.resolve(this.modsHashCache.get(hash));
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(({ data }) => {
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
|
||||
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`
|
||||
).then(({ data }) => {
|
||||
this.updateModsHashCache(data?.modVersions ?? []);
|
||||
return data?.modVersions?.at(0);
|
||||
}).catch((): undefined => {
|
||||
log.error(`Failed to get mod by hash: ${hash}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ export class BsModsManagerService {
|
||||
private readonly linuxService: LinuxService;
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private manifestMatches: BbmModVersion[];
|
||||
|
||||
public static getInstance(): BsModsManagerService {
|
||||
if (!BsModsManagerService.instance) {
|
||||
BsModsManagerService.instance = new BsModsManagerService();
|
||||
@@ -45,17 +43,24 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<BbmModVersion | undefined> {
|
||||
const mod = await this.beatModsApi.getModByHash(hash);
|
||||
const mod: BbmModVersion | undefined = await this.beatModsApi.getModByHash(hash)
|
||||
.catch((error): undefined => {
|
||||
log.warn("Could not get mod with hash", hash, "cause", error?.message);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){
|
||||
if (mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return mod;
|
||||
|
||||
}
|
||||
|
||||
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<BbmModVersion[]> {
|
||||
private async getModsInDir(
|
||||
version: BSVersion,
|
||||
modsDir: ModsInstallFolder,
|
||||
manifestMatches: BbmModVersion[]
|
||||
): Promise<BbmModVersion[]> {
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPath = path.join(bsPath, modsDir);
|
||||
|
||||
@@ -66,34 +71,35 @@ export class BsModsManagerService {
|
||||
const files = await recursiveReadDir(modsPath);
|
||||
|
||||
const promises = files.map(async filePath => {
|
||||
const ext = path.extname(filePath);
|
||||
const ext = path.extname(filePath);
|
||||
if (ext !== ".dll" && ext !== ".exe" && ext !== ".manifest") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (ext !== ".dll" && ext !== ".exe" && ext !== ".manifest") {
|
||||
return undefined;
|
||||
}
|
||||
const hash = await md5File(filePath);
|
||||
const mod = await this.getModFromHash(hash);
|
||||
log.info("Getting mod manifest", filePath);
|
||||
const hash = await md5File(filePath);
|
||||
const mod = await this.getModFromHash(hash);
|
||||
|
||||
if (!mod) {
|
||||
if (!mod) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (ext === ".manifest") {
|
||||
manifestMatches.push(mod);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (modsDir === ModsInstallFolder.LIBS || modsDir === ModsInstallFolder.LIBS_PENDING) {
|
||||
const manifestIndex = manifestMatches.findIndex(m => m.id === mod.id);
|
||||
if (manifestIndex < 0) {
|
||||
log.warn("No matching manifest for", `"${filePath}"`, "with hash:", hash);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (ext === ".manifest") {
|
||||
this.manifestMatches.push(mod);
|
||||
return undefined;
|
||||
}
|
||||
manifestMatches.splice(manifestIndex, 1);
|
||||
}
|
||||
|
||||
if (filePath.toLowerCase().includes("libs")) {
|
||||
const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id);
|
||||
|
||||
if (manifestIndex < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.manifestMatches.splice(manifestIndex, 1);
|
||||
}
|
||||
|
||||
return mod;
|
||||
return mod;
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
@@ -101,6 +107,8 @@ export class BsModsManagerService {
|
||||
return results.reduce((mods, mod) => {
|
||||
if (mod?.status === "fulfilled" && mod?.value) {
|
||||
mods.push(mod.value);
|
||||
} else if (mod?.status === "rejected") {
|
||||
log.error("MOD FAILED", mod);
|
||||
}
|
||||
return mods;
|
||||
}, [] as BbmModVersion[]);
|
||||
@@ -113,11 +121,14 @@ export class BsModsManagerService {
|
||||
return undefined;
|
||||
}
|
||||
const injectorMd5 = await md5File(injectorPath);
|
||||
return this.beatModsApi.getModByHash(injectorMd5);
|
||||
return this.beatModsApi.getModByHash(injectorMd5).catch((error): undefined => {
|
||||
log.error("Could not get bsipa mod", error?.message);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
|
||||
zipUrl = path.join(this.beatModsApi.MODS_REPO_URL, zipUrl);
|
||||
zipUrl = new URL(zipUrl, this.beatModsApi.MODS_REPO_URL).href;
|
||||
|
||||
log.info("Download mod zip", zipUrl);
|
||||
|
||||
@@ -133,9 +144,9 @@ export class BsModsManagerService {
|
||||
|
||||
return BsmZipExtractor.fromBuffer(buffer);
|
||||
}
|
||||
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
|
||||
|
||||
log.info("executeBSIPA", version?.BSVersion, args);
|
||||
private async executeIPA(version: BSVersion, args: string[]): Promise<boolean> {
|
||||
log.info("executeIPA", version?.BSVersion, args);
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const ipaPath = path.join(versionPath, "IPA.exe");
|
||||
@@ -145,40 +156,24 @@ export class BsModsManagerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
const cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
|
||||
let winePath: string = "";
|
||||
if (process.platform === "linux") {
|
||||
const { error: winePathError, result: winePathResult } =
|
||||
tryit(() => this.linuxService.getWinePath());
|
||||
if (winePathError) {
|
||||
log.error(winePathError);
|
||||
return false;
|
||||
}
|
||||
winePath = `"${winePathResult}"`;
|
||||
|
||||
const winePrefix = this.linuxService.getWinePrefixPath();
|
||||
if (winePrefix) {
|
||||
env.WINEPREFIX = winePrefix;
|
||||
} else {
|
||||
log.warn("Could not find BSManager WINEPREFIX path, using system's default value instead");
|
||||
}
|
||||
const command = await this.getCommand(ipaPath, bsExePath, args);
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const processIPA = bsmSpawn(cmd, {
|
||||
const processIPA = bsmSpawn(command.command, {
|
||||
log: BsmShellLog.Command | BsmShellLog.EnvVariables,
|
||||
options: {
|
||||
cwd: versionPath,
|
||||
detached: true,
|
||||
shell: true,
|
||||
env
|
||||
env: command.env
|
||||
},
|
||||
linux: { prefix: winePath },
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
log.info("Ipa process timeout");
|
||||
log.info("IPA process timed out");
|
||||
resolve(false)
|
||||
}, sToMs(30));
|
||||
|
||||
@@ -192,16 +187,55 @@ export class BsModsManagerService {
|
||||
processIPA.once("exit", code => {
|
||||
clearTimeout(timeout);
|
||||
if (code === 0) {
|
||||
log.info("Ipa process exist with code 0");
|
||||
return resolve(true);
|
||||
}
|
||||
log.error("Ipa process exist with non 0 code", code);
|
||||
log.error("IPA process exited with non-zero code", code);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private async getCommand(
|
||||
ipaPath: string,
|
||||
beatSaberExePath: string,
|
||||
args: string[]
|
||||
): Promise<{
|
||||
env: Record<string, string>;
|
||||
command: string;
|
||||
} | null> {
|
||||
const command = `"${ipaPath}" "${beatSaberExePath}" ${args.join(" ")}`;
|
||||
if (process.platform === "win32") {
|
||||
return {
|
||||
env: { ...process.env },
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
const { error: winePathError, result: winePathResult } =
|
||||
tryit(() => this.linuxService.getWinePath());
|
||||
if (winePathError) {
|
||||
log.error(winePathError);
|
||||
return null;
|
||||
}
|
||||
|
||||
const winePath = await this.linuxService.isNixOS()
|
||||
? `steam-run "${winePathResult}"`
|
||||
: `"${winePathResult}"`;
|
||||
|
||||
const winePrefix = this.linuxService.getWinePrefixPath();
|
||||
if (!winePrefix) {
|
||||
throw new CustomError("Could not find BSManager WINEPREFIX path", "no-wineprefix");
|
||||
}
|
||||
|
||||
return {
|
||||
env: {
|
||||
...process.env,
|
||||
WINEPREFIX: winePrefix
|
||||
},
|
||||
command: `${winePath} ${command}`,
|
||||
};
|
||||
}
|
||||
|
||||
private getModDownload(modVersion: BbmModVersion): string {
|
||||
return `/cdn/mod/${modVersion.zipHash}.zip`
|
||||
}
|
||||
@@ -211,7 +245,7 @@ export class BsModsManagerService {
|
||||
|
||||
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
|
||||
|
||||
if(isBSIPA){
|
||||
if (isBSIPA) {
|
||||
await this.clearIpaFolder(version).catch(e => log.error("Error while clearing IPA folder", e));
|
||||
}
|
||||
|
||||
@@ -258,9 +292,11 @@ export class BsModsManagerService {
|
||||
|
||||
log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted);
|
||||
|
||||
const res = isBSIPA
|
||||
// Executing IPA.exe when BSIPA is already installed could potentially corrupt the game.
|
||||
const shouldRunIPA = isBSIPA && !pathExistsSync(path.join(versionPath, "winhttp.dll"));
|
||||
const res = shouldRunIPA
|
||||
? extracted &&
|
||||
(await this.executeBSIPA(version, ["-n"]).catch(e => {
|
||||
(await this.executeIPA(version, ["-n"]).catch(e => {
|
||||
log.error(e);
|
||||
return false;
|
||||
}))
|
||||
@@ -275,14 +311,14 @@ export class BsModsManagerService {
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const ipaPath = path.join(versionPath, ModsInstallFolder.IPA);
|
||||
|
||||
if(!pathExistsSync(ipaPath)){
|
||||
if (!pathExistsSync(ipaPath)) {
|
||||
log.info("IPA folder does not exist, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = readdirSync(ipaPath, { withFileTypes: true });
|
||||
|
||||
for(const content of contents){
|
||||
for (const content of contents) {
|
||||
|
||||
if (content.name === 'Backups' || content.name === 'Pending') {
|
||||
continue;
|
||||
@@ -295,13 +331,12 @@ export class BsModsManagerService {
|
||||
: deleteFile(contentPath)
|
||||
);
|
||||
|
||||
if(res.error){
|
||||
if (res.error) {
|
||||
log.error("Error while clearing IPA folder content", content.name, res.error);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("IPA folder cleared successfully");
|
||||
|
||||
}
|
||||
|
||||
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
@@ -313,15 +348,18 @@ export class BsModsManagerService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeBSIPA(version, ["--revert", "-n"]);
|
||||
await this.executeIPA(version, ["--revert", "-n"]);
|
||||
|
||||
const promises = mod.version.contentHashes.map(content => {
|
||||
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||
return deleteFile(path.join(verionPath, file));
|
||||
return deleteFile(path.join(verionPath, file)).catch(err => {
|
||||
log.info("Unable to delete IPA file, likely because it has been removed by the --revert command. Here is the error:", err);
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
if (mod.mod.name.toLowerCase() === "bsipa") {
|
||||
return this.uninstallBSIPA(mod, version);
|
||||
@@ -329,11 +367,22 @@ export class BsModsManagerService {
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
const promises = mod.version.contentHashes.map(async content => {
|
||||
return Promise.all([
|
||||
deleteFile(path.join(versionPath, content.path)),
|
||||
deleteFile(path.join(versionPath, "IPA", "Pending", content.path))
|
||||
]);
|
||||
const promises: Promise<void>[] = mod.version.contentHashes.map(async content => {
|
||||
return (async () => {
|
||||
const modPath = path.join(versionPath, content.path);
|
||||
if (pathExistsSync(modPath)) {
|
||||
log.info("Deleting mod", modPath);
|
||||
await deleteFile(modPath);
|
||||
}
|
||||
|
||||
const pendingPath = path.join(versionPath, "IPA", "Pending", content.path);
|
||||
if (pathExistsSync(pendingPath)) {
|
||||
log.info("Deleting pending mod", pendingPath);
|
||||
return deleteFile(pendingPath);
|
||||
}
|
||||
|
||||
log.warn("Mod file not found in", versionPath, "or pending folder", pendingPath);
|
||||
})() ;
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -346,12 +395,17 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
public async getInstalledMods(version: BSVersion): Promise<BbmModVersion[]> {
|
||||
this.manifestMatches = [];
|
||||
|
||||
const bsipa = await this.getBsipaInstalled(version);
|
||||
|
||||
const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING), this.getModsInDir(version, ModsInstallFolder.PLUGINS)]);
|
||||
const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING), this.getModsInDir(version, ModsInstallFolder.LIBS)]);
|
||||
const manifestMatches: BbmModVersion[] = [];
|
||||
const pluginsMods = await Promise.all([
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING, manifestMatches),
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS, manifestMatches)
|
||||
]);
|
||||
const libsMods = await Promise.all([
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING, manifestMatches),
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS, manifestMatches)
|
||||
]);
|
||||
|
||||
const dirMods = pluginsMods.flat().concat(libsMods.flat());
|
||||
|
||||
@@ -486,12 +540,12 @@ export class BsModsManagerService {
|
||||
|
||||
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
|
||||
|
||||
if(bsipa){
|
||||
if (bsipa) {
|
||||
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
|
||||
log.error("Error while installing BSIPA", err);
|
||||
});
|
||||
|
||||
if(!bsipaInstalled){
|
||||
if (!bsipaInstalled) {
|
||||
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
|
||||
}
|
||||
|
||||
|
||||
@@ -105,8 +105,12 @@ export class OculusService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public oculusRunning(): Promise<boolean> {
|
||||
return isProcessRunning("OculusClient");
|
||||
public async oculusRunning(): Promise<boolean> {
|
||||
if (await isProcessRunning("OculusClient")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isProcessRunning("Client"); // new name of oculus client
|
||||
}
|
||||
|
||||
public async startOculus(): Promise<void>{
|
||||
|
||||
@@ -10,59 +10,325 @@ import path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import internal from 'stream';
|
||||
import { app } from 'electron';
|
||||
import { app, net } from 'electron';
|
||||
import { CookieJar } from 'tough-cookie';
|
||||
|
||||
export class RequestService {
|
||||
private static instance: RequestService;
|
||||
private preferredFamily: number | undefined = undefined;
|
||||
private readonly baseHeaders = {
|
||||
'User-Agent': `BSManager/${app.getVersion()} (Electron/${process.versions.electron} Chrome/${process.versions.chrome} Node/${process.versions.node})`,
|
||||
}
|
||||
|
||||
private readonly PREFERRED_FAMILY_TESTS = [4, 6];
|
||||
private preferredFamilyCache: Record<string, number> = {};
|
||||
|
||||
public static getInstance(): RequestService {
|
||||
if (!RequestService.instance) {
|
||||
RequestService.instance = new RequestService();
|
||||
}
|
||||
return RequestService.instance;
|
||||
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private isBeatmodsUrl(url: string): boolean {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname === 'beatmods.com' || hostname.endsWith('.beatmods.com');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Electron's Chromium network stack instead of Node's HTTP stack
|
||||
* to avoid Cloudflare timeout issues that occur with beatmods.com
|
||||
*/
|
||||
private async requestWithElectronNet<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||
return new Promise<{ data: T; headers: IncomingHttpHeaders }>((resolve, reject) => {
|
||||
const request = net.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
headers: this.baseHeaders,
|
||||
});
|
||||
|
||||
let responseBody = Buffer.alloc(0);
|
||||
let responseHeaders: IncomingHttpHeaders = {};
|
||||
let isResolved = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
request.abort();
|
||||
reject(new Error(`Request timeout for ${url}`));
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
request.on('response', (response) => {
|
||||
responseHeaders = response.headers as IncomingHttpHeaders;
|
||||
|
||||
// Validate HTTP status code (got throws on non-2xx by default)
|
||||
const { statusCode } = response;
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
isResolved = true;
|
||||
cleanup();
|
||||
reject(new Error(`Request failed with status ${statusCode} for ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
responseBody = Buffer.concat([responseBody, chunk]);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
if (isResolved) return;
|
||||
isResolved = true;
|
||||
cleanup();
|
||||
try {
|
||||
const bodyText = responseBody.toString('utf-8');
|
||||
const data = JSON.parse(bodyText) as T;
|
||||
resolve({ data, headers: responseHeaders });
|
||||
} catch (parseError) {
|
||||
reject(new Error(`Failed to parse JSON response from ${url}: ${parseError}`));
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
if (isResolved) return;
|
||||
isResolved = true;
|
||||
cleanup();
|
||||
reject(new Error(`Response stream error for ${url}: ${error.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
if (isResolved) return;
|
||||
isResolved = true;
|
||||
cleanup();
|
||||
reject(new Error(`Network error requesting ${url}: ${error.message}`));
|
||||
});
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
public async getJSON<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||
// Node's HTTP stack has Cloudflare compatibility issues with beatmods.com
|
||||
if (this.isBeatmodsUrl(url)) {
|
||||
return this.requestWithElectronNet<T>(url);
|
||||
}
|
||||
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
for (const family of familiesToTry) {
|
||||
const domain = (new URL(url)).hostname;
|
||||
const cachedFamily = this.preferredFamilyCache[domain];
|
||||
if (cachedFamily) {
|
||||
try {
|
||||
return await this.requestData<T>(url, cachedFamily);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Request failed: ${url}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
const res = await got(url, { dnsLookupIpVersion: family, responseType: 'json', headers: this.baseHeaders });
|
||||
this.preferredFamily = family;
|
||||
return { data: res.body as T, headers: res.headers };
|
||||
// Try on each IPv4/6 families on first request to a domain/website
|
||||
for (const family of this.PREFERRED_FAMILY_TESTS) {
|
||||
try {
|
||||
const response = await this.requestData<T>(url, family);
|
||||
log.info(`Caching "${domain}" with IPv${family}`);
|
||||
this.preferredFamilyCache[domain] = family;
|
||||
return response;
|
||||
} catch (err) {
|
||||
log.warn(`IPv${family} request failed, trying next one... URL: ${url}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
log.error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
this.preferredFamily = undefined;
|
||||
throw new Error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
}
|
||||
|
||||
private async requestData<T>(url: string, family: number): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const first = await got(url, {
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
dnsLookupIpVersion: family,
|
||||
cookieJar,
|
||||
headers: this.baseHeaders
|
||||
});
|
||||
|
||||
// Follow script redirect to get the JSON
|
||||
if (first.headers['content-type']?.includes('text/html')) {
|
||||
|
||||
const cookieMatch = first.body.match(/document\.cookie="([^"]+)"/);
|
||||
if (!cookieMatch) {
|
||||
throw new Error("Cookie not found in JS");
|
||||
}
|
||||
|
||||
const cookieString = cookieMatch[1];
|
||||
|
||||
const redirectMatch = first.body.match(/location\.href="([^"]+)"/);
|
||||
if (!redirectMatch) {
|
||||
throw new Error("Redirect URL not found");
|
||||
}
|
||||
|
||||
const jsonUrl = redirectMatch[1];
|
||||
await cookieJar.setCookie(cookieString, jsonUrl);
|
||||
|
||||
const second = await got(jsonUrl, {
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
dnsLookupIpVersion: family,
|
||||
responseType: "json",
|
||||
cookieJar,
|
||||
headers: this.baseHeaders
|
||||
});
|
||||
|
||||
return {
|
||||
data: second.body as T,
|
||||
headers: second.headers
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: JSON.parse(first.body) as T,
|
||||
headers: first.headers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Electron's Chromium network stack instead of Node's HTTP stack
|
||||
* to avoid Cloudflare timeout issues that occur with beatmods.com
|
||||
*/
|
||||
private downloadFileWithElectronNet(
|
||||
url: string,
|
||||
dest: string,
|
||||
opt?: { preferContentDisposition?: boolean }
|
||||
): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>((subscriber) => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
let file: WriteStream | undefined;
|
||||
let isCompleted = false;
|
||||
|
||||
const request = net.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
headers: this.baseHeaders,
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
if (file) {
|
||||
file.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
request.on('response', (response) => {
|
||||
// Validate HTTP status code (got throws on non-2xx by default)
|
||||
const { statusCode } = response;
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
isCompleted = true;
|
||||
cleanup();
|
||||
subscriber.error(new Error(`Download failed with status ${statusCode} for ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const contentLength = response.headers['content-length'];
|
||||
if (contentLength) {
|
||||
const length = Array.isArray(contentLength) ? contentLength[0] : contentLength;
|
||||
progress.total = parseInt(length, 10);
|
||||
}
|
||||
|
||||
const filename = opt?.preferContentDisposition
|
||||
? this.getFilenameFromContentDisposition(response.headers['content-disposition'] as string)
|
||||
: null;
|
||||
|
||||
if (filename) {
|
||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
||||
}
|
||||
|
||||
progress.data = dest;
|
||||
file = createWriteStream(dest);
|
||||
|
||||
file.on('error', (error) => {
|
||||
cleanup();
|
||||
tryit(() => deleteFileSync(dest));
|
||||
if (!isCompleted) {
|
||||
isCompleted = true;
|
||||
subscriber.error(new Error(`File write error for ${dest}: ${error.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
if (file && !file.destroyed) {
|
||||
progress.current += chunk.length;
|
||||
subscriber.next(progress);
|
||||
file.write(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
if (isCompleted) return;
|
||||
if (file && !file.destroyed) {
|
||||
file.once('finish', () => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
file.end();
|
||||
} else {
|
||||
isCompleted = true;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
}
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
cleanup();
|
||||
tryit(() => deleteFileSync(dest));
|
||||
subscriber.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
cleanup();
|
||||
tryit(() => deleteFileSync(dest));
|
||||
subscriber.error(error);
|
||||
});
|
||||
|
||||
request.end();
|
||||
|
||||
return () => {
|
||||
request.abort();
|
||||
cleanup();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url, dest) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public downloadFile(
|
||||
url: string,
|
||||
dest: string,
|
||||
opt?: { preferContentDisposition?: boolean }
|
||||
): Observable<Progression<string>> {
|
||||
// Node's HTTP stack has Cloudflare compatibility issues with beatmods.com
|
||||
if (this.isBeatmodsUrl(url)) {
|
||||
return this.downloadFileWithElectronNet(url, dest, opt);
|
||||
}
|
||||
|
||||
return new Observable<Progression<string>>((subscriber) => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
const domain = (new URL(url)).hostname;
|
||||
const cachedFamily = this.preferredFamilyCache[domain];
|
||||
const familiesToTry = cachedFamily
|
||||
? [ cachedFamily ] : this.PREFERRED_FAMILY_TESTS;
|
||||
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
@@ -76,7 +342,10 @@ export class RequestService {
|
||||
stream = got.stream(url, { dnsLookupIpVersion: family, headers: this.baseHeaders });
|
||||
|
||||
stream.on('response', (response) => {
|
||||
this.preferredFamily = family;
|
||||
if (!cachedFamily) {
|
||||
log.info(`Caching "${domain}" with IPv${family}`);
|
||||
this.preferredFamilyCache[domain] = family;
|
||||
}
|
||||
|
||||
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
|
||||
|
||||
@@ -125,10 +394,112 @@ export class RequestService {
|
||||
);
|
||||
}
|
||||
|
||||
public downloadBuffer(
|
||||
/**
|
||||
* Uses Electron's Chromium network stack instead of Node's HTTP stack
|
||||
* to avoid Cloudflare timeout issues that occur with beatmods.com
|
||||
*/
|
||||
private downloadBufferWithElectronNet(
|
||||
url: string,
|
||||
options?: got.GotOptions<null>
|
||||
): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
data: null,
|
||||
};
|
||||
|
||||
// Convert headers to the format expected by electron.net (string | string[])
|
||||
const electronHeaders: Record<string, string | string[]> = { ...this.baseHeaders };
|
||||
if (options?.headers) {
|
||||
for (const [key, value] of Object.entries(options.headers)) {
|
||||
if (typeof value === 'string' || Array.isArray(value)) {
|
||||
electronHeaders[key] = value;
|
||||
} else if (value != null) {
|
||||
electronHeaders[key] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = Buffer.alloc(0);
|
||||
let responseHeaders: IncomingHttpHeaders = {};
|
||||
let isCompleted = false;
|
||||
|
||||
const request = net.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
headers: electronHeaders,
|
||||
});
|
||||
|
||||
request.on('response', (response) => {
|
||||
// Validate HTTP status code (got throws on non-2xx by default)
|
||||
const { statusCode } = response;
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
isCompleted = true;
|
||||
subscriber.error(new Error(`Download failed with status ${statusCode} for ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const contentLength = response.headers['content-length'];
|
||||
if (contentLength) {
|
||||
const length = Array.isArray(contentLength) ? contentLength[0] : contentLength;
|
||||
progress.total = parseInt(length, 10);
|
||||
}
|
||||
|
||||
responseHeaders = response.headers as IncomingHttpHeaders;
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
progress.current = data.length;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
progress.data = data;
|
||||
// Required to maintain API compatibility with got-based implementation
|
||||
const mockResponse = {
|
||||
headers: responseHeaders,
|
||||
} as IncomingMessage;
|
||||
progress.extra = mockResponse;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
subscriber.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
if (isCompleted) return;
|
||||
isCompleted = true;
|
||||
subscriber.error(error);
|
||||
});
|
||||
|
||||
request.end();
|
||||
|
||||
return () => {
|
||||
request.abort();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public downloadBuffer(
|
||||
url: string,
|
||||
options?: got.GotOptions<null>
|
||||
): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
// Node's HTTP stack has Cloudflare compatibility issues with beatmods.com
|
||||
if (this.isBeatmodsUrl(url)) {
|
||||
return this.downloadBufferWithElectronNet(url, options);
|
||||
}
|
||||
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
@@ -137,11 +508,15 @@ export class RequestService {
|
||||
};
|
||||
|
||||
const headers = { ...this.baseHeaders, ...(options?.headers ?? {}) };
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
const domain = (new URL(url)).hostname;
|
||||
const cachedFamily = this.preferredFamilyCache[domain];
|
||||
const familiesToTry = cachedFamily
|
||||
? [ cachedFamily ] : this.PREFERRED_FAMILY_TESTS;
|
||||
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
@@ -156,7 +531,10 @@ export class RequestService {
|
||||
let response: IncomingMessage;
|
||||
|
||||
stream.once('response', (res) => {
|
||||
this.preferredFamily = family;
|
||||
if (!cachedFamily) {
|
||||
log.info(`Caching "${domain}" with IPv${family}`);
|
||||
this.preferredFamilyCache[domain] = family;
|
||||
}
|
||||
response = res;
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { AutoUpdate } from "shared/models/config";
|
||||
|
||||
export class StaticConfigurationService {
|
||||
private static instance: StaticConfigurationService;
|
||||
@@ -30,8 +31,8 @@ export class StaticConfigurationService {
|
||||
return this.store.has(key);
|
||||
}
|
||||
|
||||
public get<K extends StaticConfigKeys>(key: K): StaticConfigKeyValues[K] {
|
||||
return this.store.get<K>(key) as StaticConfigKeyValues[K];
|
||||
public get<K extends StaticConfigKeys>(key: K, defaultValue?: StaticConfigKeyValues[K]): StaticConfigKeyValues[K] {
|
||||
return this.store.get<K>(key, defaultValue) as StaticConfigKeyValues[K];
|
||||
}
|
||||
|
||||
public take<K extends StaticConfigKeys>(key: K, cb: (val: StaticConfigKeyValues[K]) => void): void {
|
||||
@@ -90,6 +91,7 @@ export interface StaticConfigKeyValues {
|
||||
"use-symlinks": boolean;
|
||||
"use-system-proxy": boolean;
|
||||
"last-version-launched": BSVersion;
|
||||
"auto-update": AutoUpdate;
|
||||
|
||||
// Linux Specific static configs
|
||||
"proton-folder": string;
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({
|
||||
|
||||
return (
|
||||
<VirtualScroll
|
||||
classNames={{ mainDiv: className, rows: "gap-x-2 px-2 py-2" }}
|
||||
classNames={{ mainDiv: className, rows: "gap-x-2 px-2 pt-2 pb-[108px]" }}
|
||||
itemHeight={108}
|
||||
maxColumns={3}
|
||||
minItemWidth={400}
|
||||
|
||||
+2
-6
@@ -21,7 +21,6 @@ import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
import { LocalPlaylistFilter, LocalPlaylistSort } from "./local-playlist-filter-panel.component";
|
||||
@@ -75,10 +74,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
const modals = useService(ModalService);
|
||||
const ipc = useService(IpcService);
|
||||
const osDiagnostic = useService(OsDiagnosticService);
|
||||
const notification = useService(NotificationService);
|
||||
|
||||
const isOnline = useObservable(() => osDiagnostic.isOnline$, false);
|
||||
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
|
||||
|
||||
const { maps$, playlists$, setPlaylists, setMaps } = useContext(InstalledMapsContext);
|
||||
@@ -121,7 +118,6 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
},
|
||||
syncPlaylists: async (playlists?: LocalBPList[]) => {
|
||||
if(!isOnline){ return; }
|
||||
const toSync = playlists ?? (selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value);
|
||||
if(!toSync.length){ return; }
|
||||
|
||||
@@ -391,13 +387,13 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
}}
|
||||
onClickOpen={() => openPlaylistDetails(playlist.path)}
|
||||
onClickDelete={() => deletePlaylists([playlist])}
|
||||
onClickSync={(playlist?.songs?.length && isOnline) && (() => handleClickSync(playlist))}
|
||||
onClickSync={!!playlist?.songs?.length && (() => handleClickSync(playlist))}
|
||||
onClickOpenFile={() => viewPlaylistFile(playlist.path)}
|
||||
onClickCancelDownload={() => playlistDownloader.cancelDownload(playlist.customData?.syncURL ?? playlist.path, version)}
|
||||
onClickEdit={() => editPlaylist(playlist)}
|
||||
/>
|
||||
);
|
||||
}, [isOnline, version]);
|
||||
}, [version]);
|
||||
|
||||
const filteredPlaylists = useMemo(() => {
|
||||
if(!playlists){ return []; }
|
||||
|
||||
+7
-1
@@ -15,7 +15,7 @@ import { NotificationService } from "renderer/services/notification.service";
|
||||
|
||||
export const LoginToMetaModal: ModalComponent<string> = ({ resolver }) => {
|
||||
|
||||
const { text: t } = useTranslationV2();
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const ipc = useService(IpcService);
|
||||
const modal = useService(ModalService);
|
||||
@@ -60,6 +60,12 @@ export const LoginToMetaModal: ModalComponent<string> = ({ resolver }) => {
|
||||
<p className="font-bold">{t("modals.connect-to-meta.body.token-needed")}</p>
|
||||
<p>{t("modals.connect-to-meta.body.need-cookie-enabled")}</p>
|
||||
|
||||
<p className="text-warning-400">{(
|
||||
te("modals.connect-to-meta.body.must-own-desktop-version", {link: (
|
||||
<a className="underline cursor-pointer" href="https://github.com/Zagrios/bs-manager/wiki/how-to-claim-oculus-desktop-version" target="_blank">{t("misc.this-guide")}</a>
|
||||
)})
|
||||
)}</p>
|
||||
|
||||
<div className="w-full flex flex-row justify-start items-center gap-1.5">
|
||||
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={stay} onChange={enable => setStay(() => enable)}/>
|
||||
<span>{t("modals.connect-to-meta.stay")}</span>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import BeatRunning from "../../../../../assets/images/apngs/beat-running.png";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { CustomLaunchOption } from "renderer/components/version-viewer/slides/launch/launch-options-panel.component";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
|
||||
type Data = Partial<CustomLaunchOption>;
|
||||
|
||||
export const CreateCustomLaunchOptionModal: ModalComponent<CustomLaunchOption, Data> = ({resolver, options}) => {
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
const uid = useConstant(() => options?.data?.id ?? crypto.randomUUID());
|
||||
const [name, setName] = useState(options?.data?.label ?? "");
|
||||
const [command, setCommand] = useState(options?.data?.data?.command ?? "");
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
resolver({
|
||||
exitCode: ModalExitCode.COMPLETED,
|
||||
data: {
|
||||
id: uid,
|
||||
label: name,
|
||||
data: {
|
||||
command,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
resolver({
|
||||
exitCode: ModalExitCode.CANCELED,
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="max-w-xs">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.custom-launch-option.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatRunning} />
|
||||
<p className="mb-2">{t("modals.custom-launch-option.desc")}</p>
|
||||
<div className="flex flex-col gap-px mb-3">
|
||||
<label className="font-bold cursor-pointer tracking-wide inline-block w-fit" htmlFor="name">{t("modals.custom-launch-option.name")}</label>
|
||||
<input className="h-8 px-1.5 bg-light-main-color-1 dark:bg-main-color-1 rounded-md" type="text" id="name" required value={name} onChange={e => setName(e.target.value)} placeholder={t("modals.custom-launch-option.title-placeholder")} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-px mb-3">
|
||||
<label className="font-bold cursor-pointer tracking-wide inline-block w-fit" htmlFor="launch-option">{t("modals.custom-launch-option.launch-option")}</label>
|
||||
<input className="h-8 px-1.5 bg-light-main-color-1 dark:bg-main-color-1 rounded-md" type="text" id="launch-option" required value={command} onChange={e => setCommand(e.target.value)} placeholder="--launch-option" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<BsmButton className="rounded-md flex justify-center items-center transition-all h-10 grow shrink-0" text={t("misc.cancel")} typeColor="cancel" withBar={false} onClick={cancel}/>
|
||||
<BsmButton className="rounded-md flex justify-center items-center transition-all h-10 grow shrink-0" type="submit" text={t("misc.create")} typeColor="primary" withBar={false} />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
)
|
||||
};
|
||||
+10
-17
@@ -6,13 +6,12 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { LaunchOption } from "shared/models/bs-launch";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BSLauncherService } from "renderer/services/bs-launcher.service";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { BsStore } from "shared/models/bs-store.enum";
|
||||
|
||||
export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => {
|
||||
|
||||
@@ -26,10 +25,6 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
|
||||
const [command, setCommand] = useState(launchOption.command || "");
|
||||
const [steamShortcut, setSteamShortcut] = useState(false);
|
||||
|
||||
const isSteamVersion = useMemo(() => {
|
||||
return data.steam || data.metadata?.store === BsStore.STEAM;
|
||||
}, [data]);
|
||||
|
||||
const completeModal = () => {
|
||||
launchOption.command = command.trim();
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
|
||||
@@ -96,17 +91,15 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSteamVersion && (
|
||||
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
||||
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
||||
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
|
||||
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
||||
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
||||
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
||||
</div>
|
||||
</Tippy>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="h-full flex items-center justify-center rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="h-full flex items-center justify-center rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import BeatConflictImg from "../../../../../assets/images/apngs/beat-conflict.pn
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
@@ -29,7 +28,6 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
const config = useService(ConfigurationService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
|
||||
const currentDownload = useObservable(() => mapsDownloader.currentMapDownload$);
|
||||
const mapsInQueue = useObservable(() => mapsDownloader.mapsInQueue$);
|
||||
@@ -41,7 +39,6 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
const [downloadbleMaps, setDownloadbleMaps] = useState<DownloadableMap[]>([]);
|
||||
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
sortOrder: config.get("map-sort-order"),
|
||||
filter,
|
||||
@@ -122,7 +119,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
setMaps(prev => [...prev, ...maps])
|
||||
|
||||
if (maps.length < tryToLoad) {
|
||||
handleLoadMore();
|
||||
handleLoadMore(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -188,9 +185,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
setSearchParams(() => searchParamsLocal);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
const handleLoadMore = (skipLoading: boolean = true) => {
|
||||
|
||||
if(loading){ return; }
|
||||
if(skipLoading && loading){ return; }
|
||||
|
||||
setSearchParams(prev => {
|
||||
return { ...prev, page: prev.page + 1 };
|
||||
@@ -256,10 +253,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
if (loading) {
|
||||
return t("modals.download-maps.loading-maps");
|
||||
}
|
||||
if (isOnline) {
|
||||
return t("modals.download-maps.no-maps-found");
|
||||
}
|
||||
return t("modals.download-maps.no-internet");
|
||||
return t("modals.download-maps.no-maps-found");
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,9 @@ import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { map } from "rxjs";
|
||||
import { DISCORD_URL } from "shared/constants";
|
||||
|
||||
export const LinkContentModal: ModalComponent<void, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
|
||||
export const LinkContentModal: ModalComponent<void, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"bloqs"}> = ({options: { data: { version, contentType } }, resolver }) => {
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
@@ -34,7 +35,7 @@ export const LinkContentModal: ModalComponent<void, {version: BSVersion, content
|
||||
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.saber);
|
||||
case "platforms":
|
||||
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.platform);
|
||||
case "blocks":
|
||||
case "bloqs":
|
||||
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.bloq);
|
||||
default:
|
||||
return "";
|
||||
@@ -53,7 +54,7 @@ export const LinkContentModal: ModalComponent<void, {version: BSVersion, content
|
||||
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.saber);
|
||||
case "platforms":
|
||||
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.platform);
|
||||
case "blocks":
|
||||
case "bloqs":
|
||||
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.bloq);
|
||||
default:
|
||||
return "";
|
||||
@@ -97,7 +98,7 @@ export const LinkContentModal: ModalComponent<void, {version: BSVersion, content
|
||||
<p className="max-w-sm w-full italic my-2 text-warning-400">{t("modals.link-contents.warning", {contentType: t(`misc.${contentType}`).toLowerCase()})}</p>
|
||||
<div className="flex justify-center items-center gap-3 *:underline *:text-sm *:text-neutral-200">
|
||||
<BsmLink href="https://en.qrwp.org/Symbolic_link">{t("modals.link-contents.what-is-a-symbolic-link")}</BsmLink>
|
||||
<BsmLink href="https://discord.gg/uSqbHVpKdV">{t("modals.link-contents.i-need-help")}</BsmLink>
|
||||
<BsmLink href={DISCORD_URL}>{t("modals.link-contents.i-need-help")}</BsmLink>
|
||||
</div>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-2 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all flex justify-center items-center" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
|
||||
+1
-10
@@ -12,7 +12,6 @@ import { ModelDownload, ModelsDownloaderService } from "renderer/services/models
|
||||
import { ModelSaberService } from "renderer/services/thrird-partys/model-saber.service";
|
||||
import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { MODEL_TYPES, MS_GET_QUERY_SORTS } from "shared/models/models/constants";
|
||||
@@ -26,7 +25,6 @@ import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual
|
||||
export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; type: MSModelType; owned: BsmLocalModel[] }> = ({ options: {data: { version, type, owned }} }) => {
|
||||
const modelsDownloader = useService(ModelsDownloaderService);
|
||||
const modelSaber = useService(ModelSaberService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
@@ -48,7 +46,6 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
const downloadQueue = useObservable(() => modelsDownloader.getQueue$(), []);
|
||||
const [msModels, msModels$] = useBehaviorSubject<MSModel[]>([]);
|
||||
const [renderableModels, setRenderableModels] = useState<RenderableModel[]>([]);
|
||||
const isOnline = useObservable(() => os.isOnline$, true);
|
||||
const [error, error$] = useBehaviorSubject(false);
|
||||
const [isLoading, isLoading$] = useBehaviorSubject(false);
|
||||
const [ownedModels, setOwnedModels] = useState<BsmLocalModel[]>(owned ?? []);
|
||||
@@ -197,13 +194,7 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
if (isLoading) {
|
||||
return <span className="text-lg">{t("models.panel.grid.loading")}</span>;
|
||||
}
|
||||
if (isOnline) {
|
||||
return <span className="text-lg">{t("models.modals.download-models.no-models")}</span>;
|
||||
}
|
||||
if (!isOnline) {
|
||||
return <span className="text-lg">{t("models.modals.download-models.no-internet")}</span>;
|
||||
}
|
||||
return <span className="text-lg">{t("models.modals.download-models.error-occured")}</span>;
|
||||
return <span className="text-lg">{t("models.modals.download-models.no-models")}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { BsmLink } from "renderer/components/shared/bsm-link.component";
|
||||
import { DISCORD_URL } from "shared/constants";
|
||||
|
||||
export const UnlinkContentsModal: ModalComponent<boolean, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
@@ -86,7 +87,7 @@ export const UnlinkContentsModal: ModalComponent<boolean, {version: BSVersion, c
|
||||
</p>
|
||||
<div className="flex justify-center items-center gap-3 *:underline *:text-sm *:text-neutral-200">
|
||||
<BsmLink href="https://en.qrwp.org/Symbolic_link">{t("modals.link-contents.what-is-a-symbolic-link")}</BsmLink>
|
||||
<BsmLink href="https://discord.gg/uSqbHVpKdV">{t("modals.link-contents.i-need-help")}</BsmLink>
|
||||
<BsmLink href={DISCORD_URL}>{t("modals.link-contents.i-need-help")}</BsmLink>
|
||||
</div>
|
||||
<Tippy content={t("modals.unlink-contents.do-not-copy-contents-tip", { contentType: t(`misc.${contentType}`).toLowerCase() })} theme="default">
|
||||
<div className="relative h-5 flex my-3 items-center w-fit">
|
||||
|
||||
@@ -74,9 +74,9 @@ export function Modal() {
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{currentModal ? <motion.span key={crypto.randomUUID()} onClick={onOverlayClicked} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> : undefined}
|
||||
{currentModal ? <motion.span key="modal-overlay" onClick={onOverlayClicked} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> : undefined}
|
||||
{modals?.map(modal => (
|
||||
<motion.div key={crypto.randomUUID()} className="fixed z-[90] top-1/2 left-1/2" initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0, display: modal === currentModal ? "block" : ["block", "none"]}} exit={{ y: "100vh" }}>
|
||||
<motion.div key={modal.id} className="fixed z-[90] top-1/2 left-1/2" initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0, display: modal === currentModal ? "block" : ["block", "none"]}} exit={{ y: "100vh" }}>
|
||||
{renderModal(modal)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@@ -16,6 +16,7 @@ import equal from "fast-deep-equal";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
|
||||
export function BsVersionItem(props: { version: BSVersion }) {
|
||||
|
||||
@@ -70,7 +71,7 @@ export function BsVersionItem(props: { version: BSVersion }) {
|
||||
const wasVerification = bsDownloader.isVerifying;
|
||||
bsDownloader.stopDownload().then(() => {
|
||||
if(wasVerification){ return; }
|
||||
bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions());
|
||||
bsUninstallerService.uninstall(versionDownload).then(() => verionManagerService.askInstalledVersions()).catch(noop);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ type Props<T> = {
|
||||
selectedItemId?: number;
|
||||
selectedItemValue?: T;
|
||||
direction?: CSSProperties["flexDirection"],
|
||||
columnCount?: number;
|
||||
onItemSelected?: (item: RadioItem<T>) => void
|
||||
};
|
||||
|
||||
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column" }: Props<T>) {
|
||||
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column",columnCount = 1, }: Props<T>) {
|
||||
const t = useTranslation();
|
||||
|
||||
const isSelected = (item: RadioItem<T>) => {
|
||||
@@ -19,17 +20,17 @@ export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemVa
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={id} className="w-full flex gap-1.5" style={{flexDirection: direction}}>
|
||||
<div id={id} className="w-full grid gap-1.5" style={{flexDirection: direction, gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`}}>
|
||||
{items.map(i => (
|
||||
<div onClick={() => onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
|
||||
<div onClick={() => onItemSelected(i)} key={i.id} className={`h-12 py-3 w-full flex cursor-pointer justify-between rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
|
||||
<div className="flex items-center">
|
||||
<div className="h-5 rounded-full aspect-square border-2 border-gray-800 dark:border-white p-[3px] mr-2">
|
||||
<motion.span initial={{ scale: 0 }} animate={{ scale: isSelected(i) ? 1 : 0 }} className="h-full w-full block bg-gray-800 dark:bg-white rounded-full" />
|
||||
</div>
|
||||
<h2 className="font-extrabold">{t(i.text)}</h2>
|
||||
<h2 className="font-extrabold text-nowrap">{t(i.text)}</h2>
|
||||
</div>
|
||||
{i.icon && (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center text-right">
|
||||
{i.textIcon && <span className="text-sm">{t(i.textIcon)}</span>}
|
||||
{i.icon}
|
||||
</div>
|
||||
|
||||
@@ -72,9 +72,6 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
|
||||
const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = e;
|
||||
const { scrollHeight } = listRef.current;
|
||||
|
||||
if(!scrollHeight || !listHeight){ return; }
|
||||
|
||||
const margin = scrollEnd.margin ?? 0;
|
||||
|
||||
if (scrollDirection === "forward" && !scrollUpdateWasRequested && scrollOffset + listHeight + margin >= scrollHeight) {
|
||||
|
||||
@@ -71,10 +71,11 @@ import { CancelIcon } from "./icons/cancel-icon.component";
|
||||
import { WarningIcon } from "./icons/warning-icon.component";
|
||||
import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component";
|
||||
import { UkIcon } from "./flags/uk-icon.component";
|
||||
import { PhilippineIcon } from "./flags/philippine-icon.component";
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "pt-BR-flag" | "uk-UA-flag" | "tl-PH-flag" | "null" );
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "pt-BR-flag" | "uk-UA-flag"| "null" );
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -150,6 +151,9 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if (icon === "uk-UA-flag"){
|
||||
return <UkraineIcon className={className} style={style} />;
|
||||
}
|
||||
if (icon === "tl-PH-flag") {
|
||||
return <PhilippineIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if (icon === "task") {
|
||||
return <TaskIcon className={className} style={style} />;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const PhilippineIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg ref={ref} {...props} width="71" height="48" viewBox="0 0 71 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path d="M63.89.12H6.61A6.36 6.36 0 0 0 .25 6.48v17.64h70V6.48A6.36 6.36 0 0 0 63.89.12" fill="#4758A9"/>
|
||||
<path d="M.25 40.75a6.36 6.36 0 0 0 6.36 6.36h57.27a6.36 6.36 0 0 0 6.36-6.36V24.12h-70z" fill="#ED5565"/>
|
||||
<path d="M2.15 1.95a6.34 6.34 0 0 0-1.9 4.54v34.27a6.34 6.34 0 0 0 1.95 4.57l23.05-21.21z" fill="#FFF"/>
|
||||
<circle fill="#F6D660" cx="10.25" cy="23.62" r="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -12,36 +12,47 @@ import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { useWindowControls } from "renderer/hooks/use-window-controls.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { StaticConfigurationService } from "renderer/services/static-configuration.service";
|
||||
import { AutoUpdate } from "shared/models/config";
|
||||
import { UpdateInfo } from "electron-updater";
|
||||
|
||||
function TitleBarTags() {
|
||||
function useVersion() {
|
||||
const ipcService = useService(IpcService);
|
||||
const t = useTranslationV2();
|
||||
|
||||
const [previewVersion, setPreviewVersion] = useState("");
|
||||
const [outdated, setOutdated] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState("");
|
||||
const [latestVersion, setLatestVersion] = useState<UpdateInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const requests: Promise<any>[] = [
|
||||
lastValueFrom(ipcService.sendV2("current-version"))
|
||||
];
|
||||
if (window.electron.platform === "linux") {
|
||||
requests.push(lastValueFrom(ipcService.sendV2("check-update")));
|
||||
}
|
||||
|
||||
Promise.all(requests).then(([ currentVersion, outdated ]) => {
|
||||
handlePrerelease(currentVersion);
|
||||
setOutdated(outdated);
|
||||
Promise.all([
|
||||
lastValueFrom(ipcService.sendV2("current-version")),
|
||||
lastValueFrom(ipcService.sendV2("get-available-update"))
|
||||
]).then(([ currentVersion, latestVersion ]) => {
|
||||
setCurrentVersion(currentVersion);
|
||||
setLatestVersion(latestVersion);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePrerelease = (version: string) => {
|
||||
return { currentVersion, latestVersion };
|
||||
}
|
||||
|
||||
function TitleBarTags({ version, latestVersion }: Readonly<{
|
||||
version: string;
|
||||
latestVersion: UpdateInfo | null;
|
||||
}>) {
|
||||
const t = useTranslationV2();
|
||||
|
||||
const previewVersion = (() => {
|
||||
if (version.toLowerCase().includes("alpha")) {
|
||||
return setPreviewVersion("ALPHA");
|
||||
return "ALPHA";
|
||||
}
|
||||
|
||||
if (version.toLowerCase().includes("beta")) {
|
||||
return setPreviewVersion("BETA");
|
||||
return "BETA";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
})();
|
||||
|
||||
return <>
|
||||
{previewVersion &&
|
||||
@@ -49,7 +60,7 @@ function TitleBarTags() {
|
||||
{previewVersion}
|
||||
</span>
|
||||
}
|
||||
{outdated &&
|
||||
{latestVersion &&
|
||||
<span className="bg-warning-500 text-black rounded-full ml-1 text-[10px] italic px-1 uppercase h-3.5 font-bold">
|
||||
{t.text("title-bar.outdated")}
|
||||
</span>
|
||||
@@ -57,12 +68,65 @@ function TitleBarTags() {
|
||||
</>;
|
||||
}
|
||||
|
||||
function AutoUpdateButton({ latestVersion }: Readonly<{
|
||||
latestVersion: UpdateInfo | null;
|
||||
}>) {
|
||||
const configService = useService(StaticConfigurationService);
|
||||
const ipcService = useService(IpcService);
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
const isLinux = window.electron.platform === "linux";
|
||||
|
||||
const updateAndRestart = async () => {
|
||||
await configService.set("auto-update", AutoUpdate.ONCE);
|
||||
await lastValueFrom(ipcService.sendV2("restart-app"));
|
||||
};
|
||||
|
||||
|
||||
const renderTippyContent = () => {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div>{t("title-bar.update-text", { version: latestVersion.version })}</div>
|
||||
{latestVersion?.version ? (
|
||||
<a href={`https://github.com/Zagrios/bs-manager/releases/tag/v${latestVersion.version}`} target="_blank" className="cursor-pointer underline text-sm hover:text-gray-300">{t("title-bar.see-changelog")}</a>
|
||||
) : null}
|
||||
{!isLinux &&
|
||||
<BsmButton typeColor="primary"
|
||||
className="text-center rounded-md px-2 py-1 mt-2"
|
||||
text={t("title-bar.update-button")}
|
||||
withBar={false}
|
||||
onClick={() => updateAndRestart()}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return latestVersion && (
|
||||
<Tippy
|
||||
zIndex={1000}
|
||||
placement="bottom"
|
||||
content={renderTippyContent()}
|
||||
theme="default"
|
||||
hideOnClick
|
||||
interactive
|
||||
>
|
||||
<BsmButton
|
||||
className="shrink-0 w-11 h-full aspect-square !bg-transparent flex items-start p-0.5"
|
||||
icon="download"
|
||||
withBar={false}
|
||||
/>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TitleBar({ template = "index.html" }: { template: AppWindow }) {
|
||||
const audio = useService(AudioPlayerService);
|
||||
const windowControls = useWindowControls();
|
||||
|
||||
const volume = useObservable(() => audio.volume$, audio.volume);
|
||||
const color = useThemeColor("first-color");
|
||||
const { currentVersion, latestVersion } = useVersion();
|
||||
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
@@ -107,7 +171,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
||||
<div id="drag-region" className="grow basis-0 h-full">
|
||||
<div id="window-title" className="pl-1">
|
||||
<span className="text-gray-800 dark:text-gray-100 font-bold text-xs italic">BSManager</span>
|
||||
<TitleBarTags />
|
||||
<TitleBarTags version={currentVersion} latestVersion={latestVersion} />
|
||||
</div>
|
||||
</div>
|
||||
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
||||
@@ -117,6 +181,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
||||
</div>
|
||||
<BsmButton className="shrink-0 h-[23px] w-[23px] aspect-square !bg-transparent flex items-start" iconClassName={volumeIcon === "volume-down" ? "-translate-x-[1.8px]" : null} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()} />
|
||||
</div>
|
||||
<AutoUpdateButton latestVersion={latestVersion} />
|
||||
<button onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button">
|
||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
|
||||
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
|
||||
|
||||
+46
-10
@@ -4,8 +4,11 @@ import { PinIcon } from "renderer/components/svgs/icons/pin-icon.component";
|
||||
import { UnpinIcon } from "renderer/components/svgs/icons/unpin-icon.component";
|
||||
import { SvgIcon } from "renderer/components/svgs/svg-icon.type";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { AddIcon } from "renderer/components/svgs/icons/add-icon.component";
|
||||
import { EditIcon } from "renderer/components/svgs/icons/edit-icon.component";
|
||||
import { TrashIcon } from "renderer/components/svgs/icons/trash-icon.component";
|
||||
|
||||
type Props = {
|
||||
readonly className?: string;
|
||||
@@ -13,17 +16,26 @@ type Props = {
|
||||
readonly launchArgs?: string;
|
||||
readonly launchMods?: LaunchModItemProps[];
|
||||
readonly onLaunchArgsChange?: (args: string) => void;
|
||||
readonly onAddLaunchMod?: (command?: string) => void;
|
||||
};
|
||||
|
||||
export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, onLaunchArgsChange }: Props) {
|
||||
export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, onLaunchArgsChange, onAddLaunchMod }: Props) {
|
||||
|
||||
const {text: t} = useTranslationV2();
|
||||
const color = useThemeColor("first-color");
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
return (
|
||||
<div className={cn("grid grid-rows-[0fr] transition-[grid-template-rows] bg-theme-2 rounded-md shadow-md shadow-black overflow-hidden", open && "!grid-rows-[1fr]", className)}>
|
||||
<div className="flex flex-col overflow-y-scroll scrollbar-default">
|
||||
<div className="p-3.5">
|
||||
<input className="h-10 w-full rounded-md bg-theme-1 text-center mb-2" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={launchArgs} onChange={e => onLaunchArgsChange(e.target.value)}/>
|
||||
<div className="w-full relative flex justify-center items-center mb-2">
|
||||
<input className="h-10 w-full rounded-md bg-theme-1 text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={launchArgs} onChange={e => onLaunchArgsChange(e.target.value)}/>
|
||||
<Tippy theme="default" placement="top" content={t("pages.version-viewer.launch-mods.advanced-launch.create-launch-option")}>
|
||||
<button className="absolute size-8 rounded-md p-0.5 right-1 hover:brightness-110" style={{ backgroundColor: color }} onClick={() => onAddLaunchMod?.(launchArgs)}>
|
||||
<AddIcon className="size-full"/>
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div className={cn("flex gap-2 flex-wrap")}>
|
||||
{launchMods?.map((mod) => (
|
||||
<LaunchModItem key={mod.id} {...mod}/>
|
||||
@@ -35,25 +47,35 @@ export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, on
|
||||
)
|
||||
}
|
||||
|
||||
export type CustomLaunchOption = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly data: {
|
||||
readonly command?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type LaunchModItemProps = {
|
||||
readonly id: LaunchMod;
|
||||
readonly id: string;
|
||||
readonly icon?: SvgIcon;
|
||||
readonly label: string;
|
||||
readonly description: string;
|
||||
readonly description?: string;
|
||||
readonly active: boolean;
|
||||
readonly visible?: boolean;
|
||||
readonly pinned?: boolean;
|
||||
readonly onChange?: (val: boolean) => void;
|
||||
readonly onPinChange?: (val: boolean) => void;
|
||||
readonly onEdit?: () => void;
|
||||
readonly onDelete?: () => void;
|
||||
}
|
||||
|
||||
export function LaunchModItem({ id, icon: Icon, label, description, active, visible, pinned, onChange, onPinChange }: LaunchModItemProps) {
|
||||
export function LaunchModItem({ id, icon: Icon, label, description, active, visible, pinned, onChange, onPinChange, onEdit, onDelete }: LaunchModItemProps) {
|
||||
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
return (
|
||||
<Tippy theme="default" className="break-words" placement="top" content={description}>
|
||||
<button id={id} className={cn("grow rounded-md bg-theme-1 relative flex justify-center items-center h-10 py-1 px-3", visible === false && "hidden")} onClick={e => { e.preventDefault(); e.stopPropagation(); onChange?.(!active) }}>
|
||||
<Tippy theme="default" className="break-words" placement="top" content={description ?? null} disabled={!description}>
|
||||
<div id={id} className={cn("grow rounded-md bg-theme-1 relative flex justify-center items-center h-10 py-1 px-3", visible === false && "hidden")} onClick={e => { e.preventDefault(); e.stopPropagation(); onChange?.(!active) }}>
|
||||
<BsmCheckbox className="h-4 aspect-square z-[1] relative mr-1.5" checked={active} onChange={onChange}/>
|
||||
{Icon && <Icon className="h-full w-fit py-0.5 mr-1.5 text-gray-800 dark:text-gray-200"/>}
|
||||
<span className="font-bold text-gray-800 dark:text-gray-200">{label}</span>
|
||||
@@ -66,7 +88,21 @@ export function LaunchModItem({ id, icon: Icon, label, description, active, visi
|
||||
</button>
|
||||
</Tippy>
|
||||
)}
|
||||
</button>
|
||||
{onEdit && (
|
||||
<Tippy theme="default" placement="right" content={t("misc.edit")} hideOnClick>
|
||||
<button className="h-full py-2 px-1" onClick={e => { e.preventDefault(); e.stopPropagation(); onEdit?.() }}>
|
||||
<EditIcon className="size-full text-gray-800 dark:text-gray-200"/>
|
||||
</button>
|
||||
</Tippy>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Tippy theme="default" placement="right" content={t("misc.delete")} hideOnClick>
|
||||
<button className="h-full py-2 px-1" onClick={e => { e.preventDefault(); e.stopPropagation(); onDelete?.() }}>
|
||||
<TrashIcon className="size-full text-gray-800 dark:text-gray-200"/>
|
||||
</button>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</Tippy>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
@@ -14,40 +14,28 @@ import { lastValueFrom } from "rxjs";
|
||||
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
|
||||
import { safeLt } from "shared/helpers/semver.helpers";
|
||||
import { WarningIcon } from "renderer/components/svgs/icons/warning-icon.component";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { LaunchModItemProps, LaunchOptionsPanel } from "./launch-options-panel.component";
|
||||
import { CustomLaunchOption, LaunchModItemProps, LaunchOptionsPanel } from "./launch-options-panel.component";
|
||||
import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { OculusIcon } from "renderer/components/svgs/icons/oculus-icon.component";
|
||||
import { DesktopIcon } from "renderer/components/svgs/icons/desktop-icon.component";
|
||||
import { TerminalIcon } from "renderer/components/svgs/icons/terminal-icon.component";
|
||||
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
|
||||
import { EditIcon } from "renderer/components/svgs/icons/edit-icon.component";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { CreateCustomLaunchOptionModal } from "renderer/components/modal/modal-types/create-custom-launch-option.component";
|
||||
|
||||
type Props = { version: BSVersion };
|
||||
|
||||
export function LaunchSlide({ version }: Props) {
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const configService = useService(ConfigurationService);
|
||||
function useLaunchMods() {
|
||||
const bsLauncherService = useService(BSLauncherService);
|
||||
const bsDownloader = useService(BsDownloaderService);
|
||||
const versions = useService(BSVersionManagerService);
|
||||
const configService = useService(ConfigurationService);
|
||||
|
||||
const [advancedLaunch, setAdvancedLaunch] = useState(false);
|
||||
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
|
||||
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
|
||||
const [activeLaunchMods, setActiveLaunchMods] = useState<LaunchMod[]>(configService.get("launch-mods") ?? []);
|
||||
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<LaunchMod[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
|
||||
|
||||
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
|
||||
|
||||
useEffect(() => {
|
||||
configService.set("launch-command", command);
|
||||
}, [command]);
|
||||
const [activeLaunchMods, setActiveLaunchMods] = useState<string[]>(configService.get("launch-mods") ?? []);
|
||||
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<string[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
configService.set("pinned-launch-mods", pinnedLaunchMods);
|
||||
@@ -61,13 +49,127 @@ export function LaunchSlide({ version }: Props) {
|
||||
}
|
||||
}, [activeLaunchMods]);
|
||||
|
||||
const toggleActiveLaunchMod = (checked: boolean, launchMod: LaunchMod) => checked
|
||||
const toggleActiveLaunchMod = useCallback((checked: boolean, launchMod: string) => checked
|
||||
? setActiveLaunchMods(prev => [...prev, launchMod])
|
||||
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
|
||||
[]);
|
||||
|
||||
const togglePinnedLaunchMod = (pinned: boolean, launchMod: LaunchMod) => pinned
|
||||
const togglePinnedLaunchMod = useCallback((pinned: boolean, launchMod: string) => pinned
|
||||
? setPinnedLaunchMods(prev => [...prev, launchMod])
|
||||
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
|
||||
[]);
|
||||
|
||||
return {
|
||||
activeLaunchMods,
|
||||
pinnedLaunchMods,
|
||||
toggleActiveLaunchMod,
|
||||
togglePinnedLaunchMod,
|
||||
}
|
||||
}
|
||||
|
||||
function useCustomLaunchMods({
|
||||
activeLaunchMods, pinnedLaunchMods,
|
||||
toggleActiveLaunchMod,
|
||||
togglePinnedLaunchMod,
|
||||
}: {
|
||||
activeLaunchMods: string[];
|
||||
pinnedLaunchMods: string[];
|
||||
|
||||
toggleActiveLaunchMod: (checked: boolean, launchMod: string) => void;
|
||||
togglePinnedLaunchMod: (checked: boolean, launchMod: string) => void;
|
||||
}) {
|
||||
const configService = useService(ConfigurationService);
|
||||
const modalService = useService(ModalService);
|
||||
|
||||
const customLaunchOptions = useObservable<CustomLaunchOption[]>(() => (
|
||||
configService.watch<CustomLaunchOption[]>("custom-launch-options")
|
||||
), []);
|
||||
|
||||
const [customLaunchModsArgs, setCustomLaunchModsArgs] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const command = customLaunchOptions
|
||||
?.map(option => option.data.command || "")
|
||||
.filter(Boolean);
|
||||
setCustomLaunchModsArgs(() => command ?? []);
|
||||
}, [customLaunchOptions]);
|
||||
|
||||
const deleteCustomLaunchOption = useCallback((id: string) => {
|
||||
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== id);
|
||||
configService.set("custom-launch-options", newCustomLaunchOptions);
|
||||
}, [customLaunchOptions]);
|
||||
|
||||
const saveCustomLaunchOption = useCallback(async (option: Partial<CustomLaunchOption>) => {
|
||||
const result = await modalService.openModal(CreateCustomLaunchOptionModal, { data: option });
|
||||
if(result.exitCode !== ModalExitCode.COMPLETED) { return; }
|
||||
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== result.data.id);
|
||||
newCustomLaunchOptions.push(result.data);
|
||||
configService.set("custom-launch-options", newCustomLaunchOptions);
|
||||
}, [customLaunchOptions]);
|
||||
|
||||
const getCustomLaunch = useCallback(() => {
|
||||
return customLaunchOptions?.map<LaunchModItemProps>(option => ({
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
active: activeLaunchMods.includes(option.id),
|
||||
pinned: pinnedLaunchMods.includes(option.id),
|
||||
onChange: (checked) => {
|
||||
toggleActiveLaunchMod(checked, option.id)
|
||||
setCustomLaunchModsArgs(prev => {
|
||||
if(checked){
|
||||
return [...prev, option.data.command];
|
||||
}
|
||||
return prev.filter(arg => arg !== option.data.command);
|
||||
});
|
||||
},
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, option.id),
|
||||
onEdit: () => {
|
||||
saveCustomLaunchOption(option);
|
||||
},
|
||||
onDelete: () => {
|
||||
deleteCustomLaunchOption(option.id);
|
||||
},
|
||||
})) ?? [];
|
||||
}, [customLaunchOptions, activeLaunchMods, pinnedLaunchMods])
|
||||
|
||||
return {
|
||||
customLaunchOptions,
|
||||
customLaunchModsArgs,
|
||||
getCustomLaunch,
|
||||
saveCustomLaunchOption
|
||||
};
|
||||
}
|
||||
|
||||
export function LaunchSlide({ version }: Props) {
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const configService = useService(ConfigurationService);
|
||||
const bsLauncherService = useService(BSLauncherService);
|
||||
const bsDownloader = useService(BsDownloaderService);
|
||||
const versions = useService(BSVersionManagerService);
|
||||
|
||||
const [advancedLaunch, setAdvancedLaunch] = useState(false);
|
||||
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
|
||||
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
|
||||
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
|
||||
|
||||
const {
|
||||
activeLaunchMods, pinnedLaunchMods,
|
||||
toggleActiveLaunchMod, togglePinnedLaunchMod,
|
||||
} = useLaunchMods();
|
||||
|
||||
const {
|
||||
customLaunchOptions, customLaunchModsArgs,
|
||||
getCustomLaunch, saveCustomLaunchOption,
|
||||
} = useCustomLaunchMods({
|
||||
activeLaunchMods, pinnedLaunchMods,
|
||||
toggleActiveLaunchMod,
|
||||
togglePinnedLaunchMod,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
configService.set("launch-command", command);
|
||||
}, [command]);
|
||||
|
||||
const launchModItems = useMemo<LaunchModItemProps[]>(() => {
|
||||
|
||||
@@ -78,6 +180,8 @@ export function LaunchSlide({ version }: Props) {
|
||||
: ["BSInstances", version.name, "Logs"];
|
||||
}
|
||||
|
||||
const customOptions = getCustomLaunch();
|
||||
|
||||
return [
|
||||
{
|
||||
id: LaunchMods.OCULUS,
|
||||
@@ -142,14 +246,15 @@ export function LaunchSlide({ version }: Props) {
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.PROTON_LOGS),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.PROTON_LOGS),
|
||||
},
|
||||
...customOptions,
|
||||
]
|
||||
}, [activeLaunchMods, pinnedLaunchMods, version]);
|
||||
}, [activeLaunchMods, pinnedLaunchMods, customLaunchOptions, version]);
|
||||
|
||||
const launch = async () => {
|
||||
const launch$ = bsLauncherService.launch({
|
||||
version,
|
||||
launchMods: activeLaunchMods,
|
||||
command,
|
||||
launchMods: activeLaunchMods.filter(mod => Object.values(LaunchMods).includes(mod as LaunchMod)) as LaunchMod[],
|
||||
command: [command, ...customLaunchModsArgs].filter(Boolean).join(" ").trim(),
|
||||
});
|
||||
|
||||
return lastValueFrom(launch$).catch(() => {});
|
||||
@@ -193,9 +298,9 @@ export function LaunchSlide({ version }: Props) {
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col items-center justify-center gap-3">
|
||||
<div className="relative">
|
||||
<GlowEffect className="!rounded-full" visible={!!(activeLaunchMods?.length || command)}/>
|
||||
<GlowEffect className="!rounded-full" visible={!!((activeLaunchMods?.filter(mod => !pinnedLaunchMods.includes(mod)))?.length || command)}/>
|
||||
<BsmButton
|
||||
className={cn("rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white", (advancedLaunch && command) ? "" : "shadow-md shadow-black")}
|
||||
className="rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white shadow-md shadow-black"
|
||||
text="pages.version-viewer.launch-mods.advanced-launch.button"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
@@ -205,7 +310,7 @@ export function LaunchSlide({ version }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LaunchOptionsPanel open={advancedLaunch} launchMods={launchModItems} launchArgs={command} className="w-full max-w-3xl mt-3" onLaunchArgsChange={setCommand}/>
|
||||
<LaunchOptionsPanel open={advancedLaunch} launchMods={launchModItems} launchArgs={command} className="w-full max-w-3xl mt-3" onLaunchArgsChange={setCommand} onAddLaunchMod={command => saveCustomLaunchOption({ data: { command } })}/>
|
||||
<div className='grow flex justify-center items-center p-2'>
|
||||
<BsmButton
|
||||
onClick={launch}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
|
||||
import { CSSProperties, MouseEvent, useMemo } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import striptags from "striptags";
|
||||
import { safeGt } from "shared/helpers/semver.helpers";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -21,20 +22,74 @@ type Props = {
|
||||
onUninstall?: () => void;
|
||||
};
|
||||
|
||||
type FileSizeProps = {
|
||||
fileSize?: number;
|
||||
wantInfoStyle: CSSProperties;
|
||||
};
|
||||
|
||||
function FileSizeText({ fileSize, wantInfoStyle }: Readonly<FileSizeProps>) {
|
||||
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
const verifyFileSize = fileSize !== undefined;
|
||||
|
||||
const isMediumMod = verifyFileSize && fileSize > 1024 * 1024 * 50; // 50MB
|
||||
|
||||
const isLargeMod = verifyFileSize && fileSize > 1024 * 1024 * 100; // 100MB
|
||||
|
||||
const getFormattedSize = () : string => {
|
||||
if (!verifyFileSize || fileSize === 0)
|
||||
return `-`;
|
||||
if (fileSize < 1024 * 1024)
|
||||
return `${(fileSize/1024).toFixed(2)}KB`;
|
||||
return `${(fileSize/1024/1024).toFixed(2)}MB`;
|
||||
};
|
||||
|
||||
const getWarningText = () : string => {
|
||||
if (isLargeMod)
|
||||
return `pages.version-viewer.mods.mods-grid.mod-item.this-is-a-very-large-mod`;
|
||||
if (isMediumMod)
|
||||
return `pages.version-viewer.mods.mods-grid.mod-item.this-is-a-large-mod`;
|
||||
return "";
|
||||
};
|
||||
|
||||
const getTheme = () : string => {
|
||||
if (isLargeMod)
|
||||
return 'red';
|
||||
if (isMediumMod)
|
||||
return 'yellow';
|
||||
return "";
|
||||
};
|
||||
|
||||
const getTextColor = () : string => {
|
||||
if (isLargeMod)
|
||||
return "text-red-400";
|
||||
if (isMediumMod)
|
||||
return "text-yellow-400";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<Tippy
|
||||
content={t(getWarningText())}
|
||||
placement="left"
|
||||
theme={getTheme()}
|
||||
disabled={!(isLargeMod || isMediumMod)}
|
||||
delay={[50, 0]} >
|
||||
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${getTextColor()}`} style={wantInfoStyle}>
|
||||
{getFormattedSize()}
|
||||
</span>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo, disabled, onUninstall }: Props) {
|
||||
|
||||
const themeColor = useThemeColor("second-color");
|
||||
const clickRef = useRef();
|
||||
|
||||
const isChecked = useMemo(() => isDependency || isSelected || mod.mod.category === BbmCategories.Core, [isDependency, isSelected, mod.mod.category]);
|
||||
|
||||
useDoubleClick({
|
||||
onSingleClick: e => handleWantInfo(e),
|
||||
onDoubleClick: e => handleOnChange(e),
|
||||
ref: clickRef,
|
||||
latency: 175,
|
||||
});
|
||||
|
||||
useOnUpdate(() => {
|
||||
onChange(isChecked);
|
||||
}, [isChecked]);
|
||||
@@ -46,17 +101,13 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
e.preventDefault();
|
||||
onWantInfo(mod);
|
||||
};
|
||||
const handleOnChange = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onChange(!isChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<li ref={clickRef} className={`${className} group`}>
|
||||
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
|
||||
<li className={`${className} group`} onClick={handleWantInfo}>
|
||||
<div className="h-full flex items-center justify-center p-1.5 px-4 rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="size-[18px] aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
|
||||
</div>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90 flex items-center" style={wantInfoStyle}>
|
||||
{mod.mod.name}
|
||||
</span>
|
||||
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${installedVersion && isOutDated && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>
|
||||
@@ -65,6 +116,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{mod.version.modVersion}
|
||||
</span>
|
||||
<FileSizeText fileSize={mod.version.fileSize} wantInfoStyle={wantInfoStyle} />
|
||||
<span title={striptags(mod.mod?.description ?? "", { tagReplacementText: " " })} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{striptags(mod.mod?.summary ?? "", { tagReplacementText: " " })}
|
||||
</span>
|
||||
|
||||
@@ -58,14 +58,15 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
|
||||
return (
|
||||
modsMap && (
|
||||
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
|
||||
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
|
||||
<span className="absolute z-10 top-0 w-full h-8 bg-inherit rounded-tl-md" />
|
||||
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1 rounded-tl-md">
|
||||
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
|
||||
</span>
|
||||
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
|
||||
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "200px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.size")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
|
||||
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
|
||||
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[
|
||||
|
||||
@@ -7,13 +7,11 @@ import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
|
||||
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { useTranslation, useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-disclaimer-modal.component";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { lt } from "semver";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
@@ -22,6 +20,9 @@ import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uni
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { Dropzone } from "renderer/components/shared/dropzone.component";
|
||||
import { ModsGridStatus } from "shared/models/mods/mod-ipc.model";
|
||||
import { BsmLink } from "renderer/components/shared/bsm-link.component";
|
||||
import { DISCORD_URL, GITHUB_URL } from "shared/constants";
|
||||
|
||||
export type ModsSlideRef = {
|
||||
loadMods: () => Promise<void>;
|
||||
@@ -32,22 +33,21 @@ type Props = { version: BSVersion; isActive?: boolean, onDisclamerDecline: () =>
|
||||
export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, onDisclamerDecline }, forwaredRef) => {
|
||||
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
|
||||
|
||||
const { text: t } = useTranslationV2();
|
||||
const { text: t, element: te } = useTranslationV2();
|
||||
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
const configService = useService(ConfigurationService);
|
||||
const notification = useService(NotificationService);
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const modals = useService(ModalService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
const progress = useService(ProgressBarService);
|
||||
|
||||
const [gridStatus, setGridStatus] = useState(ModsGridStatus.OK);
|
||||
const [modsAvailable, setModsAvailable] = useState(null as Map<BbmCategories, BbmFullMod[]>);
|
||||
const [modsInstalled, setModsInstalled] = useState(null as Map<BbmCategories, BbmFullMod[]>);
|
||||
const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]);
|
||||
const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod);
|
||||
const [reinstallAllMods, setReinstallAllMods] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [uninstalling, setUninstalling] = useState(false);
|
||||
const [modsDropZoneOpen, setModsDropZoneOpen] = useState(false);
|
||||
@@ -185,8 +185,23 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
setModsSelected(() => []);
|
||||
}
|
||||
|
||||
const ensureDisclaimerAccepted = async (): Promise<boolean> => {
|
||||
if (configService.get<boolean>(ACCEPTED_DISCLAIMER_KEY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const res = await modals.openModal(ModsDisclaimerModal);
|
||||
const haveAccepted = res.exitCode === ModalExitCode.COMPLETED;
|
||||
|
||||
if (haveAccepted) {
|
||||
configService.set(ACCEPTED_DISCLAIMER_KEY, true);
|
||||
}
|
||||
|
||||
return haveAccepted;
|
||||
}
|
||||
|
||||
const loadMods = async (): Promise<void> => {
|
||||
if (os.isOffline) {
|
||||
if (gridStatus !== ModsGridStatus.OK) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -204,38 +219,47 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
}), [version]);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
if(!isActive || !isOnline){
|
||||
return noop();
|
||||
if(!isActive){
|
||||
return noop;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (configService.get<boolean>(ACCEPTED_DISCLAIMER_KEY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const res = await modals.openModal(ModsDisclaimerModal);
|
||||
const haveAccepted = res.exitCode === ModalExitCode.COMPLETED;
|
||||
|
||||
if (haveAccepted) {
|
||||
configService.set(ACCEPTED_DISCLAIMER_KEY, true);
|
||||
}
|
||||
|
||||
return haveAccepted;
|
||||
})().then(canLoad => {
|
||||
ensureDisclaimerAccepted().then(async canLoad => {
|
||||
if (!canLoad) {
|
||||
return onDisclamerDecline?.();
|
||||
}
|
||||
|
||||
loadMods();
|
||||
if (isCancelled) return;
|
||||
|
||||
const status = await modsManager.getModsGridStatus();
|
||||
|
||||
if (isCancelled) return;
|
||||
|
||||
setGridStatus(() => status);
|
||||
|
||||
if (status !== ModsGridStatus.OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
modsManager.getVersionModsState(version).then(({ available, installed }) => {
|
||||
if (isCancelled) return;
|
||||
|
||||
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
|
||||
setModsAvailable(() => modsToCategoryMap(available));
|
||||
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installed.some(i => m.mod.id === i.mod.id)));
|
||||
setModsInstalled(modsToCategoryMap(installed));
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
setMoreInfoMod(null);
|
||||
setModsAvailable(null);
|
||||
setModsInstalled(null);
|
||||
setGridStatus(ModsGridStatus.OK);
|
||||
};
|
||||
}, [isActive, isOnline, version]);
|
||||
}, [isActive, version]);
|
||||
|
||||
useEffect(() => {
|
||||
// Center the progress bar between buttons
|
||||
@@ -258,9 +282,29 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
}
|
||||
}, [modsAvailable]);
|
||||
|
||||
const renderStatus = () => {
|
||||
if (gridStatus === ModsGridStatus.BEATMODS_DOWN) {
|
||||
return <ModStatus image={BeatConflictImg}>
|
||||
<span className="text-xl text-center px-2 mt-3 italic">
|
||||
{te("pages.version-viewer.mods.status.beatmods-down", {links: (<>
|
||||
<BsmLink className="text-blue-500 underline" href={DISCORD_URL}>
|
||||
Discord
|
||||
</BsmLink>
|
||||
/
|
||||
<BsmLink className="text-blue-500 underline" href={GITHUB_URL}>
|
||||
GitHub
|
||||
</BsmLink>
|
||||
</>)})}
|
||||
</span>
|
||||
</ModStatus>
|
||||
}
|
||||
|
||||
return <ModStatus text={`pages.version-viewer.mods.status.${gridStatus}`} image={BeatConflictImg} />;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isOnline) {
|
||||
return <ModStatus text="pages.version-viewer.mods.no-internet" image={BeatConflictImg} />;
|
||||
if (gridStatus !== ModsGridStatus.OK) {
|
||||
return renderStatus();
|
||||
}
|
||||
if (!modsAvailable) {
|
||||
return <ModStatus text="pages.version-viewer.mods.loading-mods" image={BeatWaitingImg} spin />;
|
||||
@@ -331,13 +375,13 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
);
|
||||
});
|
||||
|
||||
function ModStatus({ text, image, spin = false, children }: { text: string; image: string; spin?: boolean, children?: ReactNode}) {
|
||||
const t = useTranslation();
|
||||
function ModStatus({ text, image, spin = false, children }: { text?: string; image: string; spin?: boolean, children?: ReactNode}) {
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-gray-800 dark:text-gray-200">
|
||||
<img className={`w-32 h-32 ${spin ? "spin-loading" : ""}`} src={image} alt=" " />
|
||||
<span className="text-xl mt-3 italic">{t(text)}</span>
|
||||
{text && <span className="text-xl text-center px-2 mt-3 italic">{t(text)}</span>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const defaultConfiguration: {
|
||||
"second-color": "#ff4444",
|
||||
theme: "os",
|
||||
language: window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "it-IT", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "pt-BR", "uk-UA"],
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "it-IT", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "pt-BR", "uk-UA", "tl-PH"],
|
||||
default_mods: ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
|
||||
"default-shared-folders": [
|
||||
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
|
||||
|
||||
@@ -114,6 +114,22 @@
|
||||
@apply border-r-neutral-900;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='red'] {
|
||||
@apply bg-neutral-900;
|
||||
@apply text-red-400;
|
||||
}
|
||||
.tippy-box[data-theme~='red'][data-placement^='left'] > .tippy-arrow::before {
|
||||
@apply border-l-neutral-900;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='yellow'] {
|
||||
@apply bg-neutral-900;
|
||||
@apply text-yellow-400;
|
||||
}
|
||||
.tippy-box[data-theme~='yellow'][data-placement^='left'] > .tippy-arrow::before {
|
||||
@apply border-l-neutral-900;
|
||||
}
|
||||
|
||||
.bg-theme-1 { @apply bg-light-main-color-1 dark:bg-main-color-1; }
|
||||
.bg-theme-2 { @apply bg-light-main-color-2 dark:bg-main-color-2; }
|
||||
.bg-theme-3 { @apply bg-light-main-color-3 dark:bg-main-color-3; }
|
||||
|
||||
@@ -47,6 +47,8 @@ import { tryit } from "shared/helpers/error.helpers";
|
||||
import { InstallationLocationService } from "renderer/services/installation-location.service";
|
||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
|
||||
import { DISCORD_URL } from "shared/constants";
|
||||
import { AutoUpdate } from "shared/models/config";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -200,11 +202,18 @@ export function SettingsPage() {
|
||||
const fileChooserRes = await lastValueFrom(ipcService.sendV2("choose-folder"));
|
||||
|
||||
if (!fileChooserRes.canceled && fileChooserRes.filePaths?.length) {
|
||||
|
||||
const newInstallationPath = fileChooserRes.filePaths[0];
|
||||
|
||||
if(newInstallationPath === installationFolder){
|
||||
return;
|
||||
}
|
||||
|
||||
progressBarService.showFake(0.008);
|
||||
|
||||
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
|
||||
|
||||
lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => {
|
||||
lastValueFrom(installationLocationService.setInstallationFolder(newInstallationPath, true)).then(res => {
|
||||
|
||||
progressBarService.complete();
|
||||
progressBarService.hide();
|
||||
@@ -238,9 +247,9 @@ export function SettingsPage() {
|
||||
|
||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+");
|
||||
const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+");
|
||||
const openDiscord = () => linkOpener.open("https://discord.gg/uSqbHVpKdV");
|
||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?template=1-bug-report.yaml");
|
||||
const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?template=2-feature-request.yaml");
|
||||
const openDiscord = () => linkOpener.open(DISCORD_URL);
|
||||
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
|
||||
|
||||
const openLogs = () => lastValueFrom(ipcService.sendV2("open-logs"));
|
||||
@@ -496,7 +505,7 @@ export function SettingsPage() {
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.language.title" description="pages.settings.language.description">
|
||||
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} />
|
||||
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} columnCount={2} />
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
||||
@@ -507,7 +516,8 @@ export function SettingsPage() {
|
||||
<SettingContainer className="mt-3" description="pages.settings.discord.description">
|
||||
<div className="flex gap-2">
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Discord" icon="discord" iconClassName="p-0.5 mr-1" color="#5865f2" onClick={openDiscord} />
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Twitter" icon="twitter" iconClassName="p-0.5 mr-1" color="#000" onClick={openTwitter} />
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="Twitter" icon="twitter" iconClassName="p-0.5 mr-1" color="#171717" onClick={openTwitter} />
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="GitHub" icon="github" iconClassName="p-0.5 mr-1 h-full w-full" color="#171717" onClick={openGithub} />
|
||||
</div>
|
||||
</SettingContainer>
|
||||
<SettingContainer className="pt-3" description="pages.settings.contribution.description">
|
||||
@@ -518,7 +528,6 @@ export function SettingsPage() {
|
||||
</div>
|
||||
<div className="flex px-2 gap-2">
|
||||
<BsmButton onClick={openLogs} className="shrink-0 whitespace-nowrap px-2 font-bold italic text-sm rounded-md" text="pages.settings.contribution.buttons.open-logs" withBar={false} />
|
||||
<BsmButton onClick={openGithub} className="shrink-0 px-2 rounded-md" icon="github" title="GitHub" withBar={false} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingContainer>
|
||||
@@ -545,11 +554,17 @@ function AdvancedSettings() {
|
||||
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
|
||||
const [useSymlink, setUseSymlink] = useState(false);
|
||||
const [useSystemProxy, setUseSystemProxy] = useState(false);
|
||||
const [autoUpdate, setAutoUpdate] = useState<AutoUpdate>(AutoUpdate.NEVER);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
|
||||
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
|
||||
staticConfig.get("use-system-proxy").then(useSystemProxy => setUseSystemProxy(() => useSystemProxy));
|
||||
|
||||
if (window.electron.platform === "win32") {
|
||||
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
|
||||
staticConfig.get("use-system-proxy").then(useSystemProxy => setUseSystemProxy(() => useSystemProxy));
|
||||
staticConfig.get("auto-update").then(res => setAutoUpdate(() => res ?? AutoUpdate.ALWAYS));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onChangeHardwareAcceleration = async (newHardwareAccelerationEnabled: boolean) => {
|
||||
@@ -630,12 +645,43 @@ function AdvancedSettings() {
|
||||
setUseSystemProxy(() => newUseSystemProxy);
|
||||
}
|
||||
|
||||
const advancedItems: Item[] = [{
|
||||
const onChangeAutoUpdate = async (value: boolean) => {
|
||||
if (window.electron.platform !== "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newAutoUpdate = value ? AutoUpdate.ALWAYS : AutoUpdate.NEVER;
|
||||
const { error } = await tryit(() => staticConfig.set("auto-update", newAutoUpdate));
|
||||
|
||||
if (error) {
|
||||
notification.notifyError({
|
||||
title: "notifications.types.error",
|
||||
desc: "pages.settings.advanced.auto-update.error-notification.message",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setAutoUpdate(() => newAutoUpdate);
|
||||
}
|
||||
|
||||
const advancedItems: Item[] = [];
|
||||
|
||||
if (window.electron.platform === "win32") {
|
||||
advancedItems.push({
|
||||
checked: autoUpdate === AutoUpdate.ALWAYS,
|
||||
text: t.text("pages.settings.advanced.auto-update.title"),
|
||||
desc: t.text("pages.settings.advanced.auto-update.description"),
|
||||
onChange: onChangeAutoUpdate
|
||||
});
|
||||
}
|
||||
|
||||
advancedItems.push({
|
||||
checked: hardwareAccelerationEnabled,
|
||||
text: t.text("pages.settings.advanced.hardware-acceleration.title"),
|
||||
desc: t.text("pages.settings.advanced.hardware-acceleration.description"),
|
||||
onChange: onChangeHardwareAcceleration
|
||||
}];
|
||||
});
|
||||
|
||||
if (window.electron.platform === "win32") {
|
||||
advancedItems.push({
|
||||
checked: useSymlink,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { logRenderError } from "renderer";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export function VersionViewer() {
|
||||
|
||||
@@ -125,13 +126,25 @@ export function VersionViewer() {
|
||||
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(UninstallModal, { data: state });
|
||||
if (modalCompleted.exitCode === ModalExitCode.COMPLETED) {
|
||||
bsUninstallerService.uninstall(state).then(() => {
|
||||
bsVersionManagerService.askInstalledVersions().then(versions => {
|
||||
navigateToVersion(versions?.at(0));
|
||||
});
|
||||
});
|
||||
if (modalCompleted.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
bsUninstallerService.uninstall(state).then(() => {
|
||||
bsVersionManagerService.askInstalledVersions().then(versions => {
|
||||
navigateToVersion(versions?.at(0));
|
||||
});
|
||||
}).catch((err: CustomError) => {
|
||||
let desc = err?.message;
|
||||
|
||||
if (["CantDeleteOfficialVersion", "VersionNotFound"].includes(err?.code)) {
|
||||
desc = `notifications.bs-uninstall.errors.desc.${err.code}`;
|
||||
} else if (err?.code?.startsWith("generic.fs.delete-folder")) {
|
||||
desc = "notifications.bs-uninstall.errors.desc.delete-folder";
|
||||
}
|
||||
|
||||
notification.notifyError({ title: "notifications.bs-uninstall.errors.title", desc });
|
||||
});
|
||||
};
|
||||
|
||||
const edit = () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Progression } from "main/helpers/fs.helpers";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
|
||||
import { logRenderError } from "renderer";
|
||||
import { ModsGridStatus } from "shared/models/mods/mod-ipc.model";
|
||||
import { LinuxService } from "./linux.service";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -17,6 +19,7 @@ export class BsModsManagerService {
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly linux: LinuxService
|
||||
|
||||
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
@@ -31,6 +34,7 @@ export class BsModsManagerService {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.linux = LinuxService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Observable<BbmFullMod[]> {
|
||||
@@ -204,4 +208,22 @@ export class BsModsManagerService {
|
||||
return { available: (available ?? []), installed: (installedMods ?? []) };
|
||||
}
|
||||
|
||||
public async getModsGridStatus(): Promise<ModsGridStatus> {
|
||||
|
||||
if(window.electron.platform === "linux"){
|
||||
const winePrefix = await lastValueFrom(this.linux.getWinePrefixPath());
|
||||
if(!winePrefix){
|
||||
logRenderError("Could not find BSManager WINEPREFIX path");
|
||||
return ModsGridStatus.NO_WINEPREFIX;
|
||||
}
|
||||
}
|
||||
|
||||
const beatModsUp = await lastValueFrom(this.ipcService.sendV2("bs-mods.beatmods-up")).catch(() => false);
|
||||
if (!beatModsUp) {
|
||||
return ModsGridStatus.BEATMODS_DOWN;
|
||||
}
|
||||
|
||||
return ModsGridStatus.OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class BSUninstallerService {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
}
|
||||
|
||||
public uninstall(version: BSVersion): Promise<boolean> {
|
||||
return lastValueFrom(this.ipcService.sendV2("bs.uninstall", version)).catch(() => false);
|
||||
public uninstall(version: BSVersion): Promise<void> {
|
||||
return lastValueFrom(this.ipcService.sendV2("bs.uninstall", version));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user