add sidebar panel lyrics

This commit is contained in:
jeffvli
2025-11-29 17:26:28 -08:00
parent 7f95c520b2
commit 96e9d54f4e
6 changed files with 107 additions and 46 deletions
+2
View File
@@ -781,6 +781,8 @@
"playerbarWaveformRadius": "waveform radius", "playerbarWaveformRadius": "waveform radius",
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available", "preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
"preferLocalLyrics": "prefer local lyrics", "preferLocalLyrics": "prefer local lyrics",
"showLyricsInSidebar_description": "a panel will be added to the attached play queue that displays the lyrics",
"showLyricsInSidebar": "show lyrics in attached play queue",
"preservePitch_description": "preserves pitch when modifying playback speed", "preservePitch_description": "preserves pitch when modifying playback speed",
"preservePitch": "preserve pitch", "preservePitch": "preserve pitch",
"preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing", "preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing",
@@ -1,12 +1,17 @@
import clsx from 'clsx'; import clsx from 'clsx';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import styles from './synchronized-lyrics.module.css'; import styles from './synchronized-lyrics.module.css';
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line'; import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import {
import { useLyricsSettings, usePlaybackType, usePlayerActions } from '/@/renderer/store'; useLyricsSettings,
usePlaybackType,
usePlayerActions,
usePlayerStatus,
} from '/@/renderer/store';
import { usePlayerTimestamp } from '/@/renderer/store/timestamp.store';
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types'; import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types'; import { PlayerStatus, PlayerType } from '/@/shared/types/types';
@@ -30,10 +35,8 @@ export const SynchronizedLyrics = ({
const playbackType = usePlaybackType(); const playbackType = usePlaybackType();
const settings = useLyricsSettings(); const settings = useLyricsSettings();
const { mediaSeekToTimestamp } = usePlayerActions(); const { mediaSeekToTimestamp } = usePlayerActions();
const status = usePlayerStatus();
// State for player status and timestamp from events const timestamp = usePlayerTimestamp();
const [status, setStatus] = useState<PlayerStatus>(PlayerStatus.PAUSED);
const [timestamp, setTimestamp] = useState<number>(0);
const handleSeek = useCallback( const handleSeek = useCallback(
(time: number) => { (time: number) => {
@@ -154,23 +157,6 @@ export const SynchronizedLyrics = ({
setCurrentLyricRef.current = setCurrentLyric; setCurrentLyricRef.current = setCurrentLyric;
}, [setCurrentLyric]); }, [setCurrentLyric]);
// Subscribe to player events
usePlayerEvents(
{
onPlayerProgress: (properties) => {
setTimestamp(properties.timestamp);
},
onPlayerSeekToTimestamp: (properties) => {
// When seeking, update timestamp immediately
setTimestamp(properties.timestamp);
},
onPlayerStatus: (properties) => {
setStatus(properties.status);
},
},
[],
);
useEffect(() => { useEffect(() => {
// Copy the follow settings into a ref that can be accessed in the timeout // Copy the follow settings into a ref that can be accessed in the timeout
followRef.current = settings.follow; followRef.current = settings.follow;
@@ -0,0 +1,27 @@
.play-queue-section {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.lyrics-section {
position: relative;
display: flex;
flex: 0 0 300px;
flex-direction: column;
min-height: 200px;
max-height: 400px;
overflow: hidden;
background: var(--theme-colors-background);
}
.lyrics-section :global(.synchronized-lyrics) {
padding: 2rem 0 4rem !important;
transform: translateY(0) !important;
}
.lyrics-section :global(.synchronized-lyrics .lyric-line) {
padding: 0.25rem 0;
}
@@ -1,14 +1,21 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import styles from './sidebar-play-queue.module.css';
import { ItemListHandle } from '/@/renderer/components/item-list/types'; import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { Lyrics } from '/@/renderer/features/lyrics/lyrics';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue'; import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls'; import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { useLyricsSettings } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const SidebarPlayQueue = () => { export const SidebarPlayQueue = () => {
const tableRef = useRef<ItemListHandle | null>(null); const tableRef = useRef<ItemListHandle | null>(null);
const [search, setSearch] = useState<string | undefined>(undefined); const [search, setSearch] = useState<string | undefined>(undefined);
const { showLyricsInSidebar } = useLyricsSettings();
return ( return (
<Stack gap={0} h="100%" id="sidebar-play-queue-container" pos="relative" w="100%"> <Stack gap={0} h="100%" id="sidebar-play-queue-container" pos="relative" w="100%">
@@ -18,7 +25,23 @@ export const SidebarPlayQueue = () => {
tableRef={tableRef} tableRef={tableRef}
type={ItemListKey.SIDE_QUEUE} type={ItemListKey.SIDE_QUEUE}
/> />
<PlayQueue listKey={ItemListKey.SIDE_QUEUE} ref={tableRef} searchTerm={search} /> <Flex direction="column" style={{ flex: 1, minHeight: 0 }}>
<div className={styles.playQueueSection}>
<PlayQueue
listKey={ItemListKey.SIDE_QUEUE}
ref={tableRef}
searchTerm={search}
/>
</div>
{showLyricsInSidebar && (
<>
<Divider />
<div className={styles.lyricsSection}>
<Lyrics />
</div>
</>
)}
</Flex>
</Stack> </Stack>
); );
}; };
@@ -43,6 +43,27 @@ export const LyricSettings = () => {
}), }),
title: t('setting.followLyric', { postProcess: 'sentenceCase' }), title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Switch
aria-label="Show lyrics in attached play queue"
defaultChecked={settings.showLyricsInSidebar}
onChange={(e) => {
setSettings({
lyrics: {
...settings,
showLyricsInSidebar: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.showLyricsInSidebar', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.showLyricsInSidebar', { postProcess: 'sentenceCase' }),
},
{ {
control: ( control: (
<Switch <Switch
+23 -21
View File
@@ -285,6 +285,7 @@ const LyricsSettingsSchema = z.object({
gap: z.number(), gap: z.number(),
gapUnsync: z.number(), gapUnsync: z.number(),
preferLocalLyrics: z.boolean(), preferLocalLyrics: z.boolean(),
showLyricsInSidebar: z.boolean(),
showMatch: z.boolean(), showMatch: z.boolean(),
showProvider: z.boolean(), showProvider: z.boolean(),
sources: z.array(z.nativeEnum(LyricSource)), sources: z.array(z.nativeEnum(LyricSource)),
@@ -728,7 +729,7 @@ const initialState: SettingsState = {
}), }),
enableAlternateRowColors: false, enableAlternateRowColors: false,
enableHorizontalBorders: false, enableHorizontalBorders: false,
enableRowHoverHighlight: false, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'compact', size: 'compact',
}, },
@@ -753,8 +754,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -801,8 +802,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -838,8 +839,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -875,8 +876,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -919,8 +920,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'compact', size: 'compact',
@@ -951,8 +952,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -978,8 +979,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -1005,8 +1006,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -1050,8 +1051,8 @@ const initialState: SettingsState = {
pinned: column.pinned, pinned: column.pinned,
width: column.width, width: column.width,
})), })),
enableAlternateRowColors: true, enableAlternateRowColors: false,
enableHorizontalBorders: true, enableHorizontalBorders: false,
enableRowHoverHighlight: true, enableRowHoverHighlight: true,
enableVerticalBorders: false, enableVerticalBorders: false,
size: 'default', size: 'default',
@@ -1092,16 +1093,17 @@ const initialState: SettingsState = {
delayMs: 0, delayMs: 0,
enableAutoTranslation: false, enableAutoTranslation: false,
enableNeteaseTranslation: false, enableNeteaseTranslation: false,
fetch: false, fetch: true,
follow: true, follow: true,
fontSize: 24, fontSize: 24,
fontSizeUnsync: 24, fontSizeUnsync: 24,
gap: 24, gap: 24,
gapUnsync: 24, gapUnsync: 24,
preferLocalLyrics: true, preferLocalLyrics: true,
showLyricsInSidebar: true,
showMatch: true, showMatch: true,
showProvider: true, showProvider: true,
sources: [LyricSource.NETEASE, LyricSource.LRCLIB], sources: [LyricSource.NETEASE],
translationApiKey: '', translationApiKey: '',
translationApiProvider: '', translationApiProvider: '',
translationTargetLanguage: 'en', translationTargetLanguage: 'en',