mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
support limitPercent for smart playlists
This commit is contained in:
@@ -72,6 +72,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
? {
|
||||
...convertQueryGroupToNDQuery(smartPlaylist.filters),
|
||||
limit: smartPlaylist.extraFilters.limit,
|
||||
limitPercent: smartPlaylist.extraFilters.limitPercent,
|
||||
// order field is now optional - sort direction is embedded in sort field
|
||||
sort: sortValue || '+dateAdded',
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { useForm } from '/@/shared/hooks/use-form';
|
||||
@@ -51,6 +52,7 @@ type DeleteArgs = {
|
||||
|
||||
interface PlaylistQueryBuilderProps {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
playlistId?: string;
|
||||
query: any;
|
||||
sortBy: SongListSort | SongListSort[];
|
||||
@@ -155,6 +157,7 @@ export type PlaylistQueryBuilderRef = {
|
||||
getFilters: () => {
|
||||
extraFilters: {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
sortBy?: string[];
|
||||
sortOrder?: string;
|
||||
};
|
||||
@@ -164,7 +167,7 @@ export type PlaylistQueryBuilderRef = {
|
||||
|
||||
export const PlaylistQueryBuilder = forwardRef(
|
||||
(
|
||||
{ limit, playlistId, query, sortBy, sortOrder }: PlaylistQueryBuilderProps,
|
||||
{ limit, limitPercent, playlistId, query, sortBy, sortOrder }: PlaylistQueryBuilderProps,
|
||||
ref: Ref<PlaylistQueryBuilderRef>,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -213,6 +216,8 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
const extraFiltersForm = useForm({
|
||||
initialValues: {
|
||||
limit,
|
||||
limitMode: limitPercent != null ? 'limitPercent' : 'limit',
|
||||
limitPercent,
|
||||
sortEntries: initialSortEntries,
|
||||
},
|
||||
});
|
||||
@@ -224,16 +229,26 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
const sortString = convertSortEntriesToSortString(
|
||||
extraFiltersForm.values.sortEntries,
|
||||
);
|
||||
const isLimitPercent = extraFiltersForm.values.limitMode === 'limitPercent';
|
||||
return {
|
||||
extraFilters: {
|
||||
limit: extraFiltersForm.values.limit,
|
||||
limit: isLimitPercent ? undefined : extraFiltersForm.values.limit,
|
||||
limitPercent: isLimitPercent
|
||||
? extraFiltersForm.values.limitPercent
|
||||
: undefined,
|
||||
sortBy: sortString ? [sortString] : undefined,
|
||||
},
|
||||
filters,
|
||||
};
|
||||
},
|
||||
}),
|
||||
[extraFiltersForm.values.sortEntries, extraFiltersForm.values.limit, filters],
|
||||
[
|
||||
extraFiltersForm.values.sortEntries,
|
||||
extraFiltersForm.values.limit,
|
||||
extraFiltersForm.values.limitMode,
|
||||
extraFiltersForm.values.limitPercent,
|
||||
filters,
|
||||
],
|
||||
);
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
@@ -608,10 +623,50 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
))}
|
||||
</Stack>
|
||||
<NumberInput
|
||||
label={t('common.limit', { postProcess: 'titleCase' })}
|
||||
maxWidth="20%"
|
||||
label={
|
||||
<Group align="center" gap="xs" wrap="nowrap">
|
||||
{t('common.limit', { postProcess: 'titleCase' })}
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: '#', value: 'limit' },
|
||||
{ label: '%', value: 'limitPercent' },
|
||||
]}
|
||||
onChange={(value) =>
|
||||
extraFiltersForm.setFieldValue(
|
||||
'limitMode',
|
||||
value as 'limit' | 'limitPercent',
|
||||
)
|
||||
}
|
||||
size="xs"
|
||||
value={extraFiltersForm.values.limitMode}
|
||||
/>
|
||||
</Group>
|
||||
}
|
||||
max={
|
||||
extraFiltersForm.values.limitMode === 'limitPercent'
|
||||
? 100
|
||||
: undefined
|
||||
}
|
||||
min={
|
||||
extraFiltersForm.values.limitMode === 'limitPercent'
|
||||
? 0
|
||||
: undefined
|
||||
}
|
||||
onChange={(value) => {
|
||||
const nextValue =
|
||||
value === '' || value == null ? undefined : Number(value);
|
||||
if (extraFiltersForm.values.limitMode === 'limitPercent') {
|
||||
extraFiltersForm.setFieldValue('limitPercent', nextValue);
|
||||
} else {
|
||||
extraFiltersForm.setFieldValue('limit', nextValue);
|
||||
}
|
||||
}}
|
||||
value={
|
||||
extraFiltersForm.values.limitMode === 'limitPercent'
|
||||
? extraFiltersForm.values.limitPercent
|
||||
: extraFiltersForm.values.limit
|
||||
}
|
||||
width={75}
|
||||
{...extraFiltersForm.getInputProps('limit')}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -28,11 +28,21 @@ export interface PlaylistQueryEditorProps {
|
||||
detailQuery: ReturnType<typeof useQuery<any>>;
|
||||
handleSave: (
|
||||
filter: Record<string, any>,
|
||||
extraFilters: { limit?: number; sortBy?: string[]; sortOrder?: string },
|
||||
extraFilters: {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
sortBy?: string[];
|
||||
sortOrder?: string;
|
||||
},
|
||||
) => void;
|
||||
handleSaveAs: (
|
||||
filter: Record<string, any>,
|
||||
extraFilters: { limit?: number; sortBy?: string[]; sortOrder?: string },
|
||||
extraFilters: {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
sortBy?: string[];
|
||||
sortOrder?: string;
|
||||
},
|
||||
) => void;
|
||||
isQueryBuilderExpanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
@@ -43,6 +53,7 @@ export interface PlaylistQueryEditorProps {
|
||||
|
||||
type AppliedJsonState = {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
query: Record<string, any>;
|
||||
sort?: string;
|
||||
};
|
||||
@@ -50,7 +61,7 @@ type AppliedJsonState = {
|
||||
type EditorMode = 'builder' | 'json';
|
||||
|
||||
const serializeFiltersToRulesJson = (filters: {
|
||||
extraFilters: { limit?: number; sortBy?: string[] };
|
||||
extraFilters: { limit?: number; limitPercent?: number; sortBy?: string[] };
|
||||
filters: any;
|
||||
}): Record<string, any> => {
|
||||
const queryValue = convertQueryGroupToNDQuery(filters.filters);
|
||||
@@ -58,18 +69,25 @@ const serializeFiltersToRulesJson = (filters: {
|
||||
return {
|
||||
...queryValue,
|
||||
...(filters.extraFilters.limit != null && { limit: filters.extraFilters.limit }),
|
||||
...(filters.extraFilters.limitPercent != null && {
|
||||
limitPercent: filters.extraFilters.limitPercent,
|
||||
}),
|
||||
...(sortString && { sort: sortString }),
|
||||
};
|
||||
};
|
||||
|
||||
const parseRulesJsonToSaveArgs = (
|
||||
parsed: Record<string, any>,
|
||||
): { extraFilters: { limit?: number; sortBy?: string[] }; filter: Record<string, any> } => {
|
||||
): {
|
||||
extraFilters: { limit?: number; limitPercent?: number; sortBy?: string[] };
|
||||
filter: Record<string, any>;
|
||||
} => {
|
||||
const rootKey = parsed.all ? 'all' : 'any';
|
||||
const filter = rootKey in parsed ? { [rootKey]: parsed[rootKey] } : { all: [] };
|
||||
return {
|
||||
extraFilters: {
|
||||
...(parsed.limit != null && { limit: parsed.limit }),
|
||||
...(parsed.limitPercent != null && { limitPercent: parsed.limitPercent }),
|
||||
...(parsed.sort != null && { sortBy: [parsed.sort] }),
|
||||
},
|
||||
filter,
|
||||
@@ -93,7 +111,12 @@ export const PlaylistQueryEditor = ({
|
||||
const [appliedJsonState, setAppliedJsonState] = useState<AppliedJsonState | null>(null);
|
||||
|
||||
const getFiltersForSave = useCallback((): null | {
|
||||
extraFilters: { limit?: number; sortBy?: string[]; sortOrder?: string };
|
||||
extraFilters: {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
sortBy?: string[];
|
||||
sortOrder?: string;
|
||||
};
|
||||
filter: Record<string, any>;
|
||||
} => {
|
||||
if (editorMode === 'json') {
|
||||
@@ -124,6 +147,9 @@ export const PlaylistQueryEditor = ({
|
||||
const previewValue = {
|
||||
...payload.filter,
|
||||
...(payload.extraFilters.limit != null && { limit: payload.extraFilters.limit }),
|
||||
...(payload.extraFilters.limitPercent != null && {
|
||||
limitPercent: payload.extraFilters.limitPercent,
|
||||
}),
|
||||
...(payload.extraFilters.sortBy?.[0] && { sort: payload.extraFilters.sortBy[0] }),
|
||||
};
|
||||
openModal({
|
||||
@@ -208,6 +234,8 @@ export const PlaylistQueryEditor = ({
|
||||
[appliedJsonState?.query, detailQuery?.data?.rules],
|
||||
);
|
||||
const effectiveLimit = appliedJsonState?.limit ?? detailQuery?.data?.rules?.limit;
|
||||
const effectiveLimitPercent =
|
||||
appliedJsonState?.limitPercent ?? detailQuery?.data?.rules?.limitPercent;
|
||||
const effectiveSortBy = useMemo(
|
||||
() =>
|
||||
(appliedJsonState?.sort ? [appliedJsonState.sort] : parseSortBy()) as
|
||||
@@ -233,6 +261,8 @@ export const PlaylistQueryEditor = ({
|
||||
? { ...effectiveQuery }
|
||||
: { all: [] };
|
||||
if (effectiveLimit != null) fallback.limit = effectiveLimit;
|
||||
if (effectiveLimitPercent != null)
|
||||
fallback.limitPercent = effectiveLimitPercent;
|
||||
if (effectiveSortBy?.[0]) fallback.sort = effectiveSortBy[0];
|
||||
if (!fallback.sort) fallback.sort = '+dateAdded';
|
||||
setJsonText(JSON.stringify(fallback, null, 2));
|
||||
@@ -248,6 +278,7 @@ export const PlaylistQueryEditor = ({
|
||||
}
|
||||
setAppliedJsonState({
|
||||
limit: parsed.limit,
|
||||
limitPercent: parsed.limitPercent,
|
||||
query: { [rootKey]: parsed[rootKey] },
|
||||
sort: parsed.sort,
|
||||
});
|
||||
@@ -263,7 +294,16 @@ export const PlaylistQueryEditor = ({
|
||||
setEditorMode('builder');
|
||||
}
|
||||
},
|
||||
[editorMode, effectiveLimit, effectiveQuery, effectiveSortBy, jsonText, queryBuilderRef, t],
|
||||
[
|
||||
editorMode,
|
||||
effectiveLimit,
|
||||
effectiveLimitPercent,
|
||||
effectiveQuery,
|
||||
effectiveSortBy,
|
||||
jsonText,
|
||||
queryBuilderRef,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -367,6 +407,7 @@ export const PlaylistQueryEditor = ({
|
||||
<PlaylistQueryBuilder
|
||||
key={JSON.stringify(appliedJsonState ?? detailQuery?.data?.rules)}
|
||||
limit={effectiveLimit}
|
||||
limitPercent={effectiveLimitPercent}
|
||||
playlistId={playlistId}
|
||||
query={effectiveQuery}
|
||||
ref={queryBuilderRef}
|
||||
|
||||
@@ -85,7 +85,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
const handleSave = (
|
||||
filter: Record<string, any>,
|
||||
extraFilters: { limit?: number; sortBy?: string[]; sortOrder?: string },
|
||||
extraFilters: { limit?: number; limitPercent?: number; sortBy?: string[]; sortOrder?: string },
|
||||
) => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
@@ -96,7 +96,8 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
const rules = {
|
||||
...filter,
|
||||
limit: extraFilters.limit || undefined,
|
||||
limit: extraFilters.limit ?? undefined,
|
||||
limitPercent: extraFilters.limitPercent ?? undefined,
|
||||
sort: sortValue,
|
||||
};
|
||||
|
||||
@@ -123,7 +124,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
const handleSaveAs = (
|
||||
filter: Record<string, any>,
|
||||
extraFilters: { limit?: number; sortBy?: string[]; sortOrder?: string },
|
||||
extraFilters: { limit?: number; limitPercent?: number; sortBy?: string[]; sortOrder?: string },
|
||||
) => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
@@ -134,7 +135,8 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
const rules = {
|
||||
...filter,
|
||||
limit: extraFilters.limit || undefined,
|
||||
limit: extraFilters.limit ?? undefined,
|
||||
limitPercent: extraFilters.limitPercent ?? undefined,
|
||||
sort: sortValue,
|
||||
};
|
||||
|
||||
|
||||
@@ -600,6 +600,14 @@ const songListParameters = paginationParameters.extend({
|
||||
year: z.number().optional(),
|
||||
});
|
||||
|
||||
const playlistRules = z
|
||||
.object({
|
||||
limit: z.number().optional(),
|
||||
limitPercent: z.number().optional(),
|
||||
sort: z.string().optional(),
|
||||
})
|
||||
.catchall(z.any());
|
||||
|
||||
const playlist = z.object({
|
||||
comment: z.string(),
|
||||
createdAt: z.string(),
|
||||
@@ -611,7 +619,7 @@ const playlist = z.object({
|
||||
ownerName: z.string(),
|
||||
path: z.string(),
|
||||
public: z.boolean(),
|
||||
rules: z.record(z.string(), z.any()),
|
||||
rules: playlistRules,
|
||||
size: z.number(),
|
||||
songCount: z.number(),
|
||||
sync: z.boolean(),
|
||||
@@ -643,7 +651,7 @@ const createPlaylistParameters = z.object({
|
||||
name: z.string(),
|
||||
ownerId: z.string().optional(),
|
||||
public: z.boolean().optional(),
|
||||
rules: z.record(z.any()).optional(),
|
||||
rules: playlistRules.optional(),
|
||||
sync: z.boolean().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ export type Playlist = {
|
||||
owner: null | string;
|
||||
ownerId: null | string;
|
||||
public: boolean | null;
|
||||
rules?: null | Record<string, any>;
|
||||
rules?: null | PlaylistRules;
|
||||
size: null | number;
|
||||
songCount: null | number;
|
||||
sync?: boolean | null;
|
||||
@@ -947,7 +947,7 @@ export type CreatePlaylistBody = {
|
||||
name: string;
|
||||
ownerId?: string;
|
||||
public?: boolean;
|
||||
queryBuilderRules?: Record<string, any>;
|
||||
queryBuilderRules?: PlaylistRules;
|
||||
sync?: boolean;
|
||||
};
|
||||
|
||||
@@ -1009,6 +1009,12 @@ export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
|
||||
// Playlist List
|
||||
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]>;
|
||||
|
||||
export type PlaylistRules = Record<string, any> & {
|
||||
limit?: number;
|
||||
limitPercent?: number;
|
||||
sort?: string;
|
||||
};
|
||||
|
||||
export type RatingQuery = {
|
||||
id: string[];
|
||||
rating: number;
|
||||
@@ -1089,7 +1095,7 @@ export type UpdatePlaylistBody = {
|
||||
name: string;
|
||||
ownerId?: string;
|
||||
public?: boolean;
|
||||
queryBuilderRules?: Record<string, any>;
|
||||
queryBuilderRules?: PlaylistRules;
|
||||
sync?: boolean;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user