diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6f529a997..10ef8ae63 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.module.css b/src/renderer/features/now-playing/components/sidebar-play-queue.module.css index e8e8dc1cf..7bd645a6c 100644 --- a/src/renderer/features/now-playing/components/sidebar-play-queue.module.css +++ b/src/renderer/features/now-playing/components/sidebar-play-queue.module.css @@ -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); +} diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx index 201621646..591815a2d 100644 --- a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx +++ b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx @@ -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(null); const [search, setSearch] = useState(undefined); - const { showLyricsInSidebar } = useLyricsSettings(); return ( @@ -33,15 +40,80 @@ export const SidebarPlayQueue = () => { searchTerm={search} /> - {showLyricsInSidebar && ( - <> - -
- -
- - )} +
); }; + +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 ( + <> + + {showLyricsInSidebar ? ( +
+ + {showVisualizer && ( +
+ }> + + +
+ )} +
+ ) : ( + showVisualizer && ( +
+ }> + + +
+ ) + )} + + ); +}; diff --git a/src/renderer/features/player/components/visualizer.tsx b/src/renderer/features/player/components/visualizer.tsx index 1fa3ef5ac..94c4cfd2b 100644 --- a/src/renderer/features/player/components/visualizer.tsx +++ b/src/renderer/features/player/components/visualizer.tsx @@ -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); diff --git a/src/renderer/features/settings/components/general/lyric-settings.tsx b/src/renderer/features/settings/components/general/lyric-settings.tsx index ffee58f21..aafdeeb74 100644 --- a/src/renderer/features/settings/components/general/lyric-settings.tsx +++ b/src/renderer/features/settings/components/general/lyric-settings.tsx @@ -43,27 +43,6 @@ export const LyricSettings = () => { }), title: t('setting.followLyric', { postProcess: 'sentenceCase' }), }, - { - control: ( - { - setSettings({ - lyrics: { - ...settings, - showLyricsInSidebar: e.currentTarget.checked, - }, - }); - }} - /> - ), - description: t('setting.showLyricsInSidebar', { - context: 'description', - postProcess: 'sentenceCase', - }), - title: t('setting.showLyricsInSidebar', { postProcess: 'sentenceCase' }), - }, { control: ( { }), title: t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + showLyricsInSidebar: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.showLyricsInSidebar', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.showLyricsInSidebar', { postProcess: 'sentenceCase' }), + }, + { + control: ( + { + setSettings({ + general: { + ...settings, + showVisualizerInSidebar: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.showVisualizerInSidebar', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.showVisualizerInSidebar', { postProcess: 'sentenceCase' }), + }, ]; return ( diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 58124666f..f226707ee 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -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],