mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38282dd1e7 |
@@ -1,170 +0,0 @@
|
|||||||
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
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24.11.1
|
node-version: 22.11.0
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
# Update package lists
|
# Update package lists
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24.11.1
|
node-version: 22.11.0
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24.11.1
|
node-version: 22.11.0
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24.11.1
|
node-version: 22.11.0
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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
|
|
||||||
+1
-4
@@ -29,10 +29,7 @@ Hello! We’re delighted that you’re interested in contributing to **BSManager
|
|||||||
### Before You Begin
|
### Before You Begin
|
||||||
|
|
||||||
1. **[Fork the repository][fork]** and **clone** your fork locally.
|
1. **[Fork the repository][fork]** and **clone** your fork locally.
|
||||||
2. **Install the required tools (Node, etc)** (we recommend using [mise](https://mise.jdx.dev/) to manage tool versions).
|
2. **Install the required Node.js version** (we recommend using [Volta](https://volta.sh/) to manage Node versions).
|
||||||
```bash
|
|
||||||
mise install
|
|
||||||
```
|
|
||||||
3. **Install project dependencies**:
|
3. **Install project dependencies**:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Zagrios/bs-manager/issues/new?template=1-bug-report.yaml">Report
|
href="https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+">Report
|
||||||
Bug</a>
|
Bug</a>
|
||||||
·
|
·
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Zagrios/bs-manager/issues/new?template=2-feature-request.yaml">Request
|
href="https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+">Request
|
||||||
Feature</a>
|
Feature</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/Zagrios/bs-manager/security/policy">Report a security vulnerability</a>
|
<a href="https://github.com/Zagrios/bs-manager/security/policy">Report a security vulnerability</a>
|
||||||
@@ -472,7 +472,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>Credits</h2>
|
<h2>Credits</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://github.com/Zagrios">Zagrios</a> - Lead Developer & Founder. (Mathieu Gries)</li>
|
<li><a href="https://github.com/Zagrios">Zagrios</a> - Lead Developer & Founder.</li>
|
||||||
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Co-Developer & Co-Founder, Discord Bot Developer.</li>
|
<li><a href="https://github.com/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/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
|
||||||
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
|
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
|
||||||
|
|||||||
@@ -865,140 +865,7 @@
|
|||||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510708375093248769",
|
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510708375093248769",
|
||||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/0f658ed39f480446e0dc5807b1591e8a18aef2f5.png",
|
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/0f658ed39f480446e0dc5807b1591e8a18aef2f5.png",
|
||||||
"ReleaseDate": "1749135144",
|
"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",
|
"year": "2025",
|
||||||
"recommended": true
|
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"BSVersion": "1.42.3",
|
|
||||||
"BSManifest": "5798847223913166210",
|
|
||||||
"OculusBinaryId": "25563181170021654",
|
|
||||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/511860284166506401",
|
|
||||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/c48686febbf5a5c753dc232c4cde48652231b667.png",
|
|
||||||
"ReleaseDate": "1774534379",
|
|
||||||
"year": "2026"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"BSVersion": "1.43.0",
|
|
||||||
"BSManifest": "6499880290663953435",
|
|
||||||
"OculusBinaryId": "25964224259917341",
|
|
||||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/692008075043799325",
|
|
||||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/79767d59028f454d63c4425a7bfd90913b59b546.png",
|
|
||||||
"ReleaseDate": "1777467723",
|
|
||||||
"year": "2026"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"BSVersion": "1.44.0",
|
|
||||||
"BSManifest": "4180431244915746363",
|
|
||||||
"OculusBinaryId": "26135279289478503",
|
|
||||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/690884711519617603",
|
|
||||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/335654acc1a5066bb634486a4834368097c84983.png",
|
|
||||||
"ReleaseDate": "1779973295",
|
|
||||||
"year": "2026"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"BSVersion": "1.44.1",
|
|
||||||
"BSManifest": "7621452421350879947",
|
|
||||||
"OculusBinaryId": "26497783233228105",
|
|
||||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/703272148131643814",
|
|
||||||
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/30fd358099eb07fe22e429cdf64380d7c2ae28dc.png",
|
|
||||||
"ReleaseDate": "1782392653",
|
|
||||||
"year": "2026"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+133
-37
@@ -1,78 +1,174 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"username": "Shidorien"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "iPixelGalaxy"
|
"username": "iPixelGalaxy"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "Emyte",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "GoodOldNervy"
|
"username": "GoodOldNervy"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "Naysy"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "Falmil"
|
"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": "Z3t4"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "blot455",
|
||||||
|
"type": "diamond"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Lumberjack462",
|
||||||
|
"type": "diamond",
|
||||||
|
"link": "https://www.youtube.com/@lumberjack462"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "Xero"
|
"username": "Xero"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "🎵Shade"
|
"username": "Minescence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "mereknom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Jascha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Celldweller",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "rhythmshade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "liborsaf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "aatame3",
|
"username": "aatame3",
|
||||||
"type": "gold"
|
"type": "gold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Joshua Knick"
|
"username": "Joshua"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Stuijvi",
|
"username": "Stuijvi",
|
||||||
"type": "gold"
|
"type": "gold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Austin Bauman"
|
"username": "_ monaka"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Better_Axel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Riley",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Austin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Fatalution",
|
||||||
|
"type": "diamond",
|
||||||
|
"link": "https://x.com/fatalution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Taurus Arcade",
|
||||||
|
"type": "gold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Mozz_Zm"
|
"username": "Mozz_Zm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"username": "clapxz"
|
||||||
|
},
|
||||||
|
{
|
||||||
"username": "Reflected Chop",
|
"username": "Reflected Chop",
|
||||||
"type": "gold"
|
"type": "gold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"username": "Furiouspupa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Maximilian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Paul Smith"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "MunityVR",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Sk4venger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Alexander Herman",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Arts Rimuro Suraimu",
|
||||||
|
"type": "gold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Ricardo Boss"
|
||||||
|
},
|
||||||
|
{
|
||||||
"username": "Jesper Norsted"
|
"username": "Jesper Norsted"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "bosspie",
|
"username": "Marcus Hamm"
|
||||||
"type": "diamond"
|
},
|
||||||
|
{
|
||||||
|
"username": "jxkelol"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "CamVR the goat"
|
"username": "paperwasp",
|
||||||
},
|
"type": "gold"
|
||||||
{
|
},
|
||||||
"username": "bones",
|
{
|
||||||
"type": "gold"
|
"username": "KingCrocman",
|
||||||
},
|
"type": "diamond",
|
||||||
{
|
"link": "https://www.youtube.com/KingCrocman"
|
||||||
"username": "Weed"
|
},
|
||||||
},
|
{
|
||||||
{
|
"username": "bosspie",
|
||||||
"username": "minefox54"
|
"type": "diamond"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Tindux",
|
"username": "Camryn Jackson"
|
||||||
"type": "diamond"
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "Daniel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "Eric McCoy",
|
|
||||||
"type": "gold"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "Moenker"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "Melissa Avery-Weir"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"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": {
|
"nav-bar": {
|
||||||
"add-version": "Version hinzufügen",
|
"add-version": "Version hinzufügen",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"likes": "Likes",
|
"likes": "Likes",
|
||||||
"date-uploaded": "Hochladedatum",
|
"date-uploaded": "Hochladedatum"
|
||||||
"added-date": "Hinzugefügt am"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Es ist ein Fehler aufgetreten, die Systemproxyeinstellungen können nicht geändert werden."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "outdated",
|
"outdated": "outdated"
|
||||||
"update-text": "BSManager {version} is available!",
|
|
||||||
"update-button": "Update and Restart",
|
|
||||||
"see-changelog": "See changelog"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Add a version",
|
"add-version": "Add a version",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"likes": "Likes",
|
"likes": "Likes",
|
||||||
"date-uploaded": "Date Uploaded",
|
"date-uploaded": "Date Uploaded"
|
||||||
"added-date": "Added Date"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "An error occurred, unable to change system proxy settings."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "obsoleto",
|
"outdated": "obsoleto"
|
||||||
"update-text": "¡BSManager {version} está disponible!",
|
|
||||||
"update-button": "Actualizar y reiniciar",
|
|
||||||
"see-changelog": "Ver los cambios"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Agregar una versión",
|
"add-version": "Agregar una versión",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Duración",
|
"duration": "Duración",
|
||||||
"likes": "Me gusta",
|
"likes": "Me gusta",
|
||||||
"date-uploaded": "Fecha de subida",
|
"date-uploaded": "Fecha de subida"
|
||||||
"added-date": "Fecha de adición"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Ocurrió un error, no se pueden cambiar los ajustes del proxy del sistema."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-111
@@ -33,14 +33,11 @@
|
|||||||
},
|
},
|
||||||
"generic": {
|
"generic": {
|
||||||
"env": {
|
"env": {
|
||||||
"parse": "Impossible de lire les variables d'environnement."
|
"parse": "Impossible d'analyser correctement la chaîne de variable d'environnement."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"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": {
|
"nav-bar": {
|
||||||
"add-version": "Ajouter une version",
|
"add-version": "Ajouter une version",
|
||||||
@@ -54,23 +51,23 @@
|
|||||||
"version-viewer": {
|
"version-viewer": {
|
||||||
"launch-mods": {
|
"launch-mods": {
|
||||||
"oculus": "Mode Oculus",
|
"oculus": "Mode 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.",
|
"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.",
|
||||||
"desktop": "Mode FPFC",
|
"desktop": "Mode FPFC",
|
||||||
"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 !",
|
"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!",
|
||||||
"debug": "Mode Debug",
|
"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.",
|
"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.",
|
"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": {
|
"advanced-launch": {
|
||||||
"button": "Options de lancement",
|
"button": "Options de lancement",
|
||||||
"placeholder": "Options de lancement. Exemple : KEY=VALUE %command% fpfc",
|
"placeholder": "Options de lancement ex: KEY=VALUE %command% fpfc",
|
||||||
"create-launch-option": "Créer une option de lancement"
|
"create-launch-option": "Créer une option de lancement"
|
||||||
},
|
},
|
||||||
"skipsteam": "Ignorer Steam",
|
"skipsteam": "Ignorer Steam",
|
||||||
"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.",
|
"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.",
|
||||||
"map-editor": "Éditeur de map",
|
"map-editor": "Éditeur de map",
|
||||||
"map-editor-description": "Lance l'éditeur officiel de map de Beat Saber au lieu du jeu.",
|
"map-editor-description": "Lance l'éditeur officiel de map de Beat Saber au lieu du jeu.",
|
||||||
"proton-logs": "Logs de Proton",
|
"proton-logs": "Journaux de Proton",
|
||||||
"proton-logs-description": "Active l'enregistrement des logs Proton pour cette installation de Beat Saber sur \"{versionPath}\"."
|
"proton-logs-description": "Active l'enregistrement des journaux Proton pour cette installation de Beat Saber sur \"{versionPath}\"."
|
||||||
},
|
},
|
||||||
"maps": {
|
"maps": {
|
||||||
"search-bar": {
|
"search-bar": {
|
||||||
@@ -107,12 +104,11 @@
|
|||||||
"sort": {
|
"sort": {
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"song-author": "Auteur de la chanson",
|
"song-author": "Auteur de la chanson",
|
||||||
"map-author": "Auteur de la map",
|
"map-author": "Auteur de la carte",
|
||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"likes": "J'aime",
|
"likes": "J'aime",
|
||||||
"date-uploaded": "Date de publication",
|
"date-uploaded": "Date de téléchargement"
|
||||||
"added-date": "Date d'ajout"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -128,7 +124,7 @@
|
|||||||
"sort": {
|
"sort": {
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
"author": "Auteur",
|
"author": "Auteur",
|
||||||
"number-of-maps": "Nombre de maps",
|
"number-of-maps": "Nombre de cartes",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"notes-per-second": "NPS"
|
"notes-per-second": "NPS"
|
||||||
}
|
}
|
||||||
@@ -140,8 +136,8 @@
|
|||||||
"no-internet": "Pas d'accès Internet",
|
"no-internet": "Pas d'accès Internet",
|
||||||
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
|
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
|
||||||
"status": {
|
"status": {
|
||||||
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber depuis BSManager.",
|
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber dans BSManager.",
|
||||||
"beatmods-down": "BeatMods est actuellement inaccessible. Veuillez réessayer plus tard. Si le problème persiste, informez-nous sur {links}.",
|
"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 ¯\\_(ツ)_/¯"
|
"unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -153,8 +149,8 @@
|
|||||||
"header-bar": {
|
"header-bar": {
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"size": "Taille",
|
"size": "Taille",
|
||||||
"installed": "Installée",
|
"installed": "Installé",
|
||||||
"latest": "Dernière",
|
"latest": "Récent",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"dropdown": {
|
"dropdown": {
|
||||||
"import-mods": "Importer des mods",
|
"import-mods": "Importer des mods",
|
||||||
@@ -169,8 +165,8 @@
|
|||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"all-mods-already-installed": {
|
"all-mods-already-installed": {
|
||||||
"title": "Mods déjà installés",
|
"title": "Mods déjà installées",
|
||||||
"description": "Tous les mods sélectionnés sont déjà installés"
|
"description": "Tous les mods séléctionnées sont déjà installées"
|
||||||
},
|
},
|
||||||
"outdated-mods": {
|
"outdated-mods": {
|
||||||
"title": "Mod obsolète",
|
"title": "Mod obsolète",
|
||||||
@@ -210,17 +206,17 @@
|
|||||||
"steam-and-oculus": {
|
"steam-and-oculus": {
|
||||||
"title": "Steam & Oculus",
|
"title": "Steam & Oculus",
|
||||||
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
|
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
|
||||||
"logout": "Se déconnecter",
|
"logout": "Déconnexion",
|
||||||
"logout-success": "Déconnexion réussie",
|
"logout-success": "Déconnexion réussie",
|
||||||
"download-platform": {
|
"download-platform": {
|
||||||
"title": "Plateforme par défaut",
|
"title": "Plateforme par défaut",
|
||||||
"desc": "Choisissez la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
|
"desc": "Choisi la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
|
||||||
"always-ask": "Toujours demander"
|
"always-ask": "Toujours demander"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "Apparence",
|
"title": "Apparence",
|
||||||
"description": "Choisissez les deux couleurs principales de BSManager.",
|
"description": "Choisis les deux couleurs principales de BSManager.",
|
||||||
"reset": "Réinitialiser",
|
"reset": "Réinitialiser",
|
||||||
"sub-title": "Thème",
|
"sub-title": "Thème",
|
||||||
"themes": {
|
"themes": {
|
||||||
@@ -231,7 +227,7 @@
|
|||||||
},
|
},
|
||||||
"installation-folder": {
|
"installation-folder": {
|
||||||
"title": "Dossier d'installation",
|
"title": "Dossier d'installation",
|
||||||
"description": "Changez le dossier qui contiendra tout le contenu téléchargé par BSManager."
|
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager."
|
||||||
},
|
},
|
||||||
"proton-folder": {
|
"proton-folder": {
|
||||||
"title": "Dossier Proton",
|
"title": "Dossier Proton",
|
||||||
@@ -249,8 +245,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"title": "Langue",
|
"title": "Langage",
|
||||||
"description": "Sélectionnez une langue.",
|
"description": "Sélectionne un langage.",
|
||||||
"languages": {
|
"languages": {
|
||||||
"en-EN": "English, UK",
|
"en-EN": "English, UK",
|
||||||
"en-US": "English, US",
|
"en-US": "English, US",
|
||||||
@@ -313,7 +309,7 @@
|
|||||||
"description": "Paramètres avancés pour BSManager.",
|
"description": "Paramètres avancés pour BSManager.",
|
||||||
"hardware-acceleration": {
|
"hardware-acceleration": {
|
||||||
"title": "Accélération matérielle",
|
"title": "Accélération matérielle",
|
||||||
"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.",
|
"description": "Activez l'accélération matérielle pour utiliser votre GPU et améliorer les performances de BSManager. Désactivez cette option si vous rencontrez des chutes d'IPS.",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "Redémarrage nécessaire",
|
"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 ?",
|
"body": "Changer le paramètre d'accélération matérielle va quitter et relancer BSManager. Êtes-vous sûr de vouloir continuer ?",
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Une erreur est survenue, impossible de modifier les paramètres du proxy système."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,8 +360,8 @@
|
|||||||
"file-not-supported": "Fichier non supporté"
|
"file-not-supported": "Fichier non supporté"
|
||||||
},
|
},
|
||||||
"msg": {
|
"msg": {
|
||||||
"operation-running": "Veuillez attendre la fin de l'opération en cours puis recommencez.",
|
"operation-running": "Attends la fin de l'opération en cours puis recommence.",
|
||||||
"no-internet": "Vérifiez votre connexion Internet et réessayez.",
|
"no-internet": "Vérifie ta connexion internet et ressaye.",
|
||||||
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,9 +377,9 @@
|
|||||||
"warnings": {
|
"warnings": {
|
||||||
"msg": {
|
"msg": {
|
||||||
"ManifestChecksum": "Le manifeste précédemment téléchargé ne correspond pas au nouveau 🤔",
|
"ManifestChecksum": "Le manifeste précédemment téléchargé ne correspond pas au nouveau 🤔",
|
||||||
"ConnectionTimeout": "Votre connexion internet semble instable 🥶",
|
"ConnectionTimeout": "Ta connexion internet semble instable 🥶",
|
||||||
"ConnectionLost": "La connexion a été perdue, réessayez...",
|
"ConnectionLost": "La connexion a été perdue, essayez à nouveau...",
|
||||||
"ConnectionError": "Impossible de se connecter à Steam, réessayez...",
|
"ConnectionError": "Impossible de se connecter à Steam, essayez à nouveau...",
|
||||||
"Unknown": "Quelque chose d'étrange s'est produit 🤔 Votre connexion est probablement instable."
|
"Unknown": "Quelque chose d'étrange s'est produit 🤔 Votre connexion est probablement instable."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -398,26 +387,26 @@
|
|||||||
"msg": {
|
"msg": {
|
||||||
"401": "Steam ne semble pas vouloir nous laisser télécharger Beat Saber 😢",
|
"401": "Steam ne semble pas vouloir nous laisser télécharger Beat Saber 😢",
|
||||||
"404": "Impossible de contacter les serveurs de Steam.",
|
"404": "Impossible de contacter les serveurs de Steam.",
|
||||||
"ExeNotFoundWindows": "\"DepotDownloader.exe\" est manquant. Vérifiez que votre antivirus n'a pas mis le fichier en quarantaine.",
|
"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 manquant.",
|
"ExeNotFoundLinux": "L'exécutable \"DepotDownloader\" est manquanto.",
|
||||||
"Password": "Le mot de passe est invalide.",
|
"Password": "Le mot de passe est invalide.",
|
||||||
"InvalidCredentials": "Identifiants de connexion invalides, connexion non approuvée, ou trop de tentatives de connexion.",
|
"InvalidCredentials": "Identifiants de connexion invalides, connexion non approuvée, ou trop de tentatives de connexion.",
|
||||||
"NoManifest": "Aucun manifeste n'a été trouvé.",
|
"NoManifest": "Aucun manifest n'a été trouvé.",
|
||||||
"DirectoryCreate": "Impossible d'installer les dossiers nécessaires.",
|
"DirectoryCreate": "Impossible d'installer les dossiers nécessaires.",
|
||||||
"NotAvailableApp": "Tu essayes de télécharger Beat Saber alors que tu ne l'as pas ? 🤣",
|
"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éessayez plus tard 😕",
|
"DepotNotFound": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||||
"NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯",
|
"NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯",
|
||||||
"InvalidManifest": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
"InvalidManifest": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||||
"NoValidKey": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
"NoValidKey": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||||
"NoManifestCode": "Impossible de télécharger Beat Saber 😥 réessayez plus tard 😕",
|
"NoManifestCode": "Impossible de télécharger Beat Saber 😥 réessaye plus tard 😕",
|
||||||
"Unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯",
|
"Unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯",
|
||||||
"NoServer": "Impossible de contacter les serveurs de Steam.",
|
"NoServer": "Impossible de contacter les serveurs de Steam.",
|
||||||
"NotAllowed": "Apparemment tu n'es pas autorisé à télécharger Beat Saber 🥱",
|
"NotAllowed": "Apparemment tu n'es pas autorisé à télécharger Beat Saber 🥱",
|
||||||
"ConnectionTimeout": "Impossible de se connecter à Steam 😕",
|
"ConnectionTimeout": "Impossible de se connecter à Steam 😕",
|
||||||
"SteamLib": "Si tu vois cette erreur, signale le bug sur GitHub avec les logs de BSManager.",
|
"SteamLib": "Si tu as cette erreur, signale le bug sur GitHub avec les logs stp.",
|
||||||
"ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯",
|
"ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯",
|
||||||
"LicenceError": "Impossible d'obtenir la liste des licences.",
|
"LicenceError": "Impossible d'obtenir la liste des licences.",
|
||||||
"RateLimitExceeded": "Tu as essayé trop de fois, attends un peu et réessaye plus tard.",
|
"RateLimitExceeded": "Tu as essayé trop de fois attends un peu et recommence plus tard.",
|
||||||
"TokenRejected": "Votre token de connexion a été rejeté 😕 Veuillez réessayer.",
|
"TokenRejected": "Votre token de connexion a été rejeté 😕 Veuillez réessayer.",
|
||||||
"AccessDenied": "L'accès à Steam a été refusé."
|
"AccessDenied": "L'accès à Steam a été refusé."
|
||||||
}
|
}
|
||||||
@@ -426,14 +415,14 @@
|
|||||||
"oculus-download": {
|
"oculus-download": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"msg": {
|
"msg": {
|
||||||
"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.",
|
"DOWNLOAD_MANIFEST_FAILED": "Impossible de télécharger le manifest 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.",
|
"MANIFEST_FILE_NOT_FOUND": "Impossible de trouver le manifest de cette version.",
|
||||||
"PARSE_MANIFEST_FILE_FAILED": "Une erreur s'est produite lors de la lecture du manifeste.",
|
"PARSE_MANIFEST_FILE_FAILED": "Une erreur c'est produite lors de la lecture du manifest.",
|
||||||
"ALREADY_DOWNLOADING": "Une version est déjà en cours de téléchargement.",
|
"ALREADY_DOWNLOADING": "Une version est déjà en cours de téléchargement.",
|
||||||
"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.",
|
"UNABLE_TO_GET_MANIFEST": "Impossible d'obtenir le manifest 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.",
|
"VERIFY_INTEGRITY_FAILED": "Une erreur c'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.",
|
"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.",
|
"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.",
|
"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."
|
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite."
|
||||||
@@ -456,7 +445,7 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"import-error": {
|
"import-error": {
|
||||||
"desc": "Vérifie que le dossier sélectionné est une installation de Beat Saber."
|
"desc": "Vérifier que le dossier sélectionné est une installation de Beat Saber."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -510,7 +499,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"check-all-enabled": {
|
"check-all-enabled": {
|
||||||
"title": "OneClick désactivé",
|
"title": "OneClick désactivée(s)",
|
||||||
"description": "Une ou plusieurs installations OneClick sont désactivées. Rendez-vous dans les paramètres pour les activer.",
|
"description": "Une ou plusieurs installations OneClick sont désactivées. Rendez-vous dans les paramètres pour les activer.",
|
||||||
"actions": {
|
"actions": {
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
@@ -525,12 +514,12 @@
|
|||||||
"titles": {
|
"titles": {
|
||||||
"BS_LAUNCHING": "Lancement...🚀",
|
"BS_LAUNCHING": "Lancement...🚀",
|
||||||
"STEAM_LAUNCHING": "Steam se lance !",
|
"STEAM_LAUNCHING": "Steam se lance !",
|
||||||
"SKIPPING_STEAM_LAUNCH": "Contourner le lancement de Steam"
|
"SKIPPING_STEAM_LAUNCH": "Sauter le lancement de Steam"
|
||||||
},
|
},
|
||||||
"msg": {
|
"msg": {
|
||||||
"BS_LAUNCHING": "N'oublie pas de t'échauffer 😉",
|
"BS_LAUNCHING": "N'oublie pas de t'échauffer 😉",
|
||||||
"STEAM_LAUNCHING": "Beat Saber se lancera automatiquement après Steam.",
|
"STEAM_LAUNCHING": "Beat Saber se lancera automatiquement après Steam.",
|
||||||
"SKIPPING_STEAM_LAUNCH": "J'espère que tu sais ce que tu fais :)"
|
"SKIPPING_STEAM_LAUNCH": "J'espère que vous savez ce que vous faites :)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -579,7 +568,7 @@
|
|||||||
"UnknownError": "Une erreur inconnue s'est produite"
|
"UnknownError": "Une erreur inconnue s'est produite"
|
||||||
},
|
},
|
||||||
"msg": {
|
"msg": {
|
||||||
"CantEditSteam": "Vous ne pouvez pas modifier la version Steam, vous pouvez cependant la cloner."
|
"CantEditSteam": "Tu ne peux pas modifier la version Steam, cependant tu peux la cloner."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
@@ -643,8 +632,8 @@
|
|||||||
"error": "Une erreur s'est produite lors de l'installation de la map"
|
"error": "Une erreur s'est produite lors de l'installation de la map"
|
||||||
},
|
},
|
||||||
"no-duplicates-maps": {
|
"no-duplicates-maps": {
|
||||||
"title": "Aucun doublon",
|
"title": "Pas de doublons",
|
||||||
"msg": "Aucune map n'a été supprimée"
|
"msg": "Aucune carte n'a été supprimée"
|
||||||
},
|
},
|
||||||
"duplicates-maps-deleted": {
|
"duplicates-maps-deleted": {
|
||||||
"title": "Doublons supprimés",
|
"title": "Doublons supprimés",
|
||||||
@@ -680,7 +669,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"userdata-backup-created": {
|
"userdata-backup-created": {
|
||||||
"title": "Sauvegarde créée",
|
"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": {
|
"linking-error": {
|
||||||
@@ -712,7 +701,7 @@
|
|||||||
"title": "Version obsolète",
|
"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.",
|
"msg": "Cette version de Beat Saber est obsolète. Utilisez la version recommandée pour profiter des dernières fonctionnalités et correctifs.",
|
||||||
"actions": {
|
"actions": {
|
||||||
"do-not-remind": "Ne plus me le rappeler",
|
"do-not-remind": "Ne plus me rappeler",
|
||||||
"ok": "Ok"
|
"ok": "Ok"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -765,7 +754,7 @@
|
|||||||
"note": {
|
"note": {
|
||||||
"use-the": "Utilisez l'",
|
"use-the": "Utilisez l'",
|
||||||
"steam-mobile-app": "application mobile Steam",
|
"steam-mobile-app": "application mobile Steam",
|
||||||
"to-connect-with-qr": "pour vous connecter avec un code QR."
|
"to-connect-with-qr": "pour vous connecter avec un QR code."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stay": "Se souvenir de moi"
|
"stay": "Se souvenir de moi"
|
||||||
@@ -785,20 +774,20 @@
|
|||||||
"steam-credentials": {
|
"steam-credentials": {
|
||||||
"title": "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-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\", 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)."
|
"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)."
|
||||||
},
|
},
|
||||||
"bs-import-version": {
|
"bs-import-version": {
|
||||||
"title": "Importer une 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.",
|
"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": "Version Oculus",
|
||||||
"oculus-version-tooltip": "Cochez si l'installation provient du magasin Oculus",
|
"oculus-version-tooltip": "Cocher si c'est une version Oculus",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"submit": "Importer une version"
|
"submit": "Importer une version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bs-uninstall": {
|
"bs-uninstall": {
|
||||||
"title": "Désinstaller",
|
"title": "Désinstaller",
|
||||||
"description": "Êtes-vous sûr de vouloir désinstaller Beat Saber {version} ? Vous allez devoir la retélécharger pour y jouer.",
|
"description": "Es-tu sûr de vouloir désinstaller Beat Saber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"submit": "Désinstaller"
|
"submit": "Désinstaller"
|
||||||
}
|
}
|
||||||
@@ -834,12 +823,12 @@
|
|||||||
},
|
},
|
||||||
"uninstall-mod": {
|
"uninstall-mod": {
|
||||||
"title": "Désinstaller",
|
"title": "Désinstaller",
|
||||||
"description": "Êtes-vous sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
|
"description": "Es-tu 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."
|
"description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
|
||||||
},
|
},
|
||||||
"uninstall-all-mods": {
|
"uninstall-all-mods": {
|
||||||
"title": "Désinstaller les mods",
|
"title": "Désinstaller les mods",
|
||||||
"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."
|
"description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
|
||||||
},
|
},
|
||||||
"maps-actions": {
|
"maps-actions": {
|
||||||
"delete-maps": {
|
"delete-maps": {
|
||||||
@@ -848,8 +837,8 @@
|
|||||||
"multiple": "Supprimer les maps ?"
|
"multiple": "Supprimer les maps ?"
|
||||||
},
|
},
|
||||||
"desc": {
|
"desc": {
|
||||||
"single": "Êtes-vous sûr de vouloir supprimer la map {name} ?",
|
"single": "Es-tu sur de vouloir supprimer la map {name} ?",
|
||||||
"multiple": "Êtes-vous sûr de vouloir supprimer {nb} maps ?"
|
"multiple": "Es-tu sur de vouloir supprimer {nb} maps ?"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"desc": {
|
"desc": {
|
||||||
@@ -864,8 +853,8 @@
|
|||||||
},
|
},
|
||||||
"delete-duplicate-maps": {
|
"delete-duplicate-maps": {
|
||||||
"title": "Supprimer les maps ?",
|
"title": "Supprimer les maps ?",
|
||||||
"desc": "Seule la map \"{map}\" est un doublon. Êtes-vous sûr de vouloir la supprimer ?",
|
"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. Êtes-vous sûr de vouloir les supprimer ?"
|
"desc-plural": "{nb} maps en double ont été trouvées. Es-tu sûr de vouloir les supprimer ?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"link-contents": {
|
"link-contents": {
|
||||||
@@ -889,7 +878,7 @@
|
|||||||
"search-btn": "Rechercher",
|
"search-btn": "Rechercher",
|
||||||
"loading-maps": "Chargement des maps...",
|
"loading-maps": "Chargement des maps...",
|
||||||
"no-maps-found": "Aucune map trouvée",
|
"no-maps-found": "Aucune map trouvée",
|
||||||
"no-internet": "Pas d'accès Internet"
|
"no-internet": "Pas d'internet"
|
||||||
},
|
},
|
||||||
"mods-disclaimer": {
|
"mods-disclaimer": {
|
||||||
"title": "avis de non-responsabilité",
|
"title": "avis de non-responsabilité",
|
||||||
@@ -952,7 +941,7 @@
|
|||||||
"token-is-invalid": "Le Token est invalide.",
|
"token-is-invalid": "Le Token est invalide.",
|
||||||
"save-my-token": "Sauvegarder mon token",
|
"save-my-token": "Sauvegarder mon token",
|
||||||
"have-token-saved": "J'ai déjà un token sauvegardé",
|
"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 chiffrer votre token à des fins de stockage. Si jamais vous oubliez votre mot de passe, il vous suffira 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 crypter votre token à des fins de stockage. Si jamais vous oubliez votre mot de passe, il vous suffit de fournir à nouveau votre token.",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"password-too-short": "Mot de passe trop court",
|
"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.",
|
"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.",
|
||||||
@@ -973,7 +962,7 @@
|
|||||||
},
|
},
|
||||||
"ask-install-path": {
|
"ask-install-path": {
|
||||||
"title": "Dossier d'installation",
|
"title": "Dossier d'installation",
|
||||||
"choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, maps, playlists, etc.)",
|
"choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, cartes, playlists, etc.)",
|
||||||
"default": "Par défaut",
|
"default": "Par défaut",
|
||||||
"default-tooltip": "Par défaut, dans votre dossier personnel"
|
"default-tooltip": "Par défaut, dans votre dossier personnel"
|
||||||
},
|
},
|
||||||
@@ -998,13 +987,13 @@
|
|||||||
"maps": {
|
"maps": {
|
||||||
"map-filter-panel": {
|
"map-filter-panel": {
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"nps": "Notes par seconde",
|
"nps": "Notes Par Seconde",
|
||||||
"njs": "Vitesse de saut des notes",
|
"njs": "Vitesse de saut des notes",
|
||||||
"tags": "tags",
|
"tags": "tags",
|
||||||
"specificities": "général",
|
"specificities": "général",
|
||||||
"requirements": "requis",
|
"requirements": "requis",
|
||||||
"exclude": "exclure",
|
"exclude": "exclure",
|
||||||
"leaderboard": "classement"
|
"leaderboard": "tableau des leaders"
|
||||||
},
|
},
|
||||||
"map-types": {
|
"map-types": {
|
||||||
"accuracy": "précision",
|
"accuracy": "précision",
|
||||||
@@ -1057,11 +1046,11 @@
|
|||||||
"automapper": "IA",
|
"automapper": "IA",
|
||||||
"curated": "recommandée",
|
"curated": "recommandée",
|
||||||
"verified": "vérifiée",
|
"verified": "vérifiée",
|
||||||
"fullSpread": "toutes difficultés"
|
"fullSpread": "panel complet"
|
||||||
},
|
},
|
||||||
"map-leaderboard": {
|
"map-leaderboard": {
|
||||||
"All": "Tout",
|
"All": "Tout",
|
||||||
"Ranked": "Classé",
|
"Ranked": "Classée",
|
||||||
"BeatLeader": "BeatLeader",
|
"BeatLeader": "BeatLeader",
|
||||||
"ScoreSaber": "ScoreSaber"
|
"ScoreSaber": "ScoreSaber"
|
||||||
},
|
},
|
||||||
@@ -1079,10 +1068,10 @@
|
|||||||
"by": "Par {songAutor}",
|
"by": "Par {songAutor}",
|
||||||
"mapped-by": "mappée par",
|
"mapped-by": "mappée par",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"preview": "Aperçu de la map",
|
"preview": "Aperçu de la carte",
|
||||||
"bsr-code": "Code BSR",
|
"bsr-code": "Code BSR",
|
||||||
"download": "Télécharger la map",
|
"download": "Télécharger la carte",
|
||||||
"downloading": "Téléchargement de la map",
|
"downloading": "Téléchargement de la carte",
|
||||||
"cancel-download": "Annuler le téléchargement",
|
"cancel-download": "Annuler le téléchargement",
|
||||||
"hightlight-difficulty": "Surligner la difficulté"
|
"hightlight-difficulty": "Surligner la difficulté"
|
||||||
}
|
}
|
||||||
@@ -1126,12 +1115,12 @@
|
|||||||
"modals": {
|
"modals": {
|
||||||
"delete-model": {
|
"delete-model": {
|
||||||
"title": "Supprimer le modèle",
|
"title": "Supprimer le modèle",
|
||||||
"desc": "Êtes-vous sûr de vouloir supprimer le modèle {modelName} ?",
|
"desc": "Es-tu sûr de vouloir supprimer le modèle {modelName} ?",
|
||||||
"linked-annotation": "Ce modèle sera supprimé de toutes les versions liées."
|
"linked-annotation": "Ce modèle sera supprimé de toutes les versions liées."
|
||||||
},
|
},
|
||||||
"delete-models": {
|
"delete-models": {
|
||||||
"title": "Supprimer les modèles",
|
"title": "Supprimer les modèles",
|
||||||
"desc": "Êtes-vous sûr de vouloir supprimer {nb} modèles ?",
|
"desc": "Es-tu sûr de vouloir supprimer {nb} modèles ?",
|
||||||
"linked-annotation": "Ces modèles seront supprimés de toutes les versions liées."
|
"linked-annotation": "Ces modèles seront supprimés de toutes les versions liées."
|
||||||
},
|
},
|
||||||
"download-models": {
|
"download-models": {
|
||||||
@@ -1147,11 +1136,11 @@
|
|||||||
"tag-desc": "Afficher seulement les modèles avec le tag spécifié.",
|
"tag-desc": "Afficher seulement les modèles avec le tag spécifié.",
|
||||||
"name-desc": "Afficher seulement les modèles avec le nom 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é.",
|
"discordid-desc": "Afficher seulement les modèles de l'utilisateur discord spécifié.",
|
||||||
"status-desc": "Afficher seulement les modèles avec le statut spécifié. (profil seulement, et seulement pour l'auteur)"
|
"status-desc": "Afficher seulement les modèles avec le status spécifié. (profil seulement, et seulement pour l'auteur)"
|
||||||
},
|
},
|
||||||
"no-models": "Aucun modèle trouvé.",
|
"no-models": "Aucun modèles trouvés.",
|
||||||
"no-internet": "Pas de connexion internet.",
|
"no-internet": "Pas de connexion internet.",
|
||||||
"error-occured": "Une erreur est survenue, réessayez plus tard."
|
"error-occured": "Une erreur est survenue, réessaye plus tard."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
@@ -1162,16 +1151,16 @@
|
|||||||
"not-remind": "Ne plus me rappeler"
|
"not-remind": "Ne plus me rappeler"
|
||||||
},
|
},
|
||||||
"export-success": {
|
"export-success": {
|
||||||
"title": "Exportation terminée 🎉"
|
"title": "Export terminé 🎉"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"beat-saver": {
|
"beat-saver": {
|
||||||
"maps-sorts": {
|
"maps-sorts": {
|
||||||
"Latest": "Récent",
|
"Latest": "Dernière",
|
||||||
"Relevance": "Pertinence",
|
"Relevance": "Pertinence",
|
||||||
"Rating": "Évaluation",
|
"Rating": "Notes",
|
||||||
"Curated": "Recommandé"
|
"Curated": "Recommandée"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auto-update": {
|
"auto-update": {
|
||||||
@@ -1199,7 +1188,7 @@
|
|||||||
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
|
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
|
||||||
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
|
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
|
||||||
"playlist-created-title": "Playlist créée",
|
"playlist-created-title": "Playlist créée",
|
||||||
"playlist-created-desc": "La playlist a été créée avec succès. Vous pouvez maintenant synchroniser ses maps !",
|
"playlist-created-desc": "La playlist a été créée avec succès. Tu peut maintenant synchroniser ses maps !",
|
||||||
"download-playlist": "Télécharger la playlist",
|
"download-playlist": "Télécharger la playlist",
|
||||||
"synchronize-playlist": "Synchroniser la playlist",
|
"synchronize-playlist": "Synchroniser la playlist",
|
||||||
"synchronize-maps": "Synchroniser les maps",
|
"synchronize-maps": "Synchroniser les maps",
|
||||||
@@ -1220,8 +1209,8 @@
|
|||||||
"playlist-edit-error-title": "Erreur lors de l'édition de la playlist",
|
"playlist-edit-error-title": "Erreur lors de l'édition de la playlist",
|
||||||
"playlist-edit-error-desc": "Une erreur est survenue lors de l'édition de la playlist.",
|
"playlist-edit-error-desc": "Une erreur est survenue lors de l'édition de la playlist.",
|
||||||
"playlist-edited-title": "Playlist éditée !",
|
"playlist-edited-title": "Playlist éditée !",
|
||||||
"playlist-edited-desc": "La playlist a été modifiée avec succès. Vous pouvez maintenant synchroniser ses maps !",
|
"playlist-edited-desc": "La playlist a été modifiée avec succès. Tu peut maintenant synchroniser ses maps !",
|
||||||
"playlists-loading": "Chargement des playlists...",
|
"playlists-loading": "Chargement des playlist...",
|
||||||
"no-playlists": "Aucune playlist",
|
"no-playlists": "Aucune playlist",
|
||||||
"download-playlists": "Télécharger des playlists",
|
"download-playlists": "Télécharger des playlists",
|
||||||
"created-by": "Créée par",
|
"created-by": "Créée par",
|
||||||
@@ -1230,15 +1219,15 @@
|
|||||||
"open-file": "Ouvrir le fichier",
|
"open-file": "Ouvrir le fichier",
|
||||||
"delete-playlist-ask": "Supprimer la playlist ?",
|
"delete-playlist-ask": "Supprimer la playlist ?",
|
||||||
"delete-playlists-ask": "Supprimer les playlists ?",
|
"delete-playlists-ask": "Supprimer les playlists ?",
|
||||||
"delete-playlist-desc": "Êtes-vous sûr de vouloir supprimer la playlist \"{playlistTitle}\" ?",
|
"delete-playlist-desc": "Es-tu sur de vouloir supprimer la playlist \"{playlistTitle}\" ?",
|
||||||
"delete-playlists-desc": "Êtes-vous sûr de vouloir supprimer {nb} playlists ?",
|
"delete-playlists-desc": "Es-tu sur de vouloir supprimer {nb} playlists ?",
|
||||||
"delete-maps": "Supprimer les maps",
|
"delete-maps": "Supprimer les maps",
|
||||||
"delete-playlist-maps-tip": "Si activé, toutes les maps de la playlist seront supprimées",
|
"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",
|
"delete-playlists-maps-tip": "Si activé, toutes les maps des playlists seront supprimées",
|
||||||
"export-playlist-ask": "Exporter la playlist ?",
|
"export-playlist-ask": "Exporter la playlist ?",
|
||||||
"export-playlists-ask": "Exporter les playlists ?",
|
"export-playlists-ask": "Exporter les playlists ?",
|
||||||
"export-playlist-desc": "Êtes-vous sûr de vouloir exporter la playlist \"{playlistTitle}\" ?",
|
"export-playlist-desc": "Es-tu sur de vouloir exporter la playlist \"{playlistTitle}\" ?",
|
||||||
"export-playlists-desc": "Êtes-vous sûr de vouloir exporter {nb} playlists ?",
|
"export-playlists-desc": "Es-tu sur de vouloir exporter les {nb} playlists ?",
|
||||||
"export-maps": "Exporter les maps",
|
"export-maps": "Exporter les maps",
|
||||||
"export-playlist-maps-tip": "Si activé, toutes les maps de la playlist seront également exportées",
|
"export-playlist-maps-tip": "Si activé, toutes les maps de la playlist seront également exportées",
|
||||||
"export-playlists-maps-tip": "Si activé, toutes les maps des playlists seront également exportées",
|
"export-playlists-maps-tip": "Si activé, toutes les maps des playlists seront également exportées",
|
||||||
@@ -1250,15 +1239,15 @@
|
|||||||
"understood": "J'ai compris",
|
"understood": "J'ai compris",
|
||||||
"synchronize-playlist-ask": "Synchroniser la playlist ?",
|
"synchronize-playlist-ask": "Synchroniser la playlist ?",
|
||||||
"synchronize-playlists-ask": "Synchroniser les playlists ?",
|
"synchronize-playlists-ask": "Synchroniser les playlists ?",
|
||||||
"synchronize-playlist-desc": "Êtes-vous sûr de vouloir synchroniser la playlist \"{playlistTitle}\" ?",
|
"synchronize-playlist-desc": "Es-tu sur de vouloir synchroniser la playlist \"{playlistTitle}\" ?",
|
||||||
"synchronize-playlists-desc": "Êtes-vous sûr de vouloir synchroniser {nb} playlists ?",
|
"synchronize-playlists-desc": "Es-tu sur de vouloir synchroniser les {nb} playlists ?",
|
||||||
"synchronize-playlist-tip": "Cette action met à jour les playlists et télécharge les maps manquantes; cela peut durer plusieurs minutes.",
|
"synchronize-playlist-tip": "Cette action met à jour les playlists et télécharge les maps manquantes; cela peut durer plusieurs minutes.",
|
||||||
"synchronize": "Synchroniser",
|
"synchronize": "Synchroniser",
|
||||||
"curated": "Recommandée",
|
"curated": "Recommandée",
|
||||||
"verified-mapper": "Mapper vérifié",
|
"verified-mapper": "Mapper vérifié",
|
||||||
"empty-playlists": "Playlists vides",
|
"empty-playlists": "Playlists vides",
|
||||||
"search-playlist": "Rechercher une playlist",
|
"search-playlist": "Rechercher une playlist",
|
||||||
"no-playlists-found": "Aucune playlist trouvée",
|
"no-playlists-found": "Aucune playlists trouvées",
|
||||||
"error-occur-while-loading-playlists": "Une erreur est survenue lors du chargement des playlists",
|
"error-occur-while-loading-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",
|
"error-occur-while-loading-playlist": "Une erreur est survenue lors du chargement de la playlist",
|
||||||
"loading-maps": "Chargement des maps...",
|
"loading-maps": "Chargement des maps...",
|
||||||
@@ -1285,7 +1274,7 @@
|
|||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"installed": "Installée",
|
"installed": "Installée",
|
||||||
"no-map-found": "Aucune map trouvée",
|
"no-map-found": "Aucune map trouvée",
|
||||||
"edit-playlist-shortcuts": "Maintenir Maj ou Ctrl pour sélectionner plusieurs maps",
|
"edit-playlist-shortcuts": "Maintenez Maj ou Ctrl pour sélectionner plusieurs maps",
|
||||||
"add-to-playlist": "Ajouter à la playlist",
|
"add-to-playlist": "Ajouter à la playlist",
|
||||||
"remove-from-playlist": "Retirer de la playlist",
|
"remove-from-playlist": "Retirer de la playlist",
|
||||||
"playlist-is-empty": "La playlist est vide",
|
"playlist-is-empty": "La playlist est vide",
|
||||||
@@ -1295,7 +1284,7 @@
|
|||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"nps": "Notes par secondes",
|
"nps": "Notes par secondes",
|
||||||
"date-picker": {
|
"date-picker": {
|
||||||
"start-date-end-date": "Date de début — Date de fin",
|
"start-date-end-date": "Date début — Date fin",
|
||||||
"all": "Tout",
|
"all": "Tout",
|
||||||
"last-24h": "Dernières 24h",
|
"last-24h": "Dernières 24h",
|
||||||
"last-week": "Dernière semaine",
|
"last-week": "Dernière semaine",
|
||||||
@@ -1306,18 +1295,18 @@
|
|||||||
"all-playlists-have-been-successfully-imported": "Toutes les playlists ont été importées avec succès",
|
"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": "Aucune playlist trouvée",
|
||||||
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
|
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
|
||||||
"some-playlists-not-imported": "Certaines playlists n'ont pas été importées",
|
"some-playlists-not-imported": "Certaines playlists non importées",
|
||||||
"some-playlists-have-been-imported": {
|
"some-playlists-have-been-imported": {
|
||||||
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
|
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
|
||||||
"INVALID_PLAYLIST_FILE": "Certaines playlists sont invalides",
|
"INVALID_PLAYLIST_FILE": "Certaines playlists ne sont pas valides",
|
||||||
"CANNOT_PARSE_PLAYLIST": "Certaines playlists sont illisibles",
|
"CANNOT_PARSE_PLAYLIST": "Certaines playlists ne sont pas lisibles",
|
||||||
"unknown": "Certaines playlists n'ont pas pu être importées"
|
"unknown": "Certaines playlists n'ont pas pu être importées"
|
||||||
},
|
},
|
||||||
"no-playlists-imported": "Aucune playlist importée",
|
"no-playlists-imported": "Aucune playlist importée",
|
||||||
"no-playlists-imported-errors": {
|
"no-playlists-imported-errors": {
|
||||||
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
|
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
|
||||||
"INVALID_PLAYLIST_FILE": "Les playlists sont invalides",
|
"INVALID_PLAYLIST_FILE": "Les playlists ne sont pas valides",
|
||||||
"CANNOT_PARSE_PLAYLIST": "Les playlists sont illisibles",
|
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
|
||||||
"unknown": "Aucune playlist n'a pu être importée"
|
"unknown": "Aucune playlist n'a pu être importée"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"copied": "Copiato!",
|
"copied": "Copiato!",
|
||||||
"confirm": "Conferma",
|
"confirm": "Conferma",
|
||||||
"choose-folder": "Scegli cartella",
|
"choose-folder": "Scegli cartella",
|
||||||
"unknown": "Si è verificato un errore sconosciuto ¯\\_(ツ)_/¯",
|
"unknown": "È accaduto un errore sconosciuto ¯\\_(ツ)_/¯",
|
||||||
"warning": "Attenzione",
|
"warning": "Attenzione",
|
||||||
"continue": "Continua",
|
"continue": "Continua",
|
||||||
"shared": "Condivisi",
|
"shared": "Condivisi",
|
||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "obsoleta",
|
"outdated": "obsoleta"
|
||||||
"update-text": "BSManager {version} è disponibile!",
|
|
||||||
"update-button": "Aggiorna e riavvia",
|
|
||||||
"see-changelog": "Vedi i cambiamenti"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Aggiungi una versione",
|
"add-version": "Aggiungi una versione",
|
||||||
@@ -55,9 +52,9 @@
|
|||||||
"launch-mods": {
|
"launch-mods": {
|
||||||
"oculus": "Modalita Oculus",
|
"oculus": "Modalita Oculus",
|
||||||
"oculus-description": "Se stai usando Beat Saber via Steam, questa opzione ti consente di usare il composer VR di Oculus mentre bypassi SteamVR, per un potenziale aumento di performance. Questo non è richiesto per usare i visori Oculus.",
|
"oculus-description": "Se stai usando Beat Saber via Steam, questa opzione ti consente di usare il composer VR di Oculus mentre bypassi SteamVR, per un potenziale aumento di performance. Questo non è richiesto per usare i visori Oculus.",
|
||||||
"desktop": "Modalità FPFC",
|
"desktop": "Modalita FPFC",
|
||||||
"desktop-description": "La modalità FPFC (First Person Flying Controller) consente di usare i pulsanti WASD e il mouse per navigare nel menu del gioco. Questo rende i test molto più facili, perchè non devi indossare il tuo visore!",
|
"desktop-description": "La modalità FPFC (First Person Flying Controller) consente di usare i pulsanti WASD e il mouse per navigare nel menu del gioco. Questo rende i test molto più facili, perchè non devi indossare il tuo visore!",
|
||||||
"debug": "Modalità Debug",
|
"debug": "Modalita Debug",
|
||||||
"debug-description": "Abilita la finestra di uscita del log per IPA. Questo mostrerà la console di debug che lo mod usano.",
|
"debug-description": "Abilita la finestra di uscita del log per IPA. Questo mostrerà la console di debug che lo mod usano.",
|
||||||
"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.",
|
"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": {
|
"advanced-launch": {
|
||||||
@@ -66,7 +63,7 @@
|
|||||||
"create-launch-option": "Crea un'opzione di lancio"
|
"create-launch-option": "Crea un'opzione di lancio"
|
||||||
},
|
},
|
||||||
"skipsteam": "Salta Steam",
|
"skipsteam": "Salta Steam",
|
||||||
"skipsteam-description": "Impedisce a Steam di aprirsi automaticamente con Beat Saber, abilitalo se stai usando un VR runtime differente come WiVRn o Monado con cui SteamVR potrebbe interferire.",
|
"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.",
|
||||||
"map-editor": "Map Editor",
|
"map-editor": "Map Editor",
|
||||||
"map-editor-description": "Avvia il map editor di Beat Saber ufficiale invece del gioco.",
|
"map-editor-description": "Avvia il map editor di Beat Saber ufficiale invece del gioco.",
|
||||||
"proton-logs": "Log di Proton",
|
"proton-logs": "Log di Proton",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Durata",
|
"duration": "Durata",
|
||||||
"likes": "Mi piace",
|
"likes": "Mi piace",
|
||||||
"date-uploaded": "Data di caricamento",
|
"date-uploaded": "Data di caricamento"
|
||||||
"added-date": "Data di aggiunta"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -142,7 +138,7 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"no-wineprefix": "Impossibile trovare il percorso del WINEPREFIX di BSManager. Per favore avvia prima Beat Saber in BSManager.",
|
"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}.",
|
"beatmods-down": "Beatmods non è attualmente raggiungibile. Riprova più tardi. Se il problema persiste, informaci su {links}.",
|
||||||
"unknown": "Si è verificato un errore sconosciuto ¯\\_(ツ)_/¯"
|
"unknown": "È accaduto un errore sconosciuto ¯\\_(ツ)_/¯"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"more-infos": "Più info",
|
"more-infos": "Più info",
|
||||||
@@ -163,8 +159,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mod-item": {
|
"mod-item": {
|
||||||
"this-is-a-large-mod": "Questa è una mod grande!",
|
"this-is-a-large-mod": "Questo è un mod grande!",
|
||||||
"this-is-a-very-large-mod": "Questa è una mod molto grande!"
|
"this-is-a-very-large-mod": "Questo è un mod molto grande!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
@@ -174,9 +170,9 @@
|
|||||||
},
|
},
|
||||||
"outdated-mods": {
|
"outdated-mods": {
|
||||||
"title": "Mod obsoleto",
|
"title": "Mod obsoleto",
|
||||||
"title-plural": "Mod obsolete",
|
"title-plural": "Mod obsoleti",
|
||||||
"description": "La mod {name} è obsoleta. Vuoi aggiornarla?",
|
"description": "Il mod {name} è obsoleto. Vuoi aggiornarlo?",
|
||||||
"description-plural": "Questa versione ha {nb} mod obsolete. Vuoi aggiornarle?",
|
"description-plural": "Questa versione ha {nb} mod obsoleti. Vuoi aggiornarli?",
|
||||||
"dont-remind-me": "Non ricordarmelo",
|
"dont-remind-me": "Non ricordarmelo",
|
||||||
"update": "Aggiorna"
|
"update": "Aggiorna"
|
||||||
}
|
}
|
||||||
@@ -317,7 +313,7 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"title": "Riavvio Necessario",
|
"title": "Riavvio Necessario",
|
||||||
"body": "Cambiare l'impostazione dell'accelerazione hardware causerà un riavvio di BSManager. Sei sicuro di voler procedere?",
|
"body": "Cambiare l'impostazione dell'accelerazione hardware causerà un riavvio di BSManager. Sei sicuro di voler procedere?",
|
||||||
"confirm-btn": "Sì, sono sicuro"
|
"confirm-btn": "Si, sono sicuro"
|
||||||
},
|
},
|
||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "C'è stato un errore, impossibile disattivare l'accelerazione hardware."
|
"message": "C'è stato un errore, impossibile disattivare l'accelerazione hardware."
|
||||||
@@ -329,7 +325,7 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"title": "Permesso Symlink",
|
"title": "Permesso Symlink",
|
||||||
"body": "Quando crei dei Symlink, BSManager richiederà accesso amministrativo o la modalità sviluppatore sul tuo sistema. Sei sicuro di voler continuare?",
|
"body": "Quando crei dei Symlink, BSManager richiederà accesso amministrativo o la modalità sviluppatore sul tuo sistema. Sei sicuro di voler continuare?",
|
||||||
"confirm-btn": "Sì, sono sicuro"
|
"confirm-btn": "Si, sono sicuro"
|
||||||
},
|
},
|
||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "C'è stato un errore, impossibile cambiare le impostazioni di Symlinks."
|
"message": "C'è stato un errore, impossibile cambiare le impostazioni di Symlinks."
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "C'è stato un errore, impossibile cambiare le impostazioni del proxy di sistema."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,7 +360,7 @@
|
|||||||
"file-not-supported": "File non supportato"
|
"file-not-supported": "File non supportato"
|
||||||
},
|
},
|
||||||
"msg": {
|
"msg": {
|
||||||
"operation-running": "Attendi che l'operazione corrente finisca, poi riprova.",
|
"operation-running": "Attendi che l'operazione corrente finisca, poi, prova di nuovo.",
|
||||||
"no-internet": "Controlla la tua connessione e riprova.",
|
"no-internet": "Controlla la tua connessione e riprova.",
|
||||||
"file-not-supported": "Solo i file {types} sono supportati."
|
"file-not-supported": "Solo i file {types} sono supportati."
|
||||||
}
|
}
|
||||||
@@ -476,7 +465,7 @@
|
|||||||
"transfer-failed": "Trasferimento fallito 😕"
|
"transfer-failed": "Trasferimento fallito 😕"
|
||||||
},
|
},
|
||||||
"descs": {
|
"descs": {
|
||||||
"COPY_TO_SUBPATH": "La cartella di destinazione non puo essere una sottocartella della cartella di origine.",
|
"COPY_TO_SUBPATH": "La cartella di destinazione non puo essere una sotto cartella della cartella di origine.",
|
||||||
"restore-linked-folders": "C'è stato un errore durante il ripristino delle cartelle condivise. Puoi sempre ripristinarle manualmente via il menu 'Cartelle Condivise' nella pagina delle versioni."
|
"restore-linked-folders": "C'è stato un errore durante il ripristino delle cartelle condivise. Puoi sempre ripristinarle manualmente via il menu 'Cartelle Condivise' nella pagina delle versioni."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,7 +517,7 @@
|
|||||||
"SKIPPING_STEAM_LAUNCH": "Saltando l'avvio di Steam..."
|
"SKIPPING_STEAM_LAUNCH": "Saltando l'avvio di Steam..."
|
||||||
},
|
},
|
||||||
"msg": {
|
"msg": {
|
||||||
"BS_LAUNCHING": "Non scordarti di riscaldarti 😉",
|
"BS_LAUNCHING": "Non scordardi ti riscaldarti 😉",
|
||||||
"STEAM_LAUNCHING": "Beat Saber si avvierà automaticamente dopo Steam.",
|
"STEAM_LAUNCHING": "Beat Saber si avvierà automaticamente dopo Steam.",
|
||||||
"SKIPPING_STEAM_LAUNCH": "Spero che sai cosa stai facendo :)"
|
"SKIPPING_STEAM_LAUNCH": "Spero che sai cosa stai facendo :)"
|
||||||
}
|
}
|
||||||
@@ -758,7 +747,7 @@
|
|||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"placeholder": "Inserisci la password",
|
"placeholder": "Inserisci la password",
|
||||||
"max-length-warning": "La password eccede i 64 caratteri! Se la password è invalida, prova ad entrare con solo i primi 64 caratteri."
|
"max-length-warning": "La password eccede i 64 caratteri! Se la password è invalida, prova ad entrare con solo i primi 64 caretteri."
|
||||||
},
|
},
|
||||||
"qr": {
|
"qr": {
|
||||||
"label": "O con un codice QR",
|
"label": "O con un codice QR",
|
||||||
@@ -1324,7 +1313,7 @@
|
|||||||
},
|
},
|
||||||
"dateformat": {
|
"dateformat": {
|
||||||
"dayNames": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"],
|
"dayNames": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"],
|
||||||
"monthNames": ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"],
|
"monthNames": ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Augosto", "Settembre", "Ottobre", "Novembre", "Dicembre"],
|
||||||
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
|
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "時代遅れ",
|
"outdated": "時代遅れ"
|
||||||
"update-text": "BSManager {version} が利用可能です!",
|
|
||||||
"update-button": "更新して再起動",
|
|
||||||
"see-changelog": "変更点を確認"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "バージョンを追加",
|
"add-version": "バージョンを追加",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "持続時間",
|
"duration": "持続時間",
|
||||||
"likes": "いいね",
|
"likes": "いいね",
|
||||||
"date-uploaded": "アップロード日",
|
"date-uploaded": "アップロード日"
|
||||||
"added-date": "追加日"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "エラーが発生しました。システムプロキシ設定を変更できません。"
|
"message": "エラーが発生しました。システムプロキシ設定を変更できません。"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "自動更新",
|
|
||||||
"description": "アプリケーション起動時にBSManagerが自動的に更新されます。",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "エラーが発生し、自動更新の設定を変更できませんでした。"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "구식",
|
"outdated": "구식"
|
||||||
"update-text": "BSManager {version} 가 사용 가능합니다!",
|
|
||||||
"update-button": "업데이트 후 재시작",
|
|
||||||
"see-changelog": "변경 사항 보기"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "버전 추가",
|
"add-version": "버전 추가",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "지속 시간",
|
"duration": "지속 시간",
|
||||||
"likes": "좋아요",
|
"likes": "좋아요",
|
||||||
"date-uploaded": "업로드 날짜",
|
"date-uploaded": "업로드 날짜"
|
||||||
"added-date": "추가 날짜"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "오류가 발생했습니다. 시스템 프록시 설정을 변경할 수 없습니다."
|
"message": "오류가 발생했습니다. 시스템 프록시 설정을 변경할 수 없습니다."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "자동 업데이트",
|
|
||||||
"description": "애플리케이션을 실행할 때 BSManager가 자동으로 업데이트됩니다.",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "오류가 발생하여 자동 업데이트 설정을 변경할 수 없습니다."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"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": {
|
"nav-bar": {
|
||||||
"add-version": "Adicionar uma versão",
|
"add-version": "Adicionar uma versão",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Duração",
|
"duration": "Duração",
|
||||||
"likes": "Gostei",
|
"likes": "Gostei",
|
||||||
"date-uploaded": "Data de envio",
|
"date-uploaded": "Data de envio"
|
||||||
"added-date": "Data de adição"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Um erro aconteceu, não foi possível alterar a configuração de proxy de sistema."
|
"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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "устаревший",
|
"outdated": "устаревший"
|
||||||
"update-text": "BSManager {version} доступен!",
|
|
||||||
"update-button": "Обновить и перезапустить",
|
|
||||||
"see-changelog": "Смотреть изменения"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Добавить версию игры",
|
"add-version": "Добавить версию игры",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Длительность",
|
"duration": "Длительность",
|
||||||
"likes": "Лайки",
|
"likes": "Лайки",
|
||||||
"date-uploaded": "Дата загрузки",
|
"date-uploaded": "Дата загрузки"
|
||||||
"added-date": "Дата добавления"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Произошла ошибка, не удалось изменить настройки системного прокси."
|
"message": "Произошла ошибка, не удалось изменить настройки системного прокси."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "Автообновление",
|
|
||||||
"description": "BSManager будет автоматически обновляться при запуске приложения.",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "Произошла ошибка, не удалось изменить настройки автообновления."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "lipas na",
|
"outdated": "lipas na"
|
||||||
"update-text": "Ang BSManager {version} ay available!",
|
|
||||||
"update-button": "I-update at i-restart",
|
|
||||||
"see-changelog": "Tingnan ang mga pagbabago"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Magdagdag ng bersyon",
|
"add-version": "Magdagdag ng bersyon",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Haba",
|
"duration": "Haba",
|
||||||
"likes": "Likes",
|
"likes": "Likes",
|
||||||
"date-uploaded": "Petsa ng Pagka-upload",
|
"date-uploaded": "Petsa ng Pagka-upload"
|
||||||
"added-date": "Petsa ng Pagdaragdag"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Nagkaroon ng error, hindi ma-bago ang setting ng system proxy."
|
"message": "Nagkaroon ng error, hindi ma-bago ang setting ng system proxy."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "Awtomatikong Pag-update",
|
|
||||||
"description": "Ang BSManager ay awtomatikong mag-uupdate kapag binuksan mo ang aplikasyon.",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "Nagkaroon ng error, hindi mababago ang mga setting ng awtomatikong pag-update."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "застарілий",
|
"outdated": "застарілий"
|
||||||
"update-text": "BSManager {version} доступний!",
|
|
||||||
"update-button": "Оновити та перезапустити",
|
|
||||||
"see-changelog": "Переглянути зміни"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "Додати версію",
|
"add-version": "Додати версію",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "Довжина",
|
"duration": "Довжина",
|
||||||
"likes": "Лайки",
|
"likes": "Лайки",
|
||||||
"date-uploaded": "Дата завантаження",
|
"date-uploaded": "Дата завантаження"
|
||||||
"added-date": "Дата додавання"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "Сталася помилка, неможливо змінити налаштування системного проксі."
|
"message": "Сталася помилка, неможливо змінити налаштування системного проксі."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "Автоматичне оновлення (Avtomatychne onovlennya)",
|
|
||||||
"description": "BSManager автоматично оновлюватиметься, коли ви запустите програму.",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "Сталася помилка, неможливо змінити налаштування автоматичного оновлення."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "過時",
|
"outdated": "過時"
|
||||||
"update-text": "BSManager {version} 可用!",
|
|
||||||
"update-button": "更新並重新啟動",
|
|
||||||
"see-changelog": "查看變更"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "新增版本",
|
"add-version": "新增版本",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "持續時間",
|
"duration": "持續時間",
|
||||||
"likes": "喜歡",
|
"likes": "喜歡",
|
||||||
"date-uploaded": "上傳日期",
|
"date-uploaded": "上傳日期"
|
||||||
"added-date": "新增日期"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "發生錯誤,無法更改系統代理設置。"
|
"message": "發生錯誤,無法更改系統代理設置。"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "自動更新",
|
|
||||||
"description": "啟動應用程式時,BSManager 將自動更新。",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "發生錯誤,無法更改自動更新設定。"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title-bar": {
|
"title-bar": {
|
||||||
"outdated": "过时",
|
"outdated": "过时"
|
||||||
"update-text": "BSManager {version} 可用!",
|
|
||||||
"update-button": "更新并重新启动",
|
|
||||||
"see-changelog": "查看变更"
|
|
||||||
},
|
},
|
||||||
"nav-bar": {
|
"nav-bar": {
|
||||||
"add-version": "添加版本",
|
"add-version": "添加版本",
|
||||||
@@ -111,8 +108,7 @@
|
|||||||
"bpm": "BPM",
|
"bpm": "BPM",
|
||||||
"duration": "持续时间",
|
"duration": "持续时间",
|
||||||
"likes": "喜欢",
|
"likes": "喜欢",
|
||||||
"date-uploaded": "上传日期",
|
"date-uploaded": "上传日期"
|
||||||
"added-date": "添加日期"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlists": {
|
"playlists": {
|
||||||
@@ -341,13 +337,6 @@
|
|||||||
"error-notification": {
|
"error-notification": {
|
||||||
"message": "发生错误,无法启用系统代理。"
|
"message": "发生错误,无法启用系统代理。"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"auto-update": {
|
|
||||||
"title": "自动更新",
|
|
||||||
"description": "启动应用程序时,BSManager 将自动更新。",
|
|
||||||
"error-notification": {
|
|
||||||
"message": "发生错误,无法更改自动更新设置。"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,10 +15,7 @@ const config = {
|
|||||||
afterSign: ".erb/scripts/notarize.js",
|
afterSign: ".erb/scripts/notarize.js",
|
||||||
afterPack: ".erb/scripts/after-pack.js",
|
afterPack: ".erb/scripts/after-pack.js",
|
||||||
win: {
|
win: {
|
||||||
signtoolOptions: {
|
signingHashAlgorithms: ["sha256"],
|
||||||
signingHashAlgorithms: ["sha256"],
|
|
||||||
certificateSha1: "d55f8cda15bd9cba76ea796b9504860b16c7f46e",
|
|
||||||
},
|
|
||||||
target: [
|
target: [
|
||||||
"nsis",
|
"nsis",
|
||||||
"nsis-web"
|
"nsis-web"
|
||||||
|
|||||||
+1
-2
@@ -12,9 +12,8 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
|
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
|
||||||
moduleDirectories: ["node_modules", "src"],
|
moduleDirectories: ["node_modules", "src"],
|
||||||
testPathIgnorePatterns: ["<rootDir>/release/app"],
|
testPathIgnorePatterns: ["release/app/dist"],
|
||||||
setupFiles: ["./.erb/scripts/check-build-exists.ts"],
|
setupFiles: ["./.erb/scripts/check-build-exists.ts"],
|
||||||
modulePathIgnorePatterns: ["<rootDir>/release/app"]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Generated
+793
-1574
File diff suppressed because it is too large
Load Diff
+20
-23
@@ -2,29 +2,27 @@
|
|||||||
"name": "bs-manager",
|
"name": "bs-manager",
|
||||||
"description": "Manage maps, mods and more for Beat Saber",
|
"description": "Manage maps, mods and more for Beat Saber",
|
||||||
"main": "./.erb/dll/main.bundle.dev.js",
|
"main": "./.erb/dll/main.bundle.dev.js",
|
||||||
"version": "1.5.6",
|
"version": "1.5.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-rust-scripts": "tsx ./.erb/scripts/build-rust-scripts.js",
|
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
"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: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: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",
|
"build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
|
||||||
"postinstall": "tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
|
||||||
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
"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",
|
"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",
|
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"package": "tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --config electron-builder.config.js && npm run build:dll",
|
"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": "tsx ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
|
||||||
"start:main": "concurrently -k \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon .\"",
|
"start: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: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",
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
"test": "jest ./src/__tests__/**/*.test.ts",
|
"test": "jest",
|
||||||
"test:unit": "jest ./src/__tests__/unit",
|
"test:unit": "jest ./src/__tests__/unit",
|
||||||
"build:win": "npm run build && electron-builder --config electron-builder.config.js --publish never --win --x64",
|
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --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: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": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx}": [
|
"*.{js,jsx,ts,tsx}": [
|
||||||
@@ -104,16 +102,16 @@
|
|||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"browserslist-config-erb": "^0.0.3",
|
"browserslist-config-erb": "^0.0.3",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^8.2.2",
|
||||||
"core-js": "^3.36.0",
|
"core-js": "^3.36.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^6.10.0",
|
"css-loader": "^6.10.0",
|
||||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||||
"detect-port": "^2.1.0",
|
"detect-port": "^1.5.1",
|
||||||
"electron": "39.2.7",
|
"electron": "^36.4.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-devtools-installer": "^4.0.0",
|
"electron-devtools-installer": "^4.0.0",
|
||||||
"electronmon": "^2.0.4",
|
"electronmon": "^2.0.3",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-erb": "^4.1.0",
|
"eslint-config-erb": "^4.1.0",
|
||||||
@@ -146,8 +144,6 @@
|
|||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tsx": "^4.19.2",
|
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.90.3",
|
"webpack": "^5.90.3",
|
||||||
@@ -157,7 +153,6 @@
|
|||||||
"webpack-merge": "^5.10.0"
|
"webpack-merge": "^5.10.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/is-prop-valid": "1.4.0",
|
|
||||||
"@internationalized/date": "^3.5.4",
|
"@internationalized/date": "^3.5.4",
|
||||||
"@nextui-org/date-picker": "^2.0.7",
|
"@nextui-org/date-picker": "^2.0.7",
|
||||||
"@nextui-org/react": "^2.3.6",
|
"@nextui-org/react": "^2.3.6",
|
||||||
@@ -175,7 +170,7 @@
|
|||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"format-duration": "^3.0.2",
|
"format-duration": "^3.0.2",
|
||||||
"framer-motion": "12.23.26",
|
"framer-motion": "^12.17.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"global-agent": "^3.0.0",
|
"global-agent": "^3.0.0",
|
||||||
"got": "^14.4.7",
|
"got": "^14.4.7",
|
||||||
@@ -185,7 +180,7 @@
|
|||||||
"node-abi": "^4.2.0",
|
"node-abi": "^4.2.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"protobufjs": "^8.0.0",
|
"protobufjs": "^7.5.3",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"query-process": "^0.0.3",
|
"query-process": "^0.0.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -200,14 +195,13 @@
|
|||||||
"rfdc": "^1.4.1",
|
"rfdc": "^1.4.1",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"semver": "7.7.3",
|
"semver": "^7.7.2",
|
||||||
"serialize-error": "^12.0.0",
|
"serialize-error": "^12.0.0",
|
||||||
"striptags": "^4.0.0-alpha.4",
|
"striptags": "^4.0.0-alpha.4",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-scoped-groups": "^2.0.0",
|
"tailwindcss-scoped-groups": "^2.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"to-ico": "^1.1.5",
|
"to-ico": "^1.1.5",
|
||||||
"tough-cookie": "^6.0.0",
|
|
||||||
"use-double-click": "^1.0.5",
|
"use-double-click": "^1.0.5",
|
||||||
"use-fit-text": "^2.4.0",
|
"use-fit-text": "^2.4.0",
|
||||||
"yauzl": "^3.2.0"
|
"yauzl": "^3.2.0"
|
||||||
@@ -248,5 +242,8 @@
|
|||||||
".erb/dll/**"
|
".erb/dll/**"
|
||||||
],
|
],
|
||||||
"logLevel": "quiet"
|
"logLevel": "quiet"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+12
-334
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "bs-manager",
|
"name": "bs-manager",
|
||||||
"version": "1.5.6",
|
"version": "1.5.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bs-manager",
|
"name": "bs-manager",
|
||||||
"version": "1.5.6",
|
"version": "1.5.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@resvg/resvg-js": "2.6.2",
|
"@resvg/resvg-js": "2.6.2",
|
||||||
"ps-list": "^7.2.0",
|
"ps-list": "^7.2.0",
|
||||||
"query-process": "^0.0.3",
|
"query-process": "^0.0.3",
|
||||||
"regedit-rs": "1.0.4"
|
"regedit-rs": "^1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@resvg/resvg-js": {
|
"node_modules/@resvg/resvg-js": {
|
||||||
@@ -38,118 +38,6 @@
|
|||||||
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
|
"@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": {
|
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
|
||||||
@@ -166,54 +54,6 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
|
||||||
@@ -259,134 +99,6 @@
|
|||||||
"query-process-win32-x64-msvc": "0.0.3"
|
"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": {
|
"node_modules/query-process-win32-x64-msvc": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/query-process-win32-x64-msvc/-/query-process-win32-x64-msvc-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/query-process-win32-x64-msvc/-/query-process-win32-x64-msvc-0.0.3.tgz",
|
||||||
@@ -403,59 +115,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regedit-rs": {
|
"node_modules/regedit-rs": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/regedit-rs/-/regedit-rs-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/regedit-rs/-/regedit-rs-1.0.2.tgz",
|
||||||
"integrity": "sha512-G+gESK8PvuqUec8LT4TrkbGvOOw4EzGd+kRn4zzmnnrWu0mgBD6P0zQPj+9EUIYW8L4BPETWiSkkyAFamc4JEg==",
|
"integrity": "sha512-4vEgiZNO1FCG8z/Zx3v/6PU1+eZ+ELe6R0ca+VB96Vw+Mi3M0IVHAjtMFbl97lUSX11dJqpyousX/wY8QcI1lA==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"regedit-rs-win32-arm64-msvc": "1.0.4",
|
"regedit-rs-win32-arm64-msvc": "1.0.2",
|
||||||
"regedit-rs-win32-ia32-msvc": "1.0.4",
|
"regedit-rs-win32-ia32-msvc": "1.0.2",
|
||||||
"regedit-rs-win32-x64-msvc": "1.0.4"
|
"regedit-rs-win32-x64-msvc": "1.0.2"
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": {
|
"node_modules/regedit-rs-win32-x64-msvc": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/regedit-rs-win32-x64-msvc/-/regedit-rs-win32-x64-msvc-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/regedit-rs-win32-x64-msvc/-/regedit-rs-win32-x64-msvc-1.0.2.tgz",
|
||||||
"integrity": "sha512-kb+965y6NKQijtCYYSvvE8puKAT3uyrHpr6HmZdGDZQb/iKNY2GmUX1/YrlYf9Kavdr5IJ9rkat6pokND+Npzw==",
|
"integrity": "sha512-ccCSyd5vWBKVWftBKLKzegqwwPMWcQtIW0ub66dCFFuv2s+x2EcZZWGdD9dVXX2Z6V9DU2JRPKgWUNjVPaj6Xg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bs-manager",
|
"name": "bs-manager",
|
||||||
"version": "1.5.6",
|
"version": "1.5.3",
|
||||||
"description": "BSManager",
|
"description": "BSManager",
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -9,18 +9,18 @@
|
|||||||
"url": "https://github.com/Zagrios/bs-manager"
|
"url": "https://github.com/Zagrios/bs-manager"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rebuild": "tsx ../../.erb/scripts/electron-rebuild.js",
|
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
|
||||||
"link-modules": "tsx ../../.erb/scripts/link-modules.ts",
|
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
|
||||||
"postinstall": "npm run rebuild && npm run link-modules"
|
"postinstall": "npm run rebuild && npm run link-modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@resvg/resvg-js": "2.6.2",
|
"@resvg/resvg-js": "2.6.2",
|
||||||
"ps-list": "^7.2.0",
|
"ps-list": "^7.2.0",
|
||||||
"query-process": "^0.0.3",
|
"query-process": "^0.0.3",
|
||||||
"regedit-rs": "1.0.4"
|
"regedit-rs": "^1.0.2"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.11.1"
|
"node": "20.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,89 +3,59 @@ import { parseEnvString } from "main/helpers/env.helpers";
|
|||||||
describe("Test parseEnvString", () => {
|
describe("Test parseEnvString", () => {
|
||||||
|
|
||||||
it("Empty", () => {
|
it("Empty", () => {
|
||||||
const { env, command } = parseEnvString("");
|
const envVars = parseEnvString("");
|
||||||
expect(env).toEqual({});
|
expect(envVars).toEqual({});
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Single test; no quotes", () => {
|
it("Single test; no quotes", () => {
|
||||||
const envString = "HELLO=World!";
|
const envString = "HELLO=World!";
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual({
|
expect(envVars).toEqual({
|
||||||
HELLO: "World!",
|
HELLO: "World!",
|
||||||
});
|
});
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Single test; single quotes", () => {
|
it("Single test; single quotes", () => {
|
||||||
const envString = "SINGLE_QOUTE='Single quote with spaces'";
|
const envString = "SINGLE_QOUTE='Single quote with spaces'";
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual({
|
expect(envVars).toEqual({
|
||||||
SINGLE_QOUTE: "Single quote with spaces",
|
SINGLE_QOUTE: "Single quote with spaces",
|
||||||
});
|
});
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Single test; double quotes", () => {
|
it("Single test; double quotes", () => {
|
||||||
const envString = 'DOUBLE_QOUTE="Some random quote."';
|
const envString = 'DOUBLE_QOUTE="Some random quote."';
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual({
|
expect(envVars).toEqual({
|
||||||
DOUBLE_QOUTE: "Some random quote.",
|
DOUBLE_QOUTE: "Some random quote.",
|
||||||
});
|
});
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Single test; empty value", () => {
|
it("Single test; empty value", () => {
|
||||||
const envString = "EMPTY=";
|
const envString = "EMPTY=";
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual({
|
expect(envVars).toEqual({
|
||||||
EMPTY: "",
|
EMPTY: "",
|
||||||
});
|
});
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Multiple test; combined", () => {
|
it("Multiple test; combined", () => {
|
||||||
const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`
|
const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual(expect.objectContaining({
|
expect(envVars).toEqual(expect.objectContaining({
|
||||||
HELLO: "World!",
|
HELLO: "World!",
|
||||||
DOUBLE_QUOTE: "Two Words",
|
DOUBLE_QUOTE: "Two Words",
|
||||||
SINGLE_QUOTE: "",
|
SINGLE_QUOTE: "",
|
||||||
EMPTY: ""
|
EMPTY: ""
|
||||||
}));
|
}));
|
||||||
expect(command).toEqual("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Key with numbers and lower case", () => {
|
it("Key with numbers and lower case", () => {
|
||||||
const envString = "H3ll0=world";
|
const envString = "H3ll0=world";
|
||||||
const { env, command } = parseEnvString(envString);
|
const envVars = parseEnvString(envString);
|
||||||
expect(env).toEqual({
|
expect(envVars).toEqual({
|
||||||
H3ll0: "world",
|
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%");
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
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`);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -27,6 +27,7 @@ jest.mock("electron-log", () => ({
|
|||||||
|
|
||||||
jest.mock("ps-list", () => (): unknown[] => []);
|
jest.mock("ps-list", () => (): unknown[] => []);
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === "win32";
|
||||||
const IS_LINUX = process.platform === "linux";
|
const IS_LINUX = process.platform === "linux";
|
||||||
|
|
||||||
describe("Test os.helpers bsmSpawn", () => {
|
describe("Test os.helpers bsmSpawn", () => {
|
||||||
@@ -50,7 +51,6 @@ describe("Test os.helpers bsmSpawn", () => {
|
|||||||
STEAM_COMPAT_CLIENT_INSTALL_PATH: "/steam",
|
STEAM_COMPAT_CLIENT_INSTALL_PATH: "/steam",
|
||||||
STEAM_COMPAT_APP_ID: BS_APP_ID,
|
STEAM_COMPAT_APP_ID: BS_APP_ID,
|
||||||
SteamEnv: "1",
|
SteamEnv: "1",
|
||||||
OXR_PARALLEL_VIEWS: "1",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -89,11 +89,14 @@ describe("Test os.helpers bsmSpawn", () => {
|
|||||||
it("Complex spawn command call (Mods install)", () => {
|
it("Complex spawn command call (Mods install)", () => {
|
||||||
bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, {
|
bsmSpawn(`"./BSIPA.exe" "./Beat Saber.exe" -n`, {
|
||||||
log: BsmShellLog.Command,
|
log: BsmShellLog.Command,
|
||||||
|
linux: { prefix: `"./wine64"` },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(spawnSpy).toHaveBeenCalledWith(
|
expect(spawnSpy).toHaveBeenCalledWith(
|
||||||
`"./BSIPA.exe" "./Beat Saber.exe" -n`,
|
process.platform === "win32"
|
||||||
|
? `"./BSIPA.exe" "./Beat Saber.exe" -n`
|
||||||
|
: `"./wine64" "./BSIPA.exe" "./Beat Saber.exe" -n`,
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,11 +112,14 @@ describe("Test os.helpers bsmSpawn", () => {
|
|||||||
env: BS_ENV,
|
env: BS_ENV,
|
||||||
},
|
},
|
||||||
log: BsmShellLog.Command,
|
log: BsmShellLog.Command,
|
||||||
|
linux: { prefix: `"./proton" run` },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(spawnSpy).toHaveBeenCalledWith(
|
expect(spawnSpy).toHaveBeenCalledWith(
|
||||||
`"./Beat Saber.exe" --no-yeet fpfc`,
|
IS_WINDOWS
|
||||||
|
? `"./Beat Saber.exe" --no-yeet fpfc`
|
||||||
|
: `"./proton" run "./Beat Saber.exe" --no-yeet fpfc`,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: "/",
|
cwd: "/",
|
||||||
detached: true,
|
detached: true,
|
||||||
@@ -134,15 +140,14 @@ describe("Test os.helpers bsmSpawn", () => {
|
|||||||
"STEAM_COMPAT_INSTALL_PATH",
|
"STEAM_COMPAT_INSTALL_PATH",
|
||||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||||
"STEAM_COMPAT_APP_ID",
|
"STEAM_COMPAT_APP_ID",
|
||||||
"SteamEnv",
|
"SteamEnv"
|
||||||
"OXR_PARALLEL_VIEWS"
|
|
||||||
];
|
];
|
||||||
const newEnv = {
|
const newEnv = {
|
||||||
...BS_ENV,
|
...BS_ENV,
|
||||||
something: "else",
|
something: "else",
|
||||||
more: "tests",
|
more: "tests",
|
||||||
};
|
};
|
||||||
bsmSpawn(`"./proton" run "./Beat Saber.exe"`, {
|
bsmSpawn(`"./Beat Saber.exe"`, {
|
||||||
args: ["--no-yeet", "fpfc"],
|
args: ["--no-yeet", "fpfc"],
|
||||||
options: {
|
options: {
|
||||||
cwd: "/",
|
cwd: "/",
|
||||||
@@ -150,6 +155,7 @@ describe("Test os.helpers bsmSpawn", () => {
|
|||||||
env: newEnv,
|
env: newEnv,
|
||||||
},
|
},
|
||||||
log: BsmShellLog.Command,
|
log: BsmShellLog.Command,
|
||||||
|
linux: { prefix: `"./proton" run` },
|
||||||
flatpak: {
|
flatpak: {
|
||||||
host: true,
|
host: true,
|
||||||
env: flatpakEnv,
|
env: flatpakEnv,
|
||||||
|
|||||||
@@ -19,5 +19,7 @@ export const HTTP_STATUS_CODES = constants;
|
|||||||
|
|
||||||
// Linux related stuff
|
// Linux related stuff
|
||||||
|
|
||||||
|
export const PROTON_BINARY_PREFIX = "proton";
|
||||||
|
export const WINE_BINARY_PREFIX = path.join("files", "bin", "wine64");
|
||||||
export const IS_FLATPAK = process.env.container === "flatpak";
|
export const IS_FLATPAK = process.env.container === "flatpak";
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ enum EnvParserState {
|
|||||||
QUOTE_VALUE,
|
QUOTE_VALUE,
|
||||||
DQUOTE_VALUE,
|
DQUOTE_VALUE,
|
||||||
SPACE,
|
SPACE,
|
||||||
EXIT,
|
|
||||||
ERROR,
|
ERROR,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,19 +28,7 @@ const isAlphaCharacter = (c: string) =>
|
|||||||
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
||||||
const isNumber = (c: string) => c >= "0" && c <= "9";
|
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> = {};
|
const envVars: Record<string, string> = {};
|
||||||
|
|
||||||
let state: EnvParserState = EnvParserState.NAME_START;
|
let state: EnvParserState = EnvParserState.NAME_START;
|
||||||
@@ -52,13 +39,13 @@ export function parseEnvString(envString: string): {
|
|||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case EnvParserState.NAME_START:
|
case EnvParserState.NAME_START:
|
||||||
index = pos;
|
|
||||||
if (isAlphaCharacter(c) || c === "_") {
|
if (isAlphaCharacter(c) || c === "_") {
|
||||||
state = EnvParserState.NAME;
|
state = EnvParserState.NAME;
|
||||||
|
index = pos;
|
||||||
} else if (c !== " ") {
|
} else if (c !== " ") {
|
||||||
state = EnvParserState.EXIT;
|
state = EnvParserState.ERROR;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.NAME:
|
case EnvParserState.NAME:
|
||||||
if (c === "=") {
|
if (c === "=") {
|
||||||
@@ -66,65 +53,57 @@ export function parseEnvString(envString: string): {
|
|||||||
newName = envString.substring(index, pos);
|
newName = envString.substring(index, pos);
|
||||||
index = pos + 1;
|
index = pos + 1;
|
||||||
} else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") {
|
} else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") {
|
||||||
state = EnvParserState.EXIT;
|
state = EnvParserState.ERROR;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.VALUE_START:
|
case EnvParserState.VALUE_START:
|
||||||
if (c === "'") {
|
if (c === "'") {
|
||||||
++index;
|
++index;
|
||||||
state = EnvParserState.QUOTE_VALUE;
|
state = EnvParserState.QUOTE_VALUE;
|
||||||
} else if (c === '"') {
|
} else if (c === '"') {
|
||||||
++index;
|
++index;
|
||||||
state = EnvParserState.DQUOTE_VALUE;
|
state = EnvParserState.DQUOTE_VALUE;
|
||||||
} else if (c === " ") {
|
} else if (c === " ") {
|
||||||
state = EnvParserState.NAME_START;
|
state = EnvParserState.NAME_START;
|
||||||
envVars[newName] = "";
|
envVars[newName] = "";
|
||||||
} else {
|
} else {
|
||||||
state = EnvParserState.VALUE;
|
state = EnvParserState.VALUE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.VALUE:
|
case EnvParserState.VALUE:
|
||||||
if (c === " ") {
|
if (c === " ") {
|
||||||
state = EnvParserState.NAME_START;
|
state = EnvParserState.NAME_START;
|
||||||
envVars[newName] = envString.substring(index, pos);
|
envVars[newName] = envString.substring(index, pos);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.QUOTE_VALUE:
|
case EnvParserState.QUOTE_VALUE:
|
||||||
if (c === "'") {
|
if (c === "'") {
|
||||||
state = EnvParserState.SPACE;
|
state = EnvParserState.SPACE;
|
||||||
envVars[newName] = envString.substring(index, pos);
|
envVars[newName] = envString.substring(index, pos);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.DQUOTE_VALUE:
|
case EnvParserState.DQUOTE_VALUE:
|
||||||
if (c === '"') {
|
if (c === '"') {
|
||||||
state = EnvParserState.SPACE;
|
state = EnvParserState.SPACE;
|
||||||
envVars[newName] = envString.substring(index, pos);
|
envVars[newName] = envString.substring(index, pos);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnvParserState.SPACE:
|
case EnvParserState.SPACE:
|
||||||
if (c === " ") {
|
if (c === " ") {
|
||||||
state = EnvParserState.NAME_START;
|
state = EnvParserState.NAME_START;
|
||||||
} else {
|
} else {
|
||||||
state = EnvParserState.ERROR;
|
state = EnvParserState.ERROR;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early exit
|
|
||||||
if (state === EnvParserState.EXIT) {
|
|
||||||
return {
|
|
||||||
env: envVars,
|
|
||||||
command: envString.substring(index).trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === EnvParserState.ERROR) {
|
if (state === EnvParserState.ERROR) {
|
||||||
throw new CustomError(
|
throw new CustomError(
|
||||||
`parseEnvString failed: invalid character at position ${pos}`,
|
`parseEnvString failed: invalid character at position ${pos}`,
|
||||||
@@ -135,15 +114,15 @@ export function parseEnvString(envString: string): {
|
|||||||
|
|
||||||
if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) {
|
if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) {
|
||||||
envVars[newName] = envString.substring(index);
|
envVars[newName] = envString.substring(index);
|
||||||
return { env: envVars, command: "" };
|
return envVars;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) {
|
if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) {
|
||||||
return { env: envVars, command: "" };
|
return envVars;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
throw new CustomError(
|
||||||
env: envVars,
|
"parseEnvString failed: invalid ending state",
|
||||||
command: envString.substring(index + 1).trim(),
|
"generic.env.parse"
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function deleteFile(filepath: string) {
|
|||||||
log.info("Deleting file", `"${filepath}"`);
|
log.info("Deleting file", `"${filepath}"`);
|
||||||
await unlink(filepath);
|
await unlink(filepath);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error("Could not delete file", `"${filepath}"`, error);
|
log.error("Could not delete file", `"${filepath}"`);
|
||||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ export function deleteFileSync(filepath: string) {
|
|||||||
log.info("Deleting file", `"${filepath}"`);
|
log.info("Deleting file", `"${filepath}"`);
|
||||||
unlinkSync(filepath);
|
unlinkSync(filepath);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log.error("Could not delete file", `"${filepath}"`, error);
|
log.error("Could not delete file", `"${filepath}"`);
|
||||||
throw CustomError.fromError(error, "generic.fs.delete-file");
|
throw CustomError.fromError(error, "generic.fs.delete-file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -3,6 +3,13 @@ import log from "electron-log";
|
|||||||
import psList from "ps-list";
|
import psList from "ps-list";
|
||||||
import { IS_FLATPAK } from "main/constants";
|
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
|
// Only applied if package as flatpak
|
||||||
type FlatpakOptions = {
|
type FlatpakOptions = {
|
||||||
// Force to use "flatpak-spawn --host" to run commands outside of the sandbox
|
// Force to use "flatpak-spawn --host" to run commands outside of the sandbox
|
||||||
@@ -17,10 +24,11 @@ export enum BsmShellLog {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface BsmShellOptions<OptionsType> {
|
interface BsmShellOptions<OptionsType> {
|
||||||
args?: string[] | string;
|
args?: string[];
|
||||||
options?: OptionsType;
|
options?: OptionsType;
|
||||||
// Look into BsmShellLog values
|
// Look into BsmShellLog values
|
||||||
log?: number;
|
log?: number;
|
||||||
|
linux?: LinuxOptions;
|
||||||
flatpak?: FlatpakOptions;
|
flatpak?: FlatpakOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,9 +37,7 @@ export type BsmExecOptions = BsmShellOptions<cp.ExecOptions>;
|
|||||||
|
|
||||||
function updateCommand(command: string, options: BsmSpawnOptions) {
|
function updateCommand(command: string, options: BsmSpawnOptions) {
|
||||||
if (options?.args) {
|
if (options?.args) {
|
||||||
command += typeof(options.args) === "string"
|
command += ` ${options.args.join(" ")}`;
|
||||||
? ` ${options.args}`
|
|
||||||
: ` ${options.args.join(" ")}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
@@ -39,6 +45,10 @@ function updateCommand(command: string, options: BsmSpawnOptions) {
|
|||||||
// All distros should support "bash" by default
|
// All distros should support "bash" by default
|
||||||
options.options.shell = "bash";
|
options.options.shell = "bash";
|
||||||
|
|
||||||
|
if (options.linux?.prefix) {
|
||||||
|
command = `${options.linux.prefix} ${command}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.flatpak?.host) {
|
if (options?.flatpak?.host) {
|
||||||
const envArgs = (options?.flatpak?.env && options?.options?.env)
|
const envArgs = (options?.flatpak?.env && options?.options?.env)
|
||||||
&& options.flatpak.env
|
&& options.flatpak.env
|
||||||
|
|||||||
@@ -51,15 +51,7 @@ async function enableWindowsProxy(enable: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
const httpProxyServer = await getProxyServer().catch(err => log.error(err));
|
const httpProxyUrl = `http://${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.HTTP_PROXY = httpProxyUrl;
|
||||||
globalProxyAgent.HTTPS_PROXY = httpProxyUrl;
|
globalProxyAgent.HTTPS_PROXY = httpProxyUrl;
|
||||||
globalProxyAgent.NO_PROXY = `${await getProxyOverride().catch(err => log.error(err))}`;
|
globalProxyAgent.NO_PROXY = `${await getProxyOverride().catch(err => log.error(err))}`;
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ ipc.on("check-update", (_, reply) => {
|
|||||||
reply(from(updaterService.isUpdateAvailable()));
|
reply(from(updaterService.isUpdateAvailable()));
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on("get-available-update", (_, reply) => {
|
|
||||||
const updaterService = AutoUpdaterService.getInstance();
|
|
||||||
reply(from(updaterService.getAvailableUpdate()));
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.on("install-update", (_, reply) => {
|
ipc.on("install-update", (_, reply) => {
|
||||||
const updaterService = AutoUpdaterService.getInstance();
|
const updaterService = AutoUpdaterService.getInstance();
|
||||||
reply(of(updaterService.quitAndInstall()));
|
reply(of(updaterService.quitAndInstall()));
|
||||||
|
|||||||
+7
-17
@@ -28,7 +28,6 @@ import { StaticConfigurationService } from "./services/static-configuration.serv
|
|||||||
import { configureProxy } from './helpers/proxy.helpers';
|
import { configureProxy } from './helpers/proxy.helpers';
|
||||||
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
|
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
|
||||||
import { tryit } from "shared/helpers/error.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 isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||||
const staticConfig = StaticConfigurationService.getInstance();
|
const staticConfig = StaticConfigurationService.getInstance();
|
||||||
@@ -100,10 +99,6 @@ const findAssociatedFileInArgs = (args: string[]): string => {
|
|||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
initServicesMustBeInitialized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
@@ -126,8 +121,11 @@ if (!gotTheLock) {
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.setAppUserModelId(APP_NAME);
|
app.setAppUserModelId(APP_NAME);
|
||||||
init();
|
|
||||||
|
initServicesMustBeInitialized();
|
||||||
|
|
||||||
const deepLink = findDeepLinkInArgs(process.argv);
|
const deepLink = findDeepLinkInArgs(process.argv);
|
||||||
const associatedFile = findAssociatedFileInArgs(process.argv);
|
const associatedFile = findAssociatedFileInArgs(process.argv);
|
||||||
@@ -136,18 +134,10 @@ if (!gotTheLock) {
|
|||||||
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
|
||||||
} else if (associatedFile) {
|
} else if (associatedFile) {
|
||||||
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
|
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
|
||||||
} else if (process.platform === "linux") {
|
|
||||||
createWindow("index.html");
|
|
||||||
} else {
|
} else {
|
||||||
const configService = StaticConfigurationService.getInstance();
|
createWindow(process.platform === "linux"
|
||||||
const autoUpdate = configService.get("auto-update", AutoUpdate.ALWAYS);
|
? "index.html" : "launcher.html"
|
||||||
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();
|
SteamLauncherService.getInstance().restoreSteamVR();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { BSVersion } from "shared/bs-version.interface";
|
import { BSVersion } from "shared/bs-version.interface";
|
||||||
import { BsvMapDetail } from "shared/models/maps";
|
import { BsvMapDetail } from "shared/models/maps";
|
||||||
import { BsmLocalMap, BsmLocalMapMetadata, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||||
import { BSLocalVersionService } from "../../bs-local-version.service";
|
import { BSLocalVersionService } from "../../bs-local-version.service";
|
||||||
import { InstallationLocationService } from "../../installation-location.service";
|
import { InstallationLocationService } from "../../installation-location.service";
|
||||||
import { UtilsService } from "../../utils.service";
|
import { UtilsService } from "../../utils.service";
|
||||||
import crypto, { BinaryLike } from "crypto";
|
import crypto, { BinaryLike } from "crypto";
|
||||||
import { lstatSync } from "fs";
|
import { lstatSync } from "fs";
|
||||||
import { copy, createReadStream, ensureDir, existsSync, pathExists, pathExistsSync, readJson, realpath, writeJson } from "fs-extra";
|
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath } from "fs-extra";
|
||||||
import { RequestService } from "../../request.service";
|
import { RequestService } from "../../request.service";
|
||||||
import sanitize from "sanitize-filename";
|
import sanitize from "sanitize-filename";
|
||||||
import { DeepLinkService } from "../../deep-link.service";
|
import { DeepLinkService } from "../../deep-link.service";
|
||||||
@@ -31,7 +31,6 @@ import { CustomError } from "shared/models/exceptions/custom-error.class";
|
|||||||
import { tryit } from "shared/helpers/error.helpers";
|
import { tryit } from "shared/helpers/error.helpers";
|
||||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||||
import { escapeRegExp } from "../../../../shared/helpers/string.helpers";
|
import { escapeRegExp } from "../../../../shared/helpers/string.helpers";
|
||||||
import dateFormat from "dateformat";
|
|
||||||
|
|
||||||
export class LocalMapsManagerService {
|
export class LocalMapsManagerService {
|
||||||
private static instance: LocalMapsManagerService;
|
private static instance: LocalMapsManagerService;
|
||||||
@@ -47,7 +46,6 @@ export class LocalMapsManagerService {
|
|||||||
public static readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
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 RELATIVE_MAPS_FOLDER = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||||
public static readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
public static readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
||||||
public static readonly METADATA_FILE = "metadata.json";
|
|
||||||
|
|
||||||
private readonly DEEP_LINKS = {
|
private readonly DEEP_LINKS = {
|
||||||
BeatSaver: "beatsaver",
|
BeatSaver: "beatsaver",
|
||||||
@@ -140,21 +138,16 @@ export class LocalMapsManagerService {
|
|||||||
|
|
||||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||||
|
|
||||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string, metadata: BsmLocalMapMetadata): BsmLocalMap => {
|
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||||
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
||||||
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
||||||
return {
|
return { mapInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) };
|
||||||
mapInfo, coverUrl, songUrl, hash, path: mapPath,
|
|
||||||
songDetails: this.songDetailsCache.getSongDetails(hash),
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||||
|
|
||||||
if (cachedMapInfos) {
|
if (cachedMapInfos) {
|
||||||
const metadata = await this.getMetadata(mapPath);
|
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath, metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await getFilesInFolder(mapPath);
|
const files = await getFilesInFolder(mapPath);
|
||||||
@@ -173,9 +166,8 @@ export class LocalMapsManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||||
const metadata = await this.getMetadata(mapPath);
|
|
||||||
|
|
||||||
return getUrlsAndReturn(mapInfo, hash, mapPath, metadata);
|
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async downloadMapZip(zipUrl: string): Promise<string> {
|
private async downloadMapZip(zipUrl: string): Promise<string> {
|
||||||
@@ -470,21 +462,6 @@ export class LocalMapsManagerService {
|
|||||||
return localMap;
|
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>> {
|
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string): Promise<Observable<Progression>> {
|
||||||
const archive = new Archive(outPath);
|
const archive = new Archive(outPath);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { autoUpdater, CancellationToken, ProgressInfo, UpdateInfo } from "electron-updater";
|
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { gt } from "semver";
|
import { gt } from "semver";
|
||||||
import { Progression } from "main/helpers/fs.helpers";
|
import { Progression } from "main/helpers/fs.helpers";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
import { safeGt } from "shared/helpers/semver.helpers";
|
|
||||||
|
|
||||||
export class AutoUpdaterService {
|
export class AutoUpdaterService {
|
||||||
private static instance: AutoUpdaterService;
|
private static instance: AutoUpdaterService;
|
||||||
@@ -32,18 +31,6 @@ 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> {
|
public downloadUpdate(): Observable<Progression> {
|
||||||
return new Observable<Progression>(observer => {
|
return new Observable<Progression>(observer => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { LaunchOption } from "shared/models/bs-launch";
|
import { LaunchOption } from "shared/models/bs-launch";
|
||||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||||
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "child_process";
|
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "child_process";
|
||||||
|
import path from "path";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||||
import { LinuxService } from "../linux.service";
|
import { LinuxService } from "../linux.service";
|
||||||
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
|
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
|
||||||
import { IS_FLATPAK } from "main/constants";
|
import { IS_FLATPAK } from "main/constants";
|
||||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||||
|
import { parseEnvString } from "main/helpers/env.helpers";
|
||||||
|
|
||||||
export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
||||||
const launchArgs = [];
|
const launchArgs = [];
|
||||||
@@ -28,6 +30,10 @@ export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
|
|||||||
launchArgs.push("editor");
|
launchArgs.push("editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (launchOptions.command) {
|
||||||
|
launchArgs.push(launchOptions.command);
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(new Set(launchArgs).values());
|
return Array.from(new Set(launchArgs).values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,20 +47,20 @@ export abstract class AbstractLauncherService {
|
|||||||
this.localVersions = BSLocalVersionService.getInstance();
|
this.localVersions = BSLocalVersionService.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected launchBeatSaberProcess(options: LaunchBeatSaberOptions): ChildProcessWithoutNullStreams {
|
private readonly COMMAND_FORMAT = "%command%";
|
||||||
const spawnOptions: SpawnOptionsWithoutStdio = {
|
|
||||||
detached: true,
|
|
||||||
cwd: options.beatSaberFolderPath,
|
|
||||||
env: options.env,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.args?.includes("--verbose")){
|
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams {
|
||||||
|
|
||||||
|
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) };
|
||||||
|
|
||||||
|
if(args.includes("--verbose")){
|
||||||
spawnOptions.windowsVerbatimArguments = true;
|
spawnOptions.windowsVerbatimArguments = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnOptions.shell = true; // For windows to spawn properly
|
spawnOptions.shell = true; // For windows to spawn properly
|
||||||
return bsmSpawn(options.cmdlet, {
|
return bsmSpawn(`"${bsExePath}"`, {
|
||||||
args: options.args, options: spawnOptions, log: BsmShellLog.Command,
|
args, options: spawnOptions, log: BsmShellLog.Command,
|
||||||
|
linux: { prefix: options?.protonPrefix || "" },
|
||||||
flatpak: {
|
flatpak: {
|
||||||
host: IS_FLATPAK,
|
host: IS_FLATPAK,
|
||||||
env: [
|
env: [
|
||||||
@@ -67,7 +73,6 @@ export abstract class AbstractLauncherService {
|
|||||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
|
||||||
"STEAM_COMPAT_APP_ID",
|
"STEAM_COMPAT_APP_ID",
|
||||||
"SteamEnv",
|
"SteamEnv",
|
||||||
"OXR_PARALLEL_VIEWS",
|
|
||||||
"PROTON_LOG",
|
"PROTON_LOG",
|
||||||
"PROTON_LOG_DIR",
|
"PROTON_LOG_DIR",
|
||||||
],
|
],
|
||||||
@@ -75,8 +80,8 @@ export abstract class AbstractLauncherService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected launchBeatSaber(options: LaunchBeatSaberOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||||
const process = this.launchBeatSaberProcess(options);
|
const process = this.launchBSProcess(bsExePath, args, options);
|
||||||
|
|
||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
@@ -115,13 +120,23 @@ export abstract class AbstractLauncherService {
|
|||||||
return { process, exit };
|
return { process, exit };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch option helper function
|
protected injectAdditionalArgsEnvs(
|
||||||
protected mergeEnvVariables(
|
launchOptions: LaunchOption,
|
||||||
originalEnv: Record<string, string>,
|
env: Record<string, string>
|
||||||
newEnv: Record<string, string>
|
) {
|
||||||
): Record<string, string> {
|
if (!launchOptions.command) {
|
||||||
const env = { ...originalEnv };
|
return;
|
||||||
for (const [ key, value ] of Object.entries(newEnv)) {
|
}
|
||||||
|
|
||||||
|
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))) {
|
||||||
log.info(
|
log.info(
|
||||||
key in env ? "Overriding" : "Injecting",
|
key in env ? "Overriding" : "Injecting",
|
||||||
`${key}="${value}"`,
|
`${key}="${value}"`,
|
||||||
@@ -129,21 +144,13 @@ export abstract class AbstractLauncherService {
|
|||||||
);
|
);
|
||||||
env[key] = value;
|
env[key] = value;
|
||||||
}
|
}
|
||||||
return env;
|
|
||||||
|
launchOptions.command = command.substring(index + this.COMMAND_FORMAT.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LaunchBeatSaberOptions = {
|
export type SpawnBsProcessOptions = {
|
||||||
// To be passed to the bsmSpawn helper function
|
protonPrefix?: string;
|
||||||
// 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;
|
unrefAfter?: number;
|
||||||
}
|
} & SpawnOptionsWithoutStdio;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { pathExists } from "fs-extra";
|
|||||||
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
|
||||||
import { isProcessRunning } from "../../helpers/os.helpers";
|
import { isProcessRunning } from "../../helpers/os.helpers";
|
||||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
|
||||||
|
|
||||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||||
|
|
||||||
@@ -48,29 +47,21 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure Oculus is running
|
// Make sure Oculus is running
|
||||||
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err)).catch(err => {
|
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err));
|
||||||
log.warn("Unable to start Oculus client. Force launch.", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
let env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
};
|
};
|
||||||
const {
|
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||||
env: parsedEnv,
|
|
||||||
cmdlet, args,
|
|
||||||
} = parseLaunchOptions(launchOptions.command, {
|
|
||||||
commandReplacement: `"${exePath}"`,
|
|
||||||
});
|
|
||||||
env = this.mergeEnvVariables(env, parsedEnv);
|
|
||||||
|
|
||||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||||
|
|
||||||
// Launch Beat Saber
|
// Launch Beat Saber
|
||||||
const bsProcess = this.launchBeatSaber({
|
const bsProcess = this.launchBs(
|
||||||
env, cmdlet,
|
exePath,
|
||||||
beatSaberFolderPath: bsPath,
|
buildBsLaunchArgs(launchOptions),
|
||||||
args: [ args, ...buildBsLaunchArgs(launchOptions) ]
|
{ env }
|
||||||
});
|
);
|
||||||
|
|
||||||
return bsProcess.exit.catch(err => {
|
return bsProcess.exit.catch(err => {
|
||||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import { SteamService } from "../steam.service";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { AbstractLauncherService, buildBsLaunchArgs, LaunchBeatSaberOptions } from "./abstract-launcher.service";
|
import { AbstractLauncherService, buildBsLaunchArgs, SpawnBsProcessOptions } from "./abstract-launcher.service";
|
||||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||||
import { UtilsService } from "../utils.service";
|
import { UtilsService } from "../utils.service";
|
||||||
import { exec, ChildProcessWithoutNullStreams } from "child_process";
|
import { exec, ChildProcessWithoutNullStreams } from "child_process";
|
||||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||||
import { app, Event } from "electron";
|
import { app, Event } from "electron";
|
||||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
|
||||||
|
|
||||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||||
|
|
||||||
@@ -65,8 +64,8 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected launchBeatSaber(options: LaunchBeatSaberOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||||
const process = this.launchBeatSaberProcess(options);
|
const process = this.launchBSProcess(bsExePath, args, options);
|
||||||
|
|
||||||
const exit = new Promise<number>((resolve, reject) => {
|
const exit = new Promise<number>((resolve, reject) => {
|
||||||
// Don't remove, useful for debugging!
|
// Don't remove, useful for debugging!
|
||||||
@@ -147,35 +146,24 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
|||||||
|
|
||||||
const steamPath = await this.steam.getSteamPath();
|
const steamPath = await this.steam.getSteamPath();
|
||||||
|
|
||||||
let env: Record<string, string> = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
"SteamAppId": BS_APP_ID,
|
"SteamAppId": BS_APP_ID,
|
||||||
"SteamOverlayGameId": BS_APP_ID,
|
"SteamOverlayGameId": BS_APP_ID,
|
||||||
"SteamGameId": BS_APP_ID,
|
"SteamGameId": BS_APP_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let protonPrefix = "";
|
||||||
// Linux setup
|
// Linux setup
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
if (launchOptions.admin) {
|
const linuxSetup = await this.linux.setupLaunch(
|
||||||
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
|
launchOptions, steamPath, bsFolderPath
|
||||||
));
|
);
|
||||||
|
protonPrefix = linuxSetup.protonPrefix;
|
||||||
|
Object.assign(env, linuxSetup.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
this.injectAdditionalArgsEnvs(launchOptions, env);
|
||||||
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);
|
const launchArgs = buildBsLaunchArgs(launchOptions);
|
||||||
|
|
||||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||||
@@ -183,12 +171,9 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
|||||||
const spawnOpts = { env, cwd: bsFolderPath };
|
const spawnOpts = { env, cwd: bsFolderPath };
|
||||||
|
|
||||||
const launchPromise = !launchOptions.admin ? (
|
const launchPromise = !launchOptions.admin ? (
|
||||||
this.launchBeatSaber({
|
this.launchBs(bsExePath, launchArgs, {
|
||||||
env, cmdlet,
|
...spawnOpts,
|
||||||
args: args
|
protonPrefix
|
||||||
? [ args, ...launchArgs ]
|
|
||||||
: launchArgs,
|
|
||||||
beatSaberFolderPath: bsFolderPath,
|
|
||||||
}).exit
|
}).exit
|
||||||
) : (
|
) : (
|
||||||
new Promise<number>(resolve => {
|
new Promise<number>(resolve => {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class BSLocalVersionService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionsDict = (await this.remoteVersionService.getAvailableVersions()).reverse();
|
const versionsDict = await this.remoteVersionService.getAvailableVersions();
|
||||||
|
|
||||||
let stream: ReadStream;
|
let stream: ReadStream;
|
||||||
|
|
||||||
@@ -244,12 +244,8 @@ export class BSLocalVersionService {
|
|||||||
private async getSteamVersion(): Promise<BSVersion> {
|
private async getSteamVersion(): Promise<BSVersion> {
|
||||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
||||||
|
|
||||||
if (!steamBsFolder) {
|
if (!steamBsFolder || !(await pathExists(steamBsFolder))) {
|
||||||
throw new Error("No Beat Saber Steam version found");
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await pathExists(steamBsFolder))) {
|
|
||||||
throw new Error(`Beat Saber Steam version not found in "${steamBsFolder}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getVersionOfBSFolder(steamBsFolder, { steam: true });
|
return this.getVersionOfBSFolder(steamBsFolder, { steam: true });
|
||||||
@@ -269,8 +265,7 @@ export class BSLocalVersionService {
|
|||||||
const versions: BSVersion[] = [];
|
const versions: BSVersion[] = [];
|
||||||
|
|
||||||
const steamVersion = await this.getSteamVersion().catch(e => {
|
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) {
|
if (steamVersion) {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export class InstallationLocationService {
|
|||||||
private readonly staticConfig: StaticConfigurationService;
|
private readonly staticConfig: StaticConfigurationService;
|
||||||
private readonly updateListeners: Set<Listener> = new Set();
|
private readonly updateListeners: Set<Listener> = new Set();
|
||||||
|
|
||||||
private readonly installPath: string;
|
|
||||||
private _installationDirectory: string;
|
private _installationDirectory: string;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
@@ -35,13 +34,6 @@ export class InstallationLocationService {
|
|||||||
this.staticConfig.$watch(this.STORE_INSTALLATION_PATH_KEY).subscribe(() => {
|
this.staticConfig.$watch(this.STORE_INSTALLATION_PATH_KEY).subscribe(() => {
|
||||||
this.triggerListeners();
|
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 {
|
private triggerListeners(): void {
|
||||||
@@ -73,7 +65,7 @@ export class InstallationLocationService {
|
|||||||
|
|
||||||
public defaultInstallationDirectory(): string {
|
public defaultInstallationDirectory(): string {
|
||||||
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
|
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
|
||||||
const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : this.installPath;
|
const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : app.getPath("home");
|
||||||
return path.join(installationDirectory, this.INSTALLATION_FOLDER);
|
return path.join(installationDirectory, this.INSTALLATION_FOLDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +85,7 @@ export class InstallationLocationService {
|
|||||||
return app.getPath("documents");
|
return app.getPath("documents");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.installPath;
|
return app.getPath("home");
|
||||||
};
|
};
|
||||||
|
|
||||||
this._installationDirectory = installParentPath();
|
this._installationDirectory = installParentPath();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK } from "main/constants";
|
import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||||
import { InstallationLocationService } from "./installation-location.service";
|
import { InstallationLocationService } from "./installation-location.service";
|
||||||
import { StaticConfigurationService } from "./static-configuration.service";
|
import { StaticConfigurationService } from "./static-configuration.service";
|
||||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||||
@@ -10,7 +10,6 @@ import { BsmShellLog, bsmExec } from "main/helpers/os.helpers";
|
|||||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||||
import { SteamShortcutData } from "shared/models/steam/shortcut.model";
|
import { SteamShortcutData } from "shared/models/steam/shortcut.model";
|
||||||
import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service";
|
import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service";
|
||||||
import { parseLaunchOptions } from "main/helpers/launchOptions.helper";
|
|
||||||
|
|
||||||
export class LinuxService {
|
export class LinuxService {
|
||||||
private static instance: LinuxService;
|
private static instance: LinuxService;
|
||||||
@@ -22,19 +21,10 @@ export class LinuxService {
|
|||||||
return LinuxService.instance;
|
return LinuxService.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly PROTON_BINARY_PREFIX = "proton";
|
|
||||||
// Use "wine64" instead of "wine"
|
|
||||||
// https://github.com/Zagrios/bs-manager/pull/586#issuecomment-2449228826
|
|
||||||
private readonly WINE_BINARY_PREFIXES = [
|
|
||||||
path.join("files", "bin", "wine64"),
|
|
||||||
path.join("files", "lib", "wine", "x86_64-unix", "wine64"),
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly installLocationService: InstallationLocationService;
|
private readonly installLocationService: InstallationLocationService;
|
||||||
private readonly staticConfig: StaticConfigurationService;
|
private readonly staticConfig: StaticConfigurationService;
|
||||||
|
|
||||||
private nixOS: boolean | undefined;
|
private nixOS: boolean | undefined;
|
||||||
private winePath = "";
|
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.installLocationService = InstallationLocationService.getInstance();
|
this.installLocationService = InstallationLocationService.getInstance();
|
||||||
@@ -48,11 +38,26 @@ export class LinuxService {
|
|||||||
return path.resolve(sharedFolder, "compatdata");
|
return path.resolve(sharedFolder, "compatdata");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getProtonPrefix() {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const protonPath = await this.getProtonPath();
|
const protonPath = await this.getProtonPath();
|
||||||
return await this.isNixOS()
|
return {
|
||||||
? `steam-run "${protonPath}" run`
|
protonPrefix: await this.isNixOS()
|
||||||
: `"${protonPath}" run`;
|
? `steam-run "${protonPath}" run`
|
||||||
|
: `"${protonPath}" run`,
|
||||||
|
env: await this.buildEnvVariables(launchOptions, steamPath, bsFolderPath)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProtonPath(): Promise<string> {
|
private async getProtonPath(): Promise<string> {
|
||||||
@@ -64,7 +69,7 @@ export class LinuxService {
|
|||||||
}
|
}
|
||||||
const protonPath = path.join(
|
const protonPath = path.join(
|
||||||
this.staticConfig.get("proton-folder"),
|
this.staticConfig.get("proton-folder"),
|
||||||
this.PROTON_BINARY_PREFIX
|
PROTON_BINARY_PREFIX
|
||||||
);
|
);
|
||||||
if (!fs.pathExistsSync(protonPath)) {
|
if (!fs.pathExistsSync(protonPath)) {
|
||||||
throw CustomError.fromError(
|
throw CustomError.fromError(
|
||||||
@@ -76,7 +81,7 @@ export class LinuxService {
|
|||||||
return protonPath;
|
return protonPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async buildEnvVariables(
|
private async buildEnvVariables(
|
||||||
launchOptions: LaunchOption,
|
launchOptions: LaunchOption,
|
||||||
steamPath: string,
|
steamPath: string,
|
||||||
bsFolderPath: string
|
bsFolderPath: string
|
||||||
@@ -100,9 +105,6 @@ export class LinuxService {
|
|||||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||||
// Run game in steam environment; fixes #585 for unicode song titles
|
// Run game in steam environment; fixes #585 for unicode song titles
|
||||||
"SteamEnv": "1",
|
"SteamEnv": "1",
|
||||||
// Fix reflections in Monado
|
|
||||||
"OXR_PARALLEL_VIEWS": "1",
|
|
||||||
"OXR_NO_TEXTURE_SOURCE_ALPHA": "1",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
|
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
|
||||||
@@ -122,53 +124,27 @@ export class LinuxService {
|
|||||||
protonFolder = this.staticConfig.get("proton-folder");
|
protonFolder = this.staticConfig.get("proton-folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the proton binary exists
|
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||||
const protonPath = path.join(protonFolder, this.PROTON_BINARY_PREFIX);
|
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||||
if (!fs.pathExistsSync(protonPath)) {
|
return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any wine64 here exists
|
|
||||||
for (const winePath of this.WINE_BINARY_PREFIXES) {
|
|
||||||
if (fs.pathExistsSync(path.join(protonFolder, winePath))) {
|
|
||||||
// Reset this, in the case where the user reselects a new proton folder
|
|
||||||
this.winePath = "";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWinePath(): string {
|
public getWinePath(): string {
|
||||||
if (this.winePath) {
|
|
||||||
return this.winePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.staticConfig.has("proton-folder")) {
|
if (!this.staticConfig.has("proton-folder")) {
|
||||||
throw new Error("proton-folder variable not set");
|
throw new Error("proton-folder variable not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
const protonFolder = this.staticConfig.get("proton-folder");
|
const winePath = path.join(
|
||||||
let winePath = "";
|
this.staticConfig.get("proton-folder"),
|
||||||
for (const prefixes of this.WINE_BINARY_PREFIXES) {
|
WINE_BINARY_PREFIX
|
||||||
winePath = path.join(protonFolder, prefixes);
|
);
|
||||||
if (!fs.pathExistsSync(winePath)) {
|
if (!fs.pathExistsSync(winePath)) {
|
||||||
winePath = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (winePath === "") {
|
|
||||||
throw new Error(`"${winePath}" binary file not found`);
|
throw new Error(`"${winePath}" binary file not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.winePath = winePath;
|
|
||||||
return winePath;
|
return winePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be different from winePath, this is the "WINEPREFIX" env var
|
|
||||||
// that points to the wine windows files directory
|
|
||||||
public getWinePrefixPath(): string {
|
public getWinePrefixPath(): string {
|
||||||
const compatDataPath = this.getCompatDataPath();
|
const compatDataPath = this.getCompatDataPath();
|
||||||
return fs.existsSync(compatDataPath)
|
return fs.existsSync(compatDataPath)
|
||||||
@@ -198,42 +174,18 @@ export class LinuxService {
|
|||||||
|
|
||||||
// === Shortcuts === //
|
// === Shortcuts === //
|
||||||
|
|
||||||
private async getCommand(
|
private getCommand(
|
||||||
launchOptions: LaunchOption,
|
protonPrefix: string,
|
||||||
steamPath: string,
|
bsFolderPath: string,
|
||||||
beatSaberFolderPath: string
|
env: Record<string, string>,
|
||||||
): Promise<string> {
|
launchOptions: LaunchOption
|
||||||
const protonPrefix = await this.getProtonPrefix();
|
): string {
|
||||||
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)
|
const envString = Object.entries(env)
|
||||||
.map(([ key, value ]) => `${key}="${value}"`)
|
.map(([ key, value ]) => `${key}="${value}"`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
return `${envString} ${cmdlet} ${args.join(" ")}`;
|
const bsExe = path.join(bsFolderPath, BS_EXECUTABLE);
|
||||||
|
const args = buildBsLaunchArgs(launchOptions).join(" ");
|
||||||
|
return `${envString} ${protonPrefix} "${bsExe}" ${args}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createDesktopShortcut(
|
public async createDesktopShortcut(
|
||||||
@@ -242,11 +194,22 @@ export class LinuxService {
|
|||||||
icon: string,
|
icon: string,
|
||||||
launchOptions: LaunchOption,
|
launchOptions: LaunchOption,
|
||||||
steamPath: string,
|
steamPath: string,
|
||||||
beatSaberFolderPath: string
|
bsFolderPath: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const command = await this.getCommand(
|
const {
|
||||||
launchOptions, steamPath, beatSaberFolderPath
|
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 desktopEntry = [
|
const desktopEntry = [
|
||||||
@@ -254,7 +217,7 @@ export class LinuxService {
|
|||||||
"Type=Application",
|
"Type=Application",
|
||||||
`Name=${name}`,
|
`Name=${name}`,
|
||||||
`Icon=${icon}`,
|
`Icon=${icon}`,
|
||||||
`Path=${beatSaberFolderPath}`,
|
`Path=${bsFolderPath}`,
|
||||||
`Exec=${command}`
|
`Exec=${command}`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
@@ -272,20 +235,31 @@ export class LinuxService {
|
|||||||
icon: string,
|
icon: string,
|
||||||
launchOptions: LaunchOption,
|
launchOptions: LaunchOption,
|
||||||
steamPath: string,
|
steamPath: string,
|
||||||
beatSaberFolderPath: string
|
bsFolderPath: string
|
||||||
): Promise<SteamShortcutData> {
|
): Promise<SteamShortcutData> {
|
||||||
const protonPath = await this.getProtonPath();
|
const env = await this.buildEnvVariables(
|
||||||
const command = await this.getCommand(
|
launchOptions, steamPath, bsFolderPath
|
||||||
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 {
|
return {
|
||||||
AppName: shortcutName,
|
AppName: shortcutName,
|
||||||
Exe: protonPath,
|
Exe: await this.getProtonPath(),
|
||||||
StartDir: beatSaberFolderPath,
|
StartDir: bsFolderPath,
|
||||||
icon,
|
icon,
|
||||||
OpenVR: "\x01",
|
OpenVR: "\x01",
|
||||||
LaunchOptions: command
|
LaunchOptions: this.getCommand(
|
||||||
|
protonPrefix, bsFolderPath,
|
||||||
|
env, launchOptions
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class BeatModsApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getVersionModsUrl(version: BSVersion): string {
|
private getVersionModsUrl(version: BSVersion): string {
|
||||||
|
// TODO: This endpoint is now deprecated
|
||||||
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
|
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}`;
|
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,8 @@ export class BeatModsApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
|
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
|
||||||
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`
|
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`,
|
||||||
|
{ silentError: true }
|
||||||
).then(({ data }) => {
|
).then(({ data }) => {
|
||||||
this.updateModsHashCache(data?.modVersions ?? []);
|
this.updateModsHashCache(data?.modVersions ?? []);
|
||||||
return data?.modVersions?.at(0);
|
return data?.modVersions?.at(0);
|
||||||
|
|||||||
@@ -49,11 +49,12 @@ export class BsModsManagerService {
|
|||||||
return undefined;
|
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 undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getModsInDir(
|
private async getModsInDir(
|
||||||
@@ -144,9 +145,9 @@ export class BsModsManagerService {
|
|||||||
|
|
||||||
return BsmZipExtractor.fromBuffer(buffer);
|
return BsmZipExtractor.fromBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
|
||||||
|
|
||||||
private async executeIPA(version: BSVersion, args: string[]): Promise<boolean> {
|
log.info("executeBSIPA", version?.BSVersion, args);
|
||||||
log.info("executeIPA", version?.BSVersion, args);
|
|
||||||
|
|
||||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||||
const ipaPath = path.join(versionPath, "IPA.exe");
|
const ipaPath = path.join(versionPath, "IPA.exe");
|
||||||
@@ -156,24 +157,43 @@ export class BsModsManagerService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = await this.getCommand(ipaPath, bsExePath, args);
|
const env: Record<string, string> = {};
|
||||||
if (!command) {
|
const cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
|
||||||
return false;
|
let winePath: string = "";
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
const { error: winePathError, result: winePathResult } =
|
||||||
|
tryit(() => this.linuxService.getWinePath());
|
||||||
|
if (winePathError) {
|
||||||
|
log.error(winePathError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
env.WINEPREFIX = winePrefix;
|
||||||
|
Object.assign(env, process.env);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<boolean>(resolve => {
|
return new Promise<boolean>(resolve => {
|
||||||
const processIPA = bsmSpawn(command.command, {
|
const processIPA = bsmSpawn(cmd, {
|
||||||
log: BsmShellLog.Command | BsmShellLog.EnvVariables,
|
log: BsmShellLog.Command | BsmShellLog.EnvVariables,
|
||||||
options: {
|
options: {
|
||||||
cwd: versionPath,
|
cwd: versionPath,
|
||||||
detached: true,
|
detached: true,
|
||||||
shell: true,
|
shell: true,
|
||||||
env: command.env
|
env
|
||||||
},
|
},
|
||||||
|
linux: { prefix: winePath },
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
log.info("IPA process timed out");
|
log.info("Ipa process timeout");
|
||||||
resolve(false)
|
resolve(false)
|
||||||
}, sToMs(30));
|
}, sToMs(30));
|
||||||
|
|
||||||
@@ -187,55 +207,16 @@ export class BsModsManagerService {
|
|||||||
processIPA.once("exit", code => {
|
processIPA.once("exit", code => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
log.info("Ipa process exist with code 0");
|
||||||
return resolve(true);
|
return resolve(true);
|
||||||
}
|
}
|
||||||
log.error("IPA process exited with non-zero code", code);
|
log.error("Ipa process exist with non 0 code", code);
|
||||||
resolve(false);
|
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 {
|
private getModDownload(modVersion: BbmModVersion): string {
|
||||||
return `/cdn/mod/${modVersion.zipHash}.zip`
|
return `/cdn/mod/${modVersion.zipHash}.zip`
|
||||||
}
|
}
|
||||||
@@ -245,7 +226,7 @@ export class BsModsManagerService {
|
|||||||
|
|
||||||
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
|
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));
|
await this.clearIpaFolder(version).catch(e => log.error("Error while clearing IPA folder", e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,11 +273,9 @@ export class BsModsManagerService {
|
|||||||
|
|
||||||
log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted);
|
log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted);
|
||||||
|
|
||||||
// Executing IPA.exe when BSIPA is already installed could potentially corrupt the game.
|
const res = isBSIPA
|
||||||
const shouldRunIPA = isBSIPA && !pathExistsSync(path.join(versionPath, "winhttp.dll"));
|
|
||||||
const res = shouldRunIPA
|
|
||||||
? extracted &&
|
? extracted &&
|
||||||
(await this.executeIPA(version, ["-n"]).catch(e => {
|
(await this.executeBSIPA(version, ["-n"]).catch(e => {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
return false;
|
return false;
|
||||||
}))
|
}))
|
||||||
@@ -311,14 +290,14 @@ export class BsModsManagerService {
|
|||||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||||
const ipaPath = path.join(versionPath, ModsInstallFolder.IPA);
|
const ipaPath = path.join(versionPath, ModsInstallFolder.IPA);
|
||||||
|
|
||||||
if (!pathExistsSync(ipaPath)) {
|
if(!pathExistsSync(ipaPath)){
|
||||||
log.info("IPA folder does not exist, skipping");
|
log.info("IPA folder does not exist, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contents = readdirSync(ipaPath, { withFileTypes: true });
|
const contents = readdirSync(ipaPath, { withFileTypes: true });
|
||||||
|
|
||||||
for (const content of contents) {
|
for(const content of contents){
|
||||||
|
|
||||||
if (content.name === 'Backups' || content.name === 'Pending') {
|
if (content.name === 'Backups' || content.name === 'Pending') {
|
||||||
continue;
|
continue;
|
||||||
@@ -331,12 +310,13 @@ export class BsModsManagerService {
|
|||||||
: deleteFile(contentPath)
|
: deleteFile(contentPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.error) {
|
if(res.error){
|
||||||
log.error("Error while clearing IPA folder content", content.name, res.error);
|
log.error("Error while clearing IPA folder content", content.name, res.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("IPA folder cleared successfully");
|
log.info("IPA folder cleared successfully");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||||
@@ -348,18 +328,15 @@ export class BsModsManagerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.executeIPA(version, ["--revert", "-n"]);
|
await this.executeBSIPA(version, ["--revert", "-n"]);
|
||||||
|
|
||||||
const promises = mod.version.contentHashes.map(content => {
|
const promises = mod.version.contentHashes.map(content => {
|
||||||
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||||
return deleteFile(path.join(verionPath, file)).catch(err => {
|
return deleteFile(path.join(verionPath, file));
|
||||||
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);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||||
if (mod.mod.name.toLowerCase() === "bsipa") {
|
if (mod.mod.name.toLowerCase() === "bsipa") {
|
||||||
return this.uninstallBSIPA(mod, version);
|
return this.uninstallBSIPA(mod, version);
|
||||||
@@ -370,13 +347,13 @@ export class BsModsManagerService {
|
|||||||
const promises: Promise<void>[] = mod.version.contentHashes.map(async content => {
|
const promises: Promise<void>[] = mod.version.contentHashes.map(async content => {
|
||||||
return (async () => {
|
return (async () => {
|
||||||
const modPath = path.join(versionPath, content.path);
|
const modPath = path.join(versionPath, content.path);
|
||||||
if (pathExistsSync(modPath)) {
|
if(pathExistsSync(modPath)){
|
||||||
log.info("Deleting mod", modPath);
|
log.info("Deleting mod", modPath);
|
||||||
await deleteFile(modPath);
|
await deleteFile(modPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingPath = path.join(versionPath, "IPA", "Pending", content.path);
|
const pendingPath = path.join(versionPath, "IPA", "Pending", content.path);
|
||||||
if (pathExistsSync(pendingPath)) {
|
if(pathExistsSync(pendingPath)){
|
||||||
log.info("Deleting pending mod", pendingPath);
|
log.info("Deleting pending mod", pendingPath);
|
||||||
return deleteFile(pendingPath);
|
return deleteFile(pendingPath);
|
||||||
}
|
}
|
||||||
@@ -540,12 +517,12 @@ export class BsModsManagerService {
|
|||||||
|
|
||||||
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
|
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
|
||||||
|
|
||||||
if (bsipa) {
|
if(bsipa){
|
||||||
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
|
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
|
||||||
log.error("Error while installing BSIPA", err);
|
log.error("Error while installing BSIPA", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bsipaInstalled) {
|
if(!bsipaInstalled){
|
||||||
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
|
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,12 +105,8 @@ export class OculusService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async oculusRunning(): Promise<boolean> {
|
public oculusRunning(): Promise<boolean> {
|
||||||
if (await isProcessRunning("OculusClient")) {
|
return isProcessRunning("OculusClient");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isProcessRunning("Client"); // new name of oculus client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startOculus(): Promise<void>{
|
public async startOculus(): Promise<void>{
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import { tryit } from 'shared/helpers/error.helpers';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
import sanitize from 'sanitize-filename';
|
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 {
|
export class RequestService {
|
||||||
private static instance: RequestService;
|
private static instance: RequestService;
|
||||||
@@ -19,224 +17,49 @@ export class RequestService {
|
|||||||
'User-Agent': `BSManager/${app.getVersion()} (Electron/${process.versions.electron} Chrome/${process.versions.chrome} Node/${process.versions.node})`,
|
'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 {
|
public static getInstance(): RequestService {
|
||||||
if (!RequestService.instance) {
|
if (!RequestService.instance) {
|
||||||
RequestService.instance = new RequestService();
|
RequestService.instance = new RequestService();
|
||||||
}
|
}
|
||||||
return RequestService.instance;
|
return RequestService.instance;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
private isBeatmodsUrl(url: string): boolean {
|
public async getJSON<T = unknown>(url: string, options?: {
|
||||||
const { hostname } = new URL(url);
|
silentError?: boolean
|
||||||
return hostname === 'beatmods.com' || hostname.endsWith('.beatmods.com');
|
}): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* 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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||||
dnsLookupIpVersion: family,
|
const res = await got(url, { responseType: 'json', headers: this.baseHeaders });
|
||||||
cookieJar,
|
return { data: res.body as T, headers: res.headers };
|
||||||
headers: this.baseHeaders
|
} catch (err) {
|
||||||
});
|
if (options?.silentError !== true) {
|
||||||
|
log.error(`Failed to get JSON from URL: ${url}`, err);
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public downloadFile(
|
||||||
* 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,
|
url: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
opt?: { preferContentDisposition?: boolean }
|
opt?: { preferContentDisposition?: boolean }
|
||||||
): Observable<Progression<string>> {
|
): Observable<Progression<string>> {
|
||||||
return new Observable<Progression<string>>((subscriber) => {
|
return new Observable<Progression<string>>((subscriber) => {
|
||||||
const progress: Progression<string> = { current: 0, total: 0 };
|
const progress: Progression<string> = { current: 0, total: 0 };
|
||||||
|
|
||||||
let file: WriteStream | undefined;
|
let file: WriteStream | undefined;
|
||||||
let isCompleted = false;
|
|
||||||
|
|
||||||
const request = net.request({
|
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||||
method: 'GET',
|
const stream = got.stream(url, { headers: this.baseHeaders });
|
||||||
url,
|
|
||||||
headers: this.baseHeaders,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cleanup = () => {
|
stream.on('response', (response) => {
|
||||||
if (file) {
|
|
||||||
file.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
request.on('response', (response) => {
|
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
|
||||||
// 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) {
|
if (filename) {
|
||||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
dest = path.join(path.dirname(dest), sanitize(filename));
|
||||||
@@ -245,145 +68,30 @@ export class RequestService {
|
|||||||
progress.data = dest;
|
progress.data = dest;
|
||||||
file = createWriteStream(dest);
|
file = createWriteStream(dest);
|
||||||
|
|
||||||
file.on('error', (error) => {
|
pipeline(stream, file).catch(err => {
|
||||||
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 };
|
|
||||||
|
|
||||||
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}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const family = familiesToTry[attempt++];
|
|
||||||
let file: WriteStream | undefined;
|
|
||||||
|
|
||||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
|
||||||
stream = got.stream(url, { dnsLookupIpVersion: family, headers: this.baseHeaders });
|
|
||||||
|
|
||||||
stream.on('response', (response) => {
|
|
||||||
if (!cachedFamily) {
|
|
||||||
log.info(`Caching "${domain}" with IPv${family}`);
|
|
||||||
this.preferredFamilyCache[domain] = family;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
|
|
||||||
|
|
||||||
if (filename) {
|
|
||||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.data = dest;
|
|
||||||
file = createWriteStream(dest);
|
|
||||||
|
|
||||||
pipeline(stream, file).catch(err => {
|
|
||||||
file?.destroy();
|
|
||||||
tryit(() => deleteFileSync(dest));
|
|
||||||
subscriber.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
|
||||||
progress.current = transferred;
|
|
||||||
progress.total = total;
|
|
||||||
subscriber.next(progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', err => {
|
|
||||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
|
||||||
stream.destroy();
|
|
||||||
file?.destroy();
|
file?.destroy();
|
||||||
tryNextFamily();
|
tryit(() => deleteFileSync(dest));
|
||||||
|
subscriber.error(err);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
stream.on('end', () => {
|
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||||
file?.end();
|
progress.current = transferred;
|
||||||
subscriber.next(progress);
|
progress.total = total;
|
||||||
subscriber.complete();
|
subscriber.next(progress);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
tryNextFamily();
|
stream.on('error', err => {
|
||||||
|
log.error(`Download failed for URL: ${url}`, err);
|
||||||
|
stream.destroy();
|
||||||
|
file?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
file?.end();
|
||||||
|
subscriber.next(progress);
|
||||||
|
subscriber.complete();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stream?.destroy();
|
stream?.destroy();
|
||||||
@@ -394,111 +102,10 @@ export class RequestService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(
|
public downloadBuffer(
|
||||||
url: string,
|
url: string,
|
||||||
options?: got.GotOptions<null>
|
options?: got.GotOptions<null>
|
||||||
): Observable<Progression<Buffer, IncomingMessage>> {
|
): 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) => {
|
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
|
||||||
const progress: Progression<Buffer, IncomingMessage> = {
|
const progress: Progression<Buffer, IncomingMessage> = {
|
||||||
@@ -509,60 +116,37 @@ export class RequestService {
|
|||||||
|
|
||||||
const headers = { ...this.baseHeaders, ...(options?.headers ?? {}) };
|
const headers = { ...this.baseHeaders, ...(options?.headers ?? {}) };
|
||||||
|
|
||||||
let attempt = 0;
|
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||||
let stream: got.GotEmitter & internal.Duplex;
|
const stream = got.stream(url, { ...(options ?? {}), headers });
|
||||||
|
|
||||||
const domain = (new URL(url)).hostname;
|
let data = Buffer.alloc(0);
|
||||||
const cachedFamily = this.preferredFamilyCache[domain];
|
let response: IncomingMessage;
|
||||||
const familiesToTry = cachedFamily
|
|
||||||
? [ cachedFamily ] : this.PREFERRED_FAMILY_TESTS;
|
|
||||||
|
|
||||||
const tryNextFamily = () => {
|
stream.once('response', (res) => {
|
||||||
if (attempt >= familiesToTry.length) {
|
response = res;
|
||||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const family = familiesToTry[attempt++];
|
stream.on('data', (chunk: Buffer) => {
|
||||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
data = Buffer.concat([data, chunk]);
|
||||||
stream = got.stream(url, { dnsLookupIpVersion: family, ...(options ?? {}), headers });
|
});
|
||||||
|
|
||||||
let data = Buffer.alloc(0);
|
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||||
let response: IncomingMessage;
|
progress.current = transferred;
|
||||||
|
progress.total = total;
|
||||||
|
subscriber.next(progress);
|
||||||
|
});
|
||||||
|
|
||||||
stream.once('response', (res) => {
|
stream.on('error', err => {
|
||||||
if (!cachedFamily) {
|
log.error(`Download failed for URL: ${url}`, err);
|
||||||
log.info(`Caching "${domain}" with IPv${family}`);
|
stream.destroy();
|
||||||
this.preferredFamilyCache[domain] = family;
|
});
|
||||||
}
|
|
||||||
response = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('data', (chunk: Buffer) => {
|
stream.on('end', () => {
|
||||||
data = Buffer.concat([data, chunk]);
|
progress.data = data;
|
||||||
});
|
progress.extra = response;
|
||||||
|
subscriber.next(progress);
|
||||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
subscriber.complete();
|
||||||
progress.current = transferred;
|
});
|
||||||
progress.total = total;
|
|
||||||
subscriber.next(progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('error', err => {
|
|
||||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
|
||||||
stream.destroy();
|
|
||||||
tryNextFamily();
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', () => {
|
|
||||||
progress.data = data;
|
|
||||||
progress.extra = response;
|
|
||||||
subscriber.next(progress);
|
|
||||||
subscriber.complete();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
tryNextFamily();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stream?.destroy();
|
stream?.destroy();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import ElectronStore from "electron-store";
|
import ElectronStore from "electron-store";
|
||||||
|
import { pathExistsSync } from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||||
import { Observable, Subject } from "rxjs";
|
import { Observable, Subject } from "rxjs";
|
||||||
|
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||||
import { BSVersion } from "shared/bs-version.interface";
|
import { BSVersion } from "shared/bs-version.interface";
|
||||||
import { AutoUpdate } from "shared/models/config";
|
|
||||||
|
|
||||||
export class StaticConfigurationService {
|
export class StaticConfigurationService {
|
||||||
private static instance: StaticConfigurationService;
|
private static instance: StaticConfigurationService;
|
||||||
@@ -27,8 +30,8 @@ export class StaticConfigurationService {
|
|||||||
return this.store.has(key);
|
return this.store.has(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get<K extends StaticConfigKeys>(key: K, defaultValue?: StaticConfigKeyValues[K]): StaticConfigKeyValues[K] {
|
public get<K extends StaticConfigKeys>(key: K): StaticConfigKeyValues[K] {
|
||||||
return this.store.get<K>(key, defaultValue) as StaticConfigKeyValues[K];
|
return this.store.get<K>(key) as StaticConfigKeyValues[K];
|
||||||
}
|
}
|
||||||
|
|
||||||
public take<K extends StaticConfigKeys>(key: K, cb: (val: StaticConfigKeyValues[K]) => void): void {
|
public take<K extends StaticConfigKeys>(key: K, cb: (val: StaticConfigKeyValues[K]) => void): void {
|
||||||
@@ -36,6 +39,16 @@ export class StaticConfigurationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): Promise<void> {
|
public async set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): Promise<void> {
|
||||||
|
// Validate the setters
|
||||||
|
switch (key) {
|
||||||
|
case "proton-folder":
|
||||||
|
this.validateProtonFolder(value as string);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.store.set(key, value);
|
this.store.set(key, value);
|
||||||
|
|
||||||
if (this.watchers[key]) {
|
if (this.watchers[key]) {
|
||||||
@@ -43,6 +56,16 @@ export class StaticConfigurationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setters with validation
|
||||||
|
|
||||||
|
private validateProtonFolder(protonFolder: string): void {
|
||||||
|
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||||
|
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||||
|
if (!pathExistsSync(protonPath) || !pathExistsSync(winePath)) {
|
||||||
|
throw new CustomError("Invalid proton folder path", "invalid-folder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public delete<K extends StaticConfigKeys>(key: K): void {
|
public delete<K extends StaticConfigKeys>(key: K): void {
|
||||||
this.store.delete(key);
|
this.store.delete(key);
|
||||||
}
|
}
|
||||||
@@ -67,7 +90,6 @@ export interface StaticConfigKeyValues {
|
|||||||
"use-symlinks": boolean;
|
"use-symlinks": boolean;
|
||||||
"use-system-proxy": boolean;
|
"use-system-proxy": boolean;
|
||||||
"last-version-launched": BSVersion;
|
"last-version-launched": BSVersion;
|
||||||
"auto-update": AutoUpdate;
|
|
||||||
|
|
||||||
// Linux Specific static configs
|
// Linux Specific static configs
|
||||||
"proton-folder": string;
|
"proton-folder": string;
|
||||||
|
|||||||
+17
-10
@@ -6,12 +6,13 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
|||||||
import { LaunchOption } from "shared/models/bs-launch";
|
import { LaunchOption } from "shared/models/bs-launch";
|
||||||
import { useService } from "renderer/hooks/use-service.hook";
|
import { useService } from "renderer/hooks/use-service.hook";
|
||||||
import { BSLauncherService } from "renderer/services/bs-launcher.service";
|
import { BSLauncherService } from "renderer/services/bs-launcher.service";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
|
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
|
||||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
|
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}}) => {
|
export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => {
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
|
|||||||
const [command, setCommand] = useState(launchOption.command || "");
|
const [command, setCommand] = useState(launchOption.command || "");
|
||||||
const [steamShortcut, setSteamShortcut] = useState(false);
|
const [steamShortcut, setSteamShortcut] = useState(false);
|
||||||
|
|
||||||
|
const isSteamVersion = useMemo(() => {
|
||||||
|
return data.steam || data.metadata?.store === BsStore.STEAM;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const completeModal = () => {
|
const completeModal = () => {
|
||||||
launchOption.command = command.trim();
|
launchOption.command = command.trim();
|
||||||
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
|
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
|
||||||
@@ -91,15 +96,17 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
{isSteamVersion && (
|
||||||
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
||||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
||||||
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
||||||
</div>
|
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
||||||
</Tippy>
|
</div>
|
||||||
<div className="grid grid-flow-col grid-cols-2 gap-4 h-8">
|
</Tippy>
|
||||||
<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 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" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
|||||||
setMaps(prev => [...prev, ...maps])
|
setMaps(prev => [...prev, ...maps])
|
||||||
|
|
||||||
if (maps.length < tryToLoad) {
|
if (maps.length < tryToLoad) {
|
||||||
handleLoadMore(false);
|
handleLoadMore();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -185,9 +185,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
|||||||
setSearchParams(() => searchParamsLocal);
|
setSearchParams(() => searchParamsLocal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadMore = (skipLoading: boolean = true) => {
|
const handleLoadMore = () => {
|
||||||
|
|
||||||
if(skipLoading && loading){ return; }
|
if(loading){ return; }
|
||||||
|
|
||||||
setSearchParams(prev => {
|
setSearchParams(prev => {
|
||||||
return { ...prev, page: prev.page + 1 };
|
return { ...prev, page: prev.page + 1 };
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ export function Modal() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{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}
|
{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}
|
||||||
{modals?.map(modal => (
|
{modals?.map(modal => (
|
||||||
<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" }}>
|
<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" }}>
|
||||||
{renderModal(modal)}
|
{renderModal(modal)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ type Props<T> = {
|
|||||||
selectedItemId?: number;
|
selectedItemId?: number;
|
||||||
selectedItemValue?: T;
|
selectedItemValue?: T;
|
||||||
direction?: CSSProperties["flexDirection"],
|
direction?: CSSProperties["flexDirection"],
|
||||||
columnCount?: number;
|
|
||||||
onItemSelected?: (item: RadioItem<T>) => void
|
onItemSelected?: (item: RadioItem<T>) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column",columnCount = 1, }: Props<T>) {
|
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column" }: Props<T>) {
|
||||||
const t = useTranslation();
|
const t = useTranslation();
|
||||||
|
|
||||||
const isSelected = (item: RadioItem<T>) => {
|
const isSelected = (item: RadioItem<T>) => {
|
||||||
@@ -20,17 +19,17 @@ export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemVa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className="w-full grid gap-1.5" style={{flexDirection: direction, gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`}}>
|
<div id={id} className="w-full flex gap-1.5" style={{flexDirection: direction}}>
|
||||||
{items.map(i => (
|
{items.map(i => (
|
||||||
<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 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 className="flex items-center">
|
<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">
|
<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" />
|
<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>
|
</div>
|
||||||
<h2 className="font-extrabold text-nowrap">{t(i.text)}</h2>
|
<h2 className="font-extrabold">{t(i.text)}</h2>
|
||||||
</div>
|
</div>
|
||||||
{i.icon && (
|
{i.icon && (
|
||||||
<div className="flex items-center text-right">
|
<div className="flex items-center">
|
||||||
{i.textIcon && <span className="text-sm">{t(i.textIcon)}</span>}
|
{i.textIcon && <span className="text-sm">{t(i.textIcon)}</span>}
|
||||||
{i.icon}
|
{i.icon}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,47 +12,36 @@ import { useService } from "renderer/hooks/use-service.hook";
|
|||||||
import { lastValueFrom } from "rxjs";
|
import { lastValueFrom } from "rxjs";
|
||||||
import { useWindowControls } from "renderer/hooks/use-window-controls.hook";
|
import { useWindowControls } from "renderer/hooks/use-window-controls.hook";
|
||||||
import { useTranslationV2 } from "renderer/hooks/use-translation.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 useVersion() {
|
function TitleBarTags() {
|
||||||
const ipcService = useService(IpcService);
|
const ipcService = useService(IpcService);
|
||||||
|
const t = useTranslationV2();
|
||||||
|
|
||||||
const [currentVersion, setCurrentVersion] = useState("");
|
const [previewVersion, setPreviewVersion] = useState("");
|
||||||
const [latestVersion, setLatestVersion] = useState<UpdateInfo | null>(null);
|
const [outdated, setOutdated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
const requests: Promise<any>[] = [
|
||||||
lastValueFrom(ipcService.sendV2("current-version")),
|
lastValueFrom(ipcService.sendV2("current-version"))
|
||||||
lastValueFrom(ipcService.sendV2("get-available-update"))
|
];
|
||||||
]).then(([ currentVersion, latestVersion ]) => {
|
if (window.electron.platform === "linux") {
|
||||||
setCurrentVersion(currentVersion);
|
requests.push(lastValueFrom(ipcService.sendV2("check-update")));
|
||||||
setLatestVersion(latestVersion);
|
}
|
||||||
|
|
||||||
|
Promise.all(requests).then(([ currentVersion, outdated ]) => {
|
||||||
|
handlePrerelease(currentVersion);
|
||||||
|
setOutdated(outdated);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { currentVersion, latestVersion };
|
const handlePrerelease = (version: string) => {
|
||||||
}
|
|
||||||
|
|
||||||
function TitleBarTags({ version, latestVersion }: Readonly<{
|
|
||||||
version: string;
|
|
||||||
latestVersion: UpdateInfo | null;
|
|
||||||
}>) {
|
|
||||||
const t = useTranslationV2();
|
|
||||||
|
|
||||||
const previewVersion = (() => {
|
|
||||||
if (version.toLowerCase().includes("alpha")) {
|
if (version.toLowerCase().includes("alpha")) {
|
||||||
return "ALPHA";
|
return setPreviewVersion("ALPHA");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version.toLowerCase().includes("beta")) {
|
if (version.toLowerCase().includes("beta")) {
|
||||||
return "BETA";
|
return setPreviewVersion("BETA");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return "";
|
|
||||||
})();
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{previewVersion &&
|
{previewVersion &&
|
||||||
@@ -60,7 +49,7 @@ function TitleBarTags({ version, latestVersion }: Readonly<{
|
|||||||
{previewVersion}
|
{previewVersion}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{latestVersion &&
|
{outdated &&
|
||||||
<span className="bg-warning-500 text-black rounded-full ml-1 text-[10px] italic px-1 uppercase h-3.5 font-bold">
|
<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")}
|
{t.text("title-bar.outdated")}
|
||||||
</span>
|
</span>
|
||||||
@@ -68,65 +57,12 @@ function TitleBarTags({ version, latestVersion }: Readonly<{
|
|||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }) {
|
export default function TitleBar({ template = "index.html" }: { template: AppWindow }) {
|
||||||
const audio = useService(AudioPlayerService);
|
const audio = useService(AudioPlayerService);
|
||||||
const windowControls = useWindowControls();
|
const windowControls = useWindowControls();
|
||||||
|
|
||||||
const volume = useObservable(() => audio.volume$, audio.volume);
|
const volume = useObservable(() => audio.volume$, audio.volume);
|
||||||
const color = useThemeColor("first-color");
|
const color = useThemeColor("first-color");
|
||||||
const { currentVersion, latestVersion } = useVersion();
|
|
||||||
|
|
||||||
const [maximized, setMaximized] = useState(false);
|
const [maximized, setMaximized] = useState(false);
|
||||||
|
|
||||||
@@ -171,7 +107,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
|||||||
<div id="drag-region" className="grow basis-0 h-full">
|
<div id="drag-region" className="grow basis-0 h-full">
|
||||||
<div id="window-title" className="pl-1">
|
<div id="window-title" className="pl-1">
|
||||||
<span className="text-gray-800 dark:text-gray-100 font-bold text-xs italic">BSManager</span>
|
<span className="text-gray-800 dark:text-gray-100 font-bold text-xs italic">BSManager</span>
|
||||||
<TitleBarTags version={currentVersion} latestVersion={latestVersion} />
|
<TitleBarTags />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
||||||
@@ -181,7 +117,6 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
|||||||
</div>
|
</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()} />
|
<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>
|
</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">
|
<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">
|
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
|
||||||
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
|
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||||
@@ -30,13 +30,29 @@ import { CreateCustomLaunchOptionModal } from "renderer/components/modal/modal-t
|
|||||||
|
|
||||||
type Props = { version: BSVersion };
|
type Props = { version: BSVersion };
|
||||||
|
|
||||||
function useLaunchMods() {
|
export function LaunchSlide({ version }: Props) {
|
||||||
const bsLauncherService = useService(BSLauncherService);
|
const { text: t, element: te } = useTranslationV2();
|
||||||
const configService = useService(ConfigurationService);
|
|
||||||
|
|
||||||
|
const configService = useService(ConfigurationService);
|
||||||
|
const bsLauncherService = useService(BSLauncherService);
|
||||||
|
const bsDownloader = useService(BsDownloaderService);
|
||||||
|
const versions = useService(BSVersionManagerService);
|
||||||
|
const modal = useService(ModalService);
|
||||||
|
|
||||||
|
const [advancedLaunch, setAdvancedLaunch] = useState(false);
|
||||||
|
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
|
||||||
|
const customLaunchOptions = useObservable<CustomLaunchOption[]>(() => configService.watch<CustomLaunchOption[]>("custom-launch-options"), []);
|
||||||
|
const [customLaunchModsArgs, setCustomLaunchModsArgs] = useState<string[]>([]);
|
||||||
|
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
|
||||||
const [activeLaunchMods, setActiveLaunchMods] = useState<string[]>(configService.get("launch-mods") ?? []);
|
const [activeLaunchMods, setActiveLaunchMods] = useState<string[]>(configService.get("launch-mods") ?? []);
|
||||||
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<string[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
|
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<string[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
|
||||||
|
|
||||||
|
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configService.set("launch-command", command);
|
||||||
|
}, [command]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configService.set("pinned-launch-mods", pinnedLaunchMods);
|
configService.set("pinned-launch-mods", pinnedLaunchMods);
|
||||||
}, [pinnedLaunchMods])
|
}, [pinnedLaunchMods])
|
||||||
@@ -49,66 +65,24 @@ function useLaunchMods() {
|
|||||||
}
|
}
|
||||||
}, [activeLaunchMods]);
|
}, [activeLaunchMods]);
|
||||||
|
|
||||||
const toggleActiveLaunchMod = useCallback((checked: boolean, launchMod: string) => checked
|
const toggleActiveLaunchMod = (checked: boolean, launchMod: string) => checked
|
||||||
? setActiveLaunchMods(prev => [...prev, launchMod])
|
? setActiveLaunchMods(prev => [...prev, launchMod])
|
||||||
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
|
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||||
[]);
|
|
||||||
|
|
||||||
const togglePinnedLaunchMod = useCallback((pinned: boolean, launchMod: string) => pinned
|
const togglePinnedLaunchMod = (pinned: boolean, launchMod: string) => pinned
|
||||||
? setPinnedLaunchMods(prev => [...prev, launchMod])
|
? setPinnedLaunchMods(prev => [...prev, launchMod])
|
||||||
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
|
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||||
[]);
|
|
||||||
|
|
||||||
return {
|
const launchModItems = useMemo<LaunchModItemProps[]>(() => {
|
||||||
activeLaunchMods,
|
|
||||||
pinnedLaunchMods,
|
|
||||||
toggleActiveLaunchMod,
|
|
||||||
togglePinnedLaunchMod,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCustomLaunchMods({
|
let protonLogsPath: string[] = [];
|
||||||
activeLaunchMods, pinnedLaunchMods,
|
if (window.electron.platform === "linux") {
|
||||||
toggleActiveLaunchMod,
|
protonLogsPath = version.steam
|
||||||
togglePinnedLaunchMod,
|
? [version.path, "Logs"]
|
||||||
}: {
|
: ["BSInstances", version.name, "Logs"];
|
||||||
activeLaunchMods: string[];
|
}
|
||||||
pinnedLaunchMods: string[];
|
|
||||||
|
|
||||||
toggleActiveLaunchMod: (checked: boolean, launchMod: string) => void;
|
const customOptions = customLaunchOptions?.map<LaunchModItemProps>(option => ({
|
||||||
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,
|
id: option.id,
|
||||||
label: option.label,
|
label: option.label,
|
||||||
active: activeLaunchMods.includes(option.id),
|
active: activeLaunchMods.includes(option.id),
|
||||||
@@ -130,57 +104,6 @@ function useCustomLaunchMods({
|
|||||||
deleteCustomLaunchOption(option.id);
|
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[]>(() => {
|
|
||||||
|
|
||||||
let protonLogsPath: string[] = [];
|
|
||||||
if (window.electron.platform === "linux") {
|
|
||||||
protonLogsPath = version.steam
|
|
||||||
? [version.path, "Logs"]
|
|
||||||
: ["BSInstances", version.name, "Logs"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const customOptions = getCustomLaunch();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -264,6 +187,19 @@ export function LaunchSlide({ version }: Props) {
|
|||||||
return safeLt(version?.BSVersion, versions.getRecommendedVersion()?.BSVersion);
|
return safeLt(version?.BSVersion, versions.getRecommendedVersion()?.BSVersion);
|
||||||
}, [version]);
|
}, [version]);
|
||||||
|
|
||||||
|
const saveCustomLaunchOption = async (option: Partial<CustomLaunchOption>) => {
|
||||||
|
const result = await modal.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCustomLaunchOption = (id: string) => {
|
||||||
|
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== id);
|
||||||
|
configService.set("custom-launch-options", newCustomLaunchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full shrink-0 items-center relative flex flex-col justify-start overflow-hidden">
|
<div className="w-full shrink-0 items-center relative flex flex-col justify-start overflow-hidden">
|
||||||
<div className="flex flex-col gap-3 justify-center items-center mb-4">
|
<div className="flex flex-col gap-3 justify-center items-center mb-4">
|
||||||
|
|||||||
@@ -219,10 +219,9 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
|||||||
}), [version]);
|
}), [version]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCancelled = false;
|
|
||||||
|
|
||||||
if(!isActive){
|
if(!isActive){
|
||||||
return noop;
|
return noop();
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDisclaimerAccepted().then(async canLoad => {
|
ensureDisclaimerAccepted().then(async canLoad => {
|
||||||
@@ -230,30 +229,13 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
|||||||
return onDisclamerDecline?.();
|
return onDisclamerDecline?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled) return;
|
|
||||||
|
|
||||||
const status = await modsManager.getModsGridStatus();
|
const status = await modsManager.getModsGridStatus();
|
||||||
|
|
||||||
if (isCancelled) return;
|
|
||||||
|
|
||||||
setGridStatus(() => status);
|
setGridStatus(() => status);
|
||||||
|
|
||||||
if (status !== ModsGridStatus.OK) {
|
loadMods();
|
||||||
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 () => {
|
return () => {
|
||||||
isCancelled = true;
|
|
||||||
setMoreInfoMod(null);
|
setMoreInfoMod(null);
|
||||||
setModsAvailable(null);
|
setModsAvailable(null);
|
||||||
setModsInstalled(null);
|
setModsInstalled(null);
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import { InstallationLocationService } from "renderer/services/installation-loca
|
|||||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||||
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
|
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
|
||||||
import { DISCORD_URL } from "shared/constants";
|
import { DISCORD_URL } from "shared/constants";
|
||||||
import { AutoUpdate } from "shared/models/config";
|
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
|
|
||||||
@@ -202,18 +201,11 @@ export function SettingsPage() {
|
|||||||
const fileChooserRes = await lastValueFrom(ipcService.sendV2("choose-folder"));
|
const fileChooserRes = await lastValueFrom(ipcService.sendV2("choose-folder"));
|
||||||
|
|
||||||
if (!fileChooserRes.canceled && fileChooserRes.filePaths?.length) {
|
if (!fileChooserRes.canceled && fileChooserRes.filePaths?.length) {
|
||||||
|
|
||||||
const newInstallationPath = fileChooserRes.filePaths[0];
|
|
||||||
|
|
||||||
if(newInstallationPath === installationFolder){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
progressBarService.showFake(0.008);
|
progressBarService.showFake(0.008);
|
||||||
|
|
||||||
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
|
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
|
||||||
|
|
||||||
lastValueFrom(installationLocationService.setInstallationFolder(newInstallationPath, true)).then(res => {
|
lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => {
|
||||||
|
|
||||||
progressBarService.complete();
|
progressBarService.complete();
|
||||||
progressBarService.hide();
|
progressBarService.hide();
|
||||||
@@ -247,8 +239,8 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?template=1-bug-report.yaml");
|
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?template=2-feature-request.yaml");
|
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(DISCORD_URL);
|
const openDiscord = () => linkOpener.open(DISCORD_URL);
|
||||||
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
|
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
|
||||||
|
|
||||||
@@ -505,7 +497,7 @@ export function SettingsPage() {
|
|||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
|
|
||||||
<SettingContainer title="pages.settings.language.title" description="pages.settings.language.description">
|
<SettingContainer title="pages.settings.language.title" description="pages.settings.language.description">
|
||||||
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} columnCount={2} />
|
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} />
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
|
|
||||||
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
||||||
@@ -554,17 +546,11 @@ function AdvancedSettings() {
|
|||||||
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
|
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
|
||||||
const [useSymlink, setUseSymlink] = useState(false);
|
const [useSymlink, setUseSymlink] = useState(false);
|
||||||
const [useSystemProxy, setUseSystemProxy] = useState(false);
|
const [useSystemProxy, setUseSystemProxy] = useState(false);
|
||||||
const [autoUpdate, setAutoUpdate] = useState<AutoUpdate>(AutoUpdate.NEVER);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
|
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
|
||||||
|
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
|
||||||
if (window.electron.platform === "win32") {
|
staticConfig.get("use-system-proxy").then(useSystemProxy => setUseSystemProxy(() => useSystemProxy));
|
||||||
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) => {
|
const onChangeHardwareAcceleration = async (newHardwareAccelerationEnabled: boolean) => {
|
||||||
@@ -645,43 +631,12 @@ function AdvancedSettings() {
|
|||||||
setUseSystemProxy(() => newUseSystemProxy);
|
setUseSystemProxy(() => newUseSystemProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeAutoUpdate = async (value: boolean) => {
|
const advancedItems: Item[] = [{
|
||||||
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,
|
checked: hardwareAccelerationEnabled,
|
||||||
text: t.text("pages.settings.advanced.hardware-acceleration.title"),
|
text: t.text("pages.settings.advanced.hardware-acceleration.title"),
|
||||||
desc: t.text("pages.settings.advanced.hardware-acceleration.description"),
|
desc: t.text("pages.settings.advanced.hardware-acceleration.description"),
|
||||||
onChange: onChangeHardwareAcceleration
|
onChange: onChangeHardwareAcceleration
|
||||||
});
|
}];
|
||||||
|
|
||||||
if (window.electron.platform === "win32") {
|
if (window.electron.platform === "win32") {
|
||||||
advancedItems.push({
|
advancedItems.push({
|
||||||
checked: useSymlink,
|
checked: useSymlink,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class ModalService {
|
|||||||
const promise = new Promise<ModalResponse<T>>(resolve => {
|
const promise = new Promise<ModalResponse<T>>(resolve => {
|
||||||
resolver = resolve as (value: ModalResponse | PromiseLike<ModalResponse>) => void;
|
resolver = resolve as (value: ModalResponse | PromiseLike<ModalResponse>) => void;
|
||||||
});
|
});
|
||||||
const modalObj = {id: crypto.randomUUID(), modal: modal as ModalComponent, resolver, options};
|
const modalObj = {modal: modal as ModalComponent, resolver, options};
|
||||||
this._modalToShow$.next([...this._modalToShow$.getValue(), modalObj]);
|
this._modalToShow$.next([...this._modalToShow$.getValue(), modalObj]);
|
||||||
|
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
@@ -41,7 +41,7 @@ export class ModalService {
|
|||||||
|
|
||||||
export type ModalOptions<T = unknown> = { readonly data?: T, readonly noStyle?: boolean, readonly closable?: boolean }
|
export type ModalOptions<T = unknown> = { readonly data?: T, readonly noStyle?: boolean, readonly closable?: boolean }
|
||||||
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { readonly resolver: (x: ModalResponse<Return>) => void; readonly options?: ModalOptions<Receive> }) => JSX.Element;
|
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { readonly resolver: (x: ModalResponse<Return>) => void; readonly options?: ModalOptions<Receive> }) => JSX.Element;
|
||||||
export type ModalObject = {id: string, modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void, options: ModalOptions};
|
export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void, options: ModalOptions};
|
||||||
|
|
||||||
export const enum ModalExitCode {
|
export const enum ModalExitCode {
|
||||||
NO_CHOICE = -1,
|
NO_CHOICE = -1,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
export enum AutoUpdate {
|
|
||||||
ALWAYS = "always",
|
|
||||||
// When "Update and restart" is clicked
|
|
||||||
ONCE = "once",
|
|
||||||
NEVER = "never",
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -21,7 +21,6 @@ import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.mo
|
|||||||
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
|
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
|
||||||
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
|
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
|
||||||
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
|
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
|
||||||
import { UpdateInfo } from "electron-updater";
|
|
||||||
|
|
||||||
export type IpcReplier<T> = (data: Observable<T>) => void;
|
export type IpcReplier<T> = (data: Observable<T>) => void;
|
||||||
|
|
||||||
@@ -123,7 +122,6 @@ export interface IpcChannelMapping {
|
|||||||
/* ** launcher-ipcs ** */
|
/* ** launcher-ipcs ** */
|
||||||
"download-update": { request: void, response: Progression };
|
"download-update": { request: void, response: Progression };
|
||||||
"check-update": { request: void, response: boolean };
|
"check-update": { request: void, response: boolean };
|
||||||
"get-available-update": { request: void, response: UpdateInfo | null };
|
|
||||||
"install-update": { request: void, response: void };
|
"install-update": { request: void, response: void };
|
||||||
|
|
||||||
/* ** model-saber.ipcs ** */
|
/* ** model-saber.ipcs ** */
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ export interface BsmLocalMap {
|
|||||||
mapInfo: MapInfo;
|
mapInfo: MapInfo;
|
||||||
songDetails?: SongDetails;
|
songDetails?: SongDetails;
|
||||||
path: string;
|
path: string;
|
||||||
metadata?: BsmLocalMapMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BsmLocalMapMetadata {
|
|
||||||
// Date of download or import
|
|
||||||
addedDate: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BsmLocalMapsProgress {
|
export interface BsmLocalMapsProgress {
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ export const mapSorter = new Sorter<BsmLocalMap>({
|
|||||||
|
|
||||||
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt;
|
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt;
|
||||||
},
|
},
|
||||||
"added-date": (map1, map2) => {
|
|
||||||
if (!map1.metadata) {
|
|
||||||
return map2.metadata ? Comparison.LESSER : Comparison.EQUAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !map2.metadata ? Comparison.GREATER : map1.metadata.addedDate.localeCompare(map2.metadata.addedDate);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tiebreak: sortName,
|
tiebreak: sortName,
|
||||||
defaultKey: "name"
|
defaultKey: "name"
|
||||||
|
|||||||
Reference in New Issue
Block a user