mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +02:00
add visualizer to sidebar
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user