mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 06:12:43 +02:00
add sidebar playlist folder settings to env, add compact sidebar playlist view
This commit is contained in:
@@ -43,7 +43,13 @@ These variables override app settings **on first run** when no persisted setting
|
||||
| `general.showVisualizerInSidebar` | `true` | `FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR` | `true` / `false` — Show visualizer in sidebar. |
|
||||
| `general.sidebarCollapsedNavigation` | `true` | `FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION` | `true` / `false` — Start with collapsed sidebar nav. |
|
||||
| `general.sidebarCollapseShared` | `false` | `FS_GENERAL_SIDEBAR_COLLAPSE_SHARED` | `true` / `false` — Share sidebar collapse state. |
|
||||
| `general.sidebarPlaylistFolders` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS` | `true` / `false` — Group playlists into folders by name separator. |
|
||||
| `general.sidebarPlaylistFolderSeparator` | `/` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR` | Character or string that separates folder levels in a playlist name. Empty = use default. |
|
||||
| `general.sidebarPlaylistFolderTreeIndent` | `16` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT` | Pixels each tree level is indented (0–64). |
|
||||
| `general.sidebarPlaylistFolderTreeLineColor` | *(empty)* | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR` | CSS color for tree connecting lines. Empty = theme default. |
|
||||
| `general.sidebarPlaylistFolderView` | `tree` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW` | `single` / `tree` / `navigation` — How folders are displayed in the sidebar. |
|
||||
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
|
||||
| `general.sidebarPlaylistMode` | `expanded` | `FS_GENERAL_SIDEBAR_PLAYLIST_MODE` | `compact` / `expanded` — Sidebar playlist row layout. |
|
||||
| `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.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
|
||||
|
||||
@@ -38,7 +38,13 @@ window.FS_GENERAL_SHOW_RATINGS = "${FS_GENERAL_SHOW_RATINGS}";
|
||||
window.FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR = "${FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR}";
|
||||
window.FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION = "${FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION}";
|
||||
window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARED}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
|
||||
window.FS_GENERAL_SIDEBAR_PLAYLIST_MODE = "${FS_GENERAL_SIDEBAR_PLAYLIST_MODE}";
|
||||
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_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+6
@@ -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'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user