mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
introducing timer toggle player bar (#212)
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1033,6 +1033,8 @@ export const ruTranslation = {
|
|||||||
lyricsNotFound: 'Текст не найден',
|
lyricsNotFound: 'Текст не найден',
|
||||||
lyricsSourceNetease: 'Источник: Netease',
|
lyricsSourceNetease: 'Источник: Netease',
|
||||||
lyricsSourceLyricsplus: 'Источник: YouLyPlus',
|
lyricsSourceLyricsplus: 'Источник: YouLyPlus',
|
||||||
|
showDuration: 'Показать длительность',
|
||||||
|
showRemainingTime: 'Показать оставшееся время',
|
||||||
},
|
},
|
||||||
songInfo: {
|
songInfo: {
|
||||||
title: 'О треке',
|
title: 'О треке',
|
||||||
|
|||||||
@@ -968,6 +968,8 @@ export const zhTranslation = {
|
|||||||
lyricsSourceLrclib: '来源:LRCLIB',
|
lyricsSourceLrclib: '来源:LRCLIB',
|
||||||
lyricsSourceNetease: '来源:网易云',
|
lyricsSourceNetease: '来源:网易云',
|
||||||
lyricsSourceLyricsplus: '来源:YouLyPlus',
|
lyricsSourceLyricsplus: '来源:YouLyPlus',
|
||||||
|
showDuration: '显示时长',
|
||||||
|
showRemainingTime: '显示剩余时间',
|
||||||
},
|
},
|
||||||
songInfo: {
|
songInfo: {
|
||||||
title: '歌曲信息',
|
title: '歌曲信息',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user