mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 13:23:16 +02:00
normalize playerbar config designs
This commit is contained in:
@@ -110,8 +110,9 @@ export const PlayerConfig = () => {
|
|||||||
label: t('player.playbackSpeed'),
|
label: t('player.playbackSpeed'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: !preservePitch ? <PitchControls /> : <></>,
|
component: <PitchControls />,
|
||||||
id: 'pitchControls',
|
id: 'pitchControls',
|
||||||
|
isHidden: preservePitch,
|
||||||
label: '',
|
label: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -219,7 +220,7 @@ export const PlayerConfig = () => {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown maw={500} miw={320} onClick={(e) => e.stopPropagation()} p="sm">
|
<Popover.Dropdown maw={720} miw={540} onClick={(e) => e.stopPropagation()} p="sm">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Paper p="md" radius="md">
|
<Paper p="md" radius="md">
|
||||||
<ListConfigTable options={audioOptions} />
|
<ListConfigTable options={audioOptions} />
|
||||||
@@ -437,12 +438,8 @@ export const PlaybackSpeedSlider = () => {
|
|||||||
onChange={setSpeed}
|
onChange={setSpeed}
|
||||||
onDoubleClick={() => setSpeed(1)}
|
onDoubleClick={() => setSpeed(1)}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
styles={{
|
|
||||||
markLabel: {},
|
|
||||||
root: {},
|
|
||||||
}}
|
|
||||||
value={speed}
|
value={speed}
|
||||||
w="240px"
|
w="320px"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -467,13 +464,13 @@ export const PitchControls = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap={microtonal ? 'xs' : 'md'} my="sm" w="100%" wrap="nowrap">
|
<Group gap={microtonal ? 'xs' : 'md'} my="md" w="100%" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
aria-label="-1 semitone"
|
aria-label="-1 semitone"
|
||||||
fullWidth
|
fullWidth
|
||||||
fw={400}
|
fw={400}
|
||||||
onClick={() => adjustMusicalSpeed(-1)}
|
onClick={() => adjustMusicalSpeed(-1)}
|
||||||
size="compact-sm"
|
size="compact-xs"
|
||||||
>
|
>
|
||||||
-1st
|
-1st
|
||||||
</Button>
|
</Button>
|
||||||
@@ -483,7 +480,7 @@ export const PitchControls = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
fw={400}
|
fw={400}
|
||||||
onClick={() => adjustMusicalSpeed(-0.1)}
|
onClick={() => adjustMusicalSpeed(-0.1)}
|
||||||
size="compact-sm"
|
size="compact-xs"
|
||||||
>
|
>
|
||||||
-10ct
|
-10ct
|
||||||
</Button>
|
</Button>
|
||||||
@@ -499,7 +496,7 @@ export const PitchControls = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
fw={400}
|
fw={400}
|
||||||
onClick={() => adjustMusicalSpeed(0.1)}
|
onClick={() => adjustMusicalSpeed(0.1)}
|
||||||
size="compact-sm"
|
size="compact-xs"
|
||||||
>
|
>
|
||||||
+10ct
|
+10ct
|
||||||
</Button>
|
</Button>
|
||||||
@@ -509,7 +506,7 @@ export const PitchControls = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
fw={400}
|
fw={400}
|
||||||
onClick={() => adjustMusicalSpeed(1)}
|
onClick={() => adjustMusicalSpeed(1)}
|
||||||
size="compact-sm"
|
size="compact-xs"
|
||||||
>
|
>
|
||||||
+1st
|
+1st
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/pl
|
|||||||
import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button';
|
import { SleepTimerButton } from '/@/renderer/features/player/components/sleep-timer-button';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { useAudioDevices } from '/@/renderer/features/settings/components/playback/audio-settings';
|
import { useAudioDevices } from '/@/renderer/features/settings/components/playback/audio-settings';
|
||||||
|
import {
|
||||||
|
ListConfigBooleanControl,
|
||||||
|
ListConfigTable,
|
||||||
|
} from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
@@ -48,8 +52,8 @@ import { Popover } from '/@/shared/components/popover/popover';
|
|||||||
import { Rating } from '/@/shared/components/rating/rating';
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
import { useMediaQuery } from '/@/shared/hooks/use-media-query';
|
||||||
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
import { useThrottledCallback } from '/@/shared/hooks/use-throttled-callback';
|
||||||
@@ -107,13 +111,6 @@ const AutoDJButton = () => {
|
|||||||
const settings = useAutoDJSettings();
|
const settings = useAutoDJSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
const itemLabels = useMemo(() => {
|
|
||||||
return {
|
|
||||||
description: t('setting.autoDJ_itemCount_description'),
|
|
||||||
title: t('setting.autoDJ_itemCount'),
|
|
||||||
};
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const strategySelectData = useMemo(
|
const strategySelectData = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -128,22 +125,200 @@ const AutoDJButton = () => {
|
|||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const strategyLabels =
|
const strategyTitle =
|
||||||
settings.mode === AUTO_DJ_MODE.ALBUMS
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
? {
|
? t('setting.autoDJ_albumStrategy')
|
||||||
description: '',
|
: t('setting.autoDJ_songStrategy');
|
||||||
title: t('setting.autoDJ_albumStrategy'),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
description: '',
|
|
||||||
title: t('setting.autoDJ_songStrategy'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const strategyValue =
|
const strategyValue =
|
||||||
settings.mode === AUTO_DJ_MODE.ALBUMS
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
? (settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR)
|
? (settings.albumStrategy ?? AUTO_DJ_STRATEGY.SIMILAR)
|
||||||
: (settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR);
|
: (settings.songStrategy ?? AUTO_DJ_STRATEGY.SIMILAR);
|
||||||
|
|
||||||
|
const enabledOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: { enabled: value },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={settings.enabled}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'enabled',
|
||||||
|
label: t('setting.autoDJ_enabled'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[setSettings, settings.enabled, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const configOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Select
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
|
data={strategySelectData}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value) return;
|
||||||
|
setSettings({
|
||||||
|
autoDJ:
|
||||||
|
settings.mode === AUTO_DJ_MODE.ALBUMS
|
||||||
|
? { albumStrategy: value as AutoDJStrategy }
|
||||||
|
: { songStrategy: value as AutoDJStrategy },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
value={strategyValue}
|
||||||
|
variant="filled"
|
||||||
|
w="160px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'strategy',
|
||||||
|
label: strategyTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<NumberInput
|
||||||
|
aria-label={t('setting.autoDJ_itemCount')}
|
||||||
|
hideControls={false}
|
||||||
|
max={50}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
itemCount: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={Number(settings.itemCount)}
|
||||||
|
variant="filled"
|
||||||
|
w="96px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'itemCount',
|
||||||
|
label: (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_itemCount')}
|
||||||
|
</Text>
|
||||||
|
<Text isMuted isNoSelect size="xs">
|
||||||
|
{t('setting.autoDJ_itemCount_description')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Slider
|
||||||
|
aria-label={t('setting.autoDJ_timing')}
|
||||||
|
marks={[
|
||||||
|
{ label: '1', value: 1 },
|
||||||
|
{ label: '2', value: 2 },
|
||||||
|
{ label: '3', value: 3 },
|
||||||
|
{ label: '4', value: 4 },
|
||||||
|
{ label: '5', value: 5 },
|
||||||
|
]}
|
||||||
|
max={5}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
timing: Number(e),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={Number(settings.timing)}
|
||||||
|
variant="filled"
|
||||||
|
w="144px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'timing',
|
||||||
|
label: (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_timing')}
|
||||||
|
</Text>
|
||||||
|
<Text isMuted isNoSelect size="xs">
|
||||||
|
{t('setting.autoDJ_timing_description')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
setSettings,
|
||||||
|
settings.itemCount,
|
||||||
|
settings.mode,
|
||||||
|
settings.timing,
|
||||||
|
strategySelectData,
|
||||||
|
strategyTitle,
|
||||||
|
strategyValue,
|
||||||
|
t,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
allowDuplicates: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={settings.allowDuplicates}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'allowDuplicates',
|
||||||
|
label: (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_allowDuplicates')}
|
||||||
|
</Text>
|
||||||
|
<Text isMuted isNoSelect size="xs">
|
||||||
|
{t('setting.autoDJ_allowDuplicates_description')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({
|
||||||
|
autoDJ: {
|
||||||
|
onlySimilar: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={settings.onlySimilar}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'onlySimilar',
|
||||||
|
label: (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text isNoSelect size="sm">
|
||||||
|
{t('setting.autoDJ_onlySimilar')}
|
||||||
|
</Text>
|
||||||
|
<Text isMuted isNoSelect size="xs">
|
||||||
|
{t('setting.autoDJ_onlySimilar_description')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[setSettings, settings.allowDuplicates, settings.onlySimilar, t],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover position="top-end" withArrow>
|
<Popover position="top-end" withArrow>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
@@ -159,22 +334,10 @@ const AutoDJButton = () => {
|
|||||||
{t('setting.autoDJ')}
|
{t('setting.autoDJ')}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown maw={320} miw={260} onClick={(e) => e.stopPropagation()} p="sm">
|
<Popover.Dropdown maw={480} miw={320} onClick={(e) => e.stopPropagation()} p="sm">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Paper p="md" radius="md">
|
<Paper p="md" radius="md">
|
||||||
<Group align="center" gap="xs" justify="space-between" wrap="nowrap">
|
<ListConfigTable options={enabledOptions} />
|
||||||
<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>
|
</Paper>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
@@ -195,109 +358,10 @@ const AutoDJButton = () => {
|
|||||||
w="100%"
|
w="100%"
|
||||||
/>
|
/>
|
||||||
<Paper p="md" radius="md">
|
<Paper p="md" radius="md">
|
||||||
<Select
|
<ListConfigTable options={configOptions} />
|
||||||
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="sm"
|
|
||||||
value={strategyValue}
|
|
||||||
variant="filled"
|
|
||||||
w="100%"
|
|
||||||
/>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p="md" radius="md">
|
<Paper p="md" radius="md">
|
||||||
<NumberInput
|
<ListConfigTable options={toggleOptions} />
|
||||||
aria-label={itemLabels.title}
|
|
||||||
description={itemLabels.description}
|
|
||||||
hideControls={false}
|
|
||||||
label={itemLabels.title}
|
|
||||||
max={50}
|
|
||||||
min={1}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
autoDJ: {
|
|
||||||
itemCount: Number(e),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
value={Number(settings.itemCount)}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
<Paper p="md" radius="md">
|
|
||||||
<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="sm"
|
|
||||||
value={Number(settings.timing)}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
<Paper p="md" radius="md">
|
|
||||||
<Group align="center" gap="sm" justify="space-between" wrap="nowrap">
|
|
||||||
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<Text fw={600} isNoSelect size="sm">
|
|
||||||
{t('setting.autoDJ_allowDuplicates')}
|
|
||||||
</Text>
|
|
||||||
<Text isMuted isNoSelect size="xs">
|
|
||||||
{t('setting.autoDJ_allowDuplicates_description')}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Switch
|
|
||||||
checked={settings.allowDuplicates}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
autoDJ: {
|
|
||||||
allowDuplicates: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
<Paper p="md" radius="md">
|
|
||||||
<Group align="center" gap="sm" justify="space-between" wrap="nowrap">
|
|
||||||
<Stack gap="xs" style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<Text fw={600} isNoSelect size="sm">
|
|
||||||
{t('setting.autoDJ_onlySimilar')}
|
|
||||||
</Text>
|
|
||||||
<Text isMuted isNoSelect size="xs">
|
|
||||||
{t('setting.autoDJ_onlySimilar_description')}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Switch
|
|
||||||
checked={settings.onlySimilar}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
autoDJ: {
|
|
||||||
onlySimilar: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.active-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--theme-spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import styles from './sleep-timer-button.module.css';
|
||||||
|
|
||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import {
|
import {
|
||||||
@@ -17,13 +19,13 @@ import {
|
|||||||
} from '/@/renderer/store/sleep-timer.store';
|
} from '/@/renderer/store/sleep-timer.store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
|
||||||
import { Grid } from '/@/shared/components/grid/grid';
|
import { Grid } from '/@/shared/components/grid/grid';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
|
import { Paper } from '/@/shared/components/paper/paper';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
import { PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -196,7 +198,6 @@ export const SleepTimerButton = () => {
|
|||||||
const remaining = useSleepTimerRemaining();
|
const remaining = useSleepTimerRemaining();
|
||||||
const { cancelTimer, startEndOfAlbumTimer, startEndOfSongTimer, startTimedTimer } =
|
const { cancelTimer, startEndOfAlbumTimer, startEndOfSongTimer, startTimedTimer } =
|
||||||
useSleepTimerActions();
|
useSleepTimerActions();
|
||||||
const { mediaPause } = usePlayer();
|
|
||||||
const shuffle = usePlayerShuffle();
|
const shuffle = usePlayerShuffle();
|
||||||
// Track level shuffle scatters and album across a play queue making 'end-of-album'
|
// Track level shuffle scatters and album across a play queue making 'end-of-album'
|
||||||
// meaningless. Album shuffle keeps each album intact, so keep 'end-of-'album
|
// meaningless. Album shuffle keeps each album intact, so keep 'end-of-'album
|
||||||
@@ -209,9 +210,6 @@ export const SleepTimerButton = () => {
|
|||||||
const [customSeconds, setCustomSeconds] = useState<number>(0);
|
const [customSeconds, setCustomSeconds] = useState<number>(0);
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
const mediaPauseRef = useRef(mediaPause);
|
|
||||||
mediaPauseRef.current = mediaPause;
|
|
||||||
|
|
||||||
const handlePreset = useCallback(
|
const handlePreset = useCallback(
|
||||||
(option: (typeof PRESET_OPTIONS)[number]) => {
|
(option: (typeof PRESET_OPTIONS)[number]) => {
|
||||||
if (option.mode === 'endOfSong') {
|
if (option.mode === 'endOfSong') {
|
||||||
@@ -248,18 +246,12 @@ export const SleepTimerButton = () => {
|
|||||||
if (option.mode === 'endOfAlbum') {
|
if (option.mode === 'endOfAlbum') {
|
||||||
return t('player.sleepTimer_endOfAlbum');
|
return t('player.sleepTimer_endOfAlbum');
|
||||||
}
|
}
|
||||||
if (option.minutes >= 60) {
|
|
||||||
return t('player.sleepTimer_hours', {
|
return `${option.minutes}`;
|
||||||
count: option.minutes / 60,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return t('player.sleepTimer_minutes', {
|
|
||||||
count: option.minutes,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover onChange={setOpened} opened={opened} position="top" width={260}>
|
<Popover onChange={setOpened} opened={opened} position="top" withArrow>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={active ? 'sleepTimer' : 'sleepTimerOff'}
|
icon={active ? 'sleepTimer' : 'sleepTimerOff'}
|
||||||
@@ -279,167 +271,176 @@ export const SleepTimerButton = () => {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown maw={480} miw={320} onClick={(e) => e.stopPropagation()} p="sm">
|
||||||
<Stack gap="xs" p="xs">
|
<Stack gap="sm">
|
||||||
<Text fw="600" pb="md" size="sm" ta="center">
|
<Paper className={styles.activeStatus} p="md" radius="md">
|
||||||
{t('player.sleepTimer')}
|
<Text fw={600} size="sm" ta="center">
|
||||||
</Text>
|
{t('player.sleepTimer')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{active && (
|
{active && (
|
||||||
<Flex
|
<>
|
||||||
align="center"
|
{mode === 'endOfSong' ? (
|
||||||
direction="column"
|
<Text c="primary" size="xl">
|
||||||
gap={4}
|
{t('player.sleepTimer_endOfSong')}
|
||||||
mb="xs"
|
</Text>
|
||||||
style={{
|
) : mode === 'endOfAlbum' ? (
|
||||||
background: 'var(--theme-colors-surface)',
|
<Text c="primary" size="xl">
|
||||||
borderRadius: 'var(--theme-radius-md)',
|
{t('player.sleepTimer_endOfAlbum')}
|
||||||
padding: 'var(--theme-spacing-sm) var(--theme-spacing-md)',
|
</Text>
|
||||||
}}
|
) : (
|
||||||
>
|
<TextTitle c="primary" order={3}>
|
||||||
{mode === 'endOfSong' ? (
|
{formatRemaining(remaining)}
|
||||||
<Text c="primary" size="sm">
|
</TextTitle>
|
||||||
{t('player.sleepTimer_endOfSong')}
|
)}
|
||||||
</Text>
|
<Button
|
||||||
) : mode === 'endOfAlbum' ? (
|
onClick={(e) => {
|
||||||
<Text c="primary" size="sm">
|
e.stopPropagation();
|
||||||
{t('player.sleepTimer_endOfAlbum')}
|
handleCancel();
|
||||||
</Text>
|
}}
|
||||||
) : (
|
size="compact-xs"
|
||||||
<Text c="primary" fw="600" size="lg">
|
variant="subtle"
|
||||||
{formatRemaining(remaining)}
|
>
|
||||||
</Text>
|
{t('player.sleepTimer_cancel')}
|
||||||
)}
|
</Button>
|
||||||
<Button
|
</>
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.stopPropagation();
|
</Paper>
|
||||||
handleCancel();
|
|
||||||
}}
|
|
||||||
size="compact-xs"
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{t('player.sleepTimer_cancel')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{PRESET_OPTIONS.filter(
|
<Paper p="md" radius="md">
|
||||||
(option) => option.mode === 'endOfSong' || option.mode === 'endOfAlbum',
|
<Stack gap="xs">
|
||||||
).map((option) => {
|
{PRESET_OPTIONS.filter(
|
||||||
const disabled = option.mode === 'endOfAlbum' && isTrackShuffle;
|
(option) =>
|
||||||
|
option.mode === 'endOfSong' || option.mode === 'endOfAlbum',
|
||||||
|
).map((option) => {
|
||||||
|
const disabled = option.mode === 'endOfAlbum' && isTrackShuffle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
fullWidth
|
|
||||||
justify="flex-start"
|
|
||||||
key={option.mode}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handlePreset(option);
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{getPresetLabel(option)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Divider my="md" />
|
|
||||||
|
|
||||||
<Grid gap="xs">
|
|
||||||
{PRESET_OPTIONS.filter((option) => option.mode === 'timed').map(
|
|
||||||
(option, index) => (
|
|
||||||
<Grid.Col key={index} span={4}>
|
|
||||||
<Button
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
justify="flex-start"
|
justify="center"
|
||||||
key={index}
|
key={option.mode}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePreset(option);
|
handlePreset(option);
|
||||||
}}
|
}}
|
||||||
size="xs"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{getPresetLabel(option)}
|
{getPresetLabel(option)}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid.Col>
|
);
|
||||||
),
|
})}
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Divider my="md" />
|
|
||||||
|
|
||||||
{!showCustom ? (
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
justify="flex-start"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowCustom(true);
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
ta="center"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t('player.sleepTimer_custom')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group gap={4} wrap="nowrap">
|
|
||||||
<NumberInput
|
|
||||||
max={23}
|
|
||||||
min={0}
|
|
||||||
onChange={(val) => setCustomHours(Number(val) || 0)}
|
|
||||||
placeholder="hr"
|
|
||||||
size="xs"
|
|
||||||
value={customHours}
|
|
||||||
/>
|
|
||||||
<Text>:</Text>
|
|
||||||
<NumberInput
|
|
||||||
max={59}
|
|
||||||
min={0}
|
|
||||||
onChange={(val) => setCustomMinutes(Number(val) || 0)}
|
|
||||||
placeholder="min"
|
|
||||||
size="xs"
|
|
||||||
value={customMinutes}
|
|
||||||
/>
|
|
||||||
<Text>:</Text>
|
|
||||||
<NumberInput
|
|
||||||
max={59}
|
|
||||||
min={0}
|
|
||||||
onChange={(val) => setCustomSeconds(Number(val) || 0)}
|
|
||||||
placeholder="sec"
|
|
||||||
size="xs"
|
|
||||||
value={customSeconds}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs" grow>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCustomStart();
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
variant="filled"
|
|
||||||
>
|
|
||||||
{t('player.sleepTimer_setCustom')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowCustom(false);
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
variant="default"
|
|
||||||
>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
</Paper>
|
||||||
|
|
||||||
|
<Paper p="md" radius="md">
|
||||||
|
<Grid gap="xs">
|
||||||
|
{PRESET_OPTIONS.filter((option) => option.mode === 'timed').map(
|
||||||
|
(option, index) => (
|
||||||
|
<Grid.Col key={index} span={4}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
justify="center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePreset(option);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{getPresetLabel(option)}
|
||||||
|
</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper p="md" radius="md">
|
||||||
|
{!showCustom ? (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
justify="center"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowCustom(true);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
ta="center"
|
||||||
|
>
|
||||||
|
{t('player.sleepTimer_custom')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<NumberInput
|
||||||
|
max={23}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomHours(Number(val) || 0)}
|
||||||
|
rightSection={
|
||||||
|
<Text pr="md" size="xs">
|
||||||
|
hr
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={customHours}
|
||||||
|
variant="filled"
|
||||||
|
/>
|
||||||
|
<Text>:</Text>
|
||||||
|
<NumberInput
|
||||||
|
max={59}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomMinutes(Number(val) || 0)}
|
||||||
|
rightSection={
|
||||||
|
<Text pr="md" size="xs">
|
||||||
|
min
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={customMinutes}
|
||||||
|
variant="filled"
|
||||||
|
/>
|
||||||
|
<Text>:</Text>
|
||||||
|
<NumberInput
|
||||||
|
max={59}
|
||||||
|
min={0}
|
||||||
|
onChange={(val) => setCustomSeconds(Number(val) || 0)}
|
||||||
|
rightSection={
|
||||||
|
<Text pr="md" size="xs">
|
||||||
|
sec
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
value={customSeconds}
|
||||||
|
variant="filled"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" grow>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCustomStart();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('player.sleepTimer_setCustom')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowCustom(false);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -5,11 +5,21 @@
|
|||||||
.th {
|
.th {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0;
|
padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0;
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: middle;
|
||||||
background-color: initial;
|
background-color: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.td {
|
.td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider-cell {
|
.divider-cell {
|
||||||
|
|||||||
@@ -266,10 +266,13 @@ export const ListConfigTable = ({
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Tr key={option.id}>
|
<Table.Tr key={option.id}>
|
||||||
<Table.Th>{option.label}</Table.Th>
|
<Table.Th>{option.label}</Table.Th>
|
||||||
<Table.Td>{option.component}</Table.Td>
|
<Table.Td>
|
||||||
|
<div className={styles.control}>{option.component}</div>
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user