mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-09 12:09:57 +02:00
add album mode for autodj
- add selection modes: similar, random - add autodj settings in playerbar popover
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { t } from 'i18next';
|
||||
import { useCallback, useEffect, useState, WheelEvent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState, WheelEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PopoverPlayQueue } from '/@/renderer/features/now-playing/components/popover-play-queue';
|
||||
@@ -12,6 +12,9 @@ import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||
import {
|
||||
AUTO_DJ_MODE,
|
||||
AUTO_DJ_STRATEGY,
|
||||
type AutoDJStrategy,
|
||||
useAppStoreActions,
|
||||
useAutoDJSettings,
|
||||
useCurrentServer,
|
||||
@@ -34,7 +37,15 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Paper } from '/@/shared/components/paper/paper';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||
import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
|
||||
@@ -90,28 +101,148 @@ const AutoDJButton = () => {
|
||||
const settings = useAutoDJSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const toggleAutoDJ = () => {
|
||||
setSettings({
|
||||
autoDJ: {
|
||||
...settings,
|
||||
enabled: !settings.enabled,
|
||||
const itemLabels = useMemo(() => {
|
||||
return {
|
||||
description: t('setting.autoDJ_itemCount_description'),
|
||||
title: t('setting.autoDJ_itemCount'),
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const strategySelectData = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('setting.autoDJ_strategy_option_similar'),
|
||||
value: AUTO_DJ_STRATEGY.SIMILAR,
|
||||
},
|
||||
});
|
||||
};
|
||||
{
|
||||
label: t('setting.autoDJ_strategy_option_library_random'),
|
||||
value: AUTO_DJ_STRATEGY.LIBRARY_RANDOM,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const strategyLabels =
|
||||
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||
? {
|
||||
description: '',
|
||||
title: t('setting.autoDJ_albumStrategy'),
|
||||
}
|
||||
: {
|
||||
description: '',
|
||||
title: t('setting.autoDJ_songStrategy'),
|
||||
};
|
||||
|
||||
const strategyValue =
|
||||
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||
? (settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR)
|
||||
: (settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleAutoDJ();
|
||||
}}
|
||||
size="compact-xs"
|
||||
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
||||
uppercase
|
||||
variant="transparent"
|
||||
>
|
||||
{t('setting.autoDJ')}
|
||||
</Button>
|
||||
<Popover position="top-end" withArrow>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
size="compact-xs"
|
||||
style={{ color: settings.enabled ? 'var(--theme-colors-primary)' : undefined }}
|
||||
uppercase
|
||||
variant="transparent"
|
||||
>
|
||||
{t('setting.autoDJ')}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown maw={320} miw={260} onClick={(e) => e.stopPropagation()} p="sm">
|
||||
<Stack gap="sm">
|
||||
<Paper p="md" radius="md">
|
||||
<Group align="center" gap="xs" justify="space-between" wrap="nowrap">
|
||||
<Text fw={600} isNoSelect size="sm">
|
||||
{t('setting.autoDJ_enabled')}
|
||||
</Text>
|
||||
<Switch
|
||||
checked={settings.enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
autoDJ: { enabled: e.currentTarget.checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: t('setting.autoDJ_mode_songs'), value: AUTO_DJ_MODE.SONGS },
|
||||
{
|
||||
label: t('setting.autoDJ_mode_albums'),
|
||||
value: AUTO_DJ_MODE.ALBUMS,
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setSettings({
|
||||
autoDJ: {
|
||||
mode: value as 'albums' | 'songs',
|
||||
},
|
||||
})
|
||||
}
|
||||
value={settings.mode}
|
||||
w="100%"
|
||||
/>
|
||||
<Select
|
||||
comboboxProps={{ withinPortal: false }}
|
||||
data={strategySelectData}
|
||||
description={strategyLabels.description}
|
||||
label={strategyLabels.title}
|
||||
onChange={(value) => {
|
||||
if (!value) return;
|
||||
setSettings({
|
||||
autoDJ:
|
||||
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||
? { albumStrategy: value as AutoDJStrategy }
|
||||
: { songStrategy: value as AutoDJStrategy },
|
||||
});
|
||||
}}
|
||||
size="md"
|
||||
value={strategyValue}
|
||||
w="100%"
|
||||
/>
|
||||
<NumberInput
|
||||
aria-label={itemLabels.title}
|
||||
description={itemLabels.description}
|
||||
hideControls={false}
|
||||
label={itemLabels.title}
|
||||
max={50}
|
||||
min={1}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
autoDJ: {
|
||||
itemCount: Number(e),
|
||||
},
|
||||
})
|
||||
}
|
||||
size="md"
|
||||
value={Number(settings.itemCount)}
|
||||
/>
|
||||
<NumberInput
|
||||
aria-label={t('setting.autoDJ_timing')}
|
||||
description={t('setting.autoDJ_timing_description')}
|
||||
hideControls={false}
|
||||
label={t('setting.autoDJ_timing')}
|
||||
max={5}
|
||||
min={1}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
autoDJ: {
|
||||
timing: Number(e),
|
||||
},
|
||||
})
|
||||
}
|
||||
size="md"
|
||||
value={Number(settings.timing)}
|
||||
/>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
||||
@@ -18,9 +19,18 @@ import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Played, RandomSongListQuery, ServerType } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
Played,
|
||||
RandomSongListQuery,
|
||||
ServerType,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface ShuffleAllSlice extends RandomSongListQuery {
|
||||
@@ -29,6 +39,7 @@ interface ShuffleAllSlice extends RandomSongListQuery {
|
||||
};
|
||||
enableMaxYear: boolean;
|
||||
enableMinYear: boolean;
|
||||
playbackKind: 'albums' | 'songs';
|
||||
}
|
||||
|
||||
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
||||
@@ -42,16 +53,28 @@ const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
||||
enableMaxYear: false,
|
||||
enableMinYear: false,
|
||||
genre: '',
|
||||
limit: 100,
|
||||
maxYear: 2020,
|
||||
minYear: 2000,
|
||||
musicFolder: '',
|
||||
playbackKind: 'songs',
|
||||
played: Played.All,
|
||||
songCount: 100,
|
||||
})),
|
||||
{
|
||||
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
||||
migrate: (persisted, version: number) => {
|
||||
if (!persisted) {
|
||||
return persisted;
|
||||
}
|
||||
|
||||
if (version >= 2) {
|
||||
return persisted;
|
||||
}
|
||||
|
||||
return persisted;
|
||||
},
|
||||
name: 'store_shuffle_all',
|
||||
version: 1,
|
||||
version: 2,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -66,13 +89,24 @@ export const useShuffleAllStoreActions = () => useShuffleAllStore((state) => sta
|
||||
|
||||
export const ShuffleAllContextModal = () => {
|
||||
const server = useCurrentServer();
|
||||
const { addToQueueByData } = usePlayer();
|
||||
const { addToQueueByData, addToQueueByFetch } = usePlayer();
|
||||
const { t } = useTranslation();
|
||||
const { enableMaxYear, enableMinYear, genre, limit, maxYear, minYear, musicFolderId, played } =
|
||||
useShuffleAllStore();
|
||||
const {
|
||||
enableMaxYear,
|
||||
enableMinYear,
|
||||
genre,
|
||||
limit,
|
||||
maxYear,
|
||||
minYear,
|
||||
musicFolderId,
|
||||
playbackKind,
|
||||
played,
|
||||
} = useShuffleAllStore();
|
||||
const { setStore } = useShuffleAllStoreActions();
|
||||
|
||||
const { isFetching, refetch } = useQuery({
|
||||
const clampedLimit = Math.min(500, Math.max(1, limit || 100));
|
||||
|
||||
const { isFetching: isFetchingSongs, refetch: refetchSongs } = useQuery({
|
||||
...randomFetchQuery({
|
||||
query: {
|
||||
genre: genre || undefined,
|
||||
@@ -89,22 +123,75 @@ export const ShuffleAllContextModal = () => {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const { isFetching: isFetchingAlbums, refetch: refetchAlbums } = useQuery({
|
||||
...shuffleAlbumListQuery({
|
||||
query: {
|
||||
genreIds: genre ? [genre] : undefined,
|
||||
limit: clampedLimit,
|
||||
minYear: enableMinYear ? minYear || undefined : undefined,
|
||||
musicFolderId: musicFolderId || undefined,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server.id,
|
||||
}),
|
||||
enabled: false,
|
||||
gcTime: 0,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const fetchTypeRef = useRef<Play>(null);
|
||||
|
||||
const handlePlay = async (playType: Play) => {
|
||||
fetchTypeRef.current = playType;
|
||||
|
||||
const { data } = await refetch();
|
||||
if (playbackKind === 'albums') {
|
||||
const { data } = await refetchAlbums();
|
||||
|
||||
addToQueueByData(data?.items || [], playType);
|
||||
addToQueueByFetch(
|
||||
server.id,
|
||||
data?.items.map((a) => a.id) ?? [],
|
||||
LibraryItem.ALBUM,
|
||||
playType,
|
||||
);
|
||||
} else {
|
||||
const { data } = await refetchSongs();
|
||||
|
||||
addToQueueByData(data?.items || [], playType);
|
||||
}
|
||||
|
||||
closeAllModals();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{
|
||||
label: t('form.shuffleAll.input_kind_songs'),
|
||||
value: 'songs',
|
||||
},
|
||||
{
|
||||
label: t('form.shuffleAll.input_kind_albums'),
|
||||
value: 'albums',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setStore({
|
||||
playbackKind: value as 'albums' | 'songs',
|
||||
})
|
||||
}
|
||||
size="sm"
|
||||
value={playbackKind}
|
||||
w="100%"
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('form.shuffleAll.input_limit')}
|
||||
label={
|
||||
playbackKind === 'albums'
|
||||
? t('form.shuffleAll.input_limit_albums')
|
||||
: t('form.shuffleAll.input_limit_songs')
|
||||
}
|
||||
max={500}
|
||||
min={1}
|
||||
onChange={(e) => setStore({ limit: e ? Number(e) : 500 })}
|
||||
@@ -127,6 +214,7 @@ export const ShuffleAllContextModal = () => {
|
||||
value={minYear}
|
||||
/>
|
||||
<NumberInput
|
||||
disabled={playbackKind === 'albums'}
|
||||
label={t('form.shuffleAll.input_maxYear')}
|
||||
max={2050}
|
||||
min={1850}
|
||||
@@ -134,6 +222,7 @@ export const ShuffleAllContextModal = () => {
|
||||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMaxYear}
|
||||
disabled={playbackKind === 'albums'}
|
||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
@@ -144,7 +233,7 @@ export const ShuffleAllContextModal = () => {
|
||||
<Suspense fallback={<Select data={[]} />}>
|
||||
<GenreSelect />
|
||||
</Suspense>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
{server?.type === ServerType.JELLYFIN && playbackKind === 'songs' && (
|
||||
<Select
|
||||
clearable
|
||||
data={PLAYED_DATA}
|
||||
@@ -156,10 +245,7 @@ export const ShuffleAllContextModal = () => {
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<PlayButtonGroup
|
||||
loading={(isFetching && fetchTypeRef.current) || false}
|
||||
onPlay={handlePlay}
|
||||
/>
|
||||
<PlayButtonGroup loading={isFetchingSongs || isFetchingAlbums} onPlay={handlePlay} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -186,6 +272,13 @@ const randomFetchQuery = (args: {
|
||||
});
|
||||
};
|
||||
|
||||
const shuffleAlbumListQuery = (args: { query: AlbumListQuery; serverId: string }) => {
|
||||
return albumQueries.list({
|
||||
query: args.query,
|
||||
serverId: args.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
export const openShuffleAllModal = async () => {
|
||||
openContextModal({
|
||||
innerProps: {},
|
||||
|
||||
Reference in New Issue
Block a user