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 = () => { {shouldAddTopMargin &&
} {showPanel ? ( - - {orderedPanels.map((panel, index) => ( - - {renderPanel(panel)} - - ))} - + orderedPanels.length === 1 ? ( +
{renderPanel(orderedPanels[0])}
+ ) : ( + + {orderedPanels.map((panel, index) => ( + + {renderPanel(panel)} + + ))} + + ) ) : ( - - - -
- -
-
-
+ showQueue && ( + + + +
+ +
+
+
+ ) )} ); @@ -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);