refactor: remove Tauri auto-updater, replace with simple GitHub-check modal

Remove tauri-plugin-updater, relaunch_after_update command, generate-update-
manifest.js, the generate-manifest CI job, and all signing/upload steps for
.sig files and latest.json. The updater plugin, its pubkey and endpoint config
in tauri.conf.json, and the updater:default capability are all gone.

Replace AppUpdater.tsx with a lightweight component that:
- Fetches https://api.github.com/repos/Psychotoxical/psysonic/releases/latest
  after a 4 s delay (no install, no download, no progress bar)
- Shows a dismissible toast with two buttons:
  GitHub → releases/latest
  Website → https://psysonic.psychotoxic.eu/#downloads

i18n: remove updaterInstall/Downloading/Installing/Download/ExperimentalHint
keys from all 7 locales; add updaterWebsite; update updaterVersion wording.

CI: remove generate-manifest job and all .sig signing/upload steps from
build-macos-windows. Also remove TAURI_SIGNING_PRIVATE_KEY env vars (no
longer needed). Release now just builds and uploads the installable binaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-07 13:42:52 +02:00
parent 3f1b6fd92d
commit 95468fb137
19 changed files with 40 additions and 728 deletions
-68
View File
@@ -80,9 +80,6 @@ jobs:
needs: create-release needs: create-release
permissions: permissions:
contents: write contents: write
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -124,42 +121,6 @@ jobs:
with: with:
releaseId: ${{ needs.create-release.outputs.release_id }} releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.settings.args }} args: ${{ matrix.settings.args }}
# Prevent tauri-action from auto-uploading a latest.json that uses
# the PUBLIC KEY as the signature value. Our generate-update-manifest.js
# builds the correct manifest with real .sig file contents.
updaterJsonKeepUniversal: false
- name: sign and upload Windows NSIS updater bundle
if: matrix.settings.platform == 'windows-latest'
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.create-release.outputs.package_version }}
run: |
$dir = "src-tauri/target/release/bundle/nsis"
$exe = (Get-ChildItem $dir -Filter "*.exe")[0].FullName
$zip = $exe -replace '\.exe$', '.nsis.zip'
Compress-Archive -Path $exe -DestinationPath $zip -Force
npx tauri signer sign --private-key "$env:TAURI_SIGNING_PRIVATE_KEY" --password "$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD" $zip
gh release upload "app-v$env:VERSION" $zip "$zip.sig" --clobber
- name: sign and upload macOS bundle signature
if: matrix.settings.platform == 'macos-latest'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.create-release.outputs.package_version }}
run: |
if [[ "${{ matrix.settings.args }}" == *"aarch64"* ]]; then
BUNDLE="src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Psysonic.app.tar.gz"
SIG_NAME="Psysonic_aarch64.app.tar.gz.sig"
else
BUNDLE="src-tauri/target/x86_64-apple-darwin/release/bundle/macos/Psysonic.app.tar.gz"
SIG_NAME="Psysonic_x64.app.tar.gz.sig"
fi
npx tauri signer sign --private-key "$TAURI_SIGNING_PRIVATE_KEY" --password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$BUNDLE"
cp "${BUNDLE}.sig" "$SIG_NAME"
gh release upload "app-v${VERSION}" "$SIG_NAME" --clobber
build-linux: build-linux:
needs: create-release needs: create-release
@@ -213,32 +174,3 @@ jobs:
find src-tauri/target/release/bundle \ find src-tauri/target/release/bundle \
\( -name "*.deb" -o -name "*.rpm" \) \ \( -name "*.deb" -o -name "*.rpm" \) \
| xargs gh release upload "app-v${VERSION}" --clobber | xargs gh release upload "app-v${VERSION}" --clobber
generate-manifest:
needs: [create-release, build-macos-windows]
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: setup node
uses: actions/setup-node@v5
with:
node-version: lts/*
- name: cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
- name: generate latest.json
env:
VERSION: ${{ needs.create-release.outputs.package_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/generate-update-manifest.js
- name: upload latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=${{ needs.create-release.outputs.package_version }}
gh release upload "app-v${VERSION}" latest.json --clobber
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env node
// Generates latest.json for the Tauri updater from a GitHub release.
// Reads .sig files uploaded by tauri-action, assembles the manifest, writes latest.json.
//
// Required env vars: VERSION, GITHUB_TOKEN
// Usage: node scripts/generate-update-manifest.js
const { execSync } = require('child_process');
const fs = require('fs');
const VERSION = process.env.VERSION;
const REPO = 'Psychotoxical/psysonic';
const TAG = `app-v${VERSION}`;
if (!VERSION) {
console.error('VERSION env var required');
process.exit(1);
}
// Platform → update bundle filename (produced by tauri-action with updater plugin)
const PLATFORM_FILES = {
'darwin-aarch64': `Psysonic_aarch64.app.tar.gz`,
'darwin-x86_64': `Psysonic_x64.app.tar.gz`,
'windows-x86_64': `Psysonic_${VERSION}_x64-setup.nsis.zip`,
};
const platforms = {};
// A real minisign .sig file is multi-line and ~200+ chars.
// A public key (RWTxxx... single line, ~56 chars) must never appear here.
function validateSignature(sig, platform, sigFile) {
// Public keys start with RWT and are a single short base64 token
if (/^RWT[A-Za-z0-9+/]{10,}={0,2}$/.test(sig)) {
throw new Error(
`${platform}: .sig file "${sigFile}" contains a PUBLIC KEY instead of a signature.\n` +
` Got: ${sig}\n` +
` The TAURI_SIGNING_PUBLIC_KEY env var must never be used as the signature value.\n` +
` Ensure the signing step correctly writes the .sig file from the private key.`
);
}
if (sig.length < 80) {
throw new Error(
`${platform}: .sig file "${sigFile}" looks too short (${sig.length} chars) to be a valid signature.`
);
}
}
for (const [platform, filename] of Object.entries(PLATFORM_FILES)) {
const sigFile = `${filename}.sig`;
try {
execSync(
`gh release download "${TAG}" --repo "${REPO}" -p "${sigFile}" --clobber`,
{ stdio: 'pipe' }
);
const signature = fs.readFileSync(sigFile, 'utf8').trim();
validateSignature(signature, platform, sigFile);
const url = `https://github.com/${REPO}/releases/download/${TAG}/${filename}`;
platforms[platform] = { signature, url };
console.log(`${platform}`);
} catch (e) {
console.warn(`⚠ Skipping ${platform}: ${e.message}`);
}
}
if (Object.keys(platforms).length === 0) {
console.error('No platforms found — aborting manifest generation');
process.exit(1);
}
// Pull release notes from GitHub
let notes = '';
try {
const raw = execSync(
`gh release view "${TAG}" --repo "${REPO}" --json body`,
{ stdio: 'pipe' }
).toString();
notes = JSON.parse(raw).body ?? '';
} catch {
console.warn('Could not fetch release notes');
}
const manifest = {
version: VERSION,
notes,
pub_date: new Date().toISOString(),
platforms,
};
fs.writeFileSync('latest.json', JSON.stringify(manifest, null, 2));
console.log(`\nWrote latest.json for v${VERSION} with platforms: ${Object.keys(platforms).join(', ')}`);
+2 -242
View File
@@ -69,15 +69,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -937,17 +928,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@@ -1260,17 +1240,6 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -2415,10 +2384,7 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
dependencies = [ dependencies = [
"bitflags 2.11.0",
"libc", "libc",
"plain",
"redox_syscall 0.7.3",
] ]
[[package]] [[package]]
@@ -2573,12 +2539,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -2845,7 +2805,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"block2", "block2",
"libc",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
] ]
@@ -2861,18 +2820,6 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.3.2"
@@ -2952,12 +2899,6 @@ dependencies = [
"pathdiff", "pathdiff",
] ]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
@@ -2984,20 +2925,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -3047,7 +2974,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.5.18", "redox_syscall",
"smallvec", "smallvec",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
@@ -3227,12 +3154,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]] [[package]]
name = "plist" name = "plist"
version = "1.8.0" version = "1.8.0"
@@ -3440,7 +3361,6 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"thread-priority", "thread-priority",
"tokio", "tokio",
@@ -3663,15 +3583,6 @@ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
] ]
[[package]]
name = "redox_syscall"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [
"bitflags 2.11.0",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.5.2" version = "0.5.2"
@@ -3789,20 +3700,15 @@ dependencies = [
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@@ -3941,18 +3847,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.0"
@@ -3963,33 +3857,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.9" version = "0.103.9"
@@ -4022,15 +3889,6 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.22" version = "0.8.22"
@@ -4088,29 +3946,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.24.0" version = "0.24.0"
@@ -4444,7 +4279,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.5.18", "redox_syscall",
"tracing", "tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@@ -4818,17 +4653,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tar"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@@ -5084,39 +4908,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]] [[package]]
name = "tauri-plugin-window-state" name = "tauri-plugin-window-state"
version = "2.4.1" version = "2.4.1"
@@ -6046,15 +5837,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.6" version = "1.0.6"
@@ -6843,16 +6625,6 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix 1.1.4",
]
[[package]] [[package]]
name = "xdg-home" name = "xdg-home"
version = "1.3.0" version = "1.3.0"
@@ -7099,18 +6871,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.13.0",
"memchr",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
-1
View File
@@ -39,7 +39,6 @@ tokio = { version = "1", features = ["rt", "time"] }
biquad = "0.4" biquad = "0.4"
ringbuf = "0.3" ringbuf = "0.3"
tauri-plugin-window-state = "2.4.1" tauri-plugin-window-state = "2.4.1"
tauri-plugin-updater = "2"
tauri-plugin-process = "2" tauri-plugin-process = "2"
souvlaki = { version = "0.8", default-features = false, features = ["use_zbus"] } souvlaki = { version = "0.8", default-features = false, features = ["use_zbus"] }
discord-rich-presence = "0.2" discord-rich-presence = "0.2"
-1
View File
@@ -34,7 +34,6 @@
"core:window:allow-is-fullscreen", "core:window:allow-is-fullscreen",
"core:window:allow-create", "core:window:allow-create",
"core:webview:allow-create-webview-window", "core:webview:allow-create-webview-window",
"updater:default",
"process:allow-restart" "process:allow-restart"
] ]
} }
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for Psysonic","local":true,"windows":["main"],"permissions":["core:default","shell:default",{"identifier":"shell:allow-open","allow":[{"url":"https://**"}]},"global-shortcut:allow-register","global-shortcut:allow-unregister","store:default","store:allow-load","store:allow-set","store:allow-get","store:allow-save","dialog:default","dialog:allow-open","dialog:allow-save","fs:default","fs:allow-write-file","fs:allow-read-file","fs:allow-mkdir","fs:scope-download-recursive","fs:scope-home-recursive","window-state:allow-save-window-state","window-state:allow-restore-state","core:window:allow-set-title","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-fullscreen","core:window:allow-is-fullscreen","core:window:allow-create","core:webview:allow-create-webview-window","updater:default","process:allow-restart"],"platforms":["linux","macOS","windows"]}} {"default":{"identifier":"default","description":"Default capabilities for Psysonic","local":true,"windows":["main"],"permissions":["core:default","shell:default",{"identifier":"shell:allow-open","allow":[{"url":"https://**"}]},"global-shortcut:allow-register","global-shortcut:allow-unregister","store:default","store:allow-load","store:allow-set","store:allow-get","store:allow-save","dialog:default","dialog:allow-open","dialog:allow-save","fs:default","fs:allow-write-file","fs:allow-read-file","fs:allow-mkdir","fs:scope-download-recursive","fs:scope-home-recursive","window-state:allow-save-window-state","window-state:allow-restore-state","core:window:allow-set-title","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-fullscreen","core:window:allow-is-fullscreen","core:window:allow-create","core:webview:allow-create-webview-window","process:allow-restart"],"platforms":["linux","macOS","windows"]}}
-54
View File
@@ -6284,60 +6284,6 @@
"const": "store:deny-values", "const": "store:deny-values",
"markdownDescription": "Denies the values command without any pre-configured scope." "markdownDescription": "Denies the values command without any pre-configured scope."
}, },
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
},
{ {
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`", "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
"type": "string", "type": "string",
-54
View File
@@ -6284,60 +6284,6 @@
"const": "store:deny-values", "const": "store:deny-values",
"markdownDescription": "Denies the values command without any pre-configured scope." "markdownDescription": "Denies the values command without any pre-configured scope."
}, },
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
},
{ {
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`", "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
"type": "string", "type": "string",
-45
View File
@@ -36,49 +36,6 @@ fn exit_app(app_handle: tauri::AppHandle) {
app_handle.exit(0); app_handle.exit(0);
} }
/// Restart after an in-app update.
///
/// `relaunch()` from tauri-plugin-process spawns the new process while the old one
/// is still alive, so the single-instance plugin in the new process sees the old
/// socket and kills itself immediately (endless loop).
///
/// This command instead:
/// 1. Spawns a shell snippet that waits ~1.5 s and then opens the app again —
/// by then the old process and its single-instance socket are fully gone.
/// 2. Calls `app.exit(0)` directly, which bypasses `on_window_event`
/// prevent_close() and releases the single-instance lock immediately.
#[tauri::command]
fn relaunch_after_update(app: tauri::AppHandle) {
#[cfg(target_os = "windows")]
{
let exe = std::env::current_exe().unwrap_or_default();
let exe_str = exe.to_string_lossy().to_string().replace('\'', "''");
let script = format!(
"Start-Sleep -Milliseconds 1500; Start-Process '{exe_str}'"
);
let _ = std::process::Command::new("powershell")
.args(["-WindowStyle", "Hidden", "-NonInteractive", "-Command", &script])
.spawn();
}
#[cfg(target_os = "macos")]
{
let exe = std::env::current_exe().unwrap_or_default();
// exe lives at Psysonic.app/Contents/MacOS/psysonic — walk up to the bundle.
let bundle = exe
.parent() // MacOS/
.and_then(|p| p.parent()) // Contents/
.and_then(|p| p.parent()); // Psysonic.app
if let Some(bundle_path) = bundle {
let escaped = bundle_path.to_string_lossy().replace('"', "\\\"");
let _ = std::process::Command::new("sh")
.args(["-c", &format!("sleep 1.5 && open \"{escaped}\"")])
.spawn();
}
}
app.exit(0);
}
/// Authenticate with Navidrome's own REST API and return a Bearer token. /// Authenticate with Navidrome's own REST API and return a Bearer token.
async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> { async fn navidrome_token(server_url: &str, username: &str, password: &str) -> Result<String, String> {
@@ -854,7 +811,6 @@ pub fn run() {
.manage(ShortcutMap::default()) .manage(ShortcutMap::default())
.manage(discord::DiscordState::new()) .manage(discord::DiscordState::new())
.manage(TrayState::default()) .manage(TrayState::default())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
@@ -1031,7 +987,6 @@ pub fn run() {
get_hot_cache_size, get_hot_cache_size,
delete_hot_cache_track, delete_hot_cache_track,
purge_hot_cache, purge_hot_cache,
relaunch_after_update,
toggle_tray_icon, toggle_tray_icon,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
-8
View File
@@ -31,14 +31,6 @@
"csp": null "csp": null
} }
}, },
"plugins": {
"updater": {
"pubkey": "RWTgsDNd9LqppH6DMq7ypolI0bsLCNsjOvif4mrHyr4UDidkUT69IY1K",
"endpoints": [
"https://github.com/Psychotoxical/psysonic/releases/latest/download/latest.json"
]
}
},
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
+21 -100
View File
@@ -1,9 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { check, Update, DownloadEvent } from '@tauri-apps/plugin-updater';
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-shell'; import { open } from '@tauri-apps/plugin-shell';
import { RefreshCw, Download, X } from 'lucide-react'; import { RefreshCw, X } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { version as currentVersion } from '../../package.json'; import { version as currentVersion } from '../../package.json';
@@ -18,85 +16,31 @@ function isNewer(a: string, b: string): boolean {
return false; return false;
} }
type State =
| { phase: 'idle' }
| { phase: 'available'; version: string; update: Update | null; error?: string }
| { phase: 'downloading'; pct: number }
| { phase: 'installing' }
| { phase: 'done' };
export default function AppUpdater() { export default function AppUpdater() {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState<State>({ phase: 'idle' }); const [newVersion, setNewVersion] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
if (cancelled) return; if (cancelled) return;
try {
// Try Tauri native updater first (macOS + Windows)
const update = await check();
if (cancelled) return;
if (update) {
setState({ phase: 'available', version: update.version, update });
return;
}
} catch {
// Tauri updater unavailable or network error — fall through to GitHub check
}
// Fallback: GitHub API check (Linux / offline Tauri updater)
try { try {
const res = await fetch('https://api.github.com/repos/Psychotoxical/psysonic/releases/latest'); const res = await fetch('https://api.github.com/repos/Psychotoxical/psysonic/releases/latest');
if (!res.ok || cancelled) return; if (!res.ok || cancelled) return;
const data = await res.json(); const data = await res.json();
const tag: string = data.tag_name ?? ''; const tag: string = data.tag_name ?? '';
if (!cancelled && tag && isNewer(tag, currentVersion)) { if (!cancelled && tag && isNewer(tag, currentVersion)) {
setState({ phase: 'available', version: tag.replace(/^[^0-9]*/, ''), update: null }); setNewVersion(tag.replace(/^[^0-9]*/, ''));
} }
} catch { } catch {
// No update check possible — stay idle // No network or rate-limited — stay idle
} }
}, 3000); }, 4000);
return () => { cancelled = true; clearTimeout(timer); }; return () => { cancelled = true; clearTimeout(timer); };
}, []); }, []);
if (dismissed || state.phase === 'idle' || state.phase === 'done') return null; if (!newVersion || dismissed) return null;
const handleInstall = async () => {
if (state.phase !== 'available' || !state.update) return;
const update = state.update;
const savedVersion = state.version;
let total = 0;
let downloaded = 0;
setState({ phase: 'downloading', pct: 0 });
try {
await update.downloadAndInstall((event: DownloadEvent) => {
if (event.event === 'Started') {
total = event.data.contentLength ?? 0;
} else if (event.event === 'Progress') {
downloaded += event.data.chunkLength;
setState({ phase: 'downloading', pct: total ? Math.round((downloaded / total) * 100) : 0 });
} else if (event.event === 'Finished') {
setState({ phase: 'installing' });
}
});
await invoke('relaunch_after_update');
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
console.error('Update failed:', msg);
// Surface the error so the user (and developer) can see what went wrong
setState({ phase: 'available', version: savedVersion, update, error: msg });
}
};
const handleDownload = () => {
open('https://github.com/Psychotoxical/psysonic/releases/latest');
};
const version = state.phase === 'available' ? state.version : '';
const canInstall = state.phase === 'available' && state.update !== null;
const isLinuxFallback = state.phase === 'available' && state.update === null;
return createPortal( return createPortal(
<div className="app-updater-toast"> <div className="app-updater-toast">
@@ -107,44 +51,21 @@ export default function AppUpdater() {
<X size={12} /> <X size={12} />
</button> </button>
</div> </div>
<div className="app-updater-version">{t('common.updaterVersion', { version })}</div> <div className="app-updater-version">{t('common.updaterVersion', { version: newVersion })}</div>
<div className="app-updater-actions">
{state.phase === 'downloading' && ( <button
<div className="app-updater-progress-wrap"> className="app-updater-btn-primary"
<div className="app-updater-progress-bar"> onClick={() => open('https://github.com/Psychotoxical/psysonic/releases/latest')}
<div className="app-updater-progress-fill" style={{ width: `${state.pct}%` }} /> >
</div> GitHub
<span className="app-updater-pct">{state.pct}%</span> </button>
</div> <button
)} className="app-updater-btn-secondary"
onClick={() => open('https://psysonic.psychotoxic.eu/#downloads')}
{state.phase === 'installing' && ( >
<div className="app-updater-status">{t('common.updaterInstalling')}</div> {t('common.updaterWebsite')}
)} </button>
</div>
{state.phase === 'available' && (
<div className="app-updater-actions">
{state.error && (
<div className="app-updater-error">{state.error}</div>
)}
{canInstall && (
<>
<p className="app-updater-hint">{t('common.updaterExperimentalHint')}</p>
<button className="app-updater-btn-primary" onClick={handleInstall}>
<Download size={12} /> {t('common.updaterInstall')}
</button>
<button className="app-updater-btn-secondary" onClick={handleDownload}>
<Download size={12} /> {t('common.updaterDownload')}
</button>
</>
)}
{isLinuxFallback && (
<button className="app-updater-btn-primary" onClick={handleDownload}>
<Download size={12} /> {t('common.updaterDownload')}
</button>
)}
</div>
)}
</div>, </div>,
document.body document.body
); );
+2 -9
View File
@@ -15,9 +15,6 @@ export const deTranslation = {
help: 'Hilfe', help: 'Hilfe',
expand: 'Sidebar einblenden', expand: 'Sidebar einblenden',
collapse: 'Sidebar ausblenden', collapse: 'Sidebar ausblenden',
updateAvailable: 'Update verfügbar',
updateReady: '{{version}} ist bereit',
updateLink: 'Zum Release →',
downloadingTracks: '{{n}} Tracks werden gecacht…', downloadingTracks: '{{n}} Tracks werden gecacht…',
offlineLibrary: 'Offline-Bibliothek', offlineLibrary: 'Offline-Bibliothek',
genres: 'Genres', genres: 'Genres',
@@ -328,12 +325,8 @@ export const deTranslation = {
bulkRemoveFromPlaylist: 'Aus Playlist entfernen', bulkRemoveFromPlaylist: 'Aus Playlist entfernen',
bulkClear: 'Auswahl aufheben', bulkClear: 'Auswahl aufheben',
updaterAvailable: 'Update verfügbar', updaterAvailable: 'Update verfügbar',
updaterVersion: 'v{{version}} ist bereit', updaterVersion: 'v{{version}} verfügbar',
updaterInstall: 'Installieren & Neustart', updaterWebsite: 'Website',
updaterDownloading: 'Wird geladen…',
updaterInstalling: 'Wird installiert…',
updaterDownload: 'Von GitHub herunterladen',
updaterExperimentalHint: 'Automatische Updates sind noch in Entwicklung',
}, },
settings: { settings: {
title: 'Einstellungen', title: 'Einstellungen',
+3 -9
View File
@@ -15,9 +15,7 @@ export const enTranslation = {
help: 'Help', help: 'Help',
expand: 'Expand Sidebar', expand: 'Expand Sidebar',
collapse: 'Collapse Sidebar', collapse: 'Collapse Sidebar',
updateAvailable: 'Update available',
updateReady: '{{version}} is ready',
updateLink: 'Go to release →',
downloadingTracks: 'Caching {{n}} tracks…', downloadingTracks: 'Caching {{n}} tracks…',
offlineLibrary: 'Offline Library', offlineLibrary: 'Offline Library',
genres: 'Genres', genres: 'Genres',
@@ -328,12 +326,8 @@ export const enTranslation = {
bulkRemoveFromPlaylist: 'Remove from Playlist', bulkRemoveFromPlaylist: 'Remove from Playlist',
bulkClear: 'Clear selection', bulkClear: 'Clear selection',
updaterAvailable: 'Update available', updaterAvailable: 'Update available',
updaterVersion: 'v{{version}} is ready', updaterVersion: 'v{{version}} is available',
updaterInstall: 'Install & Restart', updaterWebsite: 'Website',
updaterDownloading: 'Downloading…',
updaterInstalling: 'Installing…',
updaterDownload: 'Download from GitHub',
updaterExperimentalHint: 'Auto-update is still in development',
}, },
settings: { settings: {
title: 'Settings', title: 'Settings',
+2 -9
View File
@@ -15,9 +15,6 @@ export const frTranslation = {
help: 'Aide', help: 'Aide',
expand: 'Développer la barre latérale', expand: 'Développer la barre latérale',
collapse: 'Réduire la barre latérale', collapse: 'Réduire la barre latérale',
updateAvailable: 'Mise à jour disponible',
updateReady: '{{version}} est prêt',
updateLink: 'Voir la version →',
downloadingTracks: '{{n}} pistes en cache…', downloadingTracks: '{{n}} pistes en cache…',
offlineLibrary: 'Bibliothèque hors ligne', offlineLibrary: 'Bibliothèque hors ligne',
genres: 'Genres', genres: 'Genres',
@@ -328,12 +325,8 @@ export const frTranslation = {
bulkRemoveFromPlaylist: 'Retirer de la playlist', bulkRemoveFromPlaylist: 'Retirer de la playlist',
bulkClear: 'Désélectionner', bulkClear: 'Désélectionner',
updaterAvailable: 'Mise à jour disponible', updaterAvailable: 'Mise à jour disponible',
updaterVersion: 'v{{version}} est prête', updaterVersion: 'v{{version}} est disponible',
updaterInstall: 'Installer & Redémarrer', updaterWebsite: 'Site Web',
updaterDownloading: 'Téléchargement…',
updaterInstalling: 'Installation…',
updaterDownload: 'Télécharger depuis GitHub',
updaterExperimentalHint: 'La mise à jour automatique est encore en développement',
}, },
settings: { settings: {
title: 'Paramètres', title: 'Paramètres',
+2 -9
View File
@@ -15,9 +15,6 @@ export const nbTranslation = {
help: 'Hjelp', help: 'Hjelp',
expand: 'Utvid sidefelt', expand: 'Utvid sidefelt',
collapse: 'Skjul sidefelt', collapse: 'Skjul sidefelt',
updateAvailable: 'Ny versjon er tilgjengelig',
updateReady: '{{version}} er klar',
updateLink: 'Hent nyeste versjon →',
downloadingTracks: 'Bufre {{n}} spor…', downloadingTracks: 'Bufre {{n}} spor…',
offlineLibrary: 'Frakoblet bibliotek', offlineLibrary: 'Frakoblet bibliotek',
genres: 'Sjangere', genres: 'Sjangere',
@@ -328,12 +325,8 @@ export const nbTranslation = {
bulkRemoveFromPlaylist: 'Fjern fra spilleliste', bulkRemoveFromPlaylist: 'Fjern fra spilleliste',
bulkClear: 'Tøm utvalg', bulkClear: 'Tøm utvalg',
updaterAvailable: 'Ny versjon er tilgjengelig', updaterAvailable: 'Ny versjon er tilgjengelig',
updaterVersion: 'v{{version}} er lansert', updaterVersion: 'v{{version}} er tilgjengelig',
updaterInstall: 'Installer og start applikasjonen på nytt', updaterWebsite: 'Nettsted',
updaterDownloading: 'Laster ned…',
updaterInstalling: 'Installerer…',
updaterDownload: 'Last ned fra GitHub',
updaterExperimentalHint: 'Auto-oppdatering er fortsatt under utvikling',
}, },
settings: { settings: {
title: 'Innstillinger', title: 'Innstillinger',
+2 -9
View File
@@ -15,9 +15,6 @@ export const nlTranslation = {
help: 'Help', help: 'Help',
expand: 'Zijbalk uitklappen', expand: 'Zijbalk uitklappen',
collapse: 'Zijbalk inklappen', collapse: 'Zijbalk inklappen',
updateAvailable: 'Update beschikbaar',
updateReady: '{{version}} is klaar',
updateLink: 'Naar release →',
downloadingTracks: '{{n}} nummers worden gecached…', downloadingTracks: '{{n}} nummers worden gecached…',
offlineLibrary: 'Offline bibliotheek', offlineLibrary: 'Offline bibliotheek',
genres: 'Genres', genres: 'Genres',
@@ -328,12 +325,8 @@ export const nlTranslation = {
bulkRemoveFromPlaylist: 'Verwijderen uit afspeellijst', bulkRemoveFromPlaylist: 'Verwijderen uit afspeellijst',
bulkClear: 'Selectie wissen', bulkClear: 'Selectie wissen',
updaterAvailable: 'Update beschikbaar', updaterAvailable: 'Update beschikbaar',
updaterVersion: 'v{{version}} is klaar', updaterVersion: 'v{{version}} beschikbaar',
updaterInstall: 'Installeren & Herstarten', updaterWebsite: 'Website',
updaterDownloading: 'Downloaden…',
updaterInstalling: 'Installeren…',
updaterDownload: 'Downloaden van GitHub',
updaterExperimentalHint: 'Automatisch bijwerken is nog in ontwikkeling',
}, },
settings: { settings: {
title: 'Instellingen', title: 'Instellingen',
+2 -9
View File
@@ -16,9 +16,6 @@ export const ruTranslation = {
help: 'Справка', help: 'Справка',
expand: 'Развернуть боковую панель', expand: 'Развернуть боковую панель',
collapse: 'Свернуть боковую панель', collapse: 'Свернуть боковую панель',
updateAvailable: 'Доступно обновление',
updateReady: '{{version}} готово',
updateLink: 'Перейти к релизу →',
downloadingTracks: 'Кэширование {{n}} треков…', downloadingTracks: 'Кэширование {{n}} треков…',
offlineLibrary: 'Офлайн-библиотека', offlineLibrary: 'Офлайн-библиотека',
genres: 'Жанры', genres: 'Жанры',
@@ -342,12 +339,8 @@ export const ruTranslation = {
bulkRemoveFromPlaylist: 'Убрать из плейлиста', bulkRemoveFromPlaylist: 'Убрать из плейлиста',
bulkClear: 'Снять выделение', bulkClear: 'Снять выделение',
updaterAvailable: 'Доступно обновление', updaterAvailable: 'Доступно обновление',
updaterVersion: 'Версия {{version}} готова', updaterVersion: 'Версия {{version}} доступна',
updaterInstall: 'Установить и перезапустить', updaterWebsite: 'Сайт',
updaterDownloading: 'Скачивание…',
updaterInstalling: 'Установка…',
updaterDownload: 'Скачать с GitHub',
updaterExperimentalHint: 'Автообновление в разработке',
}, },
settings: { settings: {
title: 'Настройки', title: 'Настройки',
+2 -9
View File
@@ -15,9 +15,6 @@ export const zhTranslation = {
help: '帮助', help: '帮助',
expand: '展开侧边栏', expand: '展开侧边栏',
collapse: '收起侧边栏', collapse: '收起侧边栏',
updateAvailable: '有可用更新',
updateReady: '{{version}} 已就绪',
updateLink: '前往发布页面 →',
downloadingTracks: '正在缓存 {{n}} 首歌曲…', downloadingTracks: '正在缓存 {{n}} 首歌曲…',
offlineLibrary: '离线音乐库', offlineLibrary: '离线音乐库',
genres: '流派', genres: '流派',
@@ -324,12 +321,8 @@ export const zhTranslation = {
bulkRemoveFromPlaylist: '从播放列表移除', bulkRemoveFromPlaylist: '从播放列表移除',
bulkClear: '取消选择', bulkClear: '取消选择',
updaterAvailable: '有可用更新', updaterAvailable: '有可用更新',
updaterVersion: 'v{{version}} 已就绪', updaterVersion: 'v{{version}} 已发布',
updaterInstall: '安装并重启', updaterWebsite: '官网',
updaterDownloading: '下载中…',
updaterInstalling: '安装中…',
updaterDownload: '从 GitHub 下载',
updaterExperimentalHint: '自动更新功能仍在开发中',
}, },
settings: { settings: {
title: '设置', title: '设置',