From eb747ba1aefdbc931d5f22e2e786de1103d4be98 Mon Sep 17 00:00:00 2001 From: "Kveld." Date: Sun, 19 Apr 2026 07:04:01 -0300 Subject: [PATCH] introducing timer toggle player bar (#212) --- package-lock.json | 4 ++-- src/components/PlayerBar.tsx | 33 +++++++++++++++++++++++++++++++-- src/locales/de.ts | 2 ++ src/locales/en.ts | 2 ++ src/locales/es.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 ++ src/store/themeStore.ts | 4 ++++ src/styles/layout.css | 19 +++++++++++++++++++ 12 files changed, 72 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa701b57..b0e9308a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "psysonic", - "version": "1.34.13", + "version": "1.41.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "psysonic", - "version": "1.34.13", + "version": "1.41.0", "dependencies": { "@fontsource-variable/dm-sans": "^5.2.8", "@fontsource-variable/figtree": "^5.2.10", diff --git a/src/components/PlayerBar.tsx b/src/components/PlayerBar.tsx index 2289c01e..eacb8100 100644 --- a/src/components/PlayerBar.tsx +++ b/src/components/PlayerBar.tsx @@ -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 ; }); +// 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(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 ; +}); + 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() {
- {formatTime(duration)} + { + const newVal = !localShowRemaining; + setLocalShowRemaining(newVal); + useThemeStore.getState().setShowRemainingTime(newVal); + }} + title={localShowRemaining ? t('player.showDuration') : t('player.showRemainingTime')} + > + {localShowRemaining ? : formatTime(duration)} + + )} diff --git a/src/locales/de.ts b/src/locales/de.ts index cec68616..dd9243da 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -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', diff --git a/src/locales/en.ts b/src/locales/en.ts index c0847f43..a762ee3d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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', diff --git a/src/locales/es.ts b/src/locales/es.ts index 9fe6ab63..7940793d 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -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', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 519c0139..5705ae17 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -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', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index a2218e33..a2b21839 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -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', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 0e204a4b..72db201a 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -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', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index cceb4667..7d573273 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1033,6 +1033,8 @@ export const ruTranslation = { lyricsNotFound: 'Текст не найден', lyricsSourceNetease: 'Источник: Netease', lyricsSourceLyricsplus: 'Источник: YouLyPlus', + showDuration: 'Показать длительность', + showRemainingTime: 'Показать оставшееся время', }, songInfo: { title: 'О треке', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index d1cc4064..b557f102 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -968,6 +968,8 @@ export const zhTranslation = { lyricsSourceLrclib: '来源:LRCLIB', lyricsSourceNetease: '来源:网易云', lyricsSourceLyricsplus: '来源:YouLyPlus', + showDuration: '显示时长', + showRemainingTime: '显示剩余时间', }, songInfo: { title: '歌曲信息', diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts index 320ee4cb..d857757c 100644 --- a/src/store/themeStore.ts +++ b/src/store/themeStore.ts @@ -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): string { @@ -59,6 +61,8 @@ export const useThemeStore = create()( setEnablePlaylistCoverPhoto: (v) => set({ enablePlaylistCoverPhoto: v }), showBitrate: true, setShowBitrate: (v) => set({ showBitrate: v }), + showRemainingTime: false, + setShowRemainingTime: (v) => set({ showRemainingTime: v }), }), { name: 'psysonic_theme', diff --git a/src/styles/layout.css b/src/styles/layout.css index ae9d2b4b..bf7f4aa0 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -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;