introducing timer toggle player bar (#212)

This commit is contained in:
Kveld.
2026-04-19 07:04:01 -03:00
committed by GitHub
parent 6bd2bfc01c
commit eb747ba1ae
12 changed files with 72 additions and 4 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "psysonic", "name": "psysonic",
"version": "1.34.13", "version": "1.41.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "psysonic", "name": "psysonic",
"version": "1.34.13", "version": "1.41.0",
"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",
+31 -2
View File
@@ -3,12 +3,13 @@ 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, SlidersVertical, X, Heart, Cast, Square, Repeat, Repeat1, Maximize2, SlidersVertical, X, Heart, Cast,
PictureInPicture2, PictureInPicture2, ArrowLeftRight,
} from 'lucide-react'; } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating } from '../api/subsonic'; import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating } from '../api/subsonic';
import CachedImage from './CachedImage'; import CachedImage from './CachedImage';
import WaveformSeek from './WaveformSeek'; import WaveformSeek from './WaveformSeek';
@@ -43,11 +44,28 @@ const PlaybackTime = memo(function PlaybackTime({ className }: { className?: str
return <span className={className} ref={spanRef} />; return <span className={className} ref={spanRef} />;
}); });
// Renders the remaining time (duration - currentTime) without causing PlayerBar to re-render.
const RemainingTime = memo(function RemainingTime({ duration, className }: { duration: number; className?: string }) {
const spanRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const updateRemaining = () => {
if (spanRef.current) {
const remaining = Math.max(0, duration - usePlayerStore.getState().currentTime);
spanRef.current.textContent = `-${formatTime(remaining)}`;
}
};
updateRemaining();
return usePlayerStore.subscribe(updateRemaining);
}, [duration]);
return <span className={className} ref={spanRef} />;
});
export default function PlayerBar() { export default function PlayerBar() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [eqOpen, setEqOpen] = useState(false); const [eqOpen, setEqOpen] = useState(false);
const [showVolPct, setShowVolPct] = useState(false); const [showVolPct, setShowVolPct] = useState(false);
const [localShowRemaining, setLocalShowRemaining] = useState(() => useThemeStore.getState().showRemainingTime);
const premuteVolumeRef = useRef(1); const premuteVolumeRef = useRef(1);
const showLyrics = useLyricsStore(s => s.showLyrics); const showLyrics = useLyricsStore(s => s.showLyrics);
const activeTab = useLyricsStore(s => s.activeTab); const activeTab = useLyricsStore(s => s.activeTab);
@@ -297,7 +315,18 @@ export default function PlayerBar() {
<div className="player-waveform-wrap"> <div className="player-waveform-wrap">
<WaveformSeek trackId={currentTrack?.id} /> <WaveformSeek trackId={currentTrack?.id} />
</div> </div>
<span className="player-time">{formatTime(duration)}</span> <span
className="player-time player-time-toggle"
onClick={() => {
const newVal = !localShowRemaining;
setLocalShowRemaining(newVal);
useThemeStore.getState().setShowRemainingTime(newVal);
}}
title={localShowRemaining ? t('player.showDuration') : t('player.showRemainingTime')}
>
{localShowRemaining ? <RemainingTime duration={duration} /> : formatTime(duration)}
<ArrowLeftRight size={10} style={{ marginLeft: 4, opacity: 0.6 }} />
</span>
</> </>
)} )}
</div> </div>
+2
View File
@@ -985,6 +985,8 @@ export const deTranslation = {
lyricsSourceLrclib: 'Quelle: LRCLIB', lyricsSourceLrclib: 'Quelle: LRCLIB',
lyricsSourceNetease: 'Quelle: Netease', lyricsSourceNetease: 'Quelle: Netease',
lyricsSourceLyricsplus: 'Quelle: YouLyPlus', lyricsSourceLyricsplus: 'Quelle: YouLyPlus',
showDuration: 'Dauer anzeigen',
showRemainingTime: 'Verbleibende Zeit anzeigen',
}, },
songInfo: { songInfo: {
title: 'Song-Infos', title: 'Song-Infos',
+2
View File
@@ -987,6 +987,8 @@ export const enTranslation = {
lyricsSourceLrclib: 'Source: LRCLIB', lyricsSourceLrclib: 'Source: LRCLIB',
lyricsSourceNetease: 'Source: Netease', lyricsSourceNetease: 'Source: Netease',
lyricsSourceLyricsplus: 'Source: YouLyPlus', lyricsSourceLyricsplus: 'Source: YouLyPlus',
showDuration: 'Show duration',
showRemainingTime: 'Show remaining time',
}, },
songInfo: { songInfo: {
title: 'Song Info', title: 'Song Info',
+2
View File
@@ -978,6 +978,8 @@ export const esTranslation = {
lyricsSourceLrclib: 'Fuente: LRCLIB', lyricsSourceLrclib: 'Fuente: LRCLIB',
lyricsSourceNetease: 'Fuente: Netease', lyricsSourceNetease: 'Fuente: Netease',
lyricsSourceLyricsplus: 'Fuente: YouLyPlus', lyricsSourceLyricsplus: 'Fuente: YouLyPlus',
showDuration: 'Mostrar duración',
showRemainingTime: 'Mostrar tiempo restante',
}, },
songInfo: { songInfo: {
title: 'Información de Canción', title: 'Información de Canción',
+2
View File
@@ -973,6 +973,8 @@ export const frTranslation = {
lyricsSourceLrclib: 'Source : LRCLIB', lyricsSourceLrclib: 'Source : LRCLIB',
lyricsSourceNetease: 'Source : Netease', lyricsSourceNetease: 'Source : Netease',
lyricsSourceLyricsplus: 'Source : YouLyPlus', lyricsSourceLyricsplus: 'Source : YouLyPlus',
showDuration: 'Afficher la durée',
showRemainingTime: 'Afficher le temps restant',
}, },
songInfo: { songInfo: {
title: 'Infos du morceau', title: 'Infos du morceau',
+2
View File
@@ -972,6 +972,8 @@ export const nbTranslation = {
lyricsSourceLrclib: 'Kilde: LRCLIB', lyricsSourceLrclib: 'Kilde: LRCLIB',
lyricsSourceNetease: 'Kilde: Netease', lyricsSourceNetease: 'Kilde: Netease',
lyricsSourceLyricsplus: 'Kilde: YouLyPlus', lyricsSourceLyricsplus: 'Kilde: YouLyPlus',
showDuration: 'Vis varighet',
showRemainingTime: 'Vis gjenværende tid',
}, },
songInfo: { songInfo: {
title: 'Sanginfo', title: 'Sanginfo',
+2
View File
@@ -972,6 +972,8 @@ export const nlTranslation = {
lyricsSourceLrclib: 'Bron: LRCLIB', lyricsSourceLrclib: 'Bron: LRCLIB',
lyricsSourceNetease: 'Bron: Netease', lyricsSourceNetease: 'Bron: Netease',
lyricsSourceLyricsplus: 'Bron: YouLyPlus', lyricsSourceLyricsplus: 'Bron: YouLyPlus',
showDuration: 'Toon duur',
showRemainingTime: 'Toon resterende tijd',
}, },
songInfo: { songInfo: {
title: 'Nummerinfo', title: 'Nummerinfo',
+2
View File
@@ -1033,6 +1033,8 @@ export const ruTranslation = {
lyricsNotFound: 'Текст не найден', lyricsNotFound: 'Текст не найден',
lyricsSourceNetease: 'Источник: Netease', lyricsSourceNetease: 'Источник: Netease',
lyricsSourceLyricsplus: 'Источник: YouLyPlus', lyricsSourceLyricsplus: 'Источник: YouLyPlus',
showDuration: 'Показать длительность',
showRemainingTime: 'Показать оставшееся время',
}, },
songInfo: { songInfo: {
title: 'О треке', title: 'О треке',
+2
View File
@@ -968,6 +968,8 @@ export const zhTranslation = {
lyricsSourceLrclib: '来源:LRCLIB', lyricsSourceLrclib: '来源:LRCLIB',
lyricsSourceNetease: '来源:网易云', lyricsSourceNetease: '来源:网易云',
lyricsSourceLyricsplus: '来源:YouLyPlus', lyricsSourceLyricsplus: '来源:YouLyPlus',
showDuration: '显示时长',
showRemainingTime: '显示剩余时间',
}, },
songInfo: { songInfo: {
title: '歌曲信息', title: '歌曲信息',
+4
View File
@@ -22,6 +22,8 @@ interface ThemeState {
setEnablePlaylistCoverPhoto: (v: boolean) => void; setEnablePlaylistCoverPhoto: (v: boolean) => void;
showBitrate: boolean; showBitrate: boolean;
setShowBitrate: (v: boolean) => void; setShowBitrate: (v: boolean) => void;
showRemainingTime: boolean;
setShowRemainingTime: (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 {
@@ -59,6 +61,8 @@ export const useThemeStore = create<ThemeState>()(
setEnablePlaylistCoverPhoto: (v) => set({ enablePlaylistCoverPhoto: v }), setEnablePlaylistCoverPhoto: (v) => set({ enablePlaylistCoverPhoto: v }),
showBitrate: true, showBitrate: true,
setShowBitrate: (v) => set({ showBitrate: v }), setShowBitrate: (v) => set({ showBitrate: v }),
showRemainingTime: false,
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
}), }),
{ {
name: 'psysonic_theme', name: 'psysonic_theme',
+19
View File
@@ -1353,6 +1353,25 @@
text-align: left; text-align: left;
} }
/* Clickable time toggle with swap indicator */
.player-time-toggle {
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 2px 4px;
border-radius: 4px;
transition: background-color 0.15s, color 0.15s;
}
.player-time-toggle:hover {
background-color: var(--surface-hover, rgba(128, 128, 128, 0.2));
color: var(--text-primary);
}
.player-time-toggle:active {
background-color: var(--surface-active, rgba(128, 128, 128, 0.3));
}
/* Volume section */ /* Volume section */
.player-volume-section { .player-volume-section {
display: flex; display: flex;