mirror of https://git.sr.ht/~ashkeel/strimertul
448 lines
12 KiB
TypeScript
448 lines
12 KiB
TypeScript
import { PlusIcon } from '@radix-ui/react-icons';
|
|
import { TFunction } from 'i18next';
|
|
import React, { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useModule } from '~/lib/react';
|
|
import { useAppDispatch } from '~/store';
|
|
import { modules } from '~/store/api/reducer';
|
|
import { TwitchChatTimer } from '~/store/api/types';
|
|
import AlertContent from '../components/AlertContent';
|
|
import DialogContent from '../components/DialogContent';
|
|
import Interval from '../components/forms/Interval';
|
|
import { hours, minutes } from '../components/forms/units';
|
|
import MultiInput from '../components/forms/MultiInput';
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogClose,
|
|
Field,
|
|
FlexRow,
|
|
InputBox,
|
|
Label,
|
|
MultiButton,
|
|
NoneText,
|
|
PageContainer,
|
|
PageHeader,
|
|
PageTitle,
|
|
styled,
|
|
TextBlock,
|
|
} from '../theme';
|
|
import { Alert, AlertTrigger } from '../theme/alert';
|
|
|
|
const TimerList = styled('div', { marginTop: '1rem' });
|
|
const TimerItemContainer = styled('article', {
|
|
backgroundColor: '$gray3',
|
|
margin: '0.5rem 0',
|
|
padding: '0.5rem',
|
|
borderLeft: '5px solid $teal8',
|
|
borderRadius: '0.25rem',
|
|
borderBottom: '1px solid $gray4',
|
|
transition: 'all 50ms',
|
|
'&:hover': {
|
|
backgroundColor: '$gray3',
|
|
},
|
|
variants: {
|
|
status: {
|
|
enabled: {},
|
|
disabled: {
|
|
borderLeftColor: '$red7',
|
|
backgroundColor: '$gray3',
|
|
color: '$gray10',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const TimerHeader = styled('header', {
|
|
display: 'flex',
|
|
gap: '0.5rem',
|
|
alignItems: 'center',
|
|
marginBottom: '0.4rem',
|
|
});
|
|
const TimerName = styled('span', {
|
|
color: '$teal10',
|
|
fontWeight: 'bold',
|
|
variants: {
|
|
status: {
|
|
enabled: {},
|
|
disabled: {
|
|
color: '$gray10',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const TimerDescription = styled('span', {
|
|
flex: 1,
|
|
});
|
|
const TimerActions = styled('div', {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.25rem',
|
|
});
|
|
const TimerText = styled('div', {
|
|
fontFamily: 'Space Mono',
|
|
fontSize: '10pt',
|
|
margin: '0 -0.5rem',
|
|
marginTop: '0',
|
|
marginBottom: '0.3rem',
|
|
padding: '0.5rem',
|
|
backgroundColor: '$gray4',
|
|
lineHeight: '1.2rem',
|
|
'&:last-child': {
|
|
marginBottom: '-0.5rem',
|
|
},
|
|
});
|
|
|
|
function humanTime(t: TFunction<'translation'>, secs: number): string {
|
|
const mins = Math.floor(secs / 60);
|
|
const hrs = Math.floor(mins / 60);
|
|
|
|
if (hrs > 0) {
|
|
return t('time.x-hours', { time: hrs });
|
|
}
|
|
if (mins > 0) {
|
|
return t('time.x-minutes', { time: mins });
|
|
}
|
|
return t('time.x-seconds', { time: secs });
|
|
}
|
|
|
|
interface TimerItemProps {
|
|
name: string;
|
|
item: TwitchChatTimer;
|
|
onToggle?: () => void;
|
|
onEdit?: () => void;
|
|
onDelete?: () => void;
|
|
}
|
|
|
|
function TimerItem({
|
|
name,
|
|
item,
|
|
onToggle,
|
|
onEdit,
|
|
onDelete,
|
|
}: TimerItemProps): React.ReactElement {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<TimerItemContainer status={item.enabled ? 'enabled' : 'disabled'}>
|
|
<TimerHeader>
|
|
<TimerName status={item.enabled ? 'enabled' : 'disabled'}>
|
|
{name}
|
|
</TimerName>
|
|
<TimerDescription>
|
|
(
|
|
{t('pages.bottimers.timer-parameters', {
|
|
time: humanTime(t, item.minimum_delay),
|
|
messages: item.minimum_chat_activity,
|
|
interval: humanTime(t, 300),
|
|
})}
|
|
)
|
|
</TimerDescription>
|
|
<TimerActions>
|
|
<MultiButton>
|
|
<Button
|
|
styling="multi"
|
|
size="small"
|
|
onClick={() => (onToggle ? onToggle() : null)}
|
|
>
|
|
{t(item.enabled ? 'form-actions.disable' : 'form-actions.enable')}
|
|
</Button>
|
|
<Button
|
|
styling="multi"
|
|
size="small"
|
|
onClick={() => (onEdit ? onEdit() : null)}
|
|
>
|
|
{t('form-actions.edit')}
|
|
</Button>
|
|
<Alert>
|
|
<AlertTrigger asChild>
|
|
<Button styling="multi" size="small">
|
|
{t('form-actions.delete')}
|
|
</Button>
|
|
</AlertTrigger>
|
|
<AlertContent
|
|
variation="danger"
|
|
title={t('pages.bottimers.remove-timer-title', { name })}
|
|
description={t('form-actions.warning-delete')}
|
|
actionText={t('form-actions.delete')}
|
|
actionButtonProps={{ variation: 'danger' }}
|
|
showCancel={true}
|
|
onAction={() => (onDelete ? onDelete() : null)}
|
|
/>
|
|
</Alert>
|
|
</MultiButton>
|
|
</TimerActions>
|
|
</TimerHeader>
|
|
{item.messages?.map((message, index) => (
|
|
<TimerText key={index}>{message}</TimerText>
|
|
))}
|
|
</TimerItemContainer>
|
|
);
|
|
}
|
|
|
|
type DialogPrompt =
|
|
| { kind: 'new' }
|
|
| { kind: 'edit'; name: string; item: TwitchChatTimer };
|
|
|
|
function TimerDialog({
|
|
kind,
|
|
name,
|
|
item,
|
|
onSubmit,
|
|
}: {
|
|
kind: 'new' | 'edit';
|
|
name?: string;
|
|
item?: TwitchChatTimer;
|
|
onSubmit?: (name: string, item: TwitchChatTimer) => void;
|
|
}) {
|
|
const [timerConfig] = useModule(modules.twitchChatTimers);
|
|
const [timerName, setName] = useState(name ?? '');
|
|
const [messages, setMessages] = useState(item?.messages ?? ['']);
|
|
const [minDelay, setMinDelay] = useState(item?.minimum_delay ?? 300);
|
|
const [minActivity, setMinActivity] = useState(
|
|
item?.minimum_chat_activity ?? 5,
|
|
);
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<DialogContent
|
|
title={t(`pages.bottimers.timer-header-${kind}`)}
|
|
closeButton={true}
|
|
>
|
|
<form
|
|
onSubmit={(e) => {
|
|
if (!(e.target as HTMLFormElement).checkValidity()) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
if (onSubmit) {
|
|
onSubmit(timerName, {
|
|
...item,
|
|
messages,
|
|
minimum_delay: minDelay,
|
|
minimum_chat_activity: minActivity,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<Field spacing="narrow" size="fullWidth">
|
|
<Label htmlFor="timer-name">{t('pages.bottimers.timer-name')}</Label>
|
|
<InputBox
|
|
id="timer-name"
|
|
value={timerName}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
// If timer name is different but matches another defined timer, set as invalid
|
|
if (
|
|
e.target.value !== name &&
|
|
e.target.value in timerConfig.timers
|
|
) {
|
|
(e.target as HTMLInputElement).setCustomValidity(
|
|
t('pages.bottimers.name-already-in-use'),
|
|
);
|
|
} else {
|
|
(e.target as HTMLInputElement).setCustomValidity('');
|
|
}
|
|
}}
|
|
placeholder={t('pages.bottimers.timer-name-placeholder')}
|
|
required={true}
|
|
/>
|
|
</Field>
|
|
<Field spacing="narrow" size="fullWidth">
|
|
<Label htmlFor="timer-interval">
|
|
{t('pages.bottimers.timer-interval')}
|
|
</Label>
|
|
<FlexRow align="left">
|
|
<Interval
|
|
id="timer-interval"
|
|
value={minDelay}
|
|
onChange={setMinDelay}
|
|
active={true}
|
|
min={60}
|
|
units={[minutes, hours]}
|
|
required={true}
|
|
/>
|
|
</FlexRow>
|
|
</Field>
|
|
<Field spacing="narrow" size="fullWidth">
|
|
<Label htmlFor="timer-activity">
|
|
{t('pages.bottimers.timer-activity')}
|
|
</Label>
|
|
<FlexRow align="left" spacing={1}>
|
|
<InputBox
|
|
id="timer-activity"
|
|
defaultValue={minActivity}
|
|
type="number"
|
|
css={{
|
|
width: '5rem',
|
|
}}
|
|
required={true}
|
|
onChange={(ev) => {
|
|
const intNum = parseInt(ev.target.value, 10);
|
|
if (Number.isNaN(intNum)) {
|
|
return;
|
|
}
|
|
setMinActivity(intNum);
|
|
}}
|
|
placeholder="#"
|
|
/>
|
|
<span>{t('pages.bottimers.timer-activity-desc')}</span>
|
|
</FlexRow>
|
|
</Field>
|
|
|
|
<Field spacing="narrow" size="fullWidth">
|
|
<Label>{t('pages.bottimers.timer-messages')}</Label>
|
|
<MultiInput required={true} value={messages} onChange={setMessages} />
|
|
</Field>
|
|
|
|
<DialogActions>
|
|
<Button variation="primary">
|
|
{kind === 'new' ? t('form-actions.create') : t('form-actions.edit')}
|
|
</Button>
|
|
<DialogClose asChild>
|
|
<Button type="button">{t('form-actions.cancel')}</Button>
|
|
</DialogClose>
|
|
</DialogActions>
|
|
</form>
|
|
</DialogContent>
|
|
);
|
|
}
|
|
|
|
export default function TwitchChatTimersPage(): React.ReactElement {
|
|
const [timerConfig, setTimerConfig] = useModule(modules.twitchChatTimers);
|
|
const [filter, setFilter] = useState('');
|
|
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
|
const { t } = useTranslation();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const filterLC = filter.toLowerCase();
|
|
|
|
const setTimer = (newName: string, data: TwitchChatTimer): void => {
|
|
switch (activeDialog.kind) {
|
|
case 'new':
|
|
void dispatch(
|
|
setTimerConfig({
|
|
...timerConfig,
|
|
timers: {
|
|
...timerConfig.timers,
|
|
[newName]: {
|
|
...data,
|
|
enabled: true,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
break;
|
|
case 'edit': {
|
|
const oldName = activeDialog.name;
|
|
void dispatch(
|
|
setTimerConfig({
|
|
...timerConfig,
|
|
timers: {
|
|
...timerConfig.timers,
|
|
[oldName]: undefined,
|
|
[newName]: data,
|
|
},
|
|
}),
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
setActiveDialog(null);
|
|
};
|
|
|
|
const deleteTimer = (cmd: string): void => {
|
|
void dispatch(
|
|
setTimerConfig({
|
|
...timerConfig,
|
|
timers: {
|
|
...timerConfig.timers,
|
|
[cmd]: undefined,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
const toggleTimer = (cmd: string): void => {
|
|
void dispatch(
|
|
setTimerConfig({
|
|
...timerConfig,
|
|
timers: {
|
|
...timerConfig.timers,
|
|
[cmd]: {
|
|
...timerConfig.timers[cmd],
|
|
enabled: !timerConfig.timers[cmd].enabled,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader>
|
|
<PageTitle>{t('pages.bottimers.title')}</PageTitle>
|
|
<TextBlock>{t('pages.bottimers.desc')}</TextBlock>
|
|
</PageHeader>
|
|
|
|
<FlexRow spacing="1" align="left">
|
|
<Button
|
|
variation="primary"
|
|
onClick={() => setActiveDialog({ kind: 'new' })}
|
|
>
|
|
<PlusIcon /> {t('pages.bottimers.add-button')}
|
|
</Button>
|
|
|
|
<InputBox
|
|
css={{ flex: 1 }}
|
|
placeholder={t('pages.bottimers.search-placeholder')}
|
|
value={filter}
|
|
onChange={(e) => setFilter(e.target.value)}
|
|
/>
|
|
</FlexRow>
|
|
<TimerList>
|
|
{timerConfig?.timers ? (
|
|
Object.keys(timerConfig?.timers ?? {})
|
|
?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
|
|
.sort()
|
|
.map((cmd) => (
|
|
<TimerItem
|
|
key={cmd}
|
|
name={cmd}
|
|
item={timerConfig.timers[cmd]}
|
|
onToggle={() => toggleTimer(cmd)}
|
|
onEdit={() =>
|
|
setActiveDialog({
|
|
kind: 'edit',
|
|
name: cmd,
|
|
item: timerConfig.timers[cmd],
|
|
})
|
|
}
|
|
onDelete={() => deleteTimer(cmd)}
|
|
/>
|
|
))
|
|
) : (
|
|
<NoneText>{t('pages.bottimers.no-timers')}</NoneText>
|
|
)}
|
|
</TimerList>
|
|
|
|
<Dialog
|
|
open={!!activeDialog}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
// Reset dialog status on dialog close
|
|
setActiveDialog(null);
|
|
}
|
|
}}
|
|
>
|
|
{activeDialog && (
|
|
<TimerDialog
|
|
{...activeDialog}
|
|
onSubmit={(name, data) => setTimer(name, data)}
|
|
/>
|
|
)}
|
|
</Dialog>
|
|
</PageContainer>
|
|
);
|
|
}
|