diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 8927babb4..b251ec10a 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -1085,6 +1085,8 @@
"preferLocalLyrics": "Prefer local lyrics",
"showLyricsInSidebar_description": "A panel will be added to the attached play queue that displays the lyrics",
"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": "Show star ratings",
"blurExplicitImages": "Blur explicit images",
diff --git a/src/renderer/features/analytics/hooks/use-app-tracker.ts b/src/renderer/features/analytics/hooks/use-app-tracker.ts
index 24c8c719d..3af942893 100644
--- a/src/renderer/features/analytics/hooks/use-app-tracker.ts
+++ b/src/renderer/features/analytics/hooks/use-app-tracker.ts
@@ -93,6 +93,7 @@ type SettingsProperties = {
'settings.scrobble.enabled': boolean;
'settings.scrobble.notify': boolean;
'settings.showLyricsInSidebar': boolean;
+ 'settings.showQueueInSidebar': boolean;
'settings.showVisualizerInSidebar': boolean;
'settings.sideQueueType': SideQueueType;
'settings.skipButtons': boolean;
@@ -184,6 +185,7 @@ const getSettingsProperties = (): SettingsProperties => {
'settings.scrobble.enabled': settings.playback.scrobble.enabled,
'settings.scrobble.notify': ignoreWeb(settings.playback.scrobble.notify),
'settings.showLyricsInSidebar': settings.general.showLyricsInSidebar,
+ 'settings.showQueueInSidebar': settings.general.showQueueInSidebar,
'settings.showVisualizerInSidebar': settings.general.showVisualizerInSidebar,
'settings.sideQueueType': settings.general.sideQueueType,
// 'settings.skipBackwardSeconds': settings.general.skipButtons.skipBackwardSeconds,
diff --git a/src/renderer/features/now-playing/components/now-playing-header.tsx b/src/renderer/features/now-playing/components/now-playing-header.tsx
deleted file mode 100644
index 50b8b7a01..000000000
--- a/src/renderer/features/now-playing/components/now-playing-header.tsx
+++ /dev/null
@@ -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 (
-
-
- {t('page.sidebar.nowPlaying')}
-
-
- );
-};
diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.module.css b/src/renderer/features/now-playing/components/sidebar-play-queue.module.css
index bcf2a14c1..17d8c4b16 100644
--- a/src/renderer/features/now-playing/components/sidebar-play-queue.module.css
+++ b/src/renderer/features/now-playing/components/sidebar-play-queue.module.css
@@ -8,6 +8,24 @@
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 {
position: relative;
display: flex;
@@ -40,6 +58,10 @@
pointer-events: none;
}
+.visualizer-overlay-dimmed {
+ opacity: 0.2;
+}
+
.visualizer-section {
position: relative;
display: flex;
diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx
index ef6280dce..c8988ee3e 100644
--- a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx
+++ b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query';
+import clsx from 'clsx';
import isElectron from 'is-electron';
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
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 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 { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
+import { AppRoute } from '/@/renderer/router/routes';
import {
useCombinedLyricsAndVisualizer,
useFullScreenPlayerStore,
@@ -20,6 +22,7 @@ import {
useSettingsStore,
useSettingsStoreActions,
useShowLyricsInSidebar,
+ useShowQueueInSidebar,
useShowVisualizerInSidebar,
useSidebarPanelOrder,
useWindowSettings,
@@ -46,6 +49,7 @@ const ButterchurnVisualizer = lazy(() =>
export const SidebarPlayQueue = () => {
const tableRef = useRef(null);
const [search, setSearch] = useState(undefined);
+ const location = useLocation();
const {
expanded: isFullScreenPlayerExpanded,
visualizerExpanded: isFullScreenVisualizerExpanded,
@@ -53,12 +57,14 @@ export const SidebarPlayQueue = () => {
const [shouldRender, setShouldRender] = useState(!isFullScreenPlayerExpanded);
const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer();
const showLyricsInSidebar = useShowLyricsInSidebar();
+ const showQueueInSidebar = useShowQueueInSidebar();
const showVisualizerInSidebar = useShowVisualizerInSidebar();
const sidebarPanelOrder = useSidebarPanelOrder();
const { webAudio } = usePlaybackSettings();
const { windowBarStyle } = useWindowSettings();
const showVisualizer = showVisualizerInSidebar && webAudio;
const showPanel = showLyricsInSidebar || showVisualizer;
+ const showQueue = showQueueInSidebar && location.pathname !== AppRoute.NOW_PLAYING;
const shouldAddTopMargin = isElectron() && windowBarStyle === Platform.WEB;
@@ -90,7 +96,7 @@ export const SidebarPlayQueue = () => {
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 === 'queue') return showQueue;
if (panel === 'lyrics') return showLyricsInSidebar || showVisualizer;
return false;
});
@@ -98,14 +104,20 @@ export const SidebarPlayQueue = () => {
}
const visiblePanels = sidebarPanelOrder.filter((panel) => {
- if (panel === 'queue') return true;
+ if (panel === 'queue') return showQueue;
if (panel === 'lyrics') return showLyricsInSidebar;
if (panel === 'visualizer') return showVisualizer;
return false;
});
return visiblePanels;
- }, [combinedLyricsAndVisualizer, showLyricsInSidebar, showVisualizer, sidebarPanelOrder]);
+ }, [
+ combinedLyricsAndVisualizer,
+ showLyricsInSidebar,
+ showQueue,
+ showVisualizer,
+ sidebarPanelOrder,
+ ]);
const renderPanel = (panelType: SidebarPanelType) => {
if (panelType === 'queue') {
@@ -150,6 +162,25 @@ export const SidebarPlayQueue = () => {
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 &&
@@ -195,49 +226,42 @@ export const SidebarPlayQueue = () => {
);
@@ -423,10 +447,9 @@ const CombinedLyricsAndVisualizerPanel = () => {
{showLyricsInSidebar && }
{showVisualizer && (
>}>
{visualizerType === 'butterchurn' ? (
diff --git a/src/renderer/features/now-playing/routes/now-playing-route.tsx b/src/renderer/features/now-playing/routes/now-playing-route.tsx
index b74e79787..6ed726251 100644
--- a/src/renderer/features/now-playing/routes/now-playing-route.tsx
+++ b/src/renderer/features/now-playing/routes/now-playing-route.tsx
@@ -1,36 +1,18 @@
-import { useEffect, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
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 { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
-import { useAppStore, useAppStoreActions } from '/@/renderer/store';
import { ItemListKey } from '/@/shared/types/types';
const NowPlayingRoute = () => {
const [search, setSearch] = useState(undefined);
- const { setSideBar } = useAppStoreActions();
const tableRef = useRef(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 (
-
{
const { t } = useTranslation();
const preservePitch = useSettingsStore((state) => state.playback.preservePitch);
const showLyricsInSidebar = useShowLyricsInSidebar();
+ const showQueueInSidebar = useShowQueueInSidebar();
const showVisualizerInSidebar = useShowVisualizerInSidebar();
const combinedLyricsAndVisualizer = useCombinedLyricsAndVisualizer();
+ const { transitionType } = usePlayerProperties();
const playbackSettings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
@@ -56,8 +63,8 @@ export const PlayerConfig = () => {
[playbackSettings, setSettings],
);
- const options = useMemo(() => {
- const allOptions = [
+ const audioOptions = useMemo(
+ () => [
{
component: ,
id: 'audioPlayerType',
@@ -68,12 +75,12 @@ export const PlayerConfig = () => {
id: 'audioDevice',
label: t('setting.audioDevice'),
},
- {
- component: null,
- id: 'divider-1',
- isDivider: true,
- label: '',
- },
+ ],
+ [t],
+ );
+
+ const transitionOptions = useMemo(
+ () => [
{
component: ,
id: 'transitionType',
@@ -82,19 +89,21 @@ export const PlayerConfig = () => {
{
component: ,
id: 'crossfadeStyle',
+ isHidden: transitionType !== PlayerStyle.CROSSFADE,
label: t('setting.crossfadeStyle'),
},
{
component: ,
id: 'crossfadeDuration',
+ isHidden: transitionType !== PlayerStyle.CROSSFADE,
label: t('setting.crossfadeDuration'),
},
- {
- component: null,
- id: 'divider-2',
- isDivider: true,
- label: '',
- },
+ ],
+ [t, transitionType],
+ );
+
+ const playbackOptions = useMemo(
+ () => [
{
component: ,
id: 'playbackSpeed',
@@ -107,31 +116,44 @@ export const PlayerConfig = () => {
},
{
component: (
- setPreservePitch(e.currentTarget.checked)}
- />
+
),
id: 'preservePitch',
label: t('setting.preservePitch'),
},
- {
- component: null,
- id: 'divider-3',
- isDivider: true,
- label: '',
- },
+ ],
+ [preservePitch, setPreservePitch, t],
+ );
+
+ const sidebarOptions = useMemo(
+ () => [
{
component: (
- {
+ {
setSettings({
general: {
- showLyricsInSidebar: e.currentTarget.checked,
+ showQueueInSidebar: value,
},
});
}}
+ value={showQueueInSidebar}
+ />
+ ),
+ id: 'showQueueInSidebar',
+ label: t('setting.showQueueInSidebar'),
+ },
+ {
+ component: (
+ {
+ setSettings({
+ general: {
+ showLyricsInSidebar: value,
+ },
+ });
+ }}
+ value={showLyricsInSidebar}
/>
),
id: 'showLyricsInSidebar',
@@ -139,15 +161,15 @@ export const PlayerConfig = () => {
},
{
component: (
- {
+ {
setSettings({
general: {
- showVisualizerInSidebar: e.currentTarget.checked,
+ showVisualizerInSidebar: value,
},
});
}}
+ value={showVisualizerInSidebar}
/>
),
id: 'showVisualizerInSidebar',
@@ -155,35 +177,33 @@ export const PlayerConfig = () => {
},
{
component: (
- {
+ {
setSettings({
general: {
- combinedLyricsAndVisualizer: e.currentTarget.checked,
+ combinedLyricsAndVisualizer: value,
},
});
}}
+ value={combinedLyricsAndVisualizer}
/>
),
id: 'combinedLyricsAndVisualizer',
label: t('setting.combinedLyricsAndVisualizer'),
},
- ];
-
- return allOptions;
- }, [
- t,
- preservePitch,
- setSettings,
- setPreservePitch,
- showLyricsInSidebar,
- showVisualizerInSidebar,
- combinedLyricsAndVisualizer,
- ]);
+ ],
+ [
+ combinedLyricsAndVisualizer,
+ setSettings,
+ showLyricsInSidebar,
+ showQueueInSidebar,
+ showVisualizerInSidebar,
+ t,
+ ],
+ );
return (
-
+
{
variant="subtle"
/>
-
-
+ e.stopPropagation()} p="sm">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
@@ -234,6 +267,7 @@ const AudioPlayerTypeConfig = () => {
value: e,
});
}}
+ variant="filled"
width="100%"
/>
);
@@ -268,6 +302,7 @@ const AudioDeviceConfig = () => {
});
}}
value={audioDeviceId ?? getDefaultAudioDevice(audioDevices, playbackType)}
+ variant="filled"
width="100%"
/>
);
@@ -331,6 +366,7 @@ const CrossfadeStyleConfig = () => {
setCrossfadeStyle(e as CrossfadeStyle);
}
}}
+ variant="filled"
width="100%"
/>
);
@@ -406,7 +442,7 @@ export const PlaybackSpeedSlider = () => {
root: {},
}}
value={speed}
- w="100%"
+ w="240px"
/>
);
};
@@ -435,8 +471,9 @@ export const PitchControls = () => {
@@ -444,13 +481,14 @@ export const PitchControls = () => {
)}
-
+
{speed.toFixed(2)}x {speedToPitch(speed) > 0 && '+'}
{speedToPitch(speed) == 0 && '±'}
{speedToPitch(speed).toFixed(2)}st
@@ -459,8 +497,9 @@ export const PitchControls = () => {
@@ -468,8 +507,9 @@ export const PitchControls = () => {
diff --git a/src/renderer/features/settings/components/general/sidebar-settings.tsx b/src/renderer/features/settings/components/general/sidebar-settings.tsx
index 26bdc70c6..f065af74b 100644
--- a/src/renderer/features/settings/components/general/sidebar-settings.tsx
+++ b/src/renderer/features/settings/components/general/sidebar-settings.tsx
@@ -309,6 +309,25 @@ export const SidebarSettings = memo(() => {
}),
title: t('setting.sidebarCollapsedNavigation'),
},
+ {
+ control: (
+ {
+ setSettings({
+ general: {
+ showQueueInSidebar: e.currentTarget.checked,
+ },
+ });
+ }}
+ />
+ ),
+ description: t('setting.showQueueInSidebar', {
+ context: 'description',
+ }),
+ title: t('setting.showQueueInSidebar'),
+ },
{
control: (
{
return (
e.stopPropagation()}
- style={{ borderRadius: '1rem' }}
- styles={{ th: { backgroundColor: 'initial', padding: 'var(--theme-spacing-md) 0' } }}
variant="vertical"
withColumnBorders={false}
withRowBorders={false}
@@ -242,10 +253,14 @@ export const ListConfigTable = ({
>
{options.map((option) => {
+ if (option.isHidden) {
+ return null;
+ }
+
if (option.isDivider) {
return (
-
+
@@ -253,8 +268,8 @@ export const ListConfigTable = ({
}
return (
- {option.label}
- {option.component}
+ {option.label}
+ {option.component}
);
})}
diff --git a/src/renderer/store/env-settings-overrides.ts b/src/renderer/store/env-settings-overrides.ts
index d0b975474..929d14889 100644
--- a/src/renderer/store/env-settings-overrides.ts
+++ b/src/renderer/store/env-settings-overrides.ts
@@ -247,6 +247,11 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['general', 'showLyricsInSidebar'],
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_VISUALIZER_IN_SIDEBAR',
diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts
index ff5af1637..78475f0d4 100644
--- a/src/renderer/store/settings.store.ts
+++ b/src/renderer/store/settings.store.ts
@@ -538,6 +538,7 @@ export const GeneralSettingsSchema = z.object({
qobuz: z.boolean(),
resume: z.boolean(),
showLyricsInSidebar: z.boolean(),
+ showQueueInSidebar: z.boolean(),
showRatings: z.boolean(),
showVisualizerInSidebar: z.boolean(),
sidebarCollapsedNavigation: z.boolean(),
@@ -1314,6 +1315,7 @@ const initialState: SettingsState = {
qobuz: true,
resume: true,
showLyricsInSidebar: true,
+ showQueueInSidebar: true,
showRatings: true,
showVisualizerInSidebar: true,
sidebarCollapsedNavigation: true,
@@ -2725,10 +2727,16 @@ export const useSettingsStore = createWithEqualityFn()(
}
}
+ if (version < 33) {
+ if (state.general.showQueueInSidebar === undefined) {
+ state.general.showQueueInSidebar = true;
+ }
+ }
+
return persistedState;
},
name: 'store_settings',
- version: 32,
+ version: 33,
},
),
);
@@ -3002,6 +3010,9 @@ export const useCombinedLyricsAndVisualizer = () =>
export const useShowLyricsInSidebar = () =>
useSettingsStore((state) => state.general.showLyricsInSidebar, shallow);
+export const useShowQueueInSidebar = () =>
+ useSettingsStore((state) => state.general.showQueueInSidebar, shallow);
+
export const useShowVisualizerInSidebar = () =>
useSettingsStore((state) => state.general.showVisualizerInSidebar, shallow);