mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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_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_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_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",
|
||||||
|
|||||||
@@ -25,3 +25,24 @@
|
|||||||
.lyrics-section :global(.synchronized-lyrics .lyric-line) {
|
.lyrics-section :global(.synchronized-lyrics .lyric-line) {
|
||||||
padding: 0.25rem 0;
|
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 styles from './sidebar-play-queue.module.css';
|
||||||
|
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
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 { 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 { useGeneralSettings, usePlaybackSettings, usePlayerSong } from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
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, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const Visualizer = lazy(() =>
|
||||||
|
import('/@/renderer/features/player/components/visualizer').then((module) => ({
|
||||||
|
default: module.Visualizer,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
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%">
|
||||||
@@ -33,15 +40,80 @@ export const SidebarPlayQueue = () => {
|
|||||||
searchTerm={search}
|
searchTerm={search}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showLyricsInSidebar && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
<div className={styles.lyricsSection}>
|
|
||||||
<Lyrics />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<BottomPanel />
|
||||||
</Stack>
|
</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,
|
audioCtx: context,
|
||||||
connectSpeakers: false,
|
connectSpeakers: false,
|
||||||
gradient: 'prism',
|
gradient: 'prism',
|
||||||
mode: 4,
|
ledBars: true,
|
||||||
|
mode: 8,
|
||||||
overlay: true,
|
overlay: true,
|
||||||
showBgColor: false,
|
showBgColor: false,
|
||||||
showPeaks: false,
|
showPeaks: false,
|
||||||
|
showScaleX: false,
|
||||||
|
showScaleY: false,
|
||||||
smoothing: 0.8,
|
smoothing: 0.8,
|
||||||
});
|
});
|
||||||
setMotion(audioMotion);
|
setMotion(audioMotion);
|
||||||
|
|||||||
@@ -43,27 +43,6 @@ 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
|
||||||
|
|||||||
@@ -59,6 +59,48 @@ export const SidebarSettings = () => {
|
|||||||
}),
|
}),
|
||||||
title: t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' }),
|
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 (
|
return (
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ const GeneralSettingsSchema = z.object({
|
|||||||
playerbarOpenDrawer: z.boolean(),
|
playerbarOpenDrawer: z.boolean(),
|
||||||
playerbarSlider: PlayerbarSliderSchema,
|
playerbarSlider: PlayerbarSliderSchema,
|
||||||
resume: z.boolean(),
|
resume: z.boolean(),
|
||||||
|
showLyricsInSidebar: z.boolean(),
|
||||||
|
showVisualizerInSidebar: z.boolean(),
|
||||||
sidebarCollapsedNavigation: z.boolean(),
|
sidebarCollapsedNavigation: z.boolean(),
|
||||||
sidebarCollapseShared: z.boolean(),
|
sidebarCollapseShared: z.boolean(),
|
||||||
sidebarItems: z.array(SidebarItemTypeSchema),
|
sidebarItems: z.array(SidebarItemTypeSchema),
|
||||||
@@ -285,7 +287,6 @@ 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)),
|
||||||
@@ -646,6 +647,8 @@ const initialState: SettingsState = {
|
|||||||
type: PlayerbarSliderType.WAVEFORM,
|
type: PlayerbarSliderType.WAVEFORM,
|
||||||
},
|
},
|
||||||
resume: true,
|
resume: true,
|
||||||
|
showLyricsInSidebar: true,
|
||||||
|
showVisualizerInSidebar: false,
|
||||||
sidebarCollapsedNavigation: true,
|
sidebarCollapsedNavigation: true,
|
||||||
sidebarCollapseShared: false,
|
sidebarCollapseShared: false,
|
||||||
sidebarItems,
|
sidebarItems,
|
||||||
@@ -1106,7 +1109,6 @@ const initialState: SettingsState = {
|
|||||||
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, LyricSource.LRCLIB],
|
||||||
|
|||||||
Reference in New Issue
Block a user