add visualizer to sidebar

This commit is contained in:
jeffvli
2025-11-30 18:14:05 -08:00
parent 0b8ae55150
commit c7bf0d8fb8
7 changed files with 158 additions and 37 deletions
+3 -1
View File
@@ -789,7 +789,9 @@
"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",
"showLyricsInSidebar": "show lyrics in player sidebar",
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
"showVisualizerInSidebar": "show visualizer in player sidebar",
"preservePitch_description": "preserves pitch when modifying playback speed",
"preservePitch": "preserve pitch",
"preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing",
@@ -25,3 +25,24 @@
.lyrics-section :global(.synchronized-lyrics .lyric-line) {
padding: 0.25rem 0;
}
.visualizer-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
pointer-events: none;
}
.visualizer-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);
}
@@ -1,21 +1,28 @@
import { useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { lazy, Suspense, useMemo, useRef, useState } from 'react';
import styles from './sidebar-play-queue.module.css';
import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
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 { useGeneralSettings, usePlaybackSettings, usePlayerSong } 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';
import { ItemListKey, PlayerType } from '/@/shared/types/types';
const Visualizer = lazy(() =>
import('/@/renderer/features/player/components/visualizer').then((module) => ({
default: module.Visualizer,
})),
);
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%">
@@ -33,15 +40,80 @@ export const SidebarPlayQueue = () => {
searchTerm={search}
/>
</div>
{showLyricsInSidebar && (
<>
<Divider />
<div className={styles.lyricsSection}>
<Lyrics />
</div>
</>
)}
</Flex>
<BottomPanel />
</Stack>
);
};
const BottomPanel = () => {
const { showLyricsInSidebar, showVisualizerInSidebar } = useGeneralSettings();
const { type, webAudio } = usePlaybackSettings();
const currentSong = usePlayerSong();
const { data: lyricsData } = useQuery(
lyricsQueries.songLyrics(
{
query: { songId: currentSong?.id || '' },
serverId: currentSong?._serverId || '',
},
currentSong,
),
);
const hasLyrics = useMemo(() => {
if (!lyricsData) return false;
if (Array.isArray(lyricsData)) {
return lyricsData.length > 0 && !!lyricsData[0]?.lyrics;
}
const lyrics = lyricsData?.lyrics;
if (Array.isArray(lyrics)) {
return lyrics.length > 0;
}
if (typeof lyrics === 'string') {
return lyrics.trim().length > 0;
}
return false;
}, [lyricsData]);
const showVisualizer = showVisualizerInSidebar && type === PlayerType.WEB && webAudio;
const showPanel = showLyricsInSidebar || showVisualizer;
if (!showPanel) {
return null;
}
return (
<>
<Divider />
{showLyricsInSidebar ? (
<div className={styles.lyricsSection}>
<Lyrics />
{showVisualizer && (
<div
className={styles.visualizerOverlay}
style={{
opacity: hasLyrics ? 0.2 : 0.5,
}}
>
<Suspense fallback={<></>}>
<Visualizer />
</Suspense>
</div>
)}
</div>
) : (
showVisualizer && (
<div className={styles.visualizerSection}>
<Suspense fallback={<></>}>
<Visualizer />
</Suspense>
</div>
)
)}
</>
);
};
@@ -20,10 +20,13 @@ export const Visualizer = () => {
audioCtx: context,
connectSpeakers: false,
gradient: 'prism',
mode: 4,
ledBars: true,
mode: 8,
overlay: true,
showBgColor: false,
showPeaks: false,
showScaleX: false,
showScaleY: false,
smoothing: 0.8,
});
setMotion(audioMotion);
@@ -43,27 +43,6 @@ 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
@@ -59,6 +59,48 @@ export const SidebarSettings = () => {
}),
title: t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Show lyrics in attached play queue"
defaultChecked={settings.showLyricsInSidebar}
onChange={(e) => {
setSettings({
general: {
...settings,
showLyricsInSidebar: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.showLyricsInSidebar', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.showLyricsInSidebar', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Show visualizer in sidebar"
defaultChecked={settings.showVisualizerInSidebar}
onChange={(e) => {
setSettings({
general: {
...settings,
showVisualizerInSidebar: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.showVisualizerInSidebar', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.showVisualizerInSidebar', { postProcess: 'sentenceCase' }),
},
];
return (
+4 -2
View File
@@ -244,6 +244,8 @@ const GeneralSettingsSchema = z.object({
playerbarOpenDrawer: z.boolean(),
playerbarSlider: PlayerbarSliderSchema,
resume: z.boolean(),
showLyricsInSidebar: z.boolean(),
showVisualizerInSidebar: z.boolean(),
sidebarCollapsedNavigation: z.boolean(),
sidebarCollapseShared: z.boolean(),
sidebarItems: z.array(SidebarItemTypeSchema),
@@ -285,7 +287,6 @@ 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)),
@@ -646,6 +647,8 @@ const initialState: SettingsState = {
type: PlayerbarSliderType.WAVEFORM,
},
resume: true,
showLyricsInSidebar: true,
showVisualizerInSidebar: false,
sidebarCollapsedNavigation: true,
sidebarCollapseShared: false,
sidebarItems,
@@ -1106,7 +1109,6 @@ const initialState: SettingsState = {
gap: 24,
gapUnsync: 24,
preferLocalLyrics: true,
showLyricsInSidebar: true,
showMatch: true,
showProvider: true,
sources: [LyricSource.NETEASE, LyricSource.LRCLIB],