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;