From ffffe268abce23181ee0b4643a1b04413e8770c5 Mon Sep 17 00:00:00 2001 From: kveld9 Date: Sun, 19 Apr 2026 13:01:31 -0300 Subject: [PATCH 1/2] introducing floating bar with toggle --- package-lock.json | 33 +++++++++++++++- package.json | 2 + src/App.tsx | 3 +- src/components/PlayerBar.tsx | 49 +++++++++++++++++++++++- src/locales/en.ts | 2 + src/locales/es.ts | 2 + src/pages/Settings.tsx | 11 ++++++ src/store/themeStore.ts | 4 ++ src/styles/layout.css | 73 ++++++++++++++++++++++++++++++++++++ 9 files changed, 174 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0e9308a..8292084d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "psysonic", - "version": "1.41.0", + "version": "1.42.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "psysonic", - "version": "1.41.0", + "version": "1.42.1", "dependencies": { "@fontsource-variable/dm-sans": "^5.2.8", "@fontsource-variable/figtree": "^5.2.10", @@ -33,6 +33,7 @@ "@tauri-apps/plugin-updater": "^2.10.0", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", + "colorthief": "^3.3.1", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", @@ -45,6 +46,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", + "@types/colorthief": "^2.6.1", "@types/md5": "^2.3.5", "@types/node": "^25.3.5", "@types/papaparse": "^5.5.2", @@ -1720,6 +1722,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/colorthief": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/colorthief/-/colorthief-2.6.1.tgz", + "integrity": "sha512-3hlRky7ybPuQDNx3RV29P/CTvuC7zkk//Xt/NyOwNg5RhL/y54a0q4aW7MNMkvBkGJqX2g2igNCoCh3ZtK4gZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2050,6 +2062,23 @@ "node": "*" } }, + "node_modules/colorthief": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/colorthief/-/colorthief-3.3.1.tgz", + "integrity": "sha512-a3qzYXy51h6p3725pV8rnJwUBGTtvYQge2pVhKJwL+vETUD5pCi6VKmQyu51pBHdUbu/BPEXbwFLS0GnxXNhGA==", + "license": "MIT", + "bin": { + "colorthief": "dist/cli.js" + }, + "peerDependencies": { + "sharp": ">=0.33.0" + }, + "peerDependenciesMeta": { + "sharp": { + "optional": true + } + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/package.json b/package.json index 866ac93e..37bc40bb 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@tauri-apps/plugin-updater": "^2.10.0", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", + "colorthief": "^3.3.1", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", @@ -49,6 +50,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", + "@types/colorthief": "^2.6.1", "@types/md5": "^2.3.5", "@types/node": "^25.3.5", "@types/papaparse": "^5.5.2", diff --git a/src/App.tsx b/src/App.tsx index 1ab0e088..1fdde255 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -145,6 +145,7 @@ function AppShell() { const setEntityRatingSupport = useAuthStore(s => s.setEntityRatingSupport); const offlineAlbums = useOfflineStore(s => s.albums); const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); + const { floatingPlayerBar } = useThemeStore(); // Mini player → main: route requests dispatched as `psy:navigate` // CustomEvents from the bridge land here so React Router can take over. @@ -351,7 +352,7 @@ function AppShell() { return (
({}); + + useEffect(() => { + if (!floatingPlayerBar) return; + + const updatePosition = () => { + const sidebar = document.querySelector('.sidebar') as HTMLElement; + const queue = document.querySelector('.queue-panel') as HTMLElement; + + const leftOffset = sidebar ? sidebar.getBoundingClientRect().right : 0; + const rightOffset = queue ? window.innerWidth - queue.getBoundingClientRect().left : 0; + + setFloatingStyle({ + left: leftOffset + 24, + right: rightOffset + 24, + width: 'auto', + }); + }; + + updatePosition(); + + const observer = new ResizeObserver(updatePosition); + const sidebar = document.querySelector('.sidebar'); + const queue = document.querySelector('.queue-panel'); + if (sidebar) observer.observe(sidebar); + if (queue) observer.observe(queue); + window.addEventListener('resize', updatePosition); + + return () => { + observer.disconnect(); + window.removeEventListener('resize', updatePosition); + }; + }, [floatingPlayerBar]); const isRadio = !!currentRadio; @@ -150,8 +184,13 @@ export default function PlayerBar() { background: `linear-gradient(to right, var(--volume-accent, var(--accent)) ${volume * 100}%, var(--ctp-surface2) ${volume * 100}%)`, }; - return ( -
+ const playerBarContent = ( +
{/* Track Info */}
@@ -410,4 +449,10 @@ export default function PlayerBar() {
); + + if (floatingPlayerBar) { + return createPortal(playerBarContent, document.body); + } + + return playerBarContent; } diff --git a/src/locales/en.ts b/src/locales/en.ts index 33924593..64fde340 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -738,6 +738,8 @@ export const enTranslation = { playlistCoverPhotoSub: 'Show cover photo grid in playlist detail view', showBitrate: 'Show Bitrate', showBitrateSub: 'Display audio bitrate in track listings', + floatingPlayerBar: 'Floating Player Bar', + floatingPlayerBarSub: 'Keep the player bar floating above content', uiScaleTitle: 'Interface Scale', uiScaleLabel: 'Zoom', }, diff --git a/src/locales/es.ts b/src/locales/es.ts index 7f714177..7171ab4b 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -729,6 +729,8 @@ export const esTranslation = { playlistCoverPhotoSub: 'Mostrar cuadrícula de fotos de portada en la vista detallada de playlists', showBitrate: 'Mostrar Bitrate', showBitrateSub: 'Mostrar bitrate de audio en las listas de pistas', + floatingPlayerBar: 'Barra del Reproductor Flotante', + floatingPlayerBarSub: 'Mantener la barra del reproductor flotando sobre el contenido', uiScaleTitle: 'Escala de Interfaz', uiScaleLabel: 'Zoom', }, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 7b703d59..cd184844 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1774,6 +1774,17 @@ export default function Settings() {
+
+
+
+
{t('settings.floatingPlayerBar')}
+
{t('settings.floatingPlayerBarSub')}
+
+ +
diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts index 2bcb427a..6b045011 100644 --- a/src/store/themeStore.ts +++ b/src/store/themeStore.ts @@ -26,6 +26,8 @@ interface ThemeState { setShowRemainingTime: (v: boolean) => void; expandReplayGain: boolean; setExpandReplayGain: (v: boolean) => void; + floatingPlayerBar: boolean; + setFloatingPlayerBar: (v: boolean) => void; } export function getScheduledTheme(state: Pick): string { @@ -67,6 +69,8 @@ export const useThemeStore = create()( setShowRemainingTime: (v) => set({ showRemainingTime: v }), expandReplayGain: false, setExpandReplayGain: (v) => set({ expandReplayGain: v }), + floatingPlayerBar: false, + setFloatingPlayerBar: (v) => set({ floatingPlayerBar: v }), }), { name: 'psysonic_theme', diff --git a/src/styles/layout.css b/src/styles/layout.css index 859a47a3..2a6333ee 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -33,6 +33,13 @@ background: var(--bg-app); } +/* When player bar is floating, content extends to bottom */ +.app-shell.floating-player { + grid-template-rows: minmax(0, 1fr); + grid-template-areas: + "sidebar main queue"; +} + /* ─── Custom title bar (Linux only — decorations: false) ─── */ :root { --titlebar-height: 32px; @@ -46,6 +53,14 @@ "player player player"; } +/* When player bar is floating with titlebar, content extends to bottom */ +.app-shell[data-titlebar].floating-player { + grid-template-rows: var(--titlebar-height) minmax(0, 1fr); + grid-template-areas: + "titlebar titlebar titlebar" + "sidebar main queue"; +} + /* Resize grips — replace the native GTK grips lost when decorations: false */ .app-shell[data-titlebar]::before, .app-shell[data-titlebar]::after { @@ -1152,6 +1167,61 @@ contain: layout paint; } +/* Floating player bar styles - positioning handled by ResizeObserver */ +.player-bar.floating { + position: fixed; + bottom: 12px; + /* left/right handled dynamically by JS */ + z-index: 1000; + border-radius: 50px; + width: auto; + min-width: 100px; + max-width: none; + grid-area: unset; + padding: 0 24px 0 8px; + gap: 16px; + background: var(--bg-player); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.8), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +/* Internal element adjustments for floating mode */ +.player-bar.floating .player-track-info { + flex: 0 0 auto; + min-width: 240px; + max-width: 320px; +} + +.player-bar.floating .player-album-art-wrap { + border-radius: 50%; +} + +.player-bar.floating .player-album-art, +.player-bar.floating .player-album-art-placeholder { + border-radius: 0; +} + +.player-bar.floating .player-buttons { + flex-shrink: 0; + gap: 16px; +} + +.player-bar.floating .player-waveform-section { + flex: 1; + min-width: 200px; +} + +.player-bar.floating .player-volume-section { + flex: 0 0 auto; + min-width: 120px; + display: flex; + align-items: center; + gap: 8px; +} + .player-track-info { display: flex; align-items: center; @@ -1394,6 +1464,9 @@ .player-volume-slider { width: 100%; + display: block; + margin: 0; + vertical-align: middle; } .player-volume-pct { From 12bebdde06c803dc0a942da118b99ebacb3c18fc Mon Sep 17 00:00:00 2001 From: kveld9 Date: Sun, 19 Apr 2026 14:23:14 -0300 Subject: [PATCH 2/2] some fixes --- package-lock.json | 29 ----------------------------- package.json | 2 -- src/locales/de.ts | 2 ++ src/locales/fr.ts | 2 ++ src/locales/nb.ts | 2 ++ src/locales/nl.ts | 2 ++ src/locales/ru.ts | 2 ++ src/locales/zh.ts | 2 ++ 8 files changed, 12 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8292084d..ad5eb055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "@tauri-apps/plugin-updater": "^2.10.0", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", - "colorthief": "^3.3.1", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", @@ -46,7 +45,6 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", - "@types/colorthief": "^2.6.1", "@types/md5": "^2.3.5", "@types/node": "^25.3.5", "@types/papaparse": "^5.5.2", @@ -1722,16 +1720,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/colorthief": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@types/colorthief/-/colorthief-2.6.1.tgz", - "integrity": "sha512-3hlRky7ybPuQDNx3RV29P/CTvuC7zkk//Xt/NyOwNg5RhL/y54a0q4aW7MNMkvBkGJqX2g2igNCoCh3ZtK4gZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2062,23 +2050,6 @@ "node": "*" } }, - "node_modules/colorthief": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/colorthief/-/colorthief-3.3.1.tgz", - "integrity": "sha512-a3qzYXy51h6p3725pV8rnJwUBGTtvYQge2pVhKJwL+vETUD5pCi6VKmQyu51pBHdUbu/BPEXbwFLS0GnxXNhGA==", - "license": "MIT", - "bin": { - "colorthief": "dist/cli.js" - }, - "peerDependencies": { - "sharp": ">=0.33.0" - }, - "peerDependenciesMeta": { - "sharp": { - "optional": true - } - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/package.json b/package.json index 37bc40bb..866ac93e 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@tauri-apps/plugin-updater": "^2.10.0", "@tauri-apps/plugin-window-state": "^2.4.1", "axios": "^1.7.7", - "colorthief": "^3.3.1", "i18next": "^25.8.16", "lucide-react": "^0.462.0", "md5": "^2.3.0", @@ -50,7 +49,6 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", - "@types/colorthief": "^2.6.1", "@types/md5": "^2.3.5", "@types/node": "^25.3.5", "@types/papaparse": "^5.5.2", diff --git a/src/locales/de.ts b/src/locales/de.ts index 6749e0d6..5ae6c571 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -736,6 +736,8 @@ export const deTranslation = { playlistCoverPhotoSub: 'Zeigt Coverfoto-Raster in der Playlist-Detailansicht', showBitrate: 'Bitrate anzeigen', showBitrateSub: 'Audio-Bitrate in Track-Listen anzeigen', + floatingPlayerBar: 'Schwebende Player-Leiste', + floatingPlayerBarSub: 'Player-Leiste über dem Inhalt schweben lassen', uiScaleTitle: 'Interface-Skalierung', uiScaleLabel: 'Zoom', }, diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 43e03f2e..945ba3d5 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -724,6 +724,8 @@ export const frTranslation = { playlistCoverPhotoSub: 'Afficher la grille de photos de couverture dans la vue détaillée des playlists', showBitrate: 'Afficher le Débit', showBitrateSub: 'Afficher le débit audio dans les listes de pistes', + floatingPlayerBar: 'Barre de Lecteur Flottante', + floatingPlayerBarSub: 'Garder la barre du lecteur flottante au-dessus du contenu', uiScaleTitle: "Mise à l'échelle de l'interface", uiScaleLabel: 'Zoom', }, diff --git a/src/locales/nb.ts b/src/locales/nb.ts index b70d85db..d43f9772 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -723,6 +723,8 @@ export const nbTranslation = { playlistCoverPhotoSub: 'Vis coverfoto-rutenett i playlist-detailedvisning', showBitrate: 'Vis Bitrate', showBitrateSub: 'Vis audio-bitrate i sporlister', + floatingPlayerBar: 'Flytende Spillerlinje', + floatingPlayerBarSub: 'Hold spillerlinjen flytende over innholdet', uiScaleTitle: 'Grensesnittskala', uiScaleLabel: 'Zoom', }, diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 64baffae..51bee638 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -723,6 +723,8 @@ export const nlTranslation = { playlistCoverPhotoSub: 'Toon coverfoto raster in playlist detailweergave', showBitrate: 'Toon Bitrate', showBitrateSub: 'Toon audio bitrate in tracklijsten', + floatingPlayerBar: 'Zwevende Spelerbalk', + floatingPlayerBarSub: 'Houd de spelerbalk zwevend boven de inhoud', uiScaleTitle: 'Interface schaal', uiScaleLabel: 'Zoom', }, diff --git a/src/locales/ru.ts b/src/locales/ru.ts index a31344f8..f748538d 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -753,6 +753,8 @@ export const ruTranslation = { playlistCoverPhotoSub: 'Показывать сетку обложек в детальном виде плейлиста', showBitrate: 'Показывать Битрейт', showBitrateSub: 'Отображать битрейт аудио в списках треков', + floatingPlayerBar: 'Плавающая панель плеера', + floatingPlayerBarSub: 'Держать панель плеера плавающей над содержимым', uiScaleTitle: 'Масштаб интерфейса', uiScaleLabel: 'Масштаб', }, diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 38c861cd..2ba7faf7 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -719,6 +719,8 @@ export const zhTranslation = { playlistCoverPhotoSub: '在播放列表详细视图中显示封面照片网格', showBitrate: '显示比特率', showBitrateSub: '在曲目列表中显示音频比特率', + floatingPlayerBar: '浮动播放栏', + floatingPlayerBarSub: '保持播放栏悬浮在内容上方', uiScaleTitle: '界面缩放', uiScaleLabel: '缩放', },