normalize playerbar config designs

This commit is contained in:
jeffvli
2026-07-23 23:46:28 -07:00
parent 4275fc237d
commit 315d2c54c0
6 changed files with 388 additions and 307 deletions
@@ -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>
); );
})} })}