support sidebar player in now playing route (#2134)

- add toggle to show/hide the queue in the sidebarplayer
This commit is contained in:
jeffvli
2026-07-23 22:16:01 -07:00
parent 04b1f6595a
commit 207df236a3
12 changed files with 273 additions and 151 deletions
+2
View File
@@ -1085,6 +1085,8 @@
"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 player sidebar", "showLyricsInSidebar": "Show lyrics in player sidebar",
"showQueueInSidebar_description": "A panel will be added to the player sidebar that displays the play queue",
"showQueueInSidebar": "Show play queue in player sidebar",
"showRatings_description": "Controls if the star ratings feature shows up in the interface", "showRatings_description": "Controls if the star ratings feature shows up in the interface",
"showRatings": "Show star ratings", "showRatings": "Show star ratings",
"blurExplicitImages": "Blur explicit images", "blurExplicitImages": "Blur explicit images",
@@ -93,6 +93,7 @@ type SettingsProperties = {
'settings.scrobble.enabled': boolean; 'settings.scrobble.enabled': boolean;
'settings.scrobble.notify': boolean; 'settings.scrobble.notify': boolean;
'settings.showLyricsInSidebar': boolean; 'settings.showLyricsInSidebar': boolean;
'settings.showQueueInSidebar': boolean;
'settings.showVisualizerInSidebar': boolean; 'settings.showVisualizerInSidebar': boolean;
'settings.sideQueueType': SideQueueType; 'settings.sideQueueType': SideQueueType;
'settings.skipButtons': boolean; 'settings.skipButtons': boolean;
@@ -184,6 +185,7 @@ const getSettingsProperties = (): SettingsProperties => {
'settings.scrobble.enabled': settings.playback.scrobble.enabled, 'settings.scrobble.enabled': settings.playback.scrobble.enabled,
'settings.scrobble.notify': ignoreWeb(settings.playback.scrobble.notify), 'settings.scrobble.notify': ignoreWeb(settings.playback.scrobble.notify),
'settings.showLyricsInSidebar': settings.general.showLyricsInSidebar, 'settings.showLyricsInSidebar': settings.general.showLyricsInSidebar,
'settings.showQueueInSidebar': settings.general.showQueueInSidebar,
'settings.showVisualizerInSidebar': settings.general.showVisualizerInSidebar, 'settings.showVisualizerInSidebar': settings.general.showVisualizerInSidebar,
'settings.sideQueueType': settings.general.sideQueueType, 'settings.sideQueueType': settings.general.sideQueueType,
// 'settings.skipBackwardSeconds': settings.general.skipButtons.skipBackwardSeconds, // 'settings.skipBackwardSeconds': settings.general.skipButtons.skipBackwardSeconds,
@@ -1,16 +0,0 @@
import { useTranslation } from 'react-i18next';
import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
export const NowPlayingHeader = () => {
const { t } = useTranslation();
return (
<PageHeader>
<LibraryHeaderBar ignoreMaxWidth>
<LibraryHeaderBar.Title>{t('page.sidebar.nowPlaying')}</LibraryHeaderBar.Title>
</LibraryHeaderBar>
</PageHeader>
);
};
@@ -8,6 +8,24 @@
overflow: hidden; overflow: hidden;
} }
.panels-container {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.queue-only {
flex: 1;
min-height: 0;
}
.queue-only-content {
flex: 1;
min-height: 0;
}
.lyrics-section { .lyrics-section {
position: relative; position: relative;
display: flex; display: flex;
@@ -40,6 +58,10 @@
pointer-events: none; pointer-events: none;
} }
.visualizer-overlay-dimmed {
opacity: 0.2;
}
.visualizer-section { .visualizer-section {
position: relative; position: relative;
display: flex; display: flex;
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// import { Group, Panel, Separator, useDefaultLayout } from 'react-resizable-panels'; import { useLocation } from 'react-router';
import { Pane, SplitPane, usePersistence } from 'react-split-pane'; import { Pane, SplitPane, usePersistence } from 'react-split-pane';
import styles from './sidebar-play-queue.module.css'; import styles from './sidebar-play-queue.module.css';
@@ -12,6 +13,7 @@ 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 { AppRoute } from '/@/renderer/router/routes';
import { import {
useCombinedLyricsAndVisualizer, useCombinedLyricsAndVisualizer,
useFullScreenPlayerStore, useFullScreenPlayerStore,
@@ -20,6 +22,7 @@ import {
useSettingsStore, useSettingsStore,
useSettingsStoreActions, useSettingsStoreActions,
useShowLyricsInSidebar, useShowLyricsInSidebar,
useShowQueueInSidebar,
useShowVisualizerInSidebar, useShowVisualizerInSidebar,
useSidebarPanelOrder, useSidebarPanelOrder,
useWindowSettings, useWindowSettings,
@@ -46,6 +49,7 @@ 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 location = useLocation();
const { const {
expanded: isFullScreenPlayerExpanded, expanded: isFullScreenPlayerExpanded,
visualizerExpanded: isFullScreenVisualizerExpanded, visualizerExpanded: isFullScreenVisualizerExpanded,
@@ -53,12 +57,14 @@ export const SidebarPlayQueue = () => {
const [shouldRender, setShouldRender] = useState(!isFullScreenPlayerExpanded); const [shouldRender, setShouldRender] = useState(!isFullScreenPlayerExpanded);
const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer(); const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer();
const showLyricsInSidebar = useShowLyricsInSidebar(); const showLyricsInSidebar = useShowLyricsInSidebar();
const showQueueInSidebar = useShowQueueInSidebar();
const showVisualizerInSidebar = useShowVisualizerInSidebar(); const showVisualizerInSidebar = useShowVisualizerInSidebar();
const sidebarPanelOrder = useSidebarPanelOrder(); const sidebarPanelOrder = useSidebarPanelOrder();
const { webAudio } = usePlaybackSettings(); const { webAudio } = usePlaybackSettings();
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const showVisualizer = showVisualizerInSidebar && webAudio; const showVisualizer = showVisualizerInSidebar && webAudio;
const showPanel = showLyricsInSidebar || showVisualizer; const showPanel = showLyricsInSidebar || showVisualizer;
const showQueue = showQueueInSidebar && location.pathname !== AppRoute.NOW_PLAYING;
const shouldAddTopMargin = isElectron() && windowBarStyle === Platform.WEB; const shouldAddTopMargin = isElectron() && windowBarStyle === Platform.WEB;
@@ -90,7 +96,7 @@ export const SidebarPlayQueue = () => {
if (combinedLyricsAndVisualizer) { if (combinedLyricsAndVisualizer) {
// When combined, use the order from settings but filter to only show queue and lyrics (combined) // When combined, use the order from settings but filter to only show queue and lyrics (combined)
const visiblePanels = sidebarPanelOrder.filter((panel) => { const visiblePanels = sidebarPanelOrder.filter((panel) => {
if (panel === 'queue') return true; if (panel === 'queue') return showQueue;
if (panel === 'lyrics') return showLyricsInSidebar || showVisualizer; if (panel === 'lyrics') return showLyricsInSidebar || showVisualizer;
return false; return false;
}); });
@@ -98,14 +104,20 @@ export const SidebarPlayQueue = () => {
} }
const visiblePanels = sidebarPanelOrder.filter((panel) => { const visiblePanels = sidebarPanelOrder.filter((panel) => {
if (panel === 'queue') return true; if (panel === 'queue') return showQueue;
if (panel === 'lyrics') return showLyricsInSidebar; if (panel === 'lyrics') return showLyricsInSidebar;
if (panel === 'visualizer') return showVisualizer; if (panel === 'visualizer') return showVisualizer;
return false; return false;
}); });
return visiblePanels; return visiblePanels;
}, [combinedLyricsAndVisualizer, showLyricsInSidebar, showVisualizer, sidebarPanelOrder]); }, [
combinedLyricsAndVisualizer,
showLyricsInSidebar,
showQueue,
showVisualizer,
sidebarPanelOrder,
]);
const renderPanel = (panelType: SidebarPanelType) => { const renderPanel = (panelType: SidebarPanelType) => {
if (panelType === 'queue') { if (panelType === 'queue') {
@@ -150,6 +162,25 @@ export const SidebarPlayQueue = () => {
return undefined; return undefined;
} }
const hasQueue = orderedPanels.includes('queue');
// Without a queue to absorb remaining space, fill the sidebar height
if (!hasQueue) {
if (orderedPanels.length === 1 || index === orderedPanels.length - 1) {
return undefined;
}
if (
defaultLayout &&
Array.isArray(defaultLayout) &&
defaultLayout[index] !== undefined
) {
return defaultLayout[index];
}
return 100;
}
// If defaultLayout exists and has saved sizes, use them // If defaultLayout exists and has saved sizes, use them
if ( if (
defaultLayout && defaultLayout &&
@@ -195,49 +226,42 @@ export const SidebarPlayQueue = () => {
<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%">
{shouldAddTopMargin && <div className={styles.draggableRegion} />} {shouldAddTopMargin && <div className={styles.draggableRegion} />}
{showPanel ? ( {showPanel ? (
<SplitPane orderedPanels.length === 1 ? (
direction="vertical" <div className={styles.panelsContainer}>{renderPanel(orderedPanels[0])}</div>
dividerClassName={styles.resizeHandle} ) : (
onResize={onLayoutChange} <SplitPane
style={{ className={styles.panelsContainer}
display: 'flex', direction="vertical"
flex: 1, dividerClassName={styles.resizeHandle}
flexDirection: 'column', onResize={onLayoutChange}
minHeight: 0, >
overflow: 'hidden', {orderedPanels.map((panel, index) => (
}} <Pane key={panel} size={getPanelSize(panel, index)}>
> {renderPanel(panel)}
{orderedPanels.map((panel, index) => ( </Pane>
<Pane key={panel} size={getPanelSize(panel, index)}> ))}
{renderPanel(panel)} </SplitPane>
</Pane> )
))}
</SplitPane>
) : ( ) : (
<Stack showQueue && (
gap={0} <Stack className={styles.queueOnly} gap={0} w="100%">
style={{ <PlayQueueListControls
flex: 1, handleSearch={setSearch}
minHeight: 0, searchTerm={search}
}} tableRef={tableRef}
w="100%" type={ItemListKey.SIDE_QUEUE}
> />
<PlayQueueListControls <Flex className={styles.queueOnlyContent} direction="column">
handleSearch={setSearch} <div className={styles.playQueueSection}>
searchTerm={search} <PlayQueue
tableRef={tableRef} listKey={ItemListKey.SIDE_QUEUE}
type={ItemListKey.SIDE_QUEUE} ref={tableRef}
/> searchTerm={search}
<Flex direction="column" style={{ flex: 1, minHeight: 0 }}> />
<div className={styles.playQueueSection}> </div>
<PlayQueue </Flex>
listKey={ItemListKey.SIDE_QUEUE} </Stack>
ref={tableRef} )
searchTerm={search}
/>
</div>
</Flex>
</Stack>
)} )}
</Stack> </Stack>
); );
@@ -423,10 +447,9 @@ const CombinedLyricsAndVisualizerPanel = () => {
{showLyricsInSidebar && <Lyrics fadeOutNoLyricsMessage={true} settingsKey="sidebar" />} {showLyricsInSidebar && <Lyrics fadeOutNoLyricsMessage={true} settingsKey="sidebar" />}
{showVisualizer && ( {showVisualizer && (
<div <div
className={styles.visualizerOverlay} className={clsx(styles.visualizerOverlay, {
style={{ [styles.visualizerOverlayDimmed]: hasLyrics && showLyricsInSidebar,
opacity: hasLyrics && showLyricsInSidebar ? 0.2 : 1, })}
}}
> >
<Suspense fallback={<></>}> <Suspense fallback={<></>}>
{visualizerType === 'butterchurn' ? ( {visualizerType === 'butterchurn' ? (
@@ -1,36 +1,18 @@
import { useEffect, useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { ItemListHandle } from '/@/renderer/components/item-list/types'; import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/now-playing-header';
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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary'; import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { useAppStore, useAppStoreActions } from '/@/renderer/store';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
const NowPlayingRoute = () => { const NowPlayingRoute = () => {
const [search, setSearch] = useState<string | undefined>(undefined); const [search, setSearch] = useState<string | undefined>(undefined);
const { setSideBar } = useAppStoreActions();
const tableRef = useRef<ItemListHandle | null>(null); const tableRef = useRef<ItemListHandle | null>(null);
useEffect(() => {
const wasExpanded = useAppStore.getState().sidebar.rightExpanded;
// On page enter, set rightExpanded to false
setSideBar({ rightExpanded: false });
return () => {
if (wasExpanded) {
// On page exit, set rightExpanded to true if it was previously expanded
setSideBar({ rightExpanded: true });
}
};
}, [setSideBar]);
return ( return (
<AnimatedPage> <AnimatedPage>
<NowPlayingHeader />
<PlayQueueListControls <PlayQueueListControls
handleSearch={setSearch} handleSearch={setSearch}
searchTerm={search} searchTerm={search}
@@ -6,7 +6,10 @@ import {
getDefaultAudioDevice, getDefaultAudioDevice,
useAudioDevices, useAudioDevices,
} from '/@/renderer/features/settings/components/playback/audio-settings'; } from '/@/renderer/features/settings/components/playback/audio-settings';
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu'; import {
ListConfigBooleanControl,
ListConfigTable,
} from '/@/renderer/features/shared/components/list-config-menu';
import { import {
usePlaybackType, usePlaybackType,
usePlayerActions, usePlayerActions,
@@ -22,16 +25,18 @@ import {
useSettingsStore, useSettingsStore,
useSettingsStoreActions, useSettingsStoreActions,
useShowLyricsInSidebar, useShowLyricsInSidebar,
useShowQueueInSidebar,
useShowVisualizerInSidebar, useShowVisualizerInSidebar,
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Paper } from '/@/shared/components/paper/paper';
import { Popover } from '/@/shared/components/popover/popover'; import { Popover } from '/@/shared/components/popover/popover';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider'; import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types'; import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
@@ -41,8 +46,10 @@ export const PlayerConfig = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const preservePitch = useSettingsStore((state) => state.playback.preservePitch); const preservePitch = useSettingsStore((state) => state.playback.preservePitch);
const showLyricsInSidebar = useShowLyricsInSidebar(); const showLyricsInSidebar = useShowLyricsInSidebar();
const showQueueInSidebar = useShowQueueInSidebar();
const showVisualizerInSidebar = useShowVisualizerInSidebar(); const showVisualizerInSidebar = useShowVisualizerInSidebar();
const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer(); const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer();
const { transitionType } = usePlayerProperties();
const playbackSettings = usePlaybackSettings(); const playbackSettings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
@@ -56,8 +63,8 @@ export const PlayerConfig = () => {
[playbackSettings, setSettings], [playbackSettings, setSettings],
); );
const options = useMemo(() => { const audioOptions = useMemo(
const allOptions = [ () => [
{ {
component: <AudioPlayerTypeConfig />, component: <AudioPlayerTypeConfig />,
id: 'audioPlayerType', id: 'audioPlayerType',
@@ -68,12 +75,12 @@ export const PlayerConfig = () => {
id: 'audioDevice', id: 'audioDevice',
label: t('setting.audioDevice'), label: t('setting.audioDevice'),
}, },
{ ],
component: null, [t],
id: 'divider-1', );
isDivider: true,
label: '', const transitionOptions = useMemo(
}, () => [
{ {
component: <TransitionTypeConfig />, component: <TransitionTypeConfig />,
id: 'transitionType', id: 'transitionType',
@@ -82,19 +89,21 @@ export const PlayerConfig = () => {
{ {
component: <CrossfadeStyleConfig />, component: <CrossfadeStyleConfig />,
id: 'crossfadeStyle', id: 'crossfadeStyle',
isHidden: transitionType !== PlayerStyle.CROSSFADE,
label: t('setting.crossfadeStyle'), label: t('setting.crossfadeStyle'),
}, },
{ {
component: <CrossfadeDurationConfig />, component: <CrossfadeDurationConfig />,
id: 'crossfadeDuration', id: 'crossfadeDuration',
isHidden: transitionType !== PlayerStyle.CROSSFADE,
label: t('setting.crossfadeDuration'), label: t('setting.crossfadeDuration'),
}, },
{ ],
component: null, [t, transitionType],
id: 'divider-2', );
isDivider: true,
label: '', const playbackOptions = useMemo(
}, () => [
{ {
component: <PlaybackSpeedSlider />, component: <PlaybackSpeedSlider />,
id: 'playbackSpeed', id: 'playbackSpeed',
@@ -107,31 +116,44 @@ export const PlayerConfig = () => {
}, },
{ {
component: ( component: (
<Switch <ListConfigBooleanControl onChange={setPreservePitch} value={preservePitch} />
defaultChecked={preservePitch}
onChange={(e) => setPreservePitch(e.currentTarget.checked)}
/>
), ),
id: 'preservePitch', id: 'preservePitch',
label: t('setting.preservePitch'), label: t('setting.preservePitch'),
}, },
{ ],
component: null, [preservePitch, setPreservePitch, t],
id: 'divider-3', );
isDivider: true,
label: '', const sidebarOptions = useMemo(
}, () => [
{ {
component: ( component: (
<Switch <ListConfigBooleanControl
defaultChecked={showLyricsInSidebar} onChange={(value) => {
onChange={(e) => {
setSettings({ setSettings({
general: { general: {
showLyricsInSidebar: e.currentTarget.checked, showQueueInSidebar: value,
}, },
}); });
}} }}
value={showQueueInSidebar}
/>
),
id: 'showQueueInSidebar',
label: t('setting.showQueueInSidebar'),
},
{
component: (
<ListConfigBooleanControl
onChange={(value) => {
setSettings({
general: {
showLyricsInSidebar: value,
},
});
}}
value={showLyricsInSidebar}
/> />
), ),
id: 'showLyricsInSidebar', id: 'showLyricsInSidebar',
@@ -139,15 +161,15 @@ export const PlayerConfig = () => {
}, },
{ {
component: ( component: (
<Switch <ListConfigBooleanControl
defaultChecked={showVisualizerInSidebar} onChange={(value) => {
onChange={(e) => {
setSettings({ setSettings({
general: { general: {
showVisualizerInSidebar: e.currentTarget.checked, showVisualizerInSidebar: value,
}, },
}); });
}} }}
value={showVisualizerInSidebar}
/> />
), ),
id: 'showVisualizerInSidebar', id: 'showVisualizerInSidebar',
@@ -155,35 +177,33 @@ export const PlayerConfig = () => {
}, },
{ {
component: ( component: (
<Switch <ListConfigBooleanControl
defaultChecked={combinedLyricsAndVisualizer} onChange={(value) => {
onChange={(e) => {
setSettings({ setSettings({
general: { general: {
combinedLyricsAndVisualizer: e.currentTarget.checked, combinedLyricsAndVisualizer: value,
}, },
}); });
}} }}
value={combinedLyricsAndVisualizer}
/> />
), ),
id: 'combinedLyricsAndVisualizer', id: 'combinedLyricsAndVisualizer',
label: t('setting.combinedLyricsAndVisualizer'), label: t('setting.combinedLyricsAndVisualizer'),
}, },
]; ],
[
return allOptions; combinedLyricsAndVisualizer,
}, [ setSettings,
t, showLyricsInSidebar,
preservePitch, showQueueInSidebar,
setSettings, showVisualizerInSidebar,
setPreservePitch, t,
showLyricsInSidebar, ],
showVisualizerInSidebar, );
combinedLyricsAndVisualizer,
]);
return ( return (
<Popover position="top" width={500}> <Popover position="top" withArrow>
<Popover.Target> <Popover.Target>
<ActionIcon <ActionIcon
icon="mediaSettings" icon="mediaSettings"
@@ -199,8 +219,21 @@ export const PlayerConfig = () => {
variant="subtle" variant="subtle"
/> />
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown maw={500} miw={320} onClick={(e) => e.stopPropagation()} p="sm">
<ListConfigTable options={options} /> <Stack gap="sm">
<Paper p="md" radius="md">
<ListConfigTable options={audioOptions} />
</Paper>
<Paper p="md" radius="md">
<ListConfigTable options={transitionOptions} />
</Paper>
<Paper p="md" radius="md">
<ListConfigTable options={playbackOptions} />
</Paper>
<Paper p="md" radius="md">
<ListConfigTable options={sidebarOptions} />
</Paper>
</Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
); );
@@ -234,6 +267,7 @@ const AudioPlayerTypeConfig = () => {
value: e, value: e,
}); });
}} }}
variant="filled"
width="100%" width="100%"
/> />
); );
@@ -268,6 +302,7 @@ const AudioDeviceConfig = () => {
}); });
}} }}
value={audioDeviceId ?? getDefaultAudioDevice(audioDevices, playbackType)} value={audioDeviceId ?? getDefaultAudioDevice(audioDevices, playbackType)}
variant="filled"
width="100%" width="100%"
/> />
); );
@@ -331,6 +366,7 @@ const CrossfadeStyleConfig = () => {
setCrossfadeStyle(e as CrossfadeStyle); setCrossfadeStyle(e as CrossfadeStyle);
} }
}} }}
variant="filled"
width="100%" width="100%"
/> />
); );
@@ -406,7 +442,7 @@ export const PlaybackSpeedSlider = () => {
root: {}, root: {},
}} }}
value={speed} value={speed}
w="100%" w="240px"
/> />
); );
}; };
@@ -435,8 +471,9 @@ export const PitchControls = () => {
<Button <Button
aria-label="-1 semitone" aria-label="-1 semitone"
fullWidth fullWidth
fw={400}
onClick={() => adjustMusicalSpeed(-1)} onClick={() => adjustMusicalSpeed(-1)}
size="compact-xs" size="compact-sm"
> >
-1st -1st
</Button> </Button>
@@ -444,13 +481,14 @@ export const PitchControls = () => {
<Button <Button
aria-label="-10 cents" aria-label="-10 cents"
fullWidth fullWidth
fw={400}
onClick={() => adjustMusicalSpeed(-0.1)} onClick={() => adjustMusicalSpeed(-0.1)}
size="compact-xs" size="compact-sm"
> >
-10ct -10ct
</Button> </Button>
)} )}
<Text size="xs" style={{ fontFamily: 'monospace' }} ta="center" w="60px"> <Text size="sm" style={{ fontFamily: 'monospace' }} ta="center">
{speed.toFixed(2)}x {speedToPitch(speed) > 0 && '+'} {speed.toFixed(2)}x {speedToPitch(speed) > 0 && '+'}
{speedToPitch(speed) == 0 && '±'} {speedToPitch(speed) == 0 && '±'}
{speedToPitch(speed).toFixed(2)}st {speedToPitch(speed).toFixed(2)}st
@@ -459,8 +497,9 @@ export const PitchControls = () => {
<Button <Button
aria-label="+10 cents" aria-label="+10 cents"
fullWidth fullWidth
fw={400}
onClick={() => adjustMusicalSpeed(0.1)} onClick={() => adjustMusicalSpeed(0.1)}
size="compact-xs" size="compact-sm"
> >
+10ct +10ct
</Button> </Button>
@@ -468,8 +507,9 @@ export const PitchControls = () => {
<Button <Button
aria-label="+1 semitone" aria-label="+1 semitone"
fullWidth fullWidth
fw={400}
onClick={() => adjustMusicalSpeed(1)} onClick={() => adjustMusicalSpeed(1)}
size="compact-xs" size="compact-sm"
> >
+1st +1st
</Button> </Button>
@@ -309,6 +309,25 @@ export const SidebarSettings = memo(() => {
}), }),
title: t('setting.sidebarCollapsedNavigation'), title: t('setting.sidebarCollapsedNavigation'),
}, },
{
control: (
<Switch
aria-label="Show play queue in sidebar"
defaultChecked={settings.showQueueInSidebar}
onChange={(e) => {
setSettings({
general: {
showQueueInSidebar: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.showQueueInSidebar', {
context: 'description',
}),
title: t('setting.showQueueInSidebar'),
},
{ {
control: ( control: (
<Switch <Switch
@@ -0,0 +1,17 @@
.table {
border-radius: 1rem;
}
.th {
width: 50%;
padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0;
background-color: initial;
}
.td {
padding: 0;
}
.divider-cell {
padding: var(--theme-spacing-md) 0;
}
@@ -1,6 +1,8 @@
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styles from './list-config-menu.module.css';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { GridConfig } from '/@/renderer/features/shared/components/grid-config'; import { GridConfig } from '/@/renderer/features/shared/components/grid-config';
import { SettingsButton } from '/@/renderer/features/shared/components/settings-button'; import { SettingsButton } from '/@/renderer/features/shared/components/settings-button';
@@ -228,13 +230,22 @@ const Config = ({
export const ListConfigTable = ({ export const ListConfigTable = ({
options, options,
}: { }: {
options: { component: ReactNode; id: string; isDivider?: boolean; label: ReactNode | string }[]; options: {
component: ReactNode;
id: string;
isDivider?: boolean;
isHidden?: boolean;
label: ReactNode | string;
}[];
}) => { }) => {
return ( return (
<Table <Table
className={styles.table}
classNames={{
td: styles.td,
th: styles.th,
}}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ borderRadius: '1rem' }}
styles={{ th: { backgroundColor: 'initial', padding: 'var(--theme-spacing-md) 0' } }}
variant="vertical" variant="vertical"
withColumnBorders={false} withColumnBorders={false}
withRowBorders={false} withRowBorders={false}
@@ -242,10 +253,14 @@ export const ListConfigTable = ({
> >
<Table.Tbody> <Table.Tbody>
{options.map((option) => { {options.map((option) => {
if (option.isHidden) {
return null;
}
if (option.isDivider) { if (option.isDivider) {
return ( return (
<Table.Tr key={option.id}> <Table.Tr key={option.id}>
<Table.Td colSpan={2} px={0} py="md"> <Table.Td className={styles.dividerCell} colSpan={2}>
<Divider /> <Divider />
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@@ -253,8 +268,8 @@ export const ListConfigTable = ({
} }
return ( return (
<Table.Tr key={option.id}> <Table.Tr key={option.id}>
<Table.Th w="50%">{option.label}</Table.Th> <Table.Th>{option.label}</Table.Th>
<Table.Td p={0}>{option.component}</Table.Td> <Table.Td>{option.component}</Table.Td>
</Table.Tr> </Table.Tr>
); );
})} })}
@@ -247,6 +247,11 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['general', 'showLyricsInSidebar'], path: ['general', 'showLyricsInSidebar'],
type: 'bool', type: 'bool',
}, },
{
key: 'FS_GENERAL_SHOW_QUEUE_IN_SIDEBAR',
path: ['general', 'showQueueInSidebar'],
type: 'bool',
},
{ key: 'FS_GENERAL_SHOW_RATINGS', path: ['general', 'showRatings'], type: 'bool' }, { key: 'FS_GENERAL_SHOW_RATINGS', path: ['general', 'showRatings'], type: 'bool' },
{ {
key: 'FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR', key: 'FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR',
+12 -1
View File
@@ -538,6 +538,7 @@ export const GeneralSettingsSchema = z.object({
qobuz: z.boolean(), qobuz: z.boolean(),
resume: z.boolean(), resume: z.boolean(),
showLyricsInSidebar: z.boolean(), showLyricsInSidebar: z.boolean(),
showQueueInSidebar: z.boolean(),
showRatings: z.boolean(), showRatings: z.boolean(),
showVisualizerInSidebar: z.boolean(), showVisualizerInSidebar: z.boolean(),
sidebarCollapsedNavigation: z.boolean(), sidebarCollapsedNavigation: z.boolean(),
@@ -1314,6 +1315,7 @@ const initialState: SettingsState = {
qobuz: true, qobuz: true,
resume: true, resume: true,
showLyricsInSidebar: true, showLyricsInSidebar: true,
showQueueInSidebar: true,
showRatings: true, showRatings: true,
showVisualizerInSidebar: true, showVisualizerInSidebar: true,
sidebarCollapsedNavigation: true, sidebarCollapsedNavigation: true,
@@ -2725,10 +2727,16 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
} }
} }
if (version < 33) {
if (state.general.showQueueInSidebar === undefined) {
state.general.showQueueInSidebar = true;
}
}
return persistedState; return persistedState;
}, },
name: 'store_settings', name: 'store_settings',
version: 32, version: 33,
}, },
), ),
); );
@@ -3002,6 +3010,9 @@ export const useCombinedLyricsAndVisualizer = () =>
export const useShowLyricsInSidebar = () => export const useShowLyricsInSidebar = () =>
useSettingsStore((state) => state.general.showLyricsInSidebar, shallow); useSettingsStore((state) => state.general.showLyricsInSidebar, shallow);
export const useShowQueueInSidebar = () =>
useSettingsStore((state) => state.general.showQueueInSidebar, shallow);
export const useShowVisualizerInSidebar = () => export const useShowVisualizerInSidebar = () =>
useSettingsStore((state) => state.general.showVisualizerInSidebar, shallow); useSettingsStore((state) => state.general.showVisualizerInSidebar, shallow);