mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-23 02:46:40 +02:00
feat(sidebar): multi-level playlist folders with tree and navigation views (#2017)
Group playlists into folders using a configurable separator (default '/'). Three view modes: - Single: first-level grouping only (original behavior) - Tree: full recursive nesting with connecting lines (configurable indent and line color) - Navigation: drill-down view with stacked breadcrumb chain Folders are sorted before playlists at every level. New settings render as indented sub-options under the master 'Enable folders' toggle.
This commit is contained in:
@@ -7,15 +7,28 @@ import {
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { ColorInput } from '/@/shared/components/color-input/color-input';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
|
||||
|
||||
type FolderView = 'navigation' | 'single' | 'tree';
|
||||
|
||||
export const SidebarSettings = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const handleSetSidebarPlaylistFolders = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
general: {
|
||||
sidebarPlaylistFolders: e.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
general: {
|
||||
@@ -56,6 +69,45 @@ export const SidebarSettings = memo(() => {
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const [localSeparator, setLocalSeparator] = useState(settings.sidebarPlaylistFolderSeparator);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSeparator(settings.sidebarPlaylistFolderSeparator);
|
||||
}, [settings.sidebarPlaylistFolderSeparator]);
|
||||
|
||||
const debouncedSetSeparator = useDebouncedCallback((value: string) => {
|
||||
if (value.length === 0) return;
|
||||
setSettings({
|
||||
general: {
|
||||
sidebarPlaylistFolderSeparator: value,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const foldersEnabled = settings.sidebarPlaylistFolders;
|
||||
const isTreeView = settings.sidebarPlaylistFolderView === 'tree';
|
||||
|
||||
const folderViewOptions: Array<{ label: string; value: FolderView }> = [
|
||||
{
|
||||
label: t('setting.sidebarPlaylistFolderView_optionSingle', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'single',
|
||||
},
|
||||
{
|
||||
label: t('setting.sidebarPlaylistFolderView_optionTree', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'tree',
|
||||
},
|
||||
{
|
||||
label: t('setting.sidebarPlaylistFolderView_optionNavigation', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'navigation',
|
||||
},
|
||||
];
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
@@ -98,6 +150,115 @@ export const SidebarSettings = memo(() => {
|
||||
}),
|
||||
title: t('setting.sidebarPlaylistSorting'),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
checked={settings.sidebarPlaylistFolders}
|
||||
onChange={handleSetSidebarPlaylistFolders}
|
||||
/>
|
||||
),
|
||||
description: t('setting.sidebarPlaylistFolders', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.sidebarPlaylistFolders', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<TextInput
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
setLocalSeparator(value);
|
||||
debouncedSetSeparator(value);
|
||||
}}
|
||||
value={localSeparator}
|
||||
width={120}
|
||||
/>
|
||||
),
|
||||
description: t('setting.sidebarPlaylistFolderSeparator', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
indent: true,
|
||||
isHidden: !foldersEnabled,
|
||||
title: t('setting.sidebarPlaylistFolderSeparator', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={folderViewOptions}
|
||||
onChange={(value) => {
|
||||
if (!value) return;
|
||||
setSettings({
|
||||
general: {
|
||||
sidebarPlaylistFolderView: value as FolderView,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={settings.sidebarPlaylistFolderView}
|
||||
width={200}
|
||||
/>
|
||||
),
|
||||
description: t('setting.sidebarPlaylistFolderView', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
indent: true,
|
||||
isHidden: !foldersEnabled,
|
||||
title: t('setting.sidebarPlaylistFolderView', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
max={64}
|
||||
min={0}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
if (Number.isFinite(value)) {
|
||||
setSettings({
|
||||
general: {
|
||||
sidebarPlaylistFolderTreeIndent: Math.max(
|
||||
0,
|
||||
Math.min(64, Math.round(value)),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
value={settings.sidebarPlaylistFolderTreeIndent}
|
||||
width={100}
|
||||
/>
|
||||
),
|
||||
description: t('setting.sidebarPlaylistFolderTreeIndent', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
indent: true,
|
||||
isHidden: !foldersEnabled || !isTreeView,
|
||||
title: t('setting.sidebarPlaylistFolderTreeIndent', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<ColorInput
|
||||
format="rgba"
|
||||
onChangeEnd={(value) => {
|
||||
setSettings({
|
||||
general: {
|
||||
sidebarPlaylistFolderTreeLineColor: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={settings.sidebarPlaylistFolderTreeLineColor}
|
||||
/>
|
||||
),
|
||||
description: t('setting.sidebarPlaylistFolderTreeLineColor', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
indent: true,
|
||||
isHidden: !foldersEnabled || !isTreeView,
|
||||
title: t('setting.sidebarPlaylistFolderTreeLineColor', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-indented {
|
||||
padding-left: var(--theme-spacing-lg);
|
||||
margin-left: var(--theme-spacing-md);
|
||||
border-left: 2px solid var(--theme-colors-surface);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import styles from './settings-option.module.css';
|
||||
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
@@ -9,15 +11,20 @@ import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||
interface SettingsOptionProps {
|
||||
control: React.ReactNode;
|
||||
description?: React.ReactNode | string;
|
||||
indent?: boolean;
|
||||
note?: string;
|
||||
title: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const SettingsOptions = memo(
|
||||
({ control, description, note, title }: SettingsOptionProps) => {
|
||||
({ control, description, indent, note, title }: SettingsOptionProps) => {
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" style={{ alignItems: 'center' }} wrap="nowrap">
|
||||
<Group
|
||||
className={indent ? styles.rowIndented : styles.row}
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Stack
|
||||
gap="xs"
|
||||
style={{
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
export type SettingOption = {
|
||||
control: ReactNode;
|
||||
description: ReactNode | string;
|
||||
indent?: boolean;
|
||||
isHidden?: boolean;
|
||||
note?: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user