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
+31 -2
View File
@@ -3,12 +3,13 @@ import { createPortal } from 'react-dom';
import {
Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Music,
Square, Repeat, Repeat1, Maximize2, SlidersVertical, X, Heart, Cast,
PictureInPicture2,
PictureInPicture2, ArrowLeftRight,
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating } from '../api/subsonic';
import CachedImage from './CachedImage';
import WaveformSeek from './WaveformSeek';
@@ -43,11 +44,28 @@ const PlaybackTime = memo(function PlaybackTime({ className }: { className?: str
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() {
const { t } = useTranslation();
const navigate = useNavigate();
const [eqOpen, setEqOpen] = useState(false);
const [showVolPct, setShowVolPct] = useState(false);
const [localShowRemaining, setLocalShowRemaining] = useState(() => useThemeStore.getState().showRemainingTime);
const premuteVolumeRef = useRef(1);
const showLyrics = useLyricsStore(s => s.showLyrics);
const activeTab = useLyricsStore(s => s.activeTab);
@@ -297,7 +315,18 @@ export default function PlayerBar() {
<div className="player-waveform-wrap">
<WaveformSeek trackId={currentTrack?.id} />
</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>
+2
View File
@@ -985,6 +985,8 @@ export const deTranslation = {
lyricsSourceLrclib: 'Quelle: LRCLIB',
lyricsSourceNetease: 'Quelle: Netease',
lyricsSourceLyricsplus: 'Quelle: YouLyPlus',
showDuration: 'Dauer anzeigen',
showRemainingTime: 'Verbleibende Zeit anzeigen',
},
songInfo: {
title: 'Song-Infos',
+2
View File
@@ -987,6 +987,8 @@ export const enTranslation = {
lyricsSourceLrclib: 'Source: LRCLIB',
lyricsSourceNetease: 'Source: Netease',
lyricsSourceLyricsplus: 'Source: YouLyPlus',
showDuration: 'Show duration',
showRemainingTime: 'Show remaining time',
},
songInfo: {
title: 'Song Info',
+2
View File
@@ -978,6 +978,8 @@ export const esTranslation = {
lyricsSourceLrclib: 'Fuente: LRCLIB',
lyricsSourceNetease: 'Fuente: Netease',
lyricsSourceLyricsplus: 'Fuente: YouLyPlus',
showDuration: 'Mostrar duración',
showRemainingTime: 'Mostrar tiempo restante',
},
songInfo: {
title: 'Información de Canción',
+2
View File
@@ -973,6 +973,8 @@ export const frTranslation = {
lyricsSourceLrclib: 'Source : LRCLIB',
lyricsSourceNetease: 'Source : Netease',
lyricsSourceLyricsplus: 'Source : YouLyPlus',
showDuration: 'Afficher la durée',
showRemainingTime: 'Afficher le temps restant',
},
songInfo: {
title: 'Infos du morceau',
+2
View File
@@ -972,6 +972,8 @@ export const nbTranslation = {
lyricsSourceLrclib: 'Kilde: LRCLIB',
lyricsSourceNetease: 'Kilde: Netease',
lyricsSourceLyricsplus: 'Kilde: YouLyPlus',
showDuration: 'Vis varighet',
showRemainingTime: 'Vis gjenværende tid',
},
songInfo: {
title: 'Sanginfo',
+2
View File
@@ -972,6 +972,8 @@ export const nlTranslation = {
lyricsSourceLrclib: 'Bron: LRCLIB',
lyricsSourceNetease: 'Bron: Netease',
lyricsSourceLyricsplus: 'Bron: YouLyPlus',
showDuration: 'Toon duur',
showRemainingTime: 'Toon resterende tijd',
},
songInfo: {
title: 'Nummerinfo',
+2
View File
@@ -1033,6 +1033,8 @@ export const ruTranslation = {
lyricsNotFound: 'Текст не найден',
lyricsSourceNetease: 'Источник: Netease',
lyricsSourceLyricsplus: 'Источник: YouLyPlus',
showDuration: 'Показать длительность',
showRemainingTime: 'Показать оставшееся время',
},
songInfo: {
title: 'О треке',
+2
View File
@@ -968,6 +968,8 @@ export const zhTranslation = {
lyricsSourceLrclib: '来源:LRCLIB',
lyricsSourceNetease: '来源:网易云',
lyricsSourceLyricsplus: '来源:YouLyPlus',
showDuration: '显示时长',
showRemainingTime: '显示剩余时间',
},
songInfo: {
title: '歌曲信息',
+4
View File
@@ -22,6 +22,8 @@ interface ThemeState {
setEnablePlaylistCoverPhoto: (v: boolean) => void;
showBitrate: boolean;
setShowBitrate: (v: boolean) => void;
showRemainingTime: boolean;
setShowRemainingTime: (v: boolean) => void;
}
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 }),
showBitrate: true,
setShowBitrate: (v) => set({ showBitrate: v }),
showRemainingTime: false,
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
}),
{
name: 'psysonic_theme',
+19
View File
@@ -1353,6 +1353,25 @@
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 */
.player-volume-section {
display: flex;