mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
support vertical play queue layout
This commit is contained in:
@@ -44,6 +44,7 @@ These variables override app settings **on first run** when no persisted setting
|
|||||||
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
|
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
|
||||||
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
|
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
|
||||||
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
|
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
|
||||||
|
| `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
|
||||||
| `general.useThemeAccentColor` | `false` | `FS_GENERAL_USE_THEME_ACCENT_COLOR` | `true` / `false` — Use theme’s accent color instead of custom. |
|
| `general.useThemeAccentColor` | `false` | `FS_GENERAL_USE_THEME_ACCENT_COLOR` | `true` / `false` — Use theme’s accent color instead of custom. |
|
||||||
| `general.useThemePrimaryShade` | `true` | `FS_GENERAL_USE_THEME_PRIMARY_SHADE` | `true` / `false` — Use theme’s primary shade. |
|
| `general.useThemePrimaryShade` | `true` | `FS_GENERAL_USE_THEME_PRIMARY_SHADE` | `true` / `false` — Use theme’s primary shade. |
|
||||||
| `general.zoomFactor` | `100` | `FS_GENERAL_ZOOM_FACTOR` | UI zoom percentage (number). |
|
| `general.zoomFactor` | `100` | `FS_GENERAL_ZOOM_FACTOR` | UI zoom percentage (number). |
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARE
|
|||||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
|
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
|
||||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
|
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
|
||||||
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
|
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
|
||||||
|
window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
|
||||||
window.FS_GENERAL_THEME = "${FS_GENERAL_THEME}";
|
window.FS_GENERAL_THEME = "${FS_GENERAL_THEME}";
|
||||||
window.FS_GENERAL_THEME_DARK = "${FS_GENERAL_THEME_DARK}";
|
window.FS_GENERAL_THEME_DARK = "${FS_GENERAL_THEME_DARK}";
|
||||||
window.FS_GENERAL_THEME_LIGHT = "${FS_GENERAL_THEME_LIGHT}";
|
window.FS_GENERAL_THEME_LIGHT = "${FS_GENERAL_THEME_LIGHT}";
|
||||||
|
|||||||
@@ -1036,6 +1036,10 @@
|
|||||||
"sidePlayQueueStyle_description": "sets the style of the side play queue",
|
"sidePlayQueueStyle_description": "sets the style of the side play queue",
|
||||||
"sidePlayQueueStyle_optionAttached": "attached",
|
"sidePlayQueueStyle_optionAttached": "attached",
|
||||||
"sidePlayQueueStyle_optionDetached": "detached",
|
"sidePlayQueueStyle_optionDetached": "detached",
|
||||||
|
"sidePlayQueueLayout": "side play queue layout",
|
||||||
|
"sidePlayQueueLayout_description": "sets the layout of the attached side play queue",
|
||||||
|
"sidePlayQueueLayout_optionHorizontal": "horizontal",
|
||||||
|
"sidePlayQueueLayout_optionVertical": "vertical",
|
||||||
"mediaSession_description": "enables Media Session integration, displaying media controls and metadata in the system volume overlay and lock screen",
|
"mediaSession_description": "enables Media Session integration, displaying media controls and metadata in the system volume overlay and lock screen",
|
||||||
"mediaSession": "enable media session",
|
"mediaSession": "enable media session",
|
||||||
"sidePlayQueueStyle": "side play queue style",
|
"sidePlayQueueStyle": "side play queue style",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import {
|
import {
|
||||||
HomeFeatureStyle,
|
HomeFeatureStyle,
|
||||||
|
SideQueueLayout,
|
||||||
SideQueueType,
|
SideQueueType,
|
||||||
useFontSettings,
|
useFontSettings,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
@@ -74,6 +75,23 @@ const SIDE_QUEUE_OPTIONS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const SIDE_QUEUE_LAYOUT_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: t('setting.sidePlayQueueLayout', {
|
||||||
|
context: 'optionHorizontal',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'horizontal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.sidePlayQueueLayout', {
|
||||||
|
context: 'optionVertical',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
value: 'vertical',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const FONT_TYPES: Font[] = [
|
const FONT_TYPES: Font[] = [
|
||||||
{
|
{
|
||||||
label: i18n.t('setting.fontType', {
|
label: i18n.t('setting.fontType', {
|
||||||
@@ -539,6 +557,29 @@ export const ApplicationSettings = memo(() => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }),
|
title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<SegmentedControl
|
||||||
|
aria-label={t('setting.sidePlayQueueLayout', { postProcess: 'sentenceCase' })}
|
||||||
|
data={SIDE_QUEUE_LAYOUT_OPTIONS}
|
||||||
|
defaultValue={settings.sideQueueLayout}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
sideQueueLayout: e as SideQueueLayout,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.sidePlayQueueLayout', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: settings.sideQueueType !== 'sideQueue',
|
||||||
|
title: t('setting.sidePlayQueueLayout', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
|
|
||||||
.handle-top {
|
.handle-top {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-right {
|
.handle-right {
|
||||||
@@ -29,6 +33,10 @@
|
|||||||
|
|
||||||
.handle-bottom {
|
.handle-bottom {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-left {
|
.handle-left {
|
||||||
|
|||||||
@@ -10,8 +10,16 @@ import { isServerLock } from '/@/renderer/features/action-required/utils/window-
|
|||||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||||
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||||
import { openReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
import { openReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||||
import { useAppStore, useAppStoreActions, useCommandPalette } from '/@/renderer/store';
|
import {
|
||||||
|
useAppStore,
|
||||||
|
useAppStoreActions,
|
||||||
|
useCommandPalette,
|
||||||
|
useGeneralSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
|
||||||
@@ -74,6 +82,8 @@ export const AppMenu = () => {
|
|||||||
const collapsed = useAppStore((state) => state.sidebar.collapsed);
|
const collapsed = useAppStore((state) => state.sidebar.collapsed);
|
||||||
const privateMode = useAppStore((state) => state.privateMode);
|
const privateMode = useAppStore((state) => state.privateMode);
|
||||||
const { setPrivateMode, setSideBar } = useAppStoreActions();
|
const { setPrivateMode, setSideBar } = useAppStoreActions();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const settings = useGeneralSettings();
|
||||||
const { open: openCommandPalette } = useCommandPalette();
|
const { open: openCommandPalette } = useCommandPalette();
|
||||||
|
|
||||||
const handleBrowserDevTools = () => {
|
const handleBrowserDevTools = () => {
|
||||||
@@ -115,6 +125,15 @@ export const AppMenu = () => {
|
|||||||
browser?.quit();
|
browser?.quit();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetSideQueueLayout = (sideQueueLayout: 'horizontal' | 'vertical') => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
sideQueueLayout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const menuConfig: MenuItem[] = [
|
const menuConfig: MenuItem[] = [
|
||||||
{
|
{
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
@@ -265,6 +284,61 @@ export const AppMenu = () => {
|
|||||||
},
|
},
|
||||||
type: 'conditional-item',
|
type: 'conditional-item',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'divider-5',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: settings.sideQueueType === 'sideQueue',
|
||||||
|
id: 'layout-toggle-group',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Group gap="xs" grow w="100%">
|
||||||
|
<ActionIcon
|
||||||
|
icon="layoutPanelRight"
|
||||||
|
iconProps={{
|
||||||
|
size: 'xl',
|
||||||
|
}}
|
||||||
|
onClick={() => handleSetSideQueueLayout('horizontal')}
|
||||||
|
tooltip={{
|
||||||
|
label: t('setting.sidePlayQueueLayout', {
|
||||||
|
context: 'optionHorizontal',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
openDelay: 0,
|
||||||
|
position: 'bottom',
|
||||||
|
}}
|
||||||
|
variant={
|
||||||
|
settings.sideQueueLayout === 'horizontal' ? 'light' : 'default'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
icon="layoutPanelBottom"
|
||||||
|
iconProps={{
|
||||||
|
size: 'xl',
|
||||||
|
}}
|
||||||
|
onClick={() => handleSetSideQueueLayout('vertical')}
|
||||||
|
tooltip={{
|
||||||
|
label: t('setting.sidePlayQueueLayout', {
|
||||||
|
context: 'optionVertical',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
openDelay: 0,
|
||||||
|
position: 'bottom',
|
||||||
|
}}
|
||||||
|
variant={
|
||||||
|
settings.sideQueueLayout === 'vertical' ? 'light' : 'default'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
id: 'layout-toggle',
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'conditional-group',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderMenuItem = (item: MenuItem): ReactNode => {
|
const renderMenuItem = (item: MenuItem): ReactNode => {
|
||||||
|
|||||||
@@ -28,6 +28,23 @@
|
|||||||
grid-template-columns: 80px 1fr var(--right-sidebar-width);
|
grid-template-columns: 80px 1fr var(--right-sidebar-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content-container.vertical-layout {
|
||||||
|
grid-template-areas:
|
||||||
|
'sidebar .'
|
||||||
|
'sidebar right-sidebar';
|
||||||
|
grid-template-rows: minmax(0, 1fr) var(--right-sidebar-height);
|
||||||
|
grid-template-columns: var(--sidebar-width) 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-container.sidebar-collapsed.vertical-layout {
|
||||||
|
grid-template-columns: 80px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-container.vertical-layout #sidebar-queue {
|
||||||
|
border-top: 1px solid alpha(var(--theme-colors-border), 0.5);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content-body {
|
.main-content-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
useAppStore,
|
useAppStore,
|
||||||
useAppStoreActions,
|
useAppStoreActions,
|
||||||
useGlobalExpanded,
|
useGlobalExpanded,
|
||||||
|
useSideQueueLayout,
|
||||||
useSideQueueType,
|
useSideQueueType,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { constrainRightSidebarWidth, constrainSidebarWidth } from '/@/renderer/utils';
|
import { constrainRightSidebarWidth, constrainSidebarWidth } from '/@/renderer/utils';
|
||||||
@@ -24,56 +25,77 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
|||||||
const MINIMUM_SIDEBAR_WIDTH = 260;
|
const MINIMUM_SIDEBAR_WIDTH = 260;
|
||||||
|
|
||||||
export const MainContent = ({ shell }: { shell?: boolean }) => {
|
export const MainContent = ({ shell }: { shell?: boolean }) => {
|
||||||
const { collapsed, leftWidth, rightExpanded, rightWidth } = useAppStore(
|
const { collapsed, leftWidth, rightExpanded, rightHeight, rightWidth } = useAppStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
collapsed: state.sidebar.collapsed,
|
collapsed: state.sidebar.collapsed,
|
||||||
leftWidth: state.sidebar.leftWidth,
|
leftWidth: state.sidebar.leftWidth,
|
||||||
rightExpanded: state.sidebar.rightExpanded,
|
rightExpanded: state.sidebar.rightExpanded,
|
||||||
|
rightHeight: state.sidebar.rightHeight,
|
||||||
rightWidth: state.sidebar.rightWidth,
|
rightWidth: state.sidebar.rightWidth,
|
||||||
}),
|
}),
|
||||||
shallow,
|
shallow,
|
||||||
);
|
);
|
||||||
const { setSideBar } = useAppStoreActions();
|
const { setSideBar } = useAppStoreActions();
|
||||||
const sideQueueType = useSideQueueType();
|
const sideQueueType = useSideQueueType();
|
||||||
|
const sideQueueLayout = useSideQueueLayout();
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const [isResizingRight, setIsResizingRight] = useState(false);
|
const [isResizingRight, setIsResizingRight] = useState(false);
|
||||||
|
|
||||||
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
|
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mainContentRef = useRef<HTMLDivElement | null>(null);
|
const mainContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const initialRightWidthRef = useRef<string>(rightWidth);
|
const initialRightWidthRef = useRef<string>(rightWidth);
|
||||||
|
const initialRightHeightRef = useRef<string>(rightHeight);
|
||||||
const initialMouseXRef = useRef<number>(0);
|
const initialMouseXRef = useRef<number>(0);
|
||||||
|
const initialMouseYRef = useRef<number>(0);
|
||||||
const wasCollapsedDuringDragRef = useRef<boolean>(false);
|
const wasCollapsedDuringDragRef = useRef<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mainContentRef.current && !isResizing && !isResizingRight) {
|
if (mainContentRef.current && !isResizing && !isResizingRight) {
|
||||||
mainContentRef.current.style.setProperty('--sidebar-width', leftWidth);
|
mainContentRef.current.style.setProperty('--sidebar-width', leftWidth);
|
||||||
mainContentRef.current.style.setProperty('--right-sidebar-width', rightWidth);
|
mainContentRef.current.style.setProperty('--right-sidebar-width', rightWidth);
|
||||||
|
mainContentRef.current.style.setProperty('--right-sidebar-height', rightHeight);
|
||||||
initialRightWidthRef.current = rightWidth;
|
initialRightWidthRef.current = rightWidth;
|
||||||
|
initialRightHeightRef.current = rightHeight;
|
||||||
}
|
}
|
||||||
}, [leftWidth, rightWidth, isResizing, isResizingRight]);
|
}, [leftWidth, rightWidth, rightHeight, isResizing, isResizingRight]);
|
||||||
|
|
||||||
const startResizing = useCallback(
|
const startResizing = useCallback(
|
||||||
(position: 'left' | 'right', mouseEvent?: MouseEvent) => {
|
(position: 'left' | 'right' | 'top', mouseEvent?: MouseEvent) => {
|
||||||
if (position === 'left') {
|
if (position === 'left') {
|
||||||
setIsResizing(true);
|
setIsResizing(true);
|
||||||
wasCollapsedDuringDragRef.current = false;
|
wasCollapsedDuringDragRef.current = false;
|
||||||
} else {
|
} else {
|
||||||
setIsResizingRight(true);
|
setIsResizingRight(true);
|
||||||
if (mainContentRef.current && rightSidebarRef.current && mouseEvent) {
|
if (mainContentRef.current && rightSidebarRef.current && mouseEvent) {
|
||||||
const currentWidth =
|
if (position === 'top') {
|
||||||
mainContentRef.current.style.getPropertyValue('--right-sidebar-width');
|
const currentHeight =
|
||||||
if (currentWidth) {
|
mainContentRef.current.style.getPropertyValue('--right-sidebar-height');
|
||||||
initialRightWidthRef.current = currentWidth;
|
if (currentHeight) {
|
||||||
|
initialRightHeightRef.current = currentHeight;
|
||||||
|
} else {
|
||||||
|
initialRightHeightRef.current = rightHeight;
|
||||||
|
}
|
||||||
|
initialMouseYRef.current = mouseEvent.clientY;
|
||||||
|
} else {
|
||||||
|
const currentWidth =
|
||||||
|
mainContentRef.current.style.getPropertyValue('--right-sidebar-width');
|
||||||
|
if (currentWidth) {
|
||||||
|
initialRightWidthRef.current = currentWidth;
|
||||||
|
} else {
|
||||||
|
initialRightWidthRef.current = rightWidth;
|
||||||
|
}
|
||||||
|
initialMouseXRef.current = mouseEvent.clientX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (position === 'top') {
|
||||||
|
initialRightHeightRef.current = rightHeight;
|
||||||
} else {
|
} else {
|
||||||
initialRightWidthRef.current = rightWidth;
|
initialRightWidthRef.current = rightWidth;
|
||||||
}
|
}
|
||||||
initialMouseXRef.current = mouseEvent.clientX;
|
|
||||||
} else {
|
|
||||||
initialRightWidthRef.current = rightWidth;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rightWidth],
|
[rightHeight, rightWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
const stopResizing = useCallback(() => {
|
const stopResizing = useCallback(() => {
|
||||||
@@ -87,14 +109,22 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
|||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
wasCollapsedDuringDragRef.current = false;
|
wasCollapsedDuringDragRef.current = false;
|
||||||
} else if (isResizingRight && mainContentRef.current) {
|
} else if (isResizingRight && mainContentRef.current) {
|
||||||
const finalWidth =
|
if (sideQueueLayout === 'vertical') {
|
||||||
mainContentRef.current.style.getPropertyValue('--right-sidebar-width');
|
const finalHeight =
|
||||||
if (finalWidth) {
|
mainContentRef.current.style.getPropertyValue('--right-sidebar-height');
|
||||||
setSideBar({ rightWidth: finalWidth });
|
if (finalHeight) {
|
||||||
|
setSideBar({ rightHeight: finalHeight });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const finalWidth =
|
||||||
|
mainContentRef.current.style.getPropertyValue('--right-sidebar-width');
|
||||||
|
if (finalWidth) {
|
||||||
|
setSideBar({ rightWidth: finalWidth });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsResizingRight(false);
|
setIsResizingRight(false);
|
||||||
}
|
}
|
||||||
}, [isResizing, isResizingRight, setSideBar]);
|
}, [isResizing, isResizingRight, setSideBar, sideQueueLayout]);
|
||||||
|
|
||||||
const resize = useCallback(
|
const resize = useCallback(
|
||||||
(mouseMoveEvent: any) => {
|
(mouseMoveEvent: any) => {
|
||||||
@@ -118,15 +148,30 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
|||||||
mainContentRef.current.style.setProperty('--sidebar-width', constrainedWidth);
|
mainContentRef.current.style.setProperty('--sidebar-width', constrainedWidth);
|
||||||
}
|
}
|
||||||
} else if (isResizingRight) {
|
} else if (isResizingRight) {
|
||||||
const initialWidth = Number(initialRightWidthRef.current.split('px')[0]);
|
if (sideQueueLayout === 'vertical') {
|
||||||
const initialMouseX = initialMouseXRef.current;
|
const initialHeight = Number(initialRightHeightRef.current.split('px')[0]);
|
||||||
const deltaX = mouseMoveEvent.clientX - initialMouseX;
|
const initialMouseY = initialMouseYRef.current;
|
||||||
const newWidth = initialWidth - deltaX;
|
const deltaY = mouseMoveEvent.clientY - initialMouseY;
|
||||||
const width = `${constrainRightSidebarWidth(newWidth)}px`;
|
const containerHeight = mainContentRef.current.clientHeight;
|
||||||
mainContentRef.current.style.setProperty('--right-sidebar-width', width);
|
const minHeight = 220;
|
||||||
|
const maxHeight = Math.max(minHeight, containerHeight - 200);
|
||||||
|
const newHeight = initialHeight - deltaY;
|
||||||
|
const clampedHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
|
||||||
|
mainContentRef.current.style.setProperty(
|
||||||
|
'--right-sidebar-height',
|
||||||
|
`${clampedHeight}px`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const initialWidth = Number(initialRightWidthRef.current.split('px')[0]);
|
||||||
|
const initialMouseX = initialMouseXRef.current;
|
||||||
|
const deltaX = mouseMoveEvent.clientX - initialMouseX;
|
||||||
|
const newWidth = initialWidth - deltaX;
|
||||||
|
const width = `${constrainRightSidebarWidth(newWidth)}px`;
|
||||||
|
mainContentRef.current.style.setProperty('--right-sidebar-width', width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isResizing, isResizingRight, setSideBar],
|
[isResizing, isResizingRight, setSideBar, sideQueueLayout],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -145,6 +190,10 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
|||||||
[styles.shell]: shell,
|
[styles.shell]: shell,
|
||||||
[styles.sidebarCollapsed]: collapsed,
|
[styles.sidebarCollapsed]: collapsed,
|
||||||
[styles.sidebarExpanded]: !collapsed,
|
[styles.sidebarExpanded]: !collapsed,
|
||||||
|
[styles.verticalLayout]:
|
||||||
|
rightExpanded &&
|
||||||
|
sideQueueType === 'sideQueue' &&
|
||||||
|
sideQueueLayout === 'vertical',
|
||||||
})}
|
})}
|
||||||
id="main-content"
|
id="main-content"
|
||||||
ref={mainContentRef}
|
ref={mainContentRef}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-sidebar-container.vertical-layout {
|
||||||
|
border-top: 1px solid alpha(var(--theme-colors-border), 0.5);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.queue-drawer {
|
.queue-drawer {
|
||||||
border-radius: var(--theme-radius-lg);
|
border-radius: var(--theme-radius-lg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import { forwardRef, Ref } from 'react';
|
import { forwardRef, Ref } from 'react';
|
||||||
|
|
||||||
import styles from './right-sidebar.module.css';
|
import styles from './right-sidebar.module.css';
|
||||||
|
|
||||||
import { SidebarPlayQueue } from '/@/renderer/features/now-playing/components/sidebar-play-queue';
|
import { SidebarPlayQueue } from '/@/renderer/features/now-playing/components/sidebar-play-queue';
|
||||||
import { ResizeHandle } from '/@/renderer/features/shared/components/resize-handle';
|
import { ResizeHandle } from '/@/renderer/features/shared/components/resize-handle';
|
||||||
import { useAppStore, useSideQueueType } from '/@/renderer/store';
|
import { useAppStore, useSideQueueLayout, useSideQueueType } from '/@/renderer/store';
|
||||||
|
|
||||||
// const queueDrawerVariants: Variants = {
|
// const queueDrawerVariants: Variants = {
|
||||||
// closed: (windowBarStyle) => ({
|
// closed: (windowBarStyle) => ({
|
||||||
@@ -46,7 +47,7 @@ import { useAppStore, useSideQueueType } from '/@/renderer/store';
|
|||||||
|
|
||||||
interface RightSidebarProps {
|
interface RightSidebarProps {
|
||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
startResizing: (direction: 'left' | 'right', mouseEvent?: MouseEvent) => void;
|
startResizing: (direction: 'left' | 'right' | 'top', mouseEvent?: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RightSidebar = forwardRef(
|
export const RightSidebar = forwardRef(
|
||||||
@@ -56,12 +57,16 @@ export const RightSidebar = forwardRef(
|
|||||||
) => {
|
) => {
|
||||||
const rightExpanded = useAppStore((state) => state.sidebar.rightExpanded);
|
const rightExpanded = useAppStore((state) => state.sidebar.rightExpanded);
|
||||||
const sideQueueType = useSideQueueType();
|
const sideQueueType = useSideQueueType();
|
||||||
|
const sideQueueLayout = useSideQueueLayout();
|
||||||
|
const isVerticalLayout = sideQueueLayout === 'vertical';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rightExpanded && sideQueueType === 'sideQueue' && (
|
{rightExpanded && sideQueueType === 'sideQueue' && (
|
||||||
<aside
|
<aside
|
||||||
className={styles.rightSidebarContainer}
|
className={clsx(styles.rightSidebarContainer, {
|
||||||
|
[styles.verticalLayout]: isVerticalLayout,
|
||||||
|
})}
|
||||||
id="sidebar-queue"
|
id="sidebar-queue"
|
||||||
key="queue-sidebar"
|
key="queue-sidebar"
|
||||||
>
|
>
|
||||||
@@ -69,9 +74,9 @@ export const RightSidebar = forwardRef(
|
|||||||
isResizing={isResizingRight}
|
isResizing={isResizingRight}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
startResizing('right', e.nativeEvent);
|
startResizing(isVerticalLayout ? 'top' : 'right', e.nativeEvent);
|
||||||
}}
|
}}
|
||||||
placement="left"
|
placement={isVerticalLayout ? 'top' : 'left'}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
<SidebarPlayQueue />
|
<SidebarPlayQueue />
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type SidebarProps = {
|
|||||||
image: boolean;
|
image: boolean;
|
||||||
leftWidth: string;
|
leftWidth: string;
|
||||||
rightExpanded: boolean;
|
rightExpanded: boolean;
|
||||||
|
rightHeight: string;
|
||||||
rightWidth: string;
|
rightWidth: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,6 +223,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
image: false,
|
image: false,
|
||||||
leftWidth: '400px',
|
leftWidth: '400px',
|
||||||
rightExpanded: false,
|
rightExpanded: false,
|
||||||
|
rightHeight: '320px',
|
||||||
rightWidth: '600px',
|
rightWidth: '600px',
|
||||||
},
|
},
|
||||||
titlebar: {
|
titlebar: {
|
||||||
@@ -240,7 +242,12 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
return {} as AppState;
|
return {} as AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
return persistedState;
|
const state = persistedState as AppState;
|
||||||
|
if (version <= 4 && !state.sidebar.rightHeight) {
|
||||||
|
state.sidebar.rightHeight = '320px';
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
},
|
},
|
||||||
name: 'store_app',
|
name: 'store_app',
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
@@ -248,7 +255,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
const { globalExpanded: _, ...rest } = state;
|
const { globalExpanded: _, ...rest } = state;
|
||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
version: 4,
|
version: 5,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const LYRICS_ALIGNMENTS = new Set(['center', 'left', 'right']);
|
|||||||
const FONT_TYPES = new Set(['builtIn', 'custom', 'system']);
|
const FONT_TYPES = new Set(['builtIn', 'custom', 'system']);
|
||||||
const HOME_FEATURE_STYLES = new Set(['multiple', 'single']);
|
const HOME_FEATURE_STYLES = new Set(['multiple', 'single']);
|
||||||
const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
|
const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
|
||||||
|
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
|
||||||
|
|
||||||
export type EnvSettingsOverrides = DeepPartial<
|
export type EnvSettingsOverrides = DeepPartial<
|
||||||
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
|
||||||
@@ -200,6 +201,12 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
|
|||||||
path: ['general', 'sideQueueType'],
|
path: ['general', 'sideQueueType'],
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
enumSet: SIDE_QUEUE_LAYOUTS,
|
||||||
|
key: 'FS_GENERAL_SIDE_QUEUE_LAYOUT',
|
||||||
|
path: ['general', 'sideQueueLayout'],
|
||||||
|
type: 'enum',
|
||||||
|
},
|
||||||
{ key: 'FS_GENERAL_RESUME', path: ['general', 'resume'], type: 'bool' },
|
{ key: 'FS_GENERAL_RESUME', path: ['general', 'resume'], type: 'bool' },
|
||||||
{
|
{
|
||||||
key: 'FS_GENERAL_USE_THEME_ACCENT_COLOR',
|
key: 'FS_GENERAL_USE_THEME_ACCENT_COLOR',
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ const GenreTargetSchema = z.enum(['album', 'track']);
|
|||||||
const PlaylistTargetSchema = z.enum(['album', 'track']);
|
const PlaylistTargetSchema = z.enum(['album', 'track']);
|
||||||
|
|
||||||
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
||||||
|
const SideQueueLayoutSchema = z.enum(['horizontal', 'vertical']);
|
||||||
|
|
||||||
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
||||||
|
|
||||||
@@ -498,6 +499,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
sidebarPlaylistList: z.boolean(),
|
sidebarPlaylistList: z.boolean(),
|
||||||
sidebarPlaylistListFilterRegex: z.string(),
|
sidebarPlaylistListFilterRegex: z.string(),
|
||||||
sidebarPlaylistSorting: z.boolean(),
|
sidebarPlaylistSorting: z.boolean(),
|
||||||
|
sideQueueLayout: SideQueueLayoutSchema,
|
||||||
sideQueueType: SideQueueTypeSchema,
|
sideQueueType: SideQueueTypeSchema,
|
||||||
skipButtons: SkipButtonsSchema,
|
skipButtons: SkipButtonsSchema,
|
||||||
spotify: z.boolean(),
|
spotify: z.boolean(),
|
||||||
@@ -893,6 +895,7 @@ export interface SettingsSlice extends z.infer<typeof SettingsStateSchema> {
|
|||||||
export interface SettingsState extends z.infer<typeof SettingsStateSchema> {}
|
export interface SettingsState extends z.infer<typeof SettingsStateSchema> {}
|
||||||
export type SidebarItemType = z.infer<typeof SidebarItemTypeSchema>;
|
export type SidebarItemType = z.infer<typeof SidebarItemTypeSchema>;
|
||||||
|
|
||||||
|
export type SideQueueLayout = z.infer<typeof SideQueueLayoutSchema>;
|
||||||
export type SideQueueType = z.infer<typeof SideQueueTypeSchema>;
|
export type SideQueueType = z.infer<typeof SideQueueTypeSchema>;
|
||||||
|
|
||||||
export type SortableItem<T extends string> = {
|
export type SortableItem<T extends string> = {
|
||||||
@@ -1158,6 +1161,7 @@ const initialState: SettingsState = {
|
|||||||
sidebarPlaylistList: true,
|
sidebarPlaylistList: true,
|
||||||
sidebarPlaylistListFilterRegex: '',
|
sidebarPlaylistListFilterRegex: '',
|
||||||
sidebarPlaylistSorting: false,
|
sidebarPlaylistSorting: false,
|
||||||
|
sideQueueLayout: 'horizontal',
|
||||||
sideQueueType: 'sideQueue',
|
sideQueueType: 'sideQueue',
|
||||||
skipButtons: {
|
skipButtons: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -2381,10 +2385,16 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version <= 27) {
|
||||||
|
if (!state.general.sideQueueLayout) {
|
||||||
|
state.general.sideQueueLayout = initialState.general.sideQueueLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 26,
|
version: 27,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2492,6 +2502,9 @@ export const useThemeSettings = () =>
|
|||||||
export const useSideQueueType = () =>
|
export const useSideQueueType = () =>
|
||||||
useSettingsStore((state) => state.general.sideQueueType, shallow);
|
useSettingsStore((state) => state.general.sideQueueType, shallow);
|
||||||
|
|
||||||
|
export const useSideQueueLayout = () =>
|
||||||
|
useSettingsStore((state) => state.general.sideQueueLayout, shallow);
|
||||||
|
|
||||||
export const useVolumeWheelStep = () =>
|
export const useVolumeWheelStep = () =>
|
||||||
useSettingsStore((state) => state.general.volumeWheelStep, shallow);
|
useSettingsStore((state) => state.general.volumeWheelStep, shallow);
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ import {
|
|||||||
LuMusic,
|
LuMusic,
|
||||||
LuMusic2,
|
LuMusic2,
|
||||||
LuPackage2,
|
LuPackage2,
|
||||||
|
LuPanelBottom,
|
||||||
|
LuPanelRight,
|
||||||
LuPanelRightClose,
|
LuPanelRightClose,
|
||||||
LuPanelRightOpen,
|
LuPanelRightOpen,
|
||||||
LuPause,
|
LuPause,
|
||||||
@@ -198,6 +200,8 @@ export const AppIcon = {
|
|||||||
layoutDetail: LuLayoutList,
|
layoutDetail: LuLayoutList,
|
||||||
layoutGrid: LuLayoutGrid,
|
layoutGrid: LuLayoutGrid,
|
||||||
layoutList: LuList,
|
layoutList: LuList,
|
||||||
|
layoutPanelBottom: LuPanelBottom,
|
||||||
|
layoutPanelRight: LuPanelRight,
|
||||||
layoutTable: LuTable,
|
layoutTable: LuTable,
|
||||||
library: LuLibrary,
|
library: LuLibrary,
|
||||||
list: LuList,
|
list: LuList,
|
||||||
|
|||||||
Reference in New Issue
Block a user