add sidebar playlist folder settings to env, add compact sidebar playlist view

This commit is contained in:
jeffvli
2026-05-13 17:26:41 -07:00
parent 74939c6417
commit c4ef6f3799
9 changed files with 200 additions and 44 deletions
+4
View File
@@ -1063,6 +1063,10 @@
"sidebarPlaylistFolderTreeLineColor": "Tree line color",
"sidebarPlaylistList_description": "Show or hide the playlist list in the sidebar",
"sidebarPlaylistList": "Sidebar playlist list",
"sidebarPlaylistMode_description": "How each playlist is displayed in the sidebar list",
"sidebarPlaylistMode": "Sidebar playlist mode",
"sidebarPlaylistMode_optionCompact": "Compact",
"sidebarPlaylistMode_optionExpanded": "Expanded",
"sidebarPlaylistSorting_description": "Allows manual playlist sorting in the sidebar using drag and drop instead of the default server order",
"sidebarPlaylistSorting": "Sidebar playlist sorting",
"sidebarPlaylistListFilterRegex_description": "Hide playlists in the sidebar that match this regular expression",
@@ -15,6 +15,7 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
type FolderView = 'navigation' | 'single' | 'tree';
type PlaylistMode = 'compact' | 'expanded';
export const SidebarSettings = memo(() => {
const { t } = useTranslation();
@@ -84,9 +85,6 @@ export const SidebarSettings = memo(() => {
});
}, 500);
const foldersEnabled = settings.sidebarPlaylistFolders;
const isTreeView = settings.sidebarPlaylistFolderView === 'tree';
const folderViewOptions: Array<{ label: string; value: FolderView }> = [
{
label: t('setting.sidebarPlaylistFolderView_optionSingle', {
@@ -108,6 +106,24 @@ export const SidebarSettings = memo(() => {
},
];
const playlistModeOptions: Array<{ label: string; value: PlaylistMode }> = [
{
label: t('setting.sidebarPlaylistMode_optionCompact', {
postProcess: 'sentenceCase',
}),
value: 'compact',
},
{
label: t('setting.sidebarPlaylistMode_optionExpanded', {
postProcess: 'sentenceCase',
}),
value: 'expanded',
},
];
const foldersEnabled = settings.sidebarPlaylistFolders;
const isTreeView = settings.sidebarPlaylistFolderView === 'tree';
const options: SettingOption[] = [
{
control: (
@@ -150,6 +166,28 @@ export const SidebarSettings = memo(() => {
}),
title: t('setting.sidebarPlaylistSorting'),
},
{
control: (
<Select
data={playlistModeOptions}
onChange={(value) => {
if (!value) return;
setSettings({
general: {
sidebarPlaylistMode: value as PlaylistMode,
},
});
}}
value={settings.sidebarPlaylistMode}
width={200}
/>
),
description: t('setting.sidebarPlaylistMode', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.sidebarPlaylistMode', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
@@ -13,13 +13,29 @@
}
.row-hover {
.metadata {
.metadata,
.compact-name {
margin-right: 100px;
}
background-color: var(--theme-colors-surface);
}
.row-compact {
align-items: center;
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
}
.compact-name {
flex: 1;
min-width: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.controls {
position: absolute;
top: 50%;
@@ -29,6 +45,18 @@
transform: translateY(-50%);
}
.controls-compact {
position: absolute;
top: 50%;
right: var(--theme-spacing-xs);
flex-shrink: 0;
padding: 0 var(--theme-spacing-sm);
background: none;
transform: translateY(-50%);
}
.row-dragged-over {
border-radius: var(--mantine-radius-sm);
box-shadow: 0 0 0 2px var(--theme-colors-primary);
@@ -31,6 +31,7 @@ import {
useCurrentServerId,
usePermissions,
useSidebarPlaylistListFilterRegex,
useSidebarPlaylistMode,
useSidebarPlaylistSorting,
} from '/@/renderer/store';
import { formatDurationString } from '/@/renderer/utils';
@@ -73,6 +74,8 @@ export const PlaylistRowButton = memo(
};
const { t } = useTranslation();
const sidebarPlaylistSorting = useSidebarPlaylistSorting();
const sidebarPlaylistMode = useSidebarPlaylistMode();
const isCompact = sidebarPlaylistMode === 'compact';
const [isHovered, setIsHovered] = useState(false);
@@ -220,6 +223,7 @@ export const PlaylistRowButton = memo(
return (
<Link
className={clsx(styles.row, {
[styles.rowCompact]: isCompact,
[styles.rowDraggedOver]: isDraggedOver,
[styles.rowHover]: isHovered,
})}
@@ -235,50 +239,62 @@ export const PlaylistRowButton = memo(
}}
to={url}
>
<div className={styles.rowGroup}>
<Image containerClassName={styles.imageContainer} src={imageUrl} />
<div className={styles.metadata}>
<Text className={styles.name} fw={500} size="md">
{isCompact ? (
<>
<Text className={styles.compactName} fw={500} size="md">
{name}
</Text>
<div className={styles.metadataGroup}>
<div
className={clsx(
styles.metadataGroupItem,
styles.metadataGroupItemNoShrink,
)}
>
<Icon color="muted" icon="itemSong" size="sm" />
<Text isMuted size="sm">
{item.songCount || 0}
{isHovered && <RowControls id={to} onPlay={handlePlay} variant="compact" />}
</>
) : (
<>
<div className={styles.rowGroup}>
<Image containerClassName={styles.imageContainer} src={imageUrl} />
<div className={styles.metadata}>
<Text className={styles.name} fw={500} size="md">
{name}
</Text>
</div>
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="duration" size="sm" />
<Text isMuted size="sm">
{formatDurationString(item.duration ?? 0)}
</Text>
</div>
{item.ownerId === permissions.userId && Boolean(item.public) && (
<div className={styles.metadataGroupItem}>
<Text isMuted size="sm">
{t('common.public')}
</Text>
<div className={styles.metadataGroup}>
<div
className={clsx(
styles.metadataGroupItem,
styles.metadataGroupItemNoShrink,
)}
>
<Icon color="muted" icon="itemSong" size="sm" />
<Text isMuted size="sm">
{item.songCount || 0}
</Text>
</div>
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="duration" size="sm" />
<Text isMuted size="sm">
{formatDurationString(item.duration ?? 0)}
</Text>
</div>
{item.ownerId === permissions.userId &&
Boolean(item.public) && (
<div className={styles.metadataGroupItem}>
<Text isMuted size="sm">
{t('common.public')}
</Text>
</div>
)}
{item.ownerId !== permissions.userId && (
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="user" size="sm" />
<Text isMuted size="sm">
{item.owner}
</Text>
</div>
)}
</div>
)}
{item.ownerId !== permissions.userId && (
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="user" size="sm" />
<Text isMuted size="sm">
{item.owner}
</Text>
</div>
)}
</div>
</div>
</div>
</div>
{isHovered && <RowControls id={to} onPlay={handlePlay} />}
{isHovered && <RowControls id={to} onPlay={handlePlay} />}
</>
)}
</Link>
);
},
@@ -287,9 +303,11 @@ export const PlaylistRowButton = memo(
const RowControls = ({
id,
onPlay,
variant = 'expanded',
}: {
id: string;
onPlay: (id: string, playType: Play) => void;
variant?: 'compact' | 'expanded';
}) => {
const handlePlayNext = usePlayButtonClick({
onClick: () => {
@@ -319,7 +337,11 @@ const RowControls = ({
});
return (
<ActionIconGroup className={styles.controls}>
<ActionIconGroup
className={clsx(styles.controls, {
[styles.controlsCompact]: variant === 'compact',
})}
>
<PlayTooltip type={Play.NOW}>
<ActionIcon
icon="mediaPlay"
+6
View File
@@ -46,7 +46,13 @@ declare global {
FS_GENERAL_SIDE_QUEUE_TYPE?: string;
FS_GENERAL_SIDEBAR_COLLAPSE_SHARED?: string;
FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_LIST?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_MODE?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_SORTING?: string;
FS_GENERAL_THEME?: string;
FS_GENERAL_THEME_DARK?: string;
@@ -109,6 +109,8 @@ const FONT_TYPES = new Set(['builtIn', 'custom', 'system']);
const HOME_FEATURE_STYLES = new Set(['multiple', 'single']);
const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
export type EnvSettingsOverrides = DeepPartial<
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
@@ -256,11 +258,48 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['general', 'sidebarCollapseShared'],
type: 'bool',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS',
path: ['general', 'sidebarPlaylistFolders'],
type: 'bool',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR',
path: ['general', 'sidebarPlaylistFolderSeparator'],
skipIfEmpty: true,
type: 'string',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT',
path: ['general', 'sidebarPlaylistFolderTreeIndent'],
transform: (s) => {
const n = parseNum(s);
return n !== undefined ? Math.min(64, Math.max(0, Math.round(n))) : undefined;
},
type: 'num',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR',
path: ['general', 'sidebarPlaylistFolderTreeLineColor'],
type: 'string',
},
{
enumSet: SIDEBAR_PLAYLIST_FOLDER_VIEWS,
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW',
path: ['general', 'sidebarPlaylistFolderView'],
type: 'enum',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_LIST',
path: ['general', 'sidebarPlaylistList'],
type: 'bool',
},
{
enumSet: SIDEBAR_PLAYLIST_MODES,
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_MODE',
path: ['general', 'sidebarPlaylistMode'],
type: 'enum',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_SORTING',
path: ['general', 'sidebarPlaylistSorting'],
+8 -1
View File
@@ -178,6 +178,8 @@ const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
const SidebarPlaylistFolderViewSchema = z.enum(['single', 'tree', 'navigation']);
const SidebarPlaylistModeSchema = z.enum(['compact', 'expanded']);
const CollectionSchema = z.object({
filterQueryString: z.string(),
id: z.string(),
@@ -510,6 +512,7 @@ export const GeneralSettingsSchema = z.object({
sidebarPlaylistFolderView: SidebarPlaylistFolderViewSchema,
sidebarPlaylistList: z.boolean(),
sidebarPlaylistListFilterRegex: z.string(),
sidebarPlaylistMode: SidebarPlaylistModeSchema,
sidebarPlaylistSorting: z.boolean(),
sideQueueLayout: SideQueueLayoutSchema,
sideQueueType: SideQueueTypeSchema,
@@ -1182,9 +1185,10 @@ const initialState: SettingsState = {
sidebarPlaylistFolderSeparator: '/',
sidebarPlaylistFolderTreeIndent: 16,
sidebarPlaylistFolderTreeLineColor: '',
sidebarPlaylistFolderView: 'single',
sidebarPlaylistFolderView: 'tree',
sidebarPlaylistList: true,
sidebarPlaylistListFilterRegex: '',
sidebarPlaylistMode: 'expanded',
sidebarPlaylistSorting: false,
sideQueueLayout: 'horizontal',
sideQueueType: 'sideQueue',
@@ -2585,6 +2589,9 @@ export const useSidebarPlaylistFolderTreeLineColor = () =>
export const useSidebarPlaylistList = () =>
useSettingsStore((state) => state.general.sidebarPlaylistList, shallow);
export const useSidebarPlaylistMode = () =>
useSettingsStore((state) => state.general.sidebarPlaylistMode, shallow);
export const useSidebarPlaylistSorting = () =>
useSettingsStore((state) => state.general.sidebarPlaylistSorting, shallow);