diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 144ae3ef..34fee686 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2756,6 +2756,11 @@ 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"); + let mut builder = tauri::WebviewWindowBuilder::new( &app, "mini", @@ -2765,7 +2770,7 @@ fn open_mini_player(app: tauri::AppHandle) -> Result<(), String> { .inner_size(340.0, 180.0) .min_inner_size(320.0, 180.0) .resizable(true) - .decorations(true) + .decorations(use_decorations) .always_on_top(use_always_on_top) .skip_taskbar(false); diff --git a/src/components/MiniPlayer.tsx b/src/components/MiniPlayer.tsx index 1b37ee67..79926a58 100644 --- a/src/components/MiniPlayer.tsx +++ b/src/components/MiniPlayer.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { emit, listen } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; +import { useTranslation } from 'react-i18next'; import { Play, Pause, SkipBack, SkipForward, Pin, PinOff, Maximize2, X, ListMusic } from 'lucide-react'; import CachedImage from './CachedImage'; 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 MiniContextMenu from './MiniContextMenu'; import type { MiniSyncPayload, MiniControlAction, MiniTrackInfo } from '../utils/miniPlayerBridge'; @@ -87,6 +89,7 @@ function fmt(seconds: number): string { } export default function MiniPlayer() { + const { t } = useTranslation(); const [state, setState] = useState(() => initialSnapshot()); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(() => { @@ -322,74 +325,100 @@ export default function MiniPlayer() { const progress = duration > 0 ? Math.min(100, (currentTime / duration) * 100) : 0; return ( -
-
- {track?.coverArt ? ( - - ) : ( -
- )} -
+
+ {!IS_MACOS && ( +
+ + {track?.title ?? 'Psysonic Mini'} + + + + + +
+ )} -
-
-
- {track?.title ?? '—'} +
+
+ {track?.coverArt ? ( + + ) : ( +
+ )} +
+ +
+
+
+ {track?.title ?? '—'} +
+
+ {track?.artist ?? ''} +
-
- {track?.artist ?? ''} + +
+ + +
-
- - - -
- -
+
{fmt(currentTime)}
{fmt(duration)}
-
-
- - - - -
- - {queueOpen && ( + {queueOpen && (
{ @@ -412,7 +441,7 @@ export default function MiniPlayer() { >
{state.queue.length === 0 ? ( -
Queue is empty
+
{t('miniPlayer.emptyQueue')}
) : ( state.queue.map((t, i) => { let dragStyle: React.CSSProperties = {}; @@ -483,15 +512,16 @@ export default function MiniPlayer() {
)} - {ctxMenu && ( - setCtxMenu(null)} - /> - )} + {ctxMenu && ( + setCtxMenu(null)} + /> + )} +
); } diff --git a/src/locales/de.ts b/src/locales/de.ts index 47482246..3c6f4f3d 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -913,6 +913,15 @@ export const deTranslation = { sourceHot: 'Wiedergabe aus dem Cache', sourceStream: 'Wiedergabe aus dem Netzwerkstream', }, + miniPlayer: { + showQueue: 'Warteschlange einblenden', + hideQueue: 'Warteschlange ausblenden', + pinOnTop: 'Im Vordergrund halten', + pinOff: 'Vordergrund deaktivieren', + openMainWindow: 'Hauptfenster öffnen', + close: 'Schließen', + emptyQueue: 'Die Warteschlange ist leer', + }, statistics: { title: 'Statistiken', recentlyPlayed: 'Zuletzt gehört', diff --git a/src/locales/en.ts b/src/locales/en.ts index 5d2f0867..04680d0e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -915,6 +915,15 @@ export const enTranslation = { sourceHot: 'Playing from cache', sourceStream: 'Playing from network stream', }, + miniPlayer: { + showQueue: 'Show queue', + hideQueue: 'Hide queue', + pinOnTop: 'Pin on top', + pinOff: 'Unpin', + openMainWindow: 'Open main window', + close: 'Close', + emptyQueue: 'Queue is empty', + }, statistics: { title: 'Statistics', recentlyPlayed: 'Recently Played', diff --git a/src/locales/es.ts b/src/locales/es.ts index 309dd064..05a1cb0d 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -906,6 +906,15 @@ export const esTranslation = { sourceHot: 'Reproducción desde la caché', sourceStream: 'Reproducción desde la transmisión en red', }, + miniPlayer: { + showQueue: 'Mostrar cola', + hideQueue: 'Ocultar cola', + pinOnTop: 'Mantener encima', + pinOff: 'Desfijar', + openMainWindow: 'Abrir ventana principal', + close: 'Cerrar', + emptyQueue: 'La cola está vacía', + }, statistics: { title: 'Estadísticas', recentlyPlayed: 'Reproducidos Recientemente', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index fae364f1..90187ba3 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -901,6 +901,15 @@ export const frTranslation = { sourceHot: 'Lecture depuis le cache', sourceStream: 'Lecture depuis le flux réseau', }, + miniPlayer: { + showQueue: 'Afficher la file', + hideQueue: 'Masquer la file', + pinOnTop: 'Toujours visible', + pinOff: 'Désépingler', + openMainWindow: 'Ouvrir la fenêtre principale', + close: 'Fermer', + emptyQueue: 'La file est vide', + }, statistics: { title: 'Statistiques', recentlyPlayed: 'Écoutés récemment', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index aea79434..e04cb954 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -900,6 +900,15 @@ export const nbTranslation = { sourceHot: 'Spiller fra cache', sourceStream: 'Spiller fra nettverksstrøm', }, + miniPlayer: { + showQueue: 'Vis kø', + hideQueue: 'Skjul kø', + pinOnTop: 'Hold øverst', + pinOff: 'Løsne', + openMainWindow: 'Åpne hovedvindu', + close: 'Lukk', + emptyQueue: 'Køen er tom', + }, statistics: { title: 'Statistikk', recentlyPlayed: 'Nylig spilt', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index a931317d..f2b6b1a2 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -900,6 +900,15 @@ export const nlTranslation = { sourceHot: 'Afspelen vanuit cache', sourceStream: 'Afspelen vanuit netwerkstream', }, + miniPlayer: { + showQueue: 'Wachtrij tonen', + hideQueue: 'Wachtrij verbergen', + pinOnTop: 'Altijd boven', + pinOff: 'Losmaken', + openMainWindow: 'Hoofdvenster openen', + close: 'Sluiten', + emptyQueue: 'Wachtrij is leeg', + }, statistics: { title: 'Statistieken', recentlyPlayed: 'Recent afgespeeld', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 9eb674e1..82605985 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -953,6 +953,15 @@ export const ruTranslation = { sourceHot: 'Играет из кэша', sourceStream: 'Играет из сетевого потока', }, + miniPlayer: { + showQueue: 'Показать очередь', + hideQueue: 'Скрыть очередь', + pinOnTop: 'Поверх окон', + pinOff: 'Открепить', + openMainWindow: 'Открыть главное окно', + close: 'Закрыть', + emptyQueue: 'Очередь пуста', + }, statistics: { title: 'Статистика', recentlyPlayed: 'Недавно проиграно', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index a9b35e5c..db25954b 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -896,6 +896,15 @@ export const zhTranslation = { sourceHot: '正在从缓存播放', sourceStream: '正在从网络流播放', }, + miniPlayer: { + showQueue: '显示队列', + hideQueue: '隐藏队列', + pinOnTop: '置顶', + pinOff: '取消置顶', + openMainWindow: '打开主窗口', + close: '关闭', + emptyQueue: '队列为空', + }, statistics: { title: '统计', recentlyPlayed: '最近播放', diff --git a/src/styles/components.css b/src/styles/components.css index b90ea3cf..f8a48512 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -9056,30 +9056,87 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active { } /* ─ Mini Player window ───────────────────────────────────────────────────── */ -.mini-player { +/* ── Mini player shell ── outermost wrapper that fills the window ── */ +.mini-player-shell { width: 100%; height: 100vh; - display: grid; - grid-template-columns: 112px 1fr; - grid-template-rows: auto auto; - gap: 10px; - padding: 12px; + display: flex; + flex-direction: column; background: var(--bg-app); color: var(--text-primary); - overflow: hidden; user-select: none; + overflow: hidden; box-sizing: border-box; } +/* ── Custom in-page titlebar (Linux + Windows; macOS keeps the native one) ── */ +.mini-player__titlebar { + flex-shrink: 0; + height: 26px; + display: flex; + align-items: center; + gap: 6px; + padding: 0 4px 0 10px; + background: color-mix(in srgb, var(--bg-app) 85%, var(--bg-card) 15%); + border-bottom: 1px solid var(--border-subtle); +} +.mini-player__titlebar-title { + flex: 1; + font-size: 11px; + font-weight: 500; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0.02em; +} +.mini-player__titlebar-btn { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 4px; + cursor: pointer; + transition: background 0.12s, color 0.12s; +} +.mini-player__titlebar-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} +.mini-player__titlebar-btn--active { + color: var(--accent); +} +.mini-player__titlebar-btn--close:hover { + background: var(--danger); + color: var(--ctp-base); +} + +.mini-player { + flex: 1; + display: grid; + grid-template-columns: 84px 1fr; + grid-template-rows: 84px auto; + gap: 8px 10px; + padding: 10px 12px; + overflow: hidden; + box-sizing: border-box; + min-height: 0; +} + .mini-player--queue-open { - grid-template-rows: auto auto 1fr; + grid-template-rows: 84px auto 1fr; } .mini-player__art { - grid-row: 1 / span 2; + grid-column: 1; + grid-row: 1; aspect-ratio: 1; - width: 112px; - height: 112px; + width: 84px; + height: 84px; background: var(--bg-card); border-radius: 8px; overflow: hidden; @@ -9097,10 +9154,13 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active { } .mini-player__body { + grid-column: 2; + grid-row: 1; padding: 0 2px; display: flex; flex-direction: column; - gap: 6px; + justify-content: space-between; + gap: 4px; min-width: 0; } @@ -9130,7 +9190,6 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active { align-items: center; justify-content: center; gap: 10px; - margin-top: 2px; } .mini-player__btn { display: flex; @@ -9160,6 +9219,8 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active { } .mini-player__progress { + grid-column: 1 / -1; + grid-row: 2; display: flex; align-items: center; gap: 6px; @@ -9183,12 +9244,6 @@ html.no-compositing .fsr-lyric-line.fsrl-active .fsr-lyric-word.active { transition: width 0.25s linear; } -.mini-player__toolbar { - grid-column: 2; - display: flex; - gap: 4px; - justify-content: flex-end; -} .mini-queue-wrap { grid-column: 1 / -1;