mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat: v1.27.0 — In-App Auto-Update, Configurable Home, Icon Consistency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,8 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
||||||
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||||
args: ${{ matrix.settings.args }}
|
args: ${{ matrix.settings.args }}
|
||||||
@@ -146,3 +148,26 @@ 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: 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
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.27.0] - 2026-04-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **In-App Auto-Update** *(requested by [@netherguy4](https://github.com/netherguy4))*: Psysonic now checks for new releases automatically on startup (3 s delay). On macOS and Windows a native install-and-relaunch flow is available directly in the app — no browser needed. On Linux, a download link to the GitHub release page is shown instead (AppImage is not built due to WebKitGTK incompatibility with Arch/Fedora). The updater uses Tauri's signed updater plugin with minisign signatures verified against a bundled public key. **Note: this release is the preparation — auto-update will be fully active for all subsequent releases once signed bundles are in place.**
|
||||||
|
- **Configurable Home Page**: Users can now choose which sections appear on the home page. A new "Home Page" block in Settings → Library lets you toggle each section individually (Featured, Recently Added, Discover, Discover Artists, Recently Played, Personal Favorites, Most Played) with a reset-to-default button. Hidden sections are skipped entirely.
|
||||||
|
- **Consistent icon language** *(requested by [@netherguy4](https://github.com/netherguy4))*: Favorites (local star/heart) now use a filled Heart icon everywhere — Player Bar, Album Detail, Artist Detail, Tracklist, Context Menu. Last.fm love always uses the Last.fm logo. Previously the two were mixed up in several places.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Album Detail hero background not loading**: The blurred album art background in Album Detail only appeared after a track change, never on first visit. Root cause: `buildCoverArtUrl` was called without `useMemo`, generating a new salt on every re-render — causing `useCachedUrl` to cancel and restart its fetch endlessly. Fixed by memoising both the URL and cache key on `album.coverArt`. Same fix applied to Hero and Playlist Detail backgrounds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.26.1] - 2026-04-01
|
## [1.26.1] - 2026-04-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+22
-2
@@ -1,19 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.21.0",
|
"version": "1.26.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.21.0",
|
"version": "1.26.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2",
|
"@tauri-apps/plugin-global-shortcut": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"@tauri-apps/plugin-store": "^2",
|
"@tauri-apps/plugin-store": "^2",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"i18next": "^25.8.16",
|
"i18next": "^25.8.16",
|
||||||
@@ -1493,6 +1495,15 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-process": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-shell": {
|
"node_modules/@tauri-apps/plugin-shell": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||||
@@ -1511,6 +1522,15 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-updater": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-window-state": {
|
"node_modules/@tauri-apps/plugin-window-state": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.26.1",
|
"version": "1.27.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -15,8 +15,10 @@
|
|||||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2",
|
"@tauri-apps/plugin-global-shortcut": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "^2.3.1",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"@tauri-apps/plugin-store": "^2",
|
"@tauri-apps/plugin-store": "^2",
|
||||||
|
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"i18next": "^25.8.16",
|
"i18next": "^25.8.16",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/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_${VERSION}_aarch64.app.tar.gz`,
|
||||||
|
'darwin-x86_64': `Psysonic_${VERSION}_x64.app.tar.gz`,
|
||||||
|
'windows-x86_64': `Psysonic_${VERSION}_x64-setup.nsis.zip`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const platforms = {};
|
||||||
|
|
||||||
|
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();
|
||||||
|
const url = `https://github.com/${REPO}/releases/download/${TAG}/${filename}`;
|
||||||
|
platforms[platform] = { signature, url };
|
||||||
|
console.log(`✓ ${platform}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`⚠ Skipping ${platform}: asset not found (${sigFile})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(', ')}`);
|
||||||
Generated
+217
-3
@@ -69,6 +69,15 @@ 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"
|
||||||
@@ -922,6 +931,17 @@ 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"
|
||||||
@@ -1221,6 +1241,17 @@ 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"
|
||||||
@@ -2397,7 +2428,10 @@ 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]]
|
||||||
@@ -2536,6 +2570,12 @@ 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"
|
||||||
@@ -2819,6 +2859,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2834,6 +2875,18 @@ 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"
|
||||||
@@ -2983,6 +3036,20 @@ 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"
|
||||||
@@ -3032,7 +3099,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
@@ -3212,6 +3279,12 @@ 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"
|
||||||
@@ -3397,7 +3470,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.26.1"
|
version = "1.27.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"biquad",
|
"biquad",
|
||||||
"md5",
|
"md5",
|
||||||
@@ -3412,9 +3485,11 @@ dependencies = [
|
|||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
|
"tauri-plugin-process",
|
||||||
"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",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -3551,6 +3626,15 @@ 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"
|
||||||
@@ -3668,15 +3752,20 @@ 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",
|
||||||
@@ -3799,12 +3888,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
"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"
|
||||||
@@ -3814,6 +3916,33 @@ 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"
|
||||||
@@ -4268,7 +4397,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@@ -4650,6 +4779,17 @@ 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"
|
||||||
@@ -4843,6 +4983,16 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-process"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||||
|
dependencies = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.3.5"
|
version = "2.3.5"
|
||||||
@@ -4895,6 +5045,39 @@ 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"
|
||||||
@@ -5786,6 +5969,15 @@ 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 = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -6576,6 +6768,16 @@ 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"
|
||||||
@@ -6822,6 +7024,18 @@ 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,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.26.1"
|
version = "1.27.0"
|
||||||
description = "Psysonic Desktop Music Player"
|
description = "Psysonic Desktop Music Player"
|
||||||
authors = []
|
authors = []
|
||||||
license = ""
|
license = ""
|
||||||
@@ -37,4 +37,6 @@ md5 = "0.7"
|
|||||||
tokio = { version = "1", features = ["rt", "time"] }
|
tokio = { version = "1", features = ["rt", "time"] }
|
||||||
biquad = "0.4"
|
biquad = "0.4"
|
||||||
tauri-plugin-window-state = "2.4.1"
|
tauri-plugin-window-state = "2.4.1"
|
||||||
|
tauri-plugin-updater = "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"] }
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"core:window:allow-set-fullscreen",
|
"core:window:allow-set-fullscreen",
|
||||||
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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","fs:default","fs:allow-write-file","fs:allow-mkdir","fs:scope-download-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"],"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","fs:default","fs:allow-write-file","fs:allow-mkdir","fs:scope-download-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"]}}
|
||||||
@@ -6014,6 +6014,36 @@
|
|||||||
"const": "global-shortcut:deny-unregister-all",
|
"const": "global-shortcut:deny-unregister-all",
|
||||||
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-exit",
|
||||||
|
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-restart",
|
||||||
|
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-exit",
|
||||||
|
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-restart",
|
||||||
|
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -6254,6 +6284,60 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -6014,6 +6014,36 @@
|
|||||||
"const": "global-shortcut:deny-unregister-all",
|
"const": "global-shortcut:deny-unregister-all",
|
||||||
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-exit",
|
||||||
|
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-restart",
|
||||||
|
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-exit",
|
||||||
|
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-restart",
|
||||||
|
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -6254,6 +6284,60 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -296,6 +296,8 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(audio_engine)
|
.manage(audio_engine)
|
||||||
.manage(ShortcutMap::default())
|
.manage(ShortcutMap::default())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
.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())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Psysonic",
|
"productName": "Psysonic",
|
||||||
"version": "1.26.1",
|
"version": "1.27.0",
|
||||||
"identifier": "dev.psysonic.player",
|
"identifier": "dev.psysonic.player",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -31,6 +31,14 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import Genres from './pages/Genres';
|
|||||||
import GenreDetail from './pages/GenreDetail';
|
import GenreDetail from './pages/GenreDetail';
|
||||||
import ExportPickerModal from './components/ExportPickerModal';
|
import ExportPickerModal from './components/ExportPickerModal';
|
||||||
import ChangelogModal from './components/ChangelogModal';
|
import ChangelogModal from './components/ChangelogModal';
|
||||||
|
import AppUpdater from './components/AppUpdater';
|
||||||
import { version } from '../package.json';
|
import { version } from '../package.json';
|
||||||
import { useConnectionStatus } from './hooks/useConnectionStatus';
|
import { useConnectionStatus } from './hooks/useConnectionStatus';
|
||||||
import { useAuthStore } from './store/authStore';
|
import { useAuthStore } from './store/authStore';
|
||||||
@@ -276,6 +277,7 @@ function AppShell() {
|
|||||||
<SongInfoModal />
|
<SongInfoModal />
|
||||||
<DownloadFolderModal />
|
<DownloadFolderModal />
|
||||||
<TooltipPortal />
|
<TooltipPortal />
|
||||||
|
<AppUpdater />
|
||||||
{changelogModalOpen && <ChangelogModal onClose={() => setChangelogModalOpen(false)} />}
|
{changelogModalOpen && <ChangelogModal onClose={() => setChangelogModalOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Play, Star, ExternalLink, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Loader2 } from 'lucide-react';
|
import { Play, Heart, ExternalLink, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Loader2 } from 'lucide-react';
|
||||||
import { SubsonicSong, buildCoverArtUrl } from '../api/subsonic';
|
import { SubsonicSong, buildCoverArtUrl } from '../api/subsonic';
|
||||||
import CachedImage from './CachedImage';
|
import CachedImage from './CachedImage';
|
||||||
import CoverLightbox from './CoverLightbox';
|
import CoverLightbox from './CoverLightbox';
|
||||||
@@ -203,7 +203,7 @@ export default function AlbumHeader({
|
|||||||
data-tooltip={isStarred ? t('albumDetail.favoriteRemove') : t('albumDetail.favoriteAdd')}
|
data-tooltip={isStarred ? t('albumDetail.favoriteRemove') : t('albumDetail.favoriteAdd')}
|
||||||
style={{ color: isStarred ? 'var(--color-star-active, var(--accent))' : 'inherit', border: isStarred ? '1px solid var(--color-star-active, var(--accent))' : undefined }}
|
style={{ color: isStarred ? 'var(--color-star-active, var(--accent))' : 'inherit', border: isStarred ? '1px solid var(--color-star-active, var(--accent))' : undefined }}
|
||||||
>
|
>
|
||||||
<Star size={16} fill={isStarred ? 'currentColor' : 'none'} />
|
<Heart size={16} fill={isStarred ? 'currentColor' : 'none'} />
|
||||||
{t('albumDetail.favorite')}
|
{t('albumDetail.favorite')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Play, Star, ListPlus, X } from 'lucide-react';
|
import { Play, Heart, ListPlus, X } from 'lucide-react';
|
||||||
import { SubsonicSong } from '../api/subsonic';
|
import { SubsonicSong } from '../api/subsonic';
|
||||||
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -256,7 +256,7 @@ export default function AlbumTrackList({
|
|||||||
data-tooltip={starredSongs.has(song.id) ? t('albumDetail.favoriteRemove') : t('albumDetail.favoriteAdd')}
|
data-tooltip={starredSongs.has(song.id) ? t('albumDetail.favoriteRemove') : t('albumDetail.favoriteAdd')}
|
||||||
style={{ color: starredSongs.has(song.id) ? 'var(--color-star-active, var(--accent))' : 'var(--color-star-inactive, var(--text-muted))' }}
|
style={{ color: starredSongs.has(song.id) ? 'var(--color-star-active, var(--accent))' : 'var(--color-star-inactive, var(--text-muted))' }}
|
||||||
>
|
>
|
||||||
<Star size={14} fill={starredSongs.has(song.id) ? 'currentColor' : 'none'} />
|
<Heart size={14} fill={starredSongs.has(song.id) ? 'currentColor' : 'none'} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<StarRating
|
<StarRating
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { check, Update, DownloadEvent } from '@tauri-apps/plugin-updater';
|
||||||
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
|
import { RefreshCw, Download, X } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { version as currentVersion } from '../../package.json';
|
||||||
|
|
||||||
|
// Semver comparison: returns true if `a` is newer than `b`
|
||||||
|
function isNewer(a: string, b: string): boolean {
|
||||||
|
const pa = a.replace(/^[^0-9]*/, '').split('.').map(Number);
|
||||||
|
const pb = b.replace(/^[^0-9]*/, '').split('.').map(Number);
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
||||||
|
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State =
|
||||||
|
| { phase: 'idle' }
|
||||||
|
| { phase: 'available'; version: string; update: Update | null }
|
||||||
|
| { phase: 'downloading'; pct: number }
|
||||||
|
| { phase: 'installing' }
|
||||||
|
| { phase: 'done' };
|
||||||
|
|
||||||
|
export default function AppUpdater() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState<State>({ phase: 'idle' });
|
||||||
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
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 {
|
||||||
|
const res = await fetch('https://api.github.com/repos/Psychotoxical/psysonic/releases/latest');
|
||||||
|
if (!res.ok || cancelled) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const tag: string = data.tag_name ?? '';
|
||||||
|
if (!cancelled && tag && isNewer(tag, currentVersion)) {
|
||||||
|
setState({ phase: 'available', version: tag.replace(/^[^0-9]*/, ''), update: null });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No update check possible — stay idle
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
return () => { cancelled = true; clearTimeout(timer); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (dismissed || state.phase === 'idle' || state.phase === 'done') 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 relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Update failed', e);
|
||||||
|
setState({ phase: 'available', version: savedVersion, update });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
<div className="app-updater-toast">
|
||||||
|
<div className="app-updater-header">
|
||||||
|
<RefreshCw size={13} />
|
||||||
|
<span className="app-updater-label">{t('common.updaterAvailable')}</span>
|
||||||
|
<button className="app-updater-dismiss" onClick={() => setDismissed(true)} aria-label="Dismiss">
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="app-updater-version">{t('common.updaterVersion', { version })}</div>
|
||||||
|
|
||||||
|
{state.phase === 'downloading' && (
|
||||||
|
<div className="app-updater-progress-wrap">
|
||||||
|
<div className="app-updater-progress-bar">
|
||||||
|
<div className="app-updater-progress-fill" style={{ width: `${state.pct}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="app-updater-pct">{state.pct}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.phase === 'installing' && (
|
||||||
|
<div className="app-updater-status">{t('common.updaterInstalling')}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.phase === 'available' && (
|
||||||
|
<div className="app-updater-actions">
|
||||||
|
{canInstall && (
|
||||||
|
<button className="app-updater-btn-primary" onClick={handleInstall}>
|
||||||
|
<Download size={12} /> {t('common.updaterInstall')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isLinuxFallback && (
|
||||||
|
<button className="app-updater-btn-primary" onClick={handleDownload}>
|
||||||
|
<Download size={12} /> {t('common.updaterDownload')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { Play, ListPlus, Radio, Star, Download, ChevronRight, User, Disc3, Heart, ListMusic, Plus, Info } from 'lucide-react';
|
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info } from 'lucide-react';
|
||||||
|
import LastfmIcon from './LastfmIcon';
|
||||||
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
|
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
|
||||||
import { usePlayerStore, Track, songToTrack } from '../store/playerStore';
|
import { usePlayerStore, Track, songToTrack } from '../store/playerStore';
|
||||||
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getTopSongs, buildDownloadUrl, getAlbum, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, SubsonicPlaylist } from '../api/subsonic';
|
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getTopSongs, buildDownloadUrl, getAlbum, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, SubsonicPlaylist } from '../api/subsonic';
|
||||||
@@ -304,7 +305,7 @@ export default function ContextMenu() {
|
|||||||
setStarredOverride(song.id, !starred);
|
setStarredOverride(song.id, !starred);
|
||||||
return starred ? unstar(song.id, 'song') : star(song.id, 'song');
|
return starred ? unstar(song.id, 'song') : star(song.id, 'song');
|
||||||
})}>
|
})}>
|
||||||
<Star size={14} fill={isStarred(song.id, song.starred) ? 'currentColor' : 'none'} />
|
<Heart size={14} fill={isStarred(song.id, song.starred) ? 'currentColor' : 'none'} />
|
||||||
{isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
{isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||||
</div>
|
</div>
|
||||||
{auth.lastfmSessionKey && (() => {
|
{auth.lastfmSessionKey && (() => {
|
||||||
@@ -317,7 +318,7 @@ export default function ContextMenu() {
|
|||||||
if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey);
|
if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey);
|
||||||
else lastfmUnloveTrack(song, auth.lastfmSessionKey);
|
else lastfmUnloveTrack(song, auth.lastfmSessionKey);
|
||||||
})}>
|
})}>
|
||||||
<Heart size={14} fill={loved ? 'currentColor' : 'none'} style={{ color: loved ? '#e31c23' : undefined }} />
|
<LastfmIcon size={14} />
|
||||||
{loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
{loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -346,7 +347,7 @@ export default function ContextMenu() {
|
|||||||
setStarredOverride(album.id, !starred);
|
setStarredOverride(album.id, !starred);
|
||||||
return starred ? unstar(album.id, 'album') : star(album.id, 'album');
|
return starred ? unstar(album.id, 'album') : star(album.id, 'album');
|
||||||
})}>
|
})}>
|
||||||
<Star size={14} fill={isStarred(album.id, album.starred) ? 'currentColor' : 'none'} />
|
<Heart size={14} fill={isStarred(album.id, album.starred) ? 'currentColor' : 'none'} />
|
||||||
{isStarred(album.id, album.starred) ? t('contextMenu.unfavoriteAlbum') : t('contextMenu.favoriteAlbum')}
|
{isStarred(album.id, album.starred) ? t('contextMenu.unfavoriteAlbum') : t('contextMenu.favoriteAlbum')}
|
||||||
</div>
|
</div>
|
||||||
<div className="context-menu-item" onClick={() => handleAction(() => downloadAlbum(album.name, album.id))}>
|
<div className="context-menu-item" onClick={() => handleAction(() => downloadAlbum(album.name, album.id))}>
|
||||||
@@ -380,7 +381,7 @@ export default function ContextMenu() {
|
|||||||
setStarredOverride(artist.id, !starred);
|
setStarredOverride(artist.id, !starred);
|
||||||
return starred ? unstar(artist.id, 'artist') : star(artist.id, 'artist');
|
return starred ? unstar(artist.id, 'artist') : star(artist.id, 'artist');
|
||||||
})}>
|
})}>
|
||||||
<Star size={14} fill={isStarred(artist.id, artist.starred) ? 'currentColor' : 'none'} />
|
<Heart size={14} fill={isStarred(artist.id, artist.starred) ? 'currentColor' : 'none'} />
|
||||||
{isStarred(artist.id, artist.starred) ? t('contextMenu.unfavoriteArtist') : t('contextMenu.favoriteArtist')}
|
{isStarred(artist.id, artist.starred) ? t('contextMenu.unfavoriteArtist') : t('contextMenu.favoriteArtist')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -421,7 +422,7 @@ export default function ContextMenu() {
|
|||||||
setStarredOverride(song.id, !starred);
|
setStarredOverride(song.id, !starred);
|
||||||
return starred ? unstar(song.id, 'song') : star(song.id, 'song');
|
return starred ? unstar(song.id, 'song') : star(song.id, 'song');
|
||||||
})}>
|
})}>
|
||||||
<Star size={14} fill={isStarred(song.id, song.starred) ? 'currentColor' : 'none'} />
|
<Heart size={14} fill={isStarred(song.id, song.starred) ? 'currentColor' : 'none'} />
|
||||||
{isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
{isStarred(song.id, song.starred) ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||||
</div>
|
</div>
|
||||||
{auth.lastfmSessionKey && (() => {
|
{auth.lastfmSessionKey && (() => {
|
||||||
@@ -434,7 +435,7 @@ export default function ContextMenu() {
|
|||||||
if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey);
|
if (newLoved) lastfmLoveTrack(song, auth.lastfmSessionKey);
|
||||||
else lastfmUnloveTrack(song, auth.lastfmSessionKey);
|
else lastfmUnloveTrack(song, auth.lastfmSessionKey);
|
||||||
})}>
|
})}>
|
||||||
<Heart size={14} fill={loved ? 'currentColor' : 'none'} style={{ color: loved ? '#e31c23' : undefined }} />
|
<LastfmIcon size={14} />
|
||||||
{loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
{loved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
|
|||||||
// buildCoverArtUrl generates a new salt on every call — must be memoized.
|
// buildCoverArtUrl generates a new salt on every call — must be memoized.
|
||||||
const bgRawUrl = useMemo(() => album?.coverArt ? buildCoverArtUrl(album.coverArt, 800) : '', [album?.coverArt]);
|
const bgRawUrl = useMemo(() => album?.coverArt ? buildCoverArtUrl(album.coverArt, 800) : '', [album?.coverArt]);
|
||||||
const bgCacheKey = useMemo(() => album?.coverArt ? coverArtCacheKey(album.coverArt, 800) : '', [album?.coverArt]);
|
const bgCacheKey = useMemo(() => album?.coverArt ? coverArtCacheKey(album.coverArt, 800) : '', [album?.coverArt]);
|
||||||
const resolvedBgUrl = useCachedUrl(bgRawUrl, bgCacheKey, false);
|
const resolvedBgUrl = useCachedUrl(bgRawUrl, bgCacheKey);
|
||||||
|
|
||||||
// Keep the last known good URL so HeroBg never receives '' during a cache-miss
|
// Keep the last known good URL so HeroBg never receives '' during a cache-miss
|
||||||
// transition (which would cause the background to flash empty before fading in).
|
// transition (which would cause the background to flash empty before fading in).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Music,
|
Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Music,
|
||||||
Square, Repeat, Repeat1, Maximize2, SlidersHorizontal, X, Heart, Star, MicVocal
|
Square, Repeat, Repeat1, Maximize2, SlidersHorizontal, X, Heart, MicVocal
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useLyricsStore } from '../store/lyricsStore';
|
import { useLyricsStore } from '../store/lyricsStore';
|
||||||
import MarqueeText from './MarqueeText';
|
import MarqueeText from './MarqueeText';
|
||||||
|
import LastfmIcon from './LastfmIcon';
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
if (!seconds || isNaN(seconds)) return '0:00';
|
if (!seconds || isNaN(seconds)) return '0:00';
|
||||||
@@ -115,9 +116,9 @@ export default function PlayerBar() {
|
|||||||
onClick={toggleStar}
|
onClick={toggleStar}
|
||||||
aria-label={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
aria-label={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||||
data-tooltip={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
data-tooltip={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||||
style={{ color: isStarred ? 'var(--ctp-yellow)' : 'var(--text-muted)', flexShrink: 0 }}
|
style={{ color: isStarred ? 'var(--accent)' : 'var(--text-muted)', flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<Star size={15} fill={isStarred ? 'var(--ctp-yellow)' : 'none'} />
|
<Heart size={15} fill={isStarred ? 'currentColor' : 'none'} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currentTrack && lastfmSessionKey && (
|
{currentTrack && lastfmSessionKey && (
|
||||||
@@ -128,7 +129,7 @@ export default function PlayerBar() {
|
|||||||
data-tooltip={lastfmLoved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
data-tooltip={lastfmLoved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
||||||
style={{ color: lastfmLoved ? '#e31c23' : 'var(--text-muted)', flexShrink: 0 }}
|
style={{ color: lastfmLoved ? '#e31c23' : 'var(--text-muted)', flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<Heart size={15} fill={lastfmLoved ? '#e31c23' : 'none'} />
|
<LastfmIcon size={15} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useOfflineStore } from '../store/offlineStore';
|
import { useOfflineStore } from '../store/offlineStore';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { useSidebarStore } from '../store/sidebarStore';
|
import { useSidebarStore } from '../store/sidebarStore';
|
||||||
import { open } from '@tauri-apps/plugin-shell';
|
|
||||||
import { version as appVersion } from '../../package.json';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Disc3, Users, Music4, Radio, Settings, Heart, BarChart3, Shuffle,
|
Disc3, Users, Music4, Radio, Settings, Heart, BarChart3, Shuffle,
|
||||||
PanelLeftClose, PanelLeft, HelpCircle, Dices, ArrowUpCircle, AudioLines, HardDriveDownload, Tags, ListMusic
|
PanelLeftClose, PanelLeft, HelpCircle, Dices, AudioLines, HardDriveDownload, Tags, ListMusic
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import PsysonicLogo from './PsysonicLogo';
|
import PsysonicLogo from './PsysonicLogo';
|
||||||
import PSmallLogo from './PSmallLogo';
|
import PSmallLogo from './PSmallLogo';
|
||||||
@@ -30,42 +28,6 @@ export const ALL_NAV_ITEMS: Record<string, { icon: React.ElementType; labelKey:
|
|||||||
help: { icon: HelpCircle, labelKey: 'sidebar.help', to: '/help', section: 'system' },
|
help: { icon: HelpCircle, labelKey: 'sidebar.help', to: '/help', section: 'system' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function isNewer(latest: string, current: string): boolean {
|
|
||||||
const parse = (v: string) => v.replace(/^[^0-9]*/, '').split('.').map(Number);
|
|
||||||
const [lMaj, lMin, lPat] = parse(latest);
|
|
||||||
const [cMaj, cMin, cPat] = parse(current);
|
|
||||||
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
||||||
if (lMin !== cMin) return lMin > cMin;
|
|
||||||
return lPat > cPat;
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateToast({ isCollapsed, latestVersion }: { isCollapsed: boolean; latestVersion: string }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (isCollapsed) {
|
|
||||||
return (
|
|
||||||
<div className="update-toast-icon" style={{ marginTop: 'auto' }} data-tooltip={`${t('sidebar.updateAvailable')}: ${latestVersion}`} data-tooltip-pos="bottom">
|
|
||||||
<ArrowUpCircle size={20} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="update-toast">
|
|
||||||
<div className="update-toast-header">
|
|
||||||
<ArrowUpCircle size={14} />
|
|
||||||
<span className="update-toast-label">{t('sidebar.updateAvailable')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="update-toast-version">{t('sidebar.updateReady', { version: latestVersion })}</div>
|
|
||||||
<button
|
|
||||||
className="update-toast-link"
|
|
||||||
onClick={() => open('https://github.com/Psychotoxical/psysonic/releases')}
|
|
||||||
>
|
|
||||||
{t('sidebar.updateLink')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Sidebar({
|
export default function Sidebar({
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
@@ -83,8 +45,6 @@ export default function Sidebar({
|
|||||||
const serverId = useAuthStore(s => s.activeServerId ?? '');
|
const serverId = useAuthStore(s => s.activeServerId ?? '');
|
||||||
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId);
|
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId);
|
||||||
const sidebarItems = useSidebarStore(s => s.items);
|
const sidebarItems = useSidebarStore(s => s.items);
|
||||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Resolve ordered, visible items per section from store config
|
// Resolve ordered, visible items per section from store config
|
||||||
const visibleLibrary = sidebarItems
|
const visibleLibrary = sidebarItems
|
||||||
.filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'library')
|
.filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'library')
|
||||||
@@ -93,28 +53,6 @@ export default function Sidebar({
|
|||||||
.filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'system')
|
.filter(cfg => cfg.visible && ALL_NAV_ITEMS[cfg.id]?.section === 'system')
|
||||||
.map(cfg => ALL_NAV_ITEMS[cfg.id]);
|
.map(cfg => ALL_NAV_ITEMS[cfg.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
const check = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch('https://api.github.com/repos/Psychotoxical/psysonic/releases/latest');
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const tag: string = data.tag_name ?? '';
|
|
||||||
if (!cancelled && tag && isNewer(tag, appVersion)) {
|
|
||||||
setLatestVersion(tag.replace(/^v/i, ''));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// network unavailable — silently skip
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initial = setTimeout(check, 1500);
|
|
||||||
const interval = setInterval(check, 10 * 60 * 1000); // every 10 minutes
|
|
||||||
|
|
||||||
return () => { cancelled = true; clearTimeout(initial); clearInterval(interval); };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={`sidebar animate-slide-in ${isCollapsed ? 'collapsed' : ''}`}>
|
<aside className={`sidebar animate-slide-in ${isCollapsed ? 'collapsed' : ''}`}>
|
||||||
@@ -178,8 +116,7 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{visibleSystem.length > 0 && !isCollapsed && <span className="nav-section-label">{t('sidebar.system')}</span>}
|
{visibleSystem.length > 0 && !isCollapsed && <span className="nav-section-label">{t('sidebar.system')}</span>}
|
||||||
{latestVersion && <UpdateToast isCollapsed={isCollapsed} latestVersion={latestVersion} />}
|
{visibleSystem.map(item => (
|
||||||
{visibleSystem.map(item => (
|
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
|
|||||||
+40
@@ -28,6 +28,7 @@ const enTranslation = {
|
|||||||
playlists: 'Playlists',
|
playlists: 'Playlists',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
hero: 'Featured',
|
||||||
starred: 'Personal Favorites',
|
starred: 'Personal Favorites',
|
||||||
recent: 'Recently Added',
|
recent: 'Recently Added',
|
||||||
mostPlayed: 'Most Played',
|
mostPlayed: 'Most Played',
|
||||||
@@ -306,6 +307,12 @@ const enTranslation = {
|
|||||||
bulkAddToPlaylist: 'Add to Playlist',
|
bulkAddToPlaylist: 'Add to Playlist',
|
||||||
bulkRemoveFromPlaylist: 'Remove from Playlist',
|
bulkRemoveFromPlaylist: 'Remove from Playlist',
|
||||||
bulkClear: 'Clear selection',
|
bulkClear: 'Clear selection',
|
||||||
|
updaterAvailable: 'Update available',
|
||||||
|
updaterVersion: 'v{{version}} is ready',
|
||||||
|
updaterInstall: 'Install & Restart',
|
||||||
|
updaterDownloading: 'Downloading…',
|
||||||
|
updaterInstalling: 'Installing…',
|
||||||
|
updaterDownload: 'Download from GitHub',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
@@ -400,6 +407,7 @@ const enTranslation = {
|
|||||||
tabPlayback: 'Playback',
|
tabPlayback: 'Playback',
|
||||||
tabLibrary: 'Library',
|
tabLibrary: 'Library',
|
||||||
tabAppearance: 'Appearance',
|
tabAppearance: 'Appearance',
|
||||||
|
homeCustomizerTitle: 'Home Page',
|
||||||
sidebarTitle: 'Sidebar',
|
sidebarTitle: 'Sidebar',
|
||||||
sidebarReset: 'Reset to default',
|
sidebarReset: 'Reset to default',
|
||||||
sidebarDrag: 'Drag to reorder',
|
sidebarDrag: 'Drag to reorder',
|
||||||
@@ -687,6 +695,7 @@ const deTranslation = {
|
|||||||
playlists: 'Playlists',
|
playlists: 'Playlists',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
hero: 'Featured',
|
||||||
starred: 'Persönliche Favoriten',
|
starred: 'Persönliche Favoriten',
|
||||||
recent: 'Zuletzt hinzugefügt',
|
recent: 'Zuletzt hinzugefügt',
|
||||||
mostPlayed: 'Meistgehört',
|
mostPlayed: 'Meistgehört',
|
||||||
@@ -965,6 +974,12 @@ const deTranslation = {
|
|||||||
bulkAddToPlaylist: 'Zur Playlist hinzufügen',
|
bulkAddToPlaylist: 'Zur Playlist hinzufügen',
|
||||||
bulkRemoveFromPlaylist: 'Aus Playlist entfernen',
|
bulkRemoveFromPlaylist: 'Aus Playlist entfernen',
|
||||||
bulkClear: 'Auswahl aufheben',
|
bulkClear: 'Auswahl aufheben',
|
||||||
|
updaterAvailable: 'Update verfügbar',
|
||||||
|
updaterVersion: 'v{{version}} ist bereit',
|
||||||
|
updaterInstall: 'Installieren & Neustart',
|
||||||
|
updaterDownloading: 'Wird geladen…',
|
||||||
|
updaterInstalling: 'Wird installiert…',
|
||||||
|
updaterDownload: 'Von GitHub herunterladen',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Einstellungen',
|
title: 'Einstellungen',
|
||||||
@@ -1059,6 +1074,7 @@ const deTranslation = {
|
|||||||
tabPlayback: 'Wiedergabe',
|
tabPlayback: 'Wiedergabe',
|
||||||
tabLibrary: 'Bibliothek',
|
tabLibrary: 'Bibliothek',
|
||||||
tabAppearance: 'Darstellung',
|
tabAppearance: 'Darstellung',
|
||||||
|
homeCustomizerTitle: 'Startseite',
|
||||||
sidebarTitle: 'Seitenleiste',
|
sidebarTitle: 'Seitenleiste',
|
||||||
sidebarReset: 'Zurücksetzen',
|
sidebarReset: 'Zurücksetzen',
|
||||||
sidebarDrag: 'Ziehen zum Umsortieren',
|
sidebarDrag: 'Ziehen zum Umsortieren',
|
||||||
@@ -1346,6 +1362,7 @@ const frTranslation = {
|
|||||||
playlists: 'Playlists',
|
playlists: 'Playlists',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
hero: 'En vedette',
|
||||||
starred: 'Favoris personnels',
|
starred: 'Favoris personnels',
|
||||||
recent: 'Ajoutés récemment',
|
recent: 'Ajoutés récemment',
|
||||||
mostPlayed: 'Les plus écoutés',
|
mostPlayed: 'Les plus écoutés',
|
||||||
@@ -1624,6 +1641,12 @@ const frTranslation = {
|
|||||||
bulkAddToPlaylist: 'Ajouter à la playlist',
|
bulkAddToPlaylist: 'Ajouter à la playlist',
|
||||||
bulkRemoveFromPlaylist: 'Retirer de la playlist',
|
bulkRemoveFromPlaylist: 'Retirer de la playlist',
|
||||||
bulkClear: 'Désélectionner',
|
bulkClear: 'Désélectionner',
|
||||||
|
updaterAvailable: 'Mise à jour disponible',
|
||||||
|
updaterVersion: 'v{{version}} est prête',
|
||||||
|
updaterInstall: 'Installer & Redémarrer',
|
||||||
|
updaterDownloading: 'Téléchargement…',
|
||||||
|
updaterInstalling: 'Installation…',
|
||||||
|
updaterDownload: 'Télécharger depuis GitHub',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Paramètres',
|
title: 'Paramètres',
|
||||||
@@ -1718,6 +1741,7 @@ const frTranslation = {
|
|||||||
tabPlayback: 'Lecture',
|
tabPlayback: 'Lecture',
|
||||||
tabLibrary: 'Bibliothèque',
|
tabLibrary: 'Bibliothèque',
|
||||||
tabAppearance: 'Apparence',
|
tabAppearance: 'Apparence',
|
||||||
|
homeCustomizerTitle: 'Page d\'accueil',
|
||||||
sidebarTitle: 'Barre latérale',
|
sidebarTitle: 'Barre latérale',
|
||||||
sidebarReset: 'Réinitialiser',
|
sidebarReset: 'Réinitialiser',
|
||||||
sidebarDrag: 'Glisser pour réorganiser',
|
sidebarDrag: 'Glisser pour réorganiser',
|
||||||
@@ -2005,6 +2029,7 @@ const nlTranslation = {
|
|||||||
playlists: 'Playlists',
|
playlists: 'Playlists',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
hero: 'Uitgelicht',
|
||||||
starred: 'Persoonlijke favorieten',
|
starred: 'Persoonlijke favorieten',
|
||||||
recent: 'Recent toegevoegd',
|
recent: 'Recent toegevoegd',
|
||||||
mostPlayed: 'Meest gespeeld',
|
mostPlayed: 'Meest gespeeld',
|
||||||
@@ -2283,6 +2308,12 @@ const nlTranslation = {
|
|||||||
bulkAddToPlaylist: 'Toevoegen aan afspeellijst',
|
bulkAddToPlaylist: 'Toevoegen aan afspeellijst',
|
||||||
bulkRemoveFromPlaylist: 'Verwijderen uit afspeellijst',
|
bulkRemoveFromPlaylist: 'Verwijderen uit afspeellijst',
|
||||||
bulkClear: 'Selectie wissen',
|
bulkClear: 'Selectie wissen',
|
||||||
|
updaterAvailable: 'Update beschikbaar',
|
||||||
|
updaterVersion: 'v{{version}} is klaar',
|
||||||
|
updaterInstall: 'Installeren & Herstarten',
|
||||||
|
updaterDownloading: 'Downloaden…',
|
||||||
|
updaterInstalling: 'Installeren…',
|
||||||
|
updaterDownload: 'Downloaden van GitHub',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Instellingen',
|
title: 'Instellingen',
|
||||||
@@ -2377,6 +2408,7 @@ const nlTranslation = {
|
|||||||
tabPlayback: 'Afspelen',
|
tabPlayback: 'Afspelen',
|
||||||
tabLibrary: 'Bibliotheek',
|
tabLibrary: 'Bibliotheek',
|
||||||
tabAppearance: 'Weergave',
|
tabAppearance: 'Weergave',
|
||||||
|
homeCustomizerTitle: 'Startpagina',
|
||||||
sidebarTitle: 'Zijbalk',
|
sidebarTitle: 'Zijbalk',
|
||||||
sidebarReset: 'Standaard herstellen',
|
sidebarReset: 'Standaard herstellen',
|
||||||
sidebarDrag: 'Slepen om te herordenen',
|
sidebarDrag: 'Slepen om te herordenen',
|
||||||
@@ -2664,6 +2696,7 @@ const zhTranslation = {
|
|||||||
playlists: '播放列表',
|
playlists: '播放列表',
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
|
hero: '精选',
|
||||||
starred: '个人收藏',
|
starred: '个人收藏',
|
||||||
recent: '最近添加',
|
recent: '最近添加',
|
||||||
mostPlayed: '最常播放',
|
mostPlayed: '最常播放',
|
||||||
@@ -2942,6 +2975,12 @@ const zhTranslation = {
|
|||||||
bulkAddToPlaylist: '添加到播放列表',
|
bulkAddToPlaylist: '添加到播放列表',
|
||||||
bulkRemoveFromPlaylist: '从播放列表移除',
|
bulkRemoveFromPlaylist: '从播放列表移除',
|
||||||
bulkClear: '取消选择',
|
bulkClear: '取消选择',
|
||||||
|
updaterAvailable: '有可用更新',
|
||||||
|
updaterVersion: 'v{{version}} 已就绪',
|
||||||
|
updaterInstall: '安装并重启',
|
||||||
|
updaterDownloading: '下载中…',
|
||||||
|
updaterInstalling: '安装中…',
|
||||||
|
updaterDownload: '从 GitHub 下载',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: '设置',
|
title: '设置',
|
||||||
@@ -3036,6 +3075,7 @@ const zhTranslation = {
|
|||||||
tabPlayback: '播放',
|
tabPlayback: '播放',
|
||||||
tabLibrary: '音乐库',
|
tabLibrary: '音乐库',
|
||||||
tabAppearance: '外观',
|
tabAppearance: '外观',
|
||||||
|
homeCustomizerTitle: '首页',
|
||||||
sidebarTitle: '侧边栏',
|
sidebarTitle: '侧边栏',
|
||||||
sidebarReset: '重置为默认',
|
sidebarReset: '重置为默认',
|
||||||
sidebarDrag: '拖动以重新排序',
|
sidebarDrag: '拖动以重新排序',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar, SubsonicSong, SubsonicAlbum } from '../api/subsonic';
|
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar, SubsonicSong, SubsonicAlbum } from '../api/subsonic';
|
||||||
@@ -231,10 +231,13 @@ const handleEnqueueAll = () => {
|
|||||||
deleteAlbum(album.album.id, serverId);
|
deleteAlbum(album.album.id, serverId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hooks must be called unconditionally — derive from nullable album state
|
// Hooks must be called unconditionally — derive from nullable album state.
|
||||||
const coverUrl = album?.album.coverArt ? buildCoverArtUrl(album.album.coverArt, 400) : '';
|
// useMemo is required: buildCoverArtUrl generates a new salt on every call, so without
|
||||||
const coverKey = album?.album.coverArt ? coverArtCacheKey(album.album.coverArt, 400) : '';
|
// memoization every re-render (e.g. currentTrack change) produces a new fetchUrl,
|
||||||
const resolvedCoverUrl = useCachedUrl(coverUrl, coverKey, false);
|
// which cancels and restarts the useCachedUrl effect → background never resolves.
|
||||||
|
const coverUrl = useMemo(() => album?.album.coverArt ? buildCoverArtUrl(album.album.coverArt, 400) : '', [album?.album.coverArt]);
|
||||||
|
const coverKey = useMemo(() => album?.album.coverArt ? coverArtCacheKey(album.album.coverArt, 400) : '', [album?.album.coverArt]);
|
||||||
|
const resolvedCoverUrl = useCachedUrl(coverUrl, coverKey);
|
||||||
|
|
||||||
if (loading) return <div className="loading-center"><div className="spinner" /></div>;
|
if (loading) return <div className="loading-center"><div className="spinner" /></div>;
|
||||||
if (!album) return <div className="empty-state">{t('albumDetail.notFound')}</div>;
|
if (!album) return <div className="empty-state">{t('albumDetail.notFound')}</div>;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, search, Subson
|
|||||||
import AlbumCard from '../components/AlbumCard';
|
import AlbumCard from '../components/AlbumCard';
|
||||||
import CachedImage from '../components/CachedImage';
|
import CachedImage from '../components/CachedImage';
|
||||||
import CoverLightbox from '../components/CoverLightbox';
|
import CoverLightbox from '../components/CoverLightbox';
|
||||||
import { ArrowLeft, Users, ExternalLink, Star, Play, Shuffle, Radio } from 'lucide-react';
|
import { ArrowLeft, Users, ExternalLink, Heart, Play, Shuffle, Radio } from 'lucide-react';
|
||||||
import { open } from '@tauri-apps/plugin-shell';
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
import { usePlayerStore, songToTrack } from '../store/playerStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -283,7 +283,7 @@ export default function ArtistDetail() {
|
|||||||
data-tooltip={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
data-tooltip={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
||||||
style={{ color: isStarred ? 'var(--accent)' : 'inherit', border: isStarred ? '1px solid var(--accent)' : undefined }}
|
style={{ color: isStarred ? 'var(--accent)' : 'inherit', border: isStarred ? '1px solid var(--accent)' : undefined }}
|
||||||
>
|
>
|
||||||
<Star size={14} fill={isStarred ? "currentColor" : "none"} />
|
<Heart size={14} fill={isStarred ? "currentColor" : "none"} />
|
||||||
{t('artistDetail.favorite')}
|
{t('artistDetail.favorite')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+33
-23
@@ -4,8 +4,12 @@ import AlbumRow from '../components/AlbumRow';
|
|||||||
import { getAlbumList, getArtists, SubsonicAlbum, SubsonicArtist } from '../api/subsonic';
|
import { getAlbumList, getArtists, SubsonicAlbum, SubsonicArtist } from '../api/subsonic';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useHomeStore } from '../store/homeStore';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const homeSections = useHomeStore(s => s.sections);
|
||||||
|
const isVisible = (id: string) => homeSections.find(s => s.id === id)?.visible ?? true;
|
||||||
|
|
||||||
const [starred, setStarred] = useState<SubsonicAlbum[]>([]);
|
const [starred, setStarred] = useState<SubsonicAlbum[]>([]);
|
||||||
const [recent, setRecent] = useState<SubsonicAlbum[]>([]);
|
const [recent, setRecent] = useState<SubsonicAlbum[]>([]);
|
||||||
const [random, setRandom] = useState<SubsonicAlbum[]>([]);
|
const [random, setRandom] = useState<SubsonicAlbum[]>([]);
|
||||||
@@ -22,7 +26,7 @@ export default function Home() {
|
|||||||
getAlbumList('random', 20).catch(() => []),
|
getAlbumList('random', 20).catch(() => []),
|
||||||
getAlbumList('frequent', 12).catch(() => []),
|
getAlbumList('frequent', 12).catch(() => []),
|
||||||
getAlbumList('recent', 12).catch(() => []),
|
getAlbumList('recent', 12).catch(() => []),
|
||||||
getArtists().catch(() => []),
|
isVisible('discoverArtists') ? getArtists().catch(() => []) : Promise.resolve<SubsonicArtist[]>([]),
|
||||||
]).then(([s, n, r, f, rp, artists]) => {
|
]).then(([s, n, r, f, rp, artists]) => {
|
||||||
setStarred(s);
|
setStarred(s);
|
||||||
setRecent(n);
|
setRecent(n);
|
||||||
@@ -60,7 +64,7 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<Hero albums={heroAlbums} />
|
{isVisible('hero') && <Hero albums={heroAlbums} />}
|
||||||
|
|
||||||
<div className="content-body" style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
<div className="content-body" style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -69,19 +73,23 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<AlbumRow
|
{isVisible('recent') && (
|
||||||
title={t('home.recent')}
|
<AlbumRow
|
||||||
albums={recent}
|
title={t('home.recent')}
|
||||||
onLoadMore={() => loadMore('newest', recent, setRecent)}
|
albums={recent}
|
||||||
moreText={t('home.loadMore')}
|
onLoadMore={() => loadMore('newest', recent, setRecent)}
|
||||||
/>
|
moreText={t('home.loadMore')}
|
||||||
<AlbumRow
|
/>
|
||||||
title={t('home.discover')}
|
)}
|
||||||
albums={random}
|
{isVisible('discover') && (
|
||||||
onLoadMore={() => loadMore('random', random, setRandom)}
|
<AlbumRow
|
||||||
moreText={t('home.discoverMore')}
|
title={t('home.discover')}
|
||||||
/>
|
albums={random}
|
||||||
{randomArtists.length > 0 && (
|
onLoadMore={() => loadMore('random', random, setRandom)}
|
||||||
|
moreText={t('home.discoverMore')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isVisible('discoverArtists') && randomArtists.length > 0 && (
|
||||||
<section className="album-row-section">
|
<section className="album-row-section">
|
||||||
<div className="album-row-header">
|
<div className="album-row-header">
|
||||||
<h2 className="section-title" style={{ marginBottom: 0 }}>{t('home.discoverArtists')}</h2>
|
<h2 className="section-title" style={{ marginBottom: 0 }}>{t('home.discoverArtists')}</h2>
|
||||||
@@ -99,7 +107,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{recentlyPlayed.length > 0 && (
|
{isVisible('recentlyPlayed') && recentlyPlayed.length > 0 && (
|
||||||
<AlbumRow
|
<AlbumRow
|
||||||
title={t('home.recentlyPlayed')}
|
title={t('home.recentlyPlayed')}
|
||||||
albums={recentlyPlayed}
|
albums={recentlyPlayed}
|
||||||
@@ -107,7 +115,7 @@ export default function Home() {
|
|||||||
moreText={t('home.loadMore')}
|
moreText={t('home.loadMore')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{starred.length > 0 && (
|
{isVisible('starred') && starred.length > 0 && (
|
||||||
<AlbumRow
|
<AlbumRow
|
||||||
title={t('home.starred')}
|
title={t('home.starred')}
|
||||||
albums={starred}
|
albums={starred}
|
||||||
@@ -115,12 +123,14 @@ export default function Home() {
|
|||||||
moreText={t('home.loadMore')}
|
moreText={t('home.loadMore')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AlbumRow
|
{isVisible('mostPlayed') && (
|
||||||
title={t('home.mostPlayed')}
|
<AlbumRow
|
||||||
albums={mostPlayed}
|
title={t('home.mostPlayed')}
|
||||||
onLoadMore={() => loadMore('frequent', mostPlayed, setMostPlayed)}
|
albums={mostPlayed}
|
||||||
moreText={t('home.loadMore')}
|
onLoadMore={() => loadMore('frequent', mostPlayed, setMostPlayed)}
|
||||||
/>
|
moreText={t('home.loadMore')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export default function PlaylistDetail() {
|
|||||||
|
|
||||||
const bgFetchUrl = useMemo(() => buildCoverArtUrl(coverQuad[0] ?? '', 300), [coverQuad]);
|
const bgFetchUrl = useMemo(() => buildCoverArtUrl(coverQuad[0] ?? '', 300), [coverQuad]);
|
||||||
const bgCacheKey = useMemo(() => coverArtCacheKey(coverQuad[0] ?? '', 300), [coverQuad]);
|
const bgCacheKey = useMemo(() => coverArtCacheKey(coverQuad[0] ?? '', 300), [coverQuad]);
|
||||||
const resolvedBgUrl = useCachedUrl(bgFetchUrl, bgCacheKey, false);
|
const resolvedBgUrl = useCachedUrl(bgFetchUrl, bgCacheKey);
|
||||||
|
|
||||||
// Song search
|
// Song search
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
|
|||||||
+47
-1
@@ -5,7 +5,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen,
|
Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen,
|
||||||
Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play, Type, Keyboard, ChevronDown,
|
Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play, Type, Keyboard, ChevronDown,
|
||||||
GripVertical, PanelLeft, RotateCcw
|
GripVertical, PanelLeft, RotateCcw, LayoutGrid
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { open as openUrl } from '@tauri-apps/plugin-shell';
|
import { open as openUrl } from '@tauri-apps/plugin-shell';
|
||||||
@@ -21,6 +21,7 @@ import { useFontStore, FontId } from '../store/fontStore';
|
|||||||
import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore';
|
import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore';
|
||||||
import { useGlobalShortcutsStore, GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '../store/globalShortcutsStore';
|
import { useGlobalShortcutsStore, GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '../store/globalShortcutsStore';
|
||||||
import { useSidebarStore, DEFAULT_SIDEBAR_ITEMS, SidebarItemConfig } from '../store/sidebarStore';
|
import { useSidebarStore, DEFAULT_SIDEBAR_ITEMS, SidebarItemConfig } from '../store/sidebarStore';
|
||||||
|
import { useHomeStore, HomeSectionId } from '../store/homeStore';
|
||||||
import { useDragDrop, useDragSource } from '../contexts/DragDropContext';
|
import { useDragDrop, useDragSource } from '../contexts/DragDropContext';
|
||||||
import { ALL_NAV_ITEMS } from '../components/Sidebar';
|
import { ALL_NAV_ITEMS } from '../components/Sidebar';
|
||||||
import { pingWithCredentials } from '../api/subsonic';
|
import { pingWithCredentials } from '../api/subsonic';
|
||||||
@@ -562,6 +563,8 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<HomeCustomizer />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1149,6 +1152,49 @@ function renderInline(text: string): React.ReactNode[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HomeCustomizer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { sections, toggleSection, reset } = useHomeStore();
|
||||||
|
|
||||||
|
const SECTION_LABELS: Record<HomeSectionId, string> = {
|
||||||
|
hero: t('home.hero'),
|
||||||
|
recent: t('home.recent'),
|
||||||
|
discover: t('home.discover'),
|
||||||
|
discoverArtists: t('home.discoverArtists'),
|
||||||
|
recentlyPlayed: t('home.recentlyPlayed'),
|
||||||
|
starred: t('home.starred'),
|
||||||
|
mostPlayed: t('home.mostPlayed'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="settings-section">
|
||||||
|
<div className="settings-section-header">
|
||||||
|
<LayoutGrid size={18} />
|
||||||
|
<h2>{t('settings.homeCustomizerTitle')}</h2>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--text-muted)' }}
|
||||||
|
onClick={reset}
|
||||||
|
data-tooltip={t('settings.sidebarReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="settings-card" style={{ padding: '4px 0' }}>
|
||||||
|
{sections.map(sec => (
|
||||||
|
<div key={sec.id} className="settings-toggle-row" style={{ padding: '8px 16px' }}>
|
||||||
|
<span style={{ fontSize: 14 }}>{SECTION_LABELS[sec.id]}</span>
|
||||||
|
<label className="toggle-switch" aria-label={SECTION_LABELS[sec.id]}>
|
||||||
|
<input type="checkbox" checked={sec.visible} onChange={() => toggleSection(sec.id)} />
|
||||||
|
<span className="toggle-track" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SidebarGripHandle({ idx, section, label }: { idx: number; section: 'library' | 'system'; label: string }) {
|
function SidebarGripHandle({ idx, section, label }: { idx: number; section: 'library' | 'system'; label: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { onMouseDown } = useDragSource(() => ({
|
const { onMouseDown } = useDragSource(() => ({
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export type HomeSectionId = 'hero' | 'recent' | 'discover' | 'discoverArtists' | 'recentlyPlayed' | 'starred' | 'mostPlayed';
|
||||||
|
|
||||||
|
export interface HomeSectionConfig {
|
||||||
|
id: HomeSectionId;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_HOME_SECTIONS: HomeSectionConfig[] = [
|
||||||
|
{ id: 'hero', visible: true },
|
||||||
|
{ id: 'recent', visible: true },
|
||||||
|
{ id: 'discover', visible: true },
|
||||||
|
{ id: 'discoverArtists', visible: true },
|
||||||
|
{ id: 'recentlyPlayed', visible: true },
|
||||||
|
{ id: 'starred', visible: true },
|
||||||
|
{ id: 'mostPlayed', visible: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface HomeStore {
|
||||||
|
sections: HomeSectionConfig[];
|
||||||
|
toggleSection: (id: HomeSectionId) => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHomeStore = create<HomeStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
sections: DEFAULT_HOME_SECTIONS,
|
||||||
|
toggleSection: (id) => set((s) => ({
|
||||||
|
sections: s.sections.map(sec => sec.id === id ? { ...sec, visible: !sec.visible } : sec),
|
||||||
|
})),
|
||||||
|
reset: () => set({ sections: DEFAULT_HOME_SECTIONS }),
|
||||||
|
}),
|
||||||
|
{ name: 'psysonic_home' }
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -243,6 +243,105 @@
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── AppUpdater floating toast ── */
|
||||||
|
.app-updater-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(var(--player-height, 80px) + 12px);
|
||||||
|
left: 12px;
|
||||||
|
width: 210px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.35);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
z-index: 9000;
|
||||||
|
animation: update-toast-in 0.35s ease both;
|
||||||
|
}
|
||||||
|
.app-updater-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.app-updater-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.app-updater-dismiss {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
.app-updater-dismiss:hover { opacity: 1; }
|
||||||
|
.app-updater-version {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding-left: 19px;
|
||||||
|
}
|
||||||
|
.app-updater-actions {
|
||||||
|
padding-left: 19px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.app-updater-btn-primary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
.app-updater-btn-primary:hover { opacity: 1; }
|
||||||
|
.app-updater-progress-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-left: 19px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.app-updater-progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--bg-glass);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.app-updater-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.15s ease;
|
||||||
|
}
|
||||||
|
.app-updater-pct {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 26px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.app-updater-status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding-left: 19px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.update-toast {
|
.update-toast {
|
||||||
margin: 0 var(--space-1) var(--space-2);
|
margin: 0 var(--space-1) var(--space-2);
|
||||||
padding: var(--space-3) var(--space-3);
|
padding: var(--space-3) var(--space-3);
|
||||||
|
|||||||
Reference in New Issue
Block a user