mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 05:12:57 +02:00
support sidebar player in now playing route (#2134)
- add toggle to show/hide the queue in the sidebarplayer
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user