mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-25 08:45:46 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bcef81b22 | |||
| 693766134b | |||
| 71fbc717f6 | |||
| d33c7042b6 | |||
| 61c17d2e24 | |||
| e4abaf8814 |
@@ -13,6 +13,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
>
|
||||
> **📦 Version jump 1.34.x → 1.40.0:** The 1.34.x patch series was bumped a lot as each small feature landed. 1.40.0 consolidates the last few weeks of work — macOS signing + auto-updater, the Device-Sync overhaul, theme work and contrast audits — into a single coherent release. The next major bump (2.0.0) is planned once Windows code-signing + Windows auto-updater are active as well.
|
||||
|
||||
## [1.42.1] - 2026-04-19
|
||||
|
||||
> **🚨 Critical bug fix for Windows users.** On 1.42.0, opening the mini player on Windows could stall Tauri's event loop: the mini would appear as a blank white window, neither the main window nor the mini could be closed, and the only way out was killing the process via Task Manager. **Please update immediately if you're on Windows 1.42.0.** macOS and Linux were not affected.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Mini player no longer hangs the app on Windows** *([@Psychotoxical](https://github.com/Psychotoxical))*: Creating the second WebView2 webview lazily from the `open_mini_player` invoke handler reliably froze the app on Windows — the mini opened blank, both windows became unresponsive, and the user had to kill the process from Task Manager. The builder + `main.minimize()` combo racing against WebView2's first paint was the trigger. The mini webview is now pre-built hidden in Tauri's `.setup()` on Windows, so the first open is a pure show/hide instead of creation + minimize. `open_mini_player` is simpler on all platforms, the minimize-main dance around show/hide is skipped on Windows, and Windows also goes back to the native window decorations (the earlier `decorations: false` mini titlebar was part of the hang surface).
|
||||
- **Mini player syncs immediately on first open** *([@Psychotoxical](https://github.com/Psychotoxical))*: With the mini pre-created on Windows, the mount-time `mini:ready` event could race past the main window's bridge listener and leave the mini without a snapshot when the user actually opened it. The mini now also re-emits `mini:ready` on every window focus, so opening the mini always triggers a fresh sync regardless of startup ordering.
|
||||
|
||||
### Added
|
||||
|
||||
- **Optional “Preload mini player” setting on Linux + macOS** *([@Psychotoxical](https://github.com/Psychotoxical))*: Settings → General → App behaviour. Off by default. When enabled, the mini player window is built hidden at app start so the first open is instant instead of waiting a few seconds for WebKit to boot + React to hydrate + the bridge snapshot to arrive. Costs one extra WebKit process in the background permanently (~50–100 MB RAM). Windows always preloads regardless of this toggle — it's how we work around the hang above, not an opt-in feature there.
|
||||
|
||||
## [1.42.0] - 2026-04-19
|
||||
|
||||
> **🛠️ Note on the 1.41.0 jump:** The 1.41.0 tag exists as an internal Draft release on GitHub — it was used to wire up and verify the Cachix substituter pipeline and never went public. **1.42.0 is the first public release after 1.40.0** and consolidates everything that was prepared for 1.41.0 plus the work landed on top in the days since.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"npmDepsHash": "sha256-GwwfdTSGsjLvaJiSrEJPj+I027Lp6uPLkDXZJ+pDers="
|
||||
"npmDepsHash": "sha256-5YAQ2PqxJtD7+adecF++swOHVKNstEkyRQeiBrP2lvA="
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "psysonic",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3653,7 +3653,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "psysonic"
|
||||
version = "1.42.0"
|
||||
version = "1.42.1"
|
||||
dependencies = [
|
||||
"biquad",
|
||||
"discord-rich-presence",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "psysonic"
|
||||
version = "1.42.0"
|
||||
version = "1.42.1"
|
||||
description = "Psysonic Desktop Music Player"
|
||||
authors = []
|
||||
license = ""
|
||||
|
||||
@@ -342,7 +342,7 @@ impl Device {
|
||||
/// Ensures that `future_audio_client` contains a `Some` and returns a locked mutex to it.
|
||||
fn ensure_future_audio_client(
|
||||
&self,
|
||||
) -> Result<MutexGuard<Option<IAudioClientWrapper>>, windows::core::Error> {
|
||||
) -> Result<MutexGuard<'_, Option<IAudioClientWrapper>>, windows::core::Error> {
|
||||
let mut lock = self.future_audio_client.lock().unwrap();
|
||||
if lock.is_some() {
|
||||
return Ok(lock);
|
||||
|
||||
+93
-49
@@ -13,9 +13,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use tauri::{
|
||||
menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
|
||||
tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent},
|
||||
Emitter, Manager,
|
||||
};
|
||||
// MouseButtonState is only matched on non-Windows targets — on Windows the
|
||||
// tray uses DoubleClick which doesn't carry a button_state.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use tauri::tray::MouseButtonState;
|
||||
|
||||
/// Tracks which user-configured shortcuts are currently registered (shortcut_str → action).
|
||||
/// Prevents on_shortcut() accumulating duplicate handlers across JS reloads (HMR / StrictMode).
|
||||
@@ -2701,41 +2705,13 @@ fn default_mini_position(app: &tauri::AppHandle) -> Option<tauri::PhysicalPositi
|
||||
))
|
||||
}
|
||||
|
||||
/// Open (or toggle) the mini player window. Creates it on first call; on
|
||||
/// subsequent calls, hides it if visible, shows + focuses it if hidden.
|
||||
/// Opening the mini player minimizes the main window; hiding the mini player
|
||||
/// restores the main window.
|
||||
#[tauri::command]
|
||||
fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(win) = app.get_webview_window("mini") {
|
||||
let visible = win.is_visible().unwrap_or(false);
|
||||
if visible {
|
||||
win.hide().map_err(|e| e.to_string())?;
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.unminimize();
|
||||
let _ = main.show();
|
||||
let _ = main.set_focus();
|
||||
}
|
||||
} else {
|
||||
// Re-applying the saved position after show() — many Linux WMs
|
||||
// (Mutter, KWin) re-centre hidden windows when they're shown
|
||||
// again, ignoring any earlier set_position. Mark the move as
|
||||
// programmatic so the Moved-event handler doesn't echo the
|
||||
// intermediate centre coords back to disk.
|
||||
let target = read_mini_pos(&app);
|
||||
mark_mini_pos_programmatic();
|
||||
win.show().map_err(|e| e.to_string())?;
|
||||
let _ = win.set_focus();
|
||||
if let Some(p) = target {
|
||||
let _ = win.set_position(tauri::PhysicalPosition::new(p.x, p.y));
|
||||
}
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.minimize();
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// Build the mini player webview window. Caller decides `visible` so the
|
||||
/// same code path serves both pre-creation (Windows, hidden at app start)
|
||||
/// and lazy creation (other platforms, shown on demand).
|
||||
fn build_mini_player_window(
|
||||
app: &tauri::AppHandle,
|
||||
visible: bool,
|
||||
) -> Result<tauri::WebviewWindow, String> {
|
||||
let use_always_on_top = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{ !is_tiling_wm() }
|
||||
@@ -2746,9 +2722,9 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
// Resolve target position BEFORE building so the WM places the window
|
||||
// correctly from creation. Calling `set_position` after `build()` is
|
||||
// unreliable on several Linux WMs which re-centre hidden windows.
|
||||
let target_physical = read_mini_pos(&app)
|
||||
let target_physical = read_mini_pos(app)
|
||||
.map(|p| tauri::PhysicalPosition::new(p.x, p.y))
|
||||
.or_else(|| default_mini_position(&app));
|
||||
.or_else(|| default_mini_position(app));
|
||||
let scale = app
|
||||
.primary_monitor()
|
||||
.ok()
|
||||
@@ -2756,13 +2732,13 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
.map(|m| m.scale_factor())
|
||||
.unwrap_or(1.0);
|
||||
|
||||
// macOS keeps the native titlebar (traffic lights + system look).
|
||||
// Windows and Linux use a custom in-page titlebar so the mini fits a
|
||||
// tighter visual style across all WMs (incl. tiling).
|
||||
let use_decorations = cfg!(target_os = "macos");
|
||||
// macOS + Windows keep the native titlebar (traffic lights / caption
|
||||
// buttons + system look). Linux uses a custom in-page titlebar so the
|
||||
// mini fits a tighter visual style across all WMs (incl. tiling).
|
||||
let use_decorations = !cfg!(target_os = "linux");
|
||||
|
||||
let mut builder = tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
app,
|
||||
"mini",
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
)
|
||||
@@ -2772,7 +2748,8 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
.resizable(true)
|
||||
.decorations(use_decorations)
|
||||
.always_on_top(use_always_on_top)
|
||||
.skip_taskbar(false);
|
||||
.skip_taskbar(false)
|
||||
.visible(visible);
|
||||
|
||||
if let Some(pos) = target_physical {
|
||||
builder = builder.position(pos.x as f64 / scale, pos.y as f64 / scale);
|
||||
@@ -2782,13 +2759,64 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
// fire stray Moved events with default coords during the first paint.
|
||||
mark_mini_pos_programmatic();
|
||||
|
||||
let win = builder
|
||||
builder
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build mini player window: {e}"))?;
|
||||
.map_err(|e| format!("failed to build mini player window: {e}"))
|
||||
}
|
||||
|
||||
let _ = win.set_focus();
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.minimize();
|
||||
/// Pre-build the mini player window hidden, so the first `open_mini_player`
|
||||
/// call becomes a pure show/hide and the user sees content instantly. On
|
||||
/// Windows this already happens unconditionally in `.setup()` as a hang
|
||||
/// workaround; this command is used by Linux/macOS when the user opts into
|
||||
/// the `preloadMiniPlayer` setting. Idempotent — no-op if the window exists.
|
||||
#[tauri::command]
|
||||
fn preload_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
if app.get_webview_window("mini").is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
build_mini_player_window(&app, false).map(|_| ())
|
||||
}
|
||||
|
||||
/// Open (or toggle) the mini player window. On platforms where the window
|
||||
/// was pre-created at startup (Windows), this is a pure show/hide. On
|
||||
/// other platforms the window is created lazily on first call.
|
||||
/// Opening the mini player minimizes the main window; hiding the mini
|
||||
/// player restores the main window. Both steps are skipped on Windows
|
||||
/// because creating + immediately minimizing main stalls WebView2's paint
|
||||
/// pipeline and locks up the Tauri event loop.
|
||||
#[tauri::command]
|
||||
fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> {
|
||||
let win = match app.get_webview_window("mini") {
|
||||
Some(w) => w,
|
||||
None => build_mini_player_window(&app, false)?,
|
||||
};
|
||||
|
||||
let visible = win.is_visible().unwrap_or(false);
|
||||
if visible {
|
||||
win.hide().map_err(|e| e.to_string())?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.unminimize();
|
||||
let _ = main.show();
|
||||
let _ = main.set_focus();
|
||||
}
|
||||
} else {
|
||||
// Re-applying the saved position after show() — many Linux WMs
|
||||
// (Mutter, KWin) re-centre hidden windows when they're shown
|
||||
// again, ignoring any earlier set_position. Mark the move as
|
||||
// programmatic so the Moved-event handler doesn't echo the
|
||||
// intermediate centre coords back to disk.
|
||||
let target = read_mini_pos(&app);
|
||||
mark_mini_pos_programmatic();
|
||||
win.show().map_err(|e| e.to_string())?;
|
||||
let _ = win.set_focus();
|
||||
if let Some(p) = target {
|
||||
let _ = win.set_position(tauri::PhysicalPosition::new(p.x, p.y));
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(main) = app.get_webview_window("main") {
|
||||
let _ = main.minimize();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3045,6 +3073,21 @@ pub fn run() {
|
||||
audio::start_device_watcher(&engine, app.handle().clone());
|
||||
}
|
||||
|
||||
// ── Pre-create mini player window (Windows) ──────────────────
|
||||
// Creating the second WebView2 webview lazily from an invoke
|
||||
// handler on Windows reliably stalls the Tauri event loop —
|
||||
// the mini shows a blank white window, neither main nor mini
|
||||
// can be closed, and the user has to kill the process via
|
||||
// Task Manager. Building it at startup (hidden) avoids the
|
||||
// runtime-creation code path entirely; later `open_mini_player`
|
||||
// calls are pure show/hide.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Err(e) = build_mini_player_window(app.handle(), false) {
|
||||
eprintln!("[psysonic] Failed to pre-create mini window: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Cold start with `--player …`: defer emit so the webview can register listeners.
|
||||
crate::cli::spawn_deferred_cli_argv_handler(app.handle());
|
||||
|
||||
@@ -3102,6 +3145,7 @@ pub fn run() {
|
||||
no_compositing_mode,
|
||||
is_tiling_wm_cmd,
|
||||
open_mini_player,
|
||||
preload_mini_player,
|
||||
close_mini_player,
|
||||
set_mini_player_always_on_top,
|
||||
resize_mini_player,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Psysonic",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"identifier": "dev.psysonic.player",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
+10
-1
@@ -56,7 +56,7 @@ import GenreDetail from './pages/GenreDetail';
|
||||
import ExportPickerModal from './components/ExportPickerModal';
|
||||
import AppUpdater from './components/AppUpdater';
|
||||
import TitleBar from './components/TitleBar';
|
||||
import { IS_LINUX } from './utils/platform';
|
||||
import { IS_LINUX, IS_WINDOWS } from './utils/platform';
|
||||
import { version } from '../package.json';
|
||||
import { useConnectionStatus } from './hooks/useConnectionStatus';
|
||||
import { useAuthStore } from './store/authStore';
|
||||
@@ -975,6 +975,15 @@ export default function App() {
|
||||
return initMiniPlayerBridgeOnMain();
|
||||
}, [isMiniWindow]);
|
||||
|
||||
// Main window only: optionally pre-create the mini player webview hidden so
|
||||
// the first open is instant. Windows already does this unconditionally in
|
||||
// Rust .setup() as a hang workaround — skip here to avoid double-building.
|
||||
const preloadMiniPlayer = useAuthStore(s => s.preloadMiniPlayer);
|
||||
useEffect(() => {
|
||||
if (isMiniWindow || IS_WINDOWS || !preloadMiniPlayer) return;
|
||||
invoke('preload_mini_player').catch(() => {});
|
||||
}, [isMiniWindow, preloadMiniPlayer]);
|
||||
|
||||
// Mini window only: re-hydrate persisted appearance stores when the main
|
||||
// window writes new values. Both webviews share localStorage (same origin),
|
||||
// so the `storage` event fires here whenever main mutates a key — but
|
||||
|
||||
@@ -8,7 +8,7 @@ import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useKeybindingsStore, matchInAppBinding } from '../store/keybindingsStore';
|
||||
import { useDragDrop } from '../contexts/DragDropContext';
|
||||
import { IS_MACOS } from '../utils/platform';
|
||||
import { IS_LINUX } from '../utils/platform';
|
||||
import MiniContextMenu from './MiniContextMenu';
|
||||
import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge';
|
||||
|
||||
@@ -145,8 +145,16 @@ export default function MiniPlayer() {
|
||||
}, []);
|
||||
|
||||
// Announce to main window that we're mounted; it replies with a snapshot.
|
||||
// Also re-announce on window focus: on Windows the mini is pre-created at
|
||||
// app startup so the mount-time emit can race past main's bridge before
|
||||
// it has attached its listener. Re-emitting on focus means every actual
|
||||
// open of the mini (user clicks the player-bar icon) triggers a fresh
|
||||
// sync regardless of startup ordering.
|
||||
useEffect(() => {
|
||||
emit('mini:ready', {}).catch(() => {});
|
||||
const onFocus = () => { emit('mini:ready', {}).catch(() => {}); };
|
||||
window.addEventListener('focus', onFocus);
|
||||
return () => window.removeEventListener('focus', onFocus);
|
||||
}, []);
|
||||
|
||||
// Restore the expanded window size on initial mount when the queue was
|
||||
@@ -326,41 +334,53 @@ export default function MiniPlayer() {
|
||||
|
||||
return (
|
||||
<div className="mini-player-shell">
|
||||
{!IS_MACOS && (
|
||||
<div className="mini-player__titlebar" data-tauri-drag-region>
|
||||
<div
|
||||
className={`mini-player__titlebar${!IS_LINUX ? ' mini-player__titlebar--mac' : ''}`}
|
||||
{...(!IS_LINUX ? {} : { 'data-tauri-drag-region': true })}
|
||||
>
|
||||
{IS_LINUX ? (
|
||||
<span className="mini-player__titlebar-title" data-tauri-drag-region>
|
||||
{track?.title ?? 'Psysonic Mini'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`mini-player__titlebar-btn${queueOpen ? ' mini-player__titlebar-btn--active' : ''}`}
|
||||
onClick={toggleQueue}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={queueOpen ? t('miniPlayer.hideQueue') : t('miniPlayer.showQueue')}
|
||||
aria-label={queueOpen ? t('miniPlayer.hideQueue') : t('miniPlayer.showQueue')}
|
||||
>
|
||||
<ListMusic size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mini-player__titlebar-btn${alwaysOnTop ? ' mini-player__titlebar-btn--active' : ''}`}
|
||||
onClick={toggleOnTop}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={alwaysOnTop ? t('miniPlayer.pinOff') : t('miniPlayer.pinOnTop')}
|
||||
aria-label={alwaysOnTop ? t('miniPlayer.pinOff') : t('miniPlayer.pinOnTop')}
|
||||
>
|
||||
{alwaysOnTop ? <Pin size={13} /> : <PinOff size={13} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mini-player__titlebar-btn"
|
||||
onClick={showMain}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={t('miniPlayer.openMainWindow')}
|
||||
aria-label={t('miniPlayer.openMainWindow')}
|
||||
>
|
||||
<Maximize2 size={13} />
|
||||
</button>
|
||||
) : (
|
||||
// macOS/Windows already render a native titlebar with the window
|
||||
// title + close button; we just need a flexible spacer so the
|
||||
// action buttons sit right.
|
||||
<span className="mini-player__titlebar-spacer" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`mini-player__titlebar-btn${queueOpen ? ' mini-player__titlebar-btn--active' : ''}`}
|
||||
onClick={toggleQueue}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={queueOpen ? t('miniPlayer.hideQueue') : t('miniPlayer.showQueue')}
|
||||
aria-label={queueOpen ? t('miniPlayer.hideQueue') : t('miniPlayer.showQueue')}
|
||||
>
|
||||
<ListMusic size={13} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mini-player__titlebar-btn${alwaysOnTop ? ' mini-player__titlebar-btn--active' : ''}`}
|
||||
onClick={toggleOnTop}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={alwaysOnTop ? t('miniPlayer.pinOff') : t('miniPlayer.pinOnTop')}
|
||||
aria-label={alwaysOnTop ? t('miniPlayer.pinOff') : t('miniPlayer.pinOnTop')}
|
||||
>
|
||||
{alwaysOnTop ? <Pin size={13} /> : <PinOff size={13} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mini-player__titlebar-btn"
|
||||
onClick={showMain}
|
||||
data-tauri-drag-region="false"
|
||||
data-tooltip={t('miniPlayer.openMainWindow')}
|
||||
aria-label={t('miniPlayer.openMainWindow')}
|
||||
>
|
||||
<Maximize2 size={13} />
|
||||
</button>
|
||||
{/* macOS + Windows already provide Close via the native titlebar —
|
||||
skip the duplicate so the in-app titlebar stays minimal. */}
|
||||
{IS_LINUX && (
|
||||
<button
|
||||
type="button"
|
||||
className="mini-player__titlebar-btn mini-player__titlebar-btn--close"
|
||||
@@ -371,8 +391,8 @@ export default function MiniPlayer() {
|
||||
>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`mini-player${queueOpen ? ' mini-player--queue-open' : ''}`}>
|
||||
<div className="mini-player__art">
|
||||
|
||||
@@ -553,6 +553,8 @@ export const deTranslation = {
|
||||
showTrayIconDesc: 'Psysonic-Icon im System-Tray / in der Menüleiste anzeigen.',
|
||||
minimizeToTray: 'Im Tray minimieren',
|
||||
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
|
||||
preloadMiniPlayer: 'Mini-Player vorladen',
|
||||
preloadMiniPlayerDesc: 'Baut das Mini-Player-Fenster beim App-Start im Hintergrund auf, damit es beim ersten Öffnen sofort Inhalt zeigt. Kostet etwas mehr RAM.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
discordRichPresenceDesc: 'Zeigt den aktuell gespielten Titel im Discord-Profil an. Discord muss dafür geöffnet sein.',
|
||||
useCustomTitlebar: 'Eigene Titelleiste',
|
||||
|
||||
@@ -555,6 +555,8 @@ export const enTranslation = {
|
||||
showTrayIconDesc: 'Display the Psysonic icon in the system notification area / menu bar.',
|
||||
minimizeToTray: 'Minimize to Tray',
|
||||
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
|
||||
preloadMiniPlayer: 'Preload mini player',
|
||||
preloadMiniPlayerDesc: 'Build the mini player window in the background at app start so it shows content instantly on first open. Uses a little extra memory.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
discordRichPresenceDesc: 'Show the currently playing track on your Discord profile. Requires Discord to be running.',
|
||||
useCustomTitlebar: 'Custom title bar',
|
||||
|
||||
@@ -546,6 +546,8 @@ export const esTranslation = {
|
||||
showTrayIconDesc: 'Muestra el icono de Psysonic en el área de notificación / barra de menú.',
|
||||
minimizeToTray: 'Minimizar a Bandeja',
|
||||
minimizeToTrayDesc: 'Al cerrar la ventana, mantener Psysonic ejecutándose en la bandeja del sistema en lugar de salir.',
|
||||
preloadMiniPlayer: 'Precargar mini reproductor',
|
||||
preloadMiniPlayerDesc: 'Crea la ventana del mini reproductor en segundo plano al iniciar la aplicación para que muestre contenido al instante la primera vez que se abre. Consume un poco más de memoria.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
discordRichPresenceDesc: 'Muestra la pista actual en tu perfil de Discord. Requiere que Discord esté ejecutándose.',
|
||||
useCustomTitlebar: 'Barra de título personalizada',
|
||||
|
||||
@@ -543,6 +543,8 @@ export const frTranslation = {
|
||||
showTrayIconDesc: 'Affiche l\'icône Psysonic dans la zone de notification / barre des menus.',
|
||||
minimizeToTray: 'Réduire dans la barre système',
|
||||
minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.',
|
||||
preloadMiniPlayer: 'Précharger le mini-lecteur',
|
||||
preloadMiniPlayerDesc: 'Construit la fenêtre du mini-lecteur en arrière-plan au démarrage de l\'application afin qu\'elle affiche son contenu instantanément à la première ouverture. Utilise un peu plus de mémoire.',
|
||||
linuxWebkitSmoothScroll: 'Molette fluide (Linux)',
|
||||
linuxWebkitSmoothScrollDesc: 'Activé : inertie. Désactivé : pas à la ligne, style GTK.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
|
||||
@@ -542,6 +542,8 @@ export const nbTranslation = {
|
||||
showArtistImagesDesc: 'Last inn og vis artistbilder i artistoversikten. Denne er deaktivert som standard, for å redusere disk-I/O og nettverksbelastningen på store biblioteker.',
|
||||
minimizeToTray: 'Minimer til oppgavelinjen',
|
||||
minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.',
|
||||
preloadMiniPlayer: 'Forhåndslast miniavspiller',
|
||||
preloadMiniPlayerDesc: 'Bygger miniavspiller-vinduet i bakgrunnen ved appstart slik at det viser innhold umiddelbart ved første åpning. Bruker litt mer minne.',
|
||||
linuxWebkitSmoothScroll: 'Mykt musehjul (Linux)',
|
||||
linuxWebkitSmoothScrollDesc: 'På: treg rull med etterslep. Av: trinnvis som i GTK.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
|
||||
@@ -542,6 +542,8 @@ export const nlTranslation = {
|
||||
showTrayIconDesc: 'Toont het Psysonic-pictogram in het systeemvak / de menubalk.',
|
||||
minimizeToTray: 'Minimaliseren naar systeemvak',
|
||||
minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.',
|
||||
preloadMiniPlayer: 'Mini-speler vooraf laden',
|
||||
preloadMiniPlayerDesc: 'Bouwt het venster van de mini-speler op de achtergrond bij het opstarten van de app, zodat het bij de eerste opening direct inhoud toont. Gebruikt iets meer geheugen.',
|
||||
linuxWebkitSmoothScroll: 'Vloeiend muiswiel (Linux)',
|
||||
linuxWebkitSmoothScrollDesc: 'Aan: traag naloop. Uit: regel voor regel, GTK-stijl.',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
|
||||
@@ -563,6 +563,8 @@ export const ruTranslation = {
|
||||
showTrayIconDesc: 'Показывать Psysonic в области уведомлений / строке меню.',
|
||||
minimizeToTray: 'Сворачивать в трей',
|
||||
minimizeToTrayDesc: 'При закрытии окна не выходить из приложения, а оставаться в трее.',
|
||||
preloadMiniPlayer: 'Предзагрузка мини-плеера',
|
||||
preloadMiniPlayerDesc: 'Создаёт окно мини-плеера в фоне при запуске приложения, чтобы при первом открытии содержимое отображалось мгновенно. Использует немного больше памяти.',
|
||||
useCustomTitlebar: 'Своя строка заголовка',
|
||||
useCustomTitlebarDesc:
|
||||
'Заменить системную строку заголовка встроенной, в стиле темы приложения. Отключите, чтобы использовать родную строку GNOME/GTK и т.д.',
|
||||
|
||||
@@ -538,6 +538,8 @@ export const zhTranslation = {
|
||||
showTrayIconDesc: '在系统通知区域 / 菜单栏显示 Psysonic 图标。',
|
||||
minimizeToTray: '最小化到托盘',
|
||||
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
|
||||
preloadMiniPlayer: '预加载迷你播放器',
|
||||
preloadMiniPlayerDesc: '在应用启动时于后台构建迷你播放器窗口,使其首次打开即可立即显示内容。会占用少量额外内存。',
|
||||
linuxWebkitSmoothScroll: '滚轮平滑(Linux)',
|
||||
linuxWebkitSmoothScrollDesc: '开:惯性滚动。关:逐行,类似 GTK。',
|
||||
discordRichPresence: 'Discord Rich Presence',
|
||||
|
||||
+20
-1
@@ -23,7 +23,7 @@ import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAuthStore, ServerProfile, MIX_MIN_RATING_FILTER_MAX_STARS, type SeekbarStyle, type LyricsSourceId, type LyricsSourceConfig } from '../store/authStore';
|
||||
import { SeekbarPreview } from '../components/WaveformSeek';
|
||||
import { IS_LINUX, IS_MACOS } from '../utils/platform';
|
||||
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
|
||||
import { useThemeStore } from '../store/themeStore';
|
||||
import { useFontStore, FontId } from '../store/fontStore';
|
||||
import { useKeybindingsStore, KeyAction, formatBinding, buildInAppBinding } from '../store/keybindingsStore';
|
||||
@@ -1127,6 +1127,25 @@ export default function Settings() {
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
{!IS_WINDOWS && (
|
||||
<>
|
||||
<div className="settings-section-divider" />
|
||||
<div className="settings-toggle-row">
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.preloadMiniPlayer')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.preloadMiniPlayerDesc')}</div>
|
||||
</div>
|
||||
<label className="toggle-switch" aria-label={t('settings.preloadMiniPlayer')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={auth.preloadMiniPlayer}
|
||||
onChange={e => auth.setPreloadMiniPlayer(e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{IS_LINUX && !isTilingWm && (
|
||||
<>
|
||||
<div className="settings-section-divider" />
|
||||
|
||||
@@ -66,6 +66,9 @@ interface AuthState {
|
||||
discordTemplateState: string;
|
||||
discordTemplateLargeText: string;
|
||||
useCustomTitlebar: boolean;
|
||||
/** Pre-build the mini-player webview at app start on Linux/macOS so content is available instantly
|
||||
* on first open. Ignored on Windows — that platform always pre-creates as a hang workaround. */
|
||||
preloadMiniPlayer: boolean;
|
||||
/** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */
|
||||
linuxWebkitKineticScroll: boolean;
|
||||
nowPlayingEnabled: boolean;
|
||||
@@ -212,6 +215,7 @@ interface AuthState {
|
||||
setDiscordTemplateState: (v: string) => void;
|
||||
setDiscordTemplateLargeText: (v: string) => void;
|
||||
setUseCustomTitlebar: (v: boolean) => void;
|
||||
setPreloadMiniPlayer: (v: boolean) => void;
|
||||
setLinuxWebkitKineticScroll: (v: boolean) => void;
|
||||
setNowPlayingEnabled: (v: boolean) => void;
|
||||
setLyricsServerFirst: (v: boolean) => void;
|
||||
@@ -318,6 +322,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
discordTemplateState: '{album}',
|
||||
discordTemplateLargeText: '{album}',
|
||||
useCustomTitlebar: false,
|
||||
preloadMiniPlayer: false,
|
||||
linuxWebkitKineticScroll: true,
|
||||
nowPlayingEnabled: false,
|
||||
lyricsServerFirst: true,
|
||||
@@ -448,6 +453,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
setDiscordTemplateState: (v) => set({ discordTemplateState: v }),
|
||||
setDiscordTemplateLargeText: (v) => set({ discordTemplateLargeText: v }),
|
||||
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
|
||||
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),
|
||||
setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }),
|
||||
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
||||
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
|
||||
|
||||
@@ -9069,7 +9069,10 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Custom in-page titlebar (Linux + Windows; macOS keeps the native one) ── */
|
||||
/* ── Custom in-page titlebar.
|
||||
Win/Linux: full bar with drag region, track title, and action buttons.
|
||||
macOS: slim transparent bar holding only the action buttons; the
|
||||
native titlebar above already shows title + close. ── */
|
||||
.mini-player__titlebar {
|
||||
flex-shrink: 0;
|
||||
height: 26px;
|
||||
@@ -9080,6 +9083,13 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active {
|
||||
background: color-mix(in srgb, var(--bg-app) 85%, var(--bg-card) 15%);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
.mini-player__titlebar--mac {
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mini-player__titlebar-title {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
@@ -9090,6 +9100,9 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active {
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.mini-player__titlebar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.mini-player__titlebar-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
Reference in New Issue
Block a user