mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
introducing floating bar with toggle
This commit is contained in:
Generated
+31
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.41.0",
|
"version": "1.42.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.41.0",
|
"version": "1.42.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/dm-sans": "^5.2.8",
|
"@fontsource-variable/dm-sans": "^5.2.8",
|
||||||
"@fontsource-variable/figtree": "^5.2.10",
|
"@fontsource-variable/figtree": "^5.2.10",
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
"@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",
|
||||||
|
"colorthief": "^3.3.1",
|
||||||
"i18next": "^25.8.16",
|
"i18next": "^25.8.16",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/colorthief": "^2.6.1",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
@@ -1720,6 +1722,16 @@
|
|||||||
"assertion-error": "^2.0.1"
|
"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": {
|
"node_modules/@types/deep-eql": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
@@ -2050,6 +2062,23 @@
|
|||||||
"node": "*"
|
"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": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
"@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",
|
||||||
|
"colorthief": "^3.3.1",
|
||||||
"i18next": "^25.8.16",
|
"i18next": "^25.8.16",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/colorthief": "^2.6.1",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
|
|||||||
+2
-1
@@ -145,6 +145,7 @@ function AppShell() {
|
|||||||
const setEntityRatingSupport = useAuthStore(s => s.setEntityRatingSupport);
|
const setEntityRatingSupport = useAuthStore(s => s.setEntityRatingSupport);
|
||||||
const offlineAlbums = useOfflineStore(s => s.albums);
|
const offlineAlbums = useOfflineStore(s => s.albums);
|
||||||
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId);
|
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId);
|
||||||
|
const { floatingPlayerBar } = useThemeStore();
|
||||||
|
|
||||||
// Mini player → main: route requests dispatched as `psy:navigate`
|
// Mini player → main: route requests dispatched as `psy:navigate`
|
||||||
// CustomEvents from the bridge land here so React Router can take over.
|
// CustomEvents from the bridge land here so React Router can take over.
|
||||||
@@ -351,7 +352,7 @@ function AppShell() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="app-shell"
|
className={`app-shell ${floatingPlayerBar ? 'floating-player' : ''}`}
|
||||||
data-mobile={isMobile || undefined}
|
data-mobile={isMobile || undefined}
|
||||||
data-mobile-player={isMobilePlayer || undefined}
|
data-mobile-player={isMobilePlayer || undefined}
|
||||||
data-titlebar={(IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm) || undefined}
|
data-titlebar={(IS_LINUX && useCustomTitlebar && !isWindowFullscreen && !isTilingWm) || undefined}
|
||||||
|
|||||||
@@ -101,6 +101,40 @@ export default function PlayerBar() {
|
|||||||
setUserRatingOverride: s.setUserRatingOverride,
|
setUserRatingOverride: s.setUserRatingOverride,
|
||||||
})));
|
})));
|
||||||
const { lastfmSessionKey } = useAuthStore();
|
const { lastfmSessionKey } = useAuthStore();
|
||||||
|
const { floatingPlayerBar } = useThemeStore();
|
||||||
|
const [floatingStyle, setFloatingStyle] = useState<React.CSSProperties>({});
|
||||||
|
|
||||||
|
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;
|
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}%)`,
|
background: `linear-gradient(to right, var(--volume-accent, var(--accent)) ${volume * 100}%, var(--ctp-surface2) ${volume * 100}%)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const playerBarContent = (
|
||||||
<footer className="player-bar" role="region" aria-label={t('player.regionLabel')}>
|
<footer
|
||||||
|
className={`player-bar ${floatingPlayerBar ? 'floating' : ''}`}
|
||||||
|
style={floatingPlayerBar ? floatingStyle : undefined}
|
||||||
|
role="region"
|
||||||
|
aria-label={t('player.regionLabel')}
|
||||||
|
>
|
||||||
|
|
||||||
{/* Track Info */}
|
{/* Track Info */}
|
||||||
<div className="player-track-info">
|
<div className="player-track-info">
|
||||||
@@ -410,4 +449,10 @@ export default function PlayerBar() {
|
|||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (floatingPlayerBar) {
|
||||||
|
return createPortal(playerBarContent, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerBarContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -738,6 +738,8 @@ export const enTranslation = {
|
|||||||
playlistCoverPhotoSub: 'Show cover photo grid in playlist detail view',
|
playlistCoverPhotoSub: 'Show cover photo grid in playlist detail view',
|
||||||
showBitrate: 'Show Bitrate',
|
showBitrate: 'Show Bitrate',
|
||||||
showBitrateSub: 'Display audio bitrate in track listings',
|
showBitrateSub: 'Display audio bitrate in track listings',
|
||||||
|
floatingPlayerBar: 'Floating Player Bar',
|
||||||
|
floatingPlayerBarSub: 'Keep the player bar floating above content',
|
||||||
uiScaleTitle: 'Interface Scale',
|
uiScaleTitle: 'Interface Scale',
|
||||||
uiScaleLabel: 'Zoom',
|
uiScaleLabel: 'Zoom',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -729,6 +729,8 @@ export const esTranslation = {
|
|||||||
playlistCoverPhotoSub: 'Mostrar cuadrícula de fotos de portada en la vista detallada de playlists',
|
playlistCoverPhotoSub: 'Mostrar cuadrícula de fotos de portada en la vista detallada de playlists',
|
||||||
showBitrate: 'Mostrar Bitrate',
|
showBitrate: 'Mostrar Bitrate',
|
||||||
showBitrateSub: 'Mostrar bitrate de audio en las listas de pistas',
|
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',
|
uiScaleTitle: 'Escala de Interfaz',
|
||||||
uiScaleLabel: 'Zoom',
|
uiScaleLabel: 'Zoom',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1774,6 +1774,17 @@ export default function Settings() {
|
|||||||
<span className="toggle-track" />
|
<span className="toggle-track" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="settings-section-divider" />
|
||||||
|
<div className="settings-toggle-row">
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{t('settings.floatingPlayerBar')}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.floatingPlayerBarSub')}</div>
|
||||||
|
</div>
|
||||||
|
<label className="toggle-switch">
|
||||||
|
<input type="checkbox" checked={theme.floatingPlayerBar} onChange={e => theme.setFloatingPlayerBar(e.target.checked)} />
|
||||||
|
<span className="toggle-track" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface ThemeState {
|
|||||||
setShowRemainingTime: (v: boolean) => void;
|
setShowRemainingTime: (v: boolean) => void;
|
||||||
expandReplayGain: boolean;
|
expandReplayGain: boolean;
|
||||||
setExpandReplayGain: (v: boolean) => void;
|
setExpandReplayGain: (v: boolean) => void;
|
||||||
|
floatingPlayerBar: boolean;
|
||||||
|
setFloatingPlayerBar: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScheduledTheme(state: Pick<ThemeState, 'enableThemeScheduler' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'>): string {
|
export function getScheduledTheme(state: Pick<ThemeState, 'enableThemeScheduler' | 'theme' | 'themeDay' | 'themeNight' | 'timeDayStart' | 'timeNightStart'>): string {
|
||||||
@@ -67,6 +69,8 @@ export const useThemeStore = create<ThemeState>()(
|
|||||||
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
||||||
expandReplayGain: false,
|
expandReplayGain: false,
|
||||||
setExpandReplayGain: (v) => set({ expandReplayGain: v }),
|
setExpandReplayGain: (v) => set({ expandReplayGain: v }),
|
||||||
|
floatingPlayerBar: false,
|
||||||
|
setFloatingPlayerBar: (v) => set({ floatingPlayerBar: v }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'psysonic_theme',
|
name: 'psysonic_theme',
|
||||||
|
|||||||
@@ -33,6 +33,13 @@
|
|||||||
background: var(--bg-app);
|
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) ─── */
|
/* ─── Custom title bar (Linux only — decorations: false) ─── */
|
||||||
:root {
|
:root {
|
||||||
--titlebar-height: 32px;
|
--titlebar-height: 32px;
|
||||||
@@ -46,6 +53,14 @@
|
|||||||
"player player player";
|
"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 */
|
/* Resize grips — replace the native GTK grips lost when decorations: false */
|
||||||
.app-shell[data-titlebar]::before,
|
.app-shell[data-titlebar]::before,
|
||||||
.app-shell[data-titlebar]::after {
|
.app-shell[data-titlebar]::after {
|
||||||
@@ -1152,6 +1167,61 @@
|
|||||||
contain: layout paint;
|
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 {
|
.player-track-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1394,6 +1464,9 @@
|
|||||||
|
|
||||||
.player-volume-slider {
|
.player-volume-slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-volume-pct {
|
.player-volume-pct {
|
||||||
|
|||||||
Reference in New Issue
Block a user