mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-17 06:00:20 +02:00
add additional configuration to player sidebar
- allow reordering of panels - allow separation between lyrics and visualizer panels - allow resize of panels
This commit is contained in:
@@ -122,6 +122,7 @@
|
|||||||
"react-image": "^4.1.0",
|
"react-image": "^4.1.0",
|
||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-player": "^2.16.0",
|
"react-player": "^2.16.0",
|
||||||
|
"react-resizable-panels": "^4.0.15",
|
||||||
"react-router": "^7.9.6",
|
"react-router": "^7.9.6",
|
||||||
"react-virtualized-auto-sizer": "^1.0.26",
|
"react-virtualized-auto-sizer": "^1.0.26",
|
||||||
"react-window": "1.8.11",
|
"react-window": "1.8.11",
|
||||||
|
|||||||
Generated
+14
@@ -191,6 +191,9 @@ importers:
|
|||||||
react-player:
|
react-player:
|
||||||
specifier: ^2.16.0
|
specifier: ^2.16.0
|
||||||
version: 2.16.0(react@19.1.0)
|
version: 2.16.0(react@19.1.0)
|
||||||
|
react-resizable-panels:
|
||||||
|
specifier: ^4.0.15
|
||||||
|
version: 4.0.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.9.6
|
specifier: ^7.9.6
|
||||||
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -4616,6 +4619,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-resizable-panels@4.0.15:
|
||||||
|
resolution: {integrity: sha512-+ygM/EI2h4Qc/cl2fasQ2qwOgNfpQwXLNTU5PqhhPerliX+wnbf7ejcqran7lz3BqABzjddf0pJ3j3G/+A0v9Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react-router-dom@7.9.4:
|
react-router-dom@7.9.4:
|
||||||
resolution: {integrity: sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==}
|
resolution: {integrity: sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -10609,6 +10618,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.5
|
'@types/react': 19.2.5
|
||||||
|
|
||||||
|
react-resizable-panels@4.0.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|||||||
@@ -898,6 +898,8 @@
|
|||||||
"showLyricsInSidebar": "show lyrics in player sidebar",
|
"showLyricsInSidebar": "show lyrics in player sidebar",
|
||||||
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
"showVisualizerInSidebar_description": "a panel will be added to the player sidebar that displays the visualizer",
|
||||||
"showVisualizerInSidebar": "show visualizer in player sidebar",
|
"showVisualizerInSidebar": "show visualizer in player sidebar",
|
||||||
|
"combinedLyricsAndVisualizer_description": "combine lyrics and visualizer into the same panel",
|
||||||
|
"combinedLyricsAndVisualizer": "combine lyrics and 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",
|
||||||
"audioFadeOnStatusChange": "audio fade on status change",
|
"audioFadeOnStatusChange": "audio fade on status change",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
|
||||||
@@ -18,6 +19,20 @@
|
|||||||
&:focus-within {
|
&:focus-within {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(> *),
|
||||||
|
:global(> * > *),
|
||||||
|
:global(div) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(button),
|
||||||
|
:global(input),
|
||||||
|
:global([role='button']),
|
||||||
|
:global([role='combobox']),
|
||||||
|
:global([role='textbox']) {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lyrics-container {
|
.lyrics-container {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
.play-queue-section {
|
.play-queue-section {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -9,10 +11,9 @@
|
|||||||
.lyrics-section {
|
.lyrics-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 300px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 200px;
|
height: 100%;
|
||||||
max-height: 400px;
|
min-height: 0;
|
||||||
padding: var(--theme-spacing-md);
|
padding: var(--theme-spacing-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--theme-colors-background);
|
background: var(--theme-colors-background);
|
||||||
@@ -41,12 +42,38 @@
|
|||||||
.visualizer-section {
|
.visualizer-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 300px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 200px;
|
height: 100%;
|
||||||
max-height: 400px;
|
min-height: 0;
|
||||||
padding: var(--theme-spacing-md);
|
padding: var(--theme-spacing-md);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--theme-colors-background);
|
background: var(--theme-colors-background);
|
||||||
background-color: var(--theme-colors-background-alternate);
|
background-color: var(--theme-colors-background-alternate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
cursor: row-resize;
|
||||||
|
background-color: var(--theme-colors-border);
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
height 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-reorder-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--theme-spacing-md);
|
||||||
|
left: var(--theme-spacing-md);
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-section:hover .panel-reorder-controls,
|
||||||
|
.visualizer-section:hover .panel-reorder-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { lazy, Suspense, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Group, Panel, Separator } from 'react-resizable-panels';
|
||||||
|
|
||||||
import styles from './sidebar-play-queue.module.css';
|
import styles from './sidebar-play-queue.module.css';
|
||||||
|
|
||||||
@@ -13,12 +15,16 @@ import {
|
|||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
usePlayerSong,
|
usePlayerSong,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
|
useSettingsStoreActions,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||||
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, PlayerType } from '/@/shared/types/types';
|
import { ItemListKey, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
type SidebarPanelType = 'lyrics' | 'queue' | 'visualizer';
|
||||||
|
|
||||||
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
const AudioMotionAnalyzerVisualizer = lazy(() =>
|
||||||
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({
|
||||||
default: module.Visualizer,
|
default: module.Visualizer,
|
||||||
@@ -34,6 +40,91 @@ const ButterchurnVisualizer = lazy(() =>
|
|||||||
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 {
|
||||||
|
combinedLyricsAndVisualizer,
|
||||||
|
showLyricsInSidebar,
|
||||||
|
showVisualizerInSidebar,
|
||||||
|
sidebarPanelOrder,
|
||||||
|
} = useGeneralSettings();
|
||||||
|
const { type, webAudio } = usePlaybackSettings();
|
||||||
|
const showVisualizer = showVisualizerInSidebar && type === PlayerType.WEB && webAudio;
|
||||||
|
const showPanel = showLyricsInSidebar || showVisualizer;
|
||||||
|
|
||||||
|
// Filter and order panels based on what's enabled
|
||||||
|
const orderedPanels = useMemo(() => {
|
||||||
|
if (combinedLyricsAndVisualizer) {
|
||||||
|
// When combined, use the order from settings but filter to only show queue and lyrics (combined)
|
||||||
|
const visiblePanels = sidebarPanelOrder.filter((panel) => {
|
||||||
|
if (panel === 'queue') return true;
|
||||||
|
if (panel === 'lyrics') return showLyricsInSidebar || showVisualizer;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return visiblePanels;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visiblePanels = sidebarPanelOrder.filter((panel) => {
|
||||||
|
if (panel === 'queue') return true;
|
||||||
|
if (panel === 'lyrics') return showLyricsInSidebar;
|
||||||
|
if (panel === 'visualizer') return showVisualizer;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return visiblePanels;
|
||||||
|
}, [combinedLyricsAndVisualizer, showLyricsInSidebar, showVisualizer, sidebarPanelOrder]);
|
||||||
|
|
||||||
|
const renderPanel = (panelType: SidebarPanelType, index: number, totalPanels: number) => {
|
||||||
|
if (panelType === 'queue') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && <Separator className={styles.resizeHandle} />}
|
||||||
|
<Panel defaultSize={50} key="queue" minSize={20}>
|
||||||
|
<div className={styles.playQueueSection}>
|
||||||
|
<PlayQueue
|
||||||
|
listKey={ItemListKey.SIDE_QUEUE}
|
||||||
|
ref={tableRef}
|
||||||
|
searchTerm={search}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combinedLyricsAndVisualizer && (panelType === 'lyrics' || panelType === 'visualizer')) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && <Separator className={styles.resizeHandle} />}
|
||||||
|
<Panel defaultSize={50} key="combined" minSize={20}>
|
||||||
|
<CombinedLyricsAndVisualizerPanel />
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panelType === 'lyrics') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && <Separator className={styles.resizeHandle} />}
|
||||||
|
<Panel defaultSize={totalPanels > 2 ? 25 : 50} key="lyrics" minSize={15}>
|
||||||
|
<LyricsPanel />
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panelType === 'visualizer') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && <Separator className={styles.resizeHandle} />}
|
||||||
|
<Panel defaultSize={totalPanels > 2 ? 25 : 50} key="visualizer" minSize={15}>
|
||||||
|
<VisualizerPanel />
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
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%">
|
||||||
@@ -42,31 +133,174 @@ export const SidebarPlayQueue = () => {
|
|||||||
searchTerm={search}
|
searchTerm={search}
|
||||||
type={ItemListKey.SIDE_QUEUE}
|
type={ItemListKey.SIDE_QUEUE}
|
||||||
/>
|
/>
|
||||||
<Flex direction="column" style={{ flex: 1, minHeight: 0 }}>
|
{showPanel ? (
|
||||||
<div className={styles.playQueueSection}>
|
<Group orientation="vertical" style={{ flex: 1, minHeight: 0 }}>
|
||||||
<PlayQueue
|
{orderedPanels.map((panel, index) =>
|
||||||
listKey={ItemListKey.SIDE_QUEUE}
|
renderPanel(panel, index, orderedPanels.length),
|
||||||
ref={tableRef}
|
)}
|
||||||
searchTerm={search}
|
</Group>
|
||||||
/>
|
) : (
|
||||||
</div>
|
<Flex direction="column" style={{ flex: 1, minHeight: 0 }}>
|
||||||
</Flex>
|
<div className={styles.playQueueSection}>
|
||||||
<BottomPanel />
|
<PlayQueue
|
||||||
|
listKey={ItemListKey.SIDE_QUEUE}
|
||||||
|
ref={tableRef}
|
||||||
|
searchTerm={search}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const BottomPanel = () => {
|
const PanelReorderControls = ({ panelType }: { panelType: 'lyrics' | 'visualizer' }) => {
|
||||||
const { showLyricsInSidebar, showVisualizerInSidebar } = useGeneralSettings();
|
const { t } = useTranslation();
|
||||||
const { type, webAudio } = usePlaybackSettings();
|
const generalSettings = useGeneralSettings();
|
||||||
|
const { combinedLyricsAndVisualizer, sidebarPanelOrder } = generalSettings;
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const currentIndex = sidebarPanelOrder.indexOf(panelType);
|
||||||
|
const canMoveUp = currentIndex > 0;
|
||||||
|
const canMoveDown = currentIndex < sidebarPanelOrder.length - 1;
|
||||||
|
|
||||||
|
const handleMoveUp = useCallback(() => {
|
||||||
|
if (!canMoveUp) return;
|
||||||
|
|
||||||
|
const newOrder = [...sidebarPanelOrder];
|
||||||
|
const targetIndex = currentIndex - 1;
|
||||||
|
|
||||||
|
[newOrder[currentIndex], newOrder[targetIndex]] = [
|
||||||
|
newOrder[targetIndex],
|
||||||
|
newOrder[currentIndex],
|
||||||
|
];
|
||||||
|
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
sidebarPanelOrder: newOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [canMoveUp, currentIndex, generalSettings, sidebarPanelOrder, setSettings]);
|
||||||
|
|
||||||
|
const handleMoveDown = useCallback(() => {
|
||||||
|
if (!canMoveDown) return;
|
||||||
|
|
||||||
|
const newOrder = [...sidebarPanelOrder];
|
||||||
|
[newOrder[currentIndex], newOrder[currentIndex + 1]] = [
|
||||||
|
newOrder[currentIndex + 1],
|
||||||
|
newOrder[currentIndex],
|
||||||
|
];
|
||||||
|
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
sidebarPanelOrder: newOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [canMoveDown, currentIndex, generalSettings, sidebarPanelOrder, setSettings]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (combinedLyricsAndVisualizer && panelType === 'lyrics') {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
showLyricsInSidebar: false,
|
||||||
|
showVisualizerInSidebar: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (panelType === 'lyrics') {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
showLyricsInSidebar: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (panelType === 'visualizer') {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
showVisualizerInSidebar: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [combinedLyricsAndVisualizer, generalSettings, panelType, setSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panelReorderControls}>
|
||||||
|
<ActionIconGroup>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={!canMoveUp}
|
||||||
|
icon="arrowUp"
|
||||||
|
iconProps={{ size: 'sm' }}
|
||||||
|
onClick={handleMoveUp}
|
||||||
|
size="xs"
|
||||||
|
tooltip={{
|
||||||
|
label: t('action.moveUp', { postProcess: 'sentenceCase' }),
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={!canMoveDown}
|
||||||
|
icon="arrowDown"
|
||||||
|
iconProps={{ size: 'sm' }}
|
||||||
|
onClick={handleMoveDown}
|
||||||
|
size="xs"
|
||||||
|
tooltip={{
|
||||||
|
label: t('action.moveDown', { postProcess: 'sentenceCase' }),
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
iconProps={{ size: 'sm' }}
|
||||||
|
onClick={handleClose}
|
||||||
|
size="xs"
|
||||||
|
tooltip={{
|
||||||
|
label: t('common.close', { postProcess: 'sentenceCase' }),
|
||||||
|
}}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</ActionIconGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LyricsPanel = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.lyricsSection}>
|
||||||
|
<PanelReorderControls panelType="lyrics" />
|
||||||
|
<Lyrics fadeOutNoLyricsMessage={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisualizerPanel = () => {
|
||||||
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.visualizerSection}>
|
||||||
|
<PanelReorderControls panelType="visualizer" />
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
{visualizerType === 'butterchurn' ? (
|
||||||
|
<ButterchurnVisualizer />
|
||||||
|
) : (
|
||||||
|
<AudioMotionAnalyzerVisualizer />
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CombinedLyricsAndVisualizerPanel = () => {
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
|
const visualizerType = useSettingsStore((store) => store.visualizer.type);
|
||||||
|
|
||||||
const { data: lyricsData } = useQuery(
|
const { data: lyricsData } = useQuery(
|
||||||
lyricsQueries.songLyrics(
|
lyricsQueries.songLyrics(
|
||||||
{
|
{
|
||||||
options: {
|
options: {
|
||||||
enabled: showLyricsInSidebar && !!currentSong?.id,
|
enabled: !!currentSong?.id,
|
||||||
},
|
},
|
||||||
query: { songId: currentSong?.id || '' },
|
query: { songId: currentSong?.id || '' },
|
||||||
serverId: currentSong?._serverId || '',
|
serverId: currentSong?._serverId || '',
|
||||||
@@ -93,49 +327,24 @@ const BottomPanel = () => {
|
|||||||
return false;
|
return false;
|
||||||
}, [lyricsData]);
|
}, [lyricsData]);
|
||||||
|
|
||||||
const showVisualizer = showVisualizerInSidebar && type === PlayerType.WEB && webAudio;
|
|
||||||
const showPanel = showLyricsInSidebar || showVisualizer;
|
|
||||||
|
|
||||||
if (!showPanel) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.lyricsSection}>
|
||||||
<Divider />
|
<PanelReorderControls panelType="lyrics" />
|
||||||
{showLyricsInSidebar ? (
|
<Lyrics fadeOutNoLyricsMessage={true} />
|
||||||
<div className={styles.lyricsSection}>
|
<div
|
||||||
<Lyrics fadeOutNoLyricsMessage={showVisualizer} />
|
className={styles.visualizerOverlay}
|
||||||
{showVisualizer && (
|
style={{
|
||||||
<div
|
opacity: hasLyrics ? 0.2 : 1,
|
||||||
className={styles.visualizerOverlay}
|
}}
|
||||||
style={{
|
>
|
||||||
opacity: hasLyrics ? 0.2 : 1,
|
<Suspense fallback={<></>}>
|
||||||
}}
|
{visualizerType === 'butterchurn' ? (
|
||||||
>
|
<ButterchurnVisualizer />
|
||||||
<Suspense fallback={<></>}>
|
) : (
|
||||||
{visualizerType === 'butterchurn' ? (
|
<AudioMotionAnalyzerVisualizer />
|
||||||
<ButterchurnVisualizer />
|
|
||||||
) : (
|
|
||||||
<AudioMotionAnalyzerVisualizer />
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Suspense>
|
||||||
) : (
|
</div>
|
||||||
showVisualizer && (
|
</div>
|
||||||
<div className={styles.visualizerSection}>
|
|
||||||
<Suspense fallback={<></>}>
|
|
||||||
{visualizerType === 'butterchurn' ? (
|
|
||||||
<ButterchurnVisualizer />
|
|
||||||
) : (
|
|
||||||
<AudioMotionAnalyzerVisualizer />
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -356,6 +356,23 @@ export const PlayerConfig = () => {
|
|||||||
id: 'showVisualizerInSidebar',
|
id: 'showVisualizerInSidebar',
|
||||||
label: t('setting.showVisualizerInSidebar', { postProcess: 'titleCase' }),
|
label: t('setting.showVisualizerInSidebar', { postProcess: 'titleCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={generalSettings.combinedLyricsAndVisualizer}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...generalSettings,
|
||||||
|
combinedLyricsAndVisualizer: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'combinedLyricsAndVisualizer',
|
||||||
|
label: t('setting.combinedLyricsAndVisualizer', { postProcess: 'titleCase' }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return allOptions;
|
return allOptions;
|
||||||
|
|||||||
@@ -101,6 +101,27 @@ export const SidebarSettings = () => {
|
|||||||
}),
|
}),
|
||||||
title: t('setting.showVisualizerInSidebar', { postProcess: 'sentenceCase' }),
|
title: t('setting.showVisualizerInSidebar', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Combine lyrics and visualizer"
|
||||||
|
defaultChecked={settings.combinedLyricsAndVisualizer}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
combinedLyricsAndVisualizer: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.combinedLyricsAndVisualizer', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
title: t('setting.combinedLyricsAndVisualizer', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ const GenreTargetSchema = z.enum(['album', 'track']);
|
|||||||
|
|
||||||
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
||||||
|
|
||||||
|
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
||||||
|
|
||||||
const SidebarItemTypeSchema = z.object({
|
const SidebarItemTypeSchema = z.object({
|
||||||
disabled: z.boolean(),
|
disabled: z.boolean(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -346,6 +348,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
||||||
artistRadioCount: z.number(),
|
artistRadioCount: z.number(),
|
||||||
buttonSize: z.number(),
|
buttonSize: z.number(),
|
||||||
|
combinedLyricsAndVisualizer: z.boolean(),
|
||||||
disabledContextMenu: z.record(z.string(), z.boolean()),
|
disabledContextMenu: z.record(z.string(), z.boolean()),
|
||||||
externalLinks: z.boolean(),
|
externalLinks: z.boolean(),
|
||||||
followCurrentSong: z.boolean(),
|
followCurrentSong: z.boolean(),
|
||||||
@@ -375,6 +378,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
sidebarCollapsedNavigation: z.boolean(),
|
sidebarCollapsedNavigation: z.boolean(),
|
||||||
sidebarCollapseShared: z.boolean(),
|
sidebarCollapseShared: z.boolean(),
|
||||||
sidebarItems: z.array(SidebarItemTypeSchema),
|
sidebarItems: z.array(SidebarItemTypeSchema),
|
||||||
|
sidebarPanelOrder: z.array(SidebarPanelTypeSchema),
|
||||||
sidebarPlaylistList: z.boolean(),
|
sidebarPlaylistList: z.boolean(),
|
||||||
sideQueueType: SideQueueTypeSchema,
|
sideQueueType: SideQueueTypeSchema,
|
||||||
skipButtons: SkipButtonsSchema,
|
skipButtons: SkipButtonsSchema,
|
||||||
@@ -843,6 +847,7 @@ const initialState: SettingsState = {
|
|||||||
artistItems,
|
artistItems,
|
||||||
artistRadioCount: 20,
|
artistRadioCount: 20,
|
||||||
buttonSize: 15,
|
buttonSize: 15,
|
||||||
|
combinedLyricsAndVisualizer: false,
|
||||||
disabledContextMenu: {},
|
disabledContextMenu: {},
|
||||||
externalLinks: true,
|
externalLinks: true,
|
||||||
followCurrentSong: true,
|
followCurrentSong: true,
|
||||||
@@ -878,6 +883,7 @@ const initialState: SettingsState = {
|
|||||||
sidebarCollapsedNavigation: true,
|
sidebarCollapsedNavigation: true,
|
||||||
sidebarCollapseShared: false,
|
sidebarCollapseShared: false,
|
||||||
sidebarItems,
|
sidebarItems,
|
||||||
|
sidebarPanelOrder: ['queue', 'lyrics', 'visualizer'],
|
||||||
sidebarPlaylistList: true,
|
sidebarPlaylistList: true,
|
||||||
sideQueueType: 'sideQueue',
|
sideQueueType: 'sideQueue',
|
||||||
skipButtons: {
|
skipButtons: {
|
||||||
|
|||||||
Reference in New Issue
Block a user