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",
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
"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": "preserve pitch",
"preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing",
@@ -1,12 +1,17 @@
import clsx from 'clsx';
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 { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useLyricsSettings, usePlaybackType, usePlayerActions } from '/@/renderer/store';
import {
useLyricsSettings,
usePlaybackType,
usePlayerActions,
usePlayerStatus,
} from '/@/renderer/store';
import { usePlayerTimestamp } from '/@/renderer/store/timestamp.store';
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
@@ -30,10 +35,8 @@ export const SynchronizedLyrics = ({
const playbackType = usePlaybackType();
const settings = useLyricsSettings();
const { mediaSeekToTimestamp } = usePlayerActions();
// State for player status and timestamp from events
const [status, setStatus] = useState<PlayerStatus>(PlayerStatus.PAUSED);
const [timestamp, setTimestamp] = useState<number>(0);
const status = usePlayerStatus();
const timestamp = usePlayerTimestamp();
const handleSeek = useCallback(
(time: number) => {
@@ -154,23 +157,6 @@ export const SynchronizedLyrics = ({
setCurrentLyricRef.current = 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(() => {
// Copy the follow settings into a ref that can be accessed in the timeout
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 styles from './sidebar-play-queue.module.css';
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 { 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 { ItemListKey } from '/@/shared/types/types';
export const SidebarPlayQueue = () => {
const tableRef = useRef<ItemListHandle | null>(null);
const [search, setSearch] = useState<string | undefined>(undefined);
const { showLyricsInSidebar } = useLyricsSettings();
return (
<Stack gap={0} h="100%" id="sidebar-play-queue-container" pos="relative" w="100%">
@@ -18,7 +25,23 @@ export const SidebarPlayQueue = () => {
tableRef={tableRef}
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>
);
};
@@ -43,6 +43,27 @@ export const LyricSettings = () => {
}),
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: (
<Switch
+23 -21
View File
@@ -285,6 +285,7 @@ const LyricsSettingsSchema = z.object({
gap: z.number(),
gapUnsync: z.number(),
preferLocalLyrics: z.boolean(),
showLyricsInSidebar: z.boolean(),
showMatch: z.boolean(),
showProvider: z.boolean(),
sources: z.array(z.nativeEnum(LyricSource)),
@@ -728,7 +729,7 @@ const initialState: SettingsState = {
}),
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'compact',
},
@@ -753,8 +754,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -801,8 +802,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -838,8 +839,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -875,8 +876,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -919,8 +920,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'compact',
@@ -951,8 +952,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -978,8 +979,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -1005,8 +1006,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -1050,8 +1051,8 @@ const initialState: SettingsState = {
pinned: column.pinned,
width: column.width,
})),
enableAlternateRowColors: true,
enableHorizontalBorders: true,
enableAlternateRowColors: false,
enableHorizontalBorders: false,
enableRowHoverHighlight: true,
enableVerticalBorders: false,
size: 'default',
@@ -1092,16 +1093,17 @@ const initialState: SettingsState = {
delayMs: 0,
enableAutoTranslation: false,
enableNeteaseTranslation: false,
fetch: false,
fetch: true,
follow: true,
fontSize: 24,
fontSizeUnsync: 24,
gap: 24,
gapUnsync: 24,
preferLocalLyrics: true,
showLyricsInSidebar: true,
showMatch: true,
showProvider: true,
sources: [LyricSource.NETEASE, LyricSource.LRCLIB],
sources: [LyricSource.NETEASE],
translationApiKey: '',
translationApiProvider: '',
translationTargetLanguage: 'en',