mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
WIP Timer UI
This commit is contained in:
parent
f9538f062d
commit
08d3b68f2e
10 changed files with 400 additions and 32 deletions
|
@ -62,30 +62,7 @@ export function useUserPoints(): LoyaltyStorage {
|
|||
return data;
|
||||
}
|
||||
|
||||
export function useInterval(
|
||||
initialValue: number,
|
||||
): [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
(newNum: number) => void,
|
||||
(newMult: number) => void,
|
||||
] {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const [numInitialValue, multInitialValue] = getInterval(value);
|
||||
const [num, setNum] = useState(numInitialValue);
|
||||
const [mult, setMult] = useState(multInitialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(num * mult);
|
||||
}, [num, mult]);
|
||||
|
||||
return [value, num, mult, setNum, setMult];
|
||||
}
|
||||
|
||||
export default {
|
||||
useModule,
|
||||
useUserPoints,
|
||||
useInterval,
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"/twitch/settings": "Module configuration",
|
||||
"/twitch/bot/settings": "Bot configuration",
|
||||
"/twitch/bot/commands": "Bot commands",
|
||||
"/twitch/bot/timers": "Bot timers",
|
||||
"/twitch/bot/modules": "Bot modules",
|
||||
"/loyalty": "Loyalty points",
|
||||
"/loyalty/settings": "Configuration",
|
||||
|
@ -204,6 +205,20 @@
|
|||
"new-command": "New command",
|
||||
"search": "Search by name",
|
||||
"modify-command": "Modify command"
|
||||
},
|
||||
"timers": {
|
||||
"header": "Bot timers",
|
||||
"new-timer": "New timer",
|
||||
"modify-timer": "Modify timer",
|
||||
"search": "Search by timer name",
|
||||
"messages": "Messages",
|
||||
"name-hint": "Timer name",
|
||||
"name": "Name",
|
||||
"message-help": "What to write in chat",
|
||||
"minimum-delay": "Interval",
|
||||
"minimum-delay-help": "How many time must pass between each repeat.",
|
||||
"minimum-activity": "Minimum chat activity",
|
||||
"minimum-activity-post": "messages in the last 5 minutes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,6 +147,13 @@ export const modules = {
|
|||
state.moduleConfigs.twitchBotConfig = payload;
|
||||
},
|
||||
),
|
||||
twitchBotModulesConfig: makeModule(
|
||||
'twitch/bot-modules/config',
|
||||
(state) => state.twitchBot?.modules,
|
||||
(state, { payload }) => {
|
||||
state.twitchBot.modules = payload;
|
||||
},
|
||||
),
|
||||
twitchBotCommands: makeModule(
|
||||
'twitch/bot-custom-commands',
|
||||
(state) => state.twitchBot?.commands,
|
||||
|
@ -154,6 +161,13 @@ export const modules = {
|
|||
state.twitchBot.commands = payload;
|
||||
},
|
||||
),
|
||||
twitchBotTimers: makeModule(
|
||||
'twitch/bot-modules/timers/config',
|
||||
(state) => state.twitchBot?.timers,
|
||||
(state, { payload }) => {
|
||||
state.twitchBot.timers = payload;
|
||||
},
|
||||
),
|
||||
stulbeConfig: makeModule(
|
||||
'stulbe/config',
|
||||
(state) => state.moduleConfigs?.stulbeConfig,
|
||||
|
@ -229,6 +243,8 @@ const initialState: APIState = {
|
|||
},
|
||||
twitchBot: {
|
||||
commands: null,
|
||||
modules: null,
|
||||
timers: null,
|
||||
},
|
||||
moduleConfigs: {
|
||||
moduleConfig: null,
|
||||
|
|
|
@ -30,6 +30,10 @@ interface TwitchBotConfig {
|
|||
chat_history: number;
|
||||
}
|
||||
|
||||
interface TwitchModulesConfig {
|
||||
enable_timers: boolean;
|
||||
}
|
||||
|
||||
type AccessLevelType = 'everyone' | 'vip' | 'moderators' | 'streamer';
|
||||
|
||||
export interface TwitchBotCustomCommand {
|
||||
|
@ -57,6 +61,18 @@ interface LoyaltyConfig {
|
|||
banlist: string[];
|
||||
}
|
||||
|
||||
export interface TwitchBotTimer {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
minimum_chat_activity: number;
|
||||
minimum_delay: number;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
interface TwitchBotTimersConfig {
|
||||
timers: Record<string, TwitchBotTimer>;
|
||||
}
|
||||
|
||||
export interface LoyaltyPointsEntry {
|
||||
points: number;
|
||||
}
|
||||
|
@ -105,6 +121,8 @@ export interface APIState {
|
|||
};
|
||||
twitchBot: {
|
||||
commands: TwitchBotCustomCommands;
|
||||
modules: TwitchModulesConfig;
|
||||
timers: TwitchBotTimersConfig;
|
||||
};
|
||||
moduleConfigs: {
|
||||
moduleConfig: ModuleConfig;
|
||||
|
|
|
@ -22,6 +22,7 @@ import TwitchBotCommandsPage from './pages/twitch/Commands';
|
|||
import TwitchBotModulesPage from './pages/twitch/Modules';
|
||||
import StulbeConfigPage from './pages/stulbe/Config';
|
||||
import StulbeWebhooksPage from './pages/stulbe/Webhook';
|
||||
import TwitchBotTimersPage from './pages/twitch/Timers';
|
||||
|
||||
interface RouteItem {
|
||||
name?: string;
|
||||
|
@ -38,6 +39,7 @@ const menu: RouteItem[] = [
|
|||
{ route: '/twitch/settings' },
|
||||
{ route: '/twitch/bot/settings' },
|
||||
{ route: '/twitch/bot/commands' },
|
||||
{ route: '/twitch/bot/timers' },
|
||||
{ route: '/twitch/bot/modules' },
|
||||
],
|
||||
},
|
||||
|
@ -127,6 +129,7 @@ export default function App(): React.ReactElement {
|
|||
<TwitchSettingsPage path="settings" />
|
||||
<TwitchBotSettingsPage path="bot/settings" />
|
||||
<TwitchBotCommandsPage path="bot/commands" />
|
||||
<TwitchBotTimersPage path="bot/timers" />
|
||||
<TwitchBotModulesPage path="bot/modules" />
|
||||
</TwitchPage>
|
||||
<LoyaltyPage path="loyalty">
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useInterval } from '../../lib/react-utils';
|
||||
import { getInterval } from '../../lib/time-utils';
|
||||
|
||||
export interface IntervalProps {
|
||||
active: boolean;
|
||||
value: number;
|
||||
min?: number;
|
||||
onChange?: (value: number) => void;
|
||||
}
|
||||
|
||||
function Interval({ active, value, onChange }: IntervalProps) {
|
||||
function Interval({ active, value, min, onChange }: IntervalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [valueNum, num, mult, setNum, setMult] = useInterval(value);
|
||||
|
||||
const [numInitialValue, multInitialValue] = getInterval(value);
|
||||
const [num, setNum] = useState(numInitialValue);
|
||||
const [mult, setMult] = useState(multInitialValue);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(valueNum);
|
||||
}, [valueNum]);
|
||||
const seconds = num * mult;
|
||||
if (min && seconds < min) {
|
||||
setNum(5);
|
||||
setMult(1);
|
||||
}
|
||||
onChange(Math.max(min ?? 0, seconds));
|
||||
}, [num, mult]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -24,6 +34,7 @@ function Interval({ active, value, onChange }: IntervalProps) {
|
|||
type="number"
|
||||
placeholder="#"
|
||||
value={num ?? ''}
|
||||
style={{ width: '6em' }}
|
||||
onChange={(ev) => {
|
||||
const intNum = parseInt(ev.target.value, 10);
|
||||
if (Number.isNaN(intNum)) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
|||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import prettyTime from 'pretty-ms';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useInterval, useModule } from '../../../lib/react-utils';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { RootState } from '../../../store';
|
||||
import { createRedeem, modules } from '../../../store/api/reducer';
|
||||
import Modal from '../../components/Modal';
|
||||
|
|
|
@ -122,7 +122,12 @@ export default function LoyaltySettingPage(
|
|||
{t('loyalty.config.points-every')}
|
||||
</a>
|
||||
</p>
|
||||
<Interval value={interval} onChange={setInterval} active={active} />
|
||||
<Interval
|
||||
value={interval}
|
||||
onChange={setInterval}
|
||||
active={active}
|
||||
min={5}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Field name={t('loyalty.config.bonus-points')}>
|
||||
|
|
319
frontend/src/ui/pages/twitch/Timers.tsx
Normal file
319
frontend/src/ui/pages/twitch/Timers.tsx
Normal file
|
@ -0,0 +1,319 @@
|
|||
import { RouteComponentProps } from '@reach/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useModule } from '../../../lib/react-utils';
|
||||
import { modules } from '../../../store/api/reducer';
|
||||
import Modal from '../../components/Modal';
|
||||
import { TwitchBotTimer } from '../../../store/api/types';
|
||||
import Field from '../../components/Field';
|
||||
import Interval from '../../components/Interval';
|
||||
|
||||
interface TimerItemProps {
|
||||
item: TwitchBotTimer;
|
||||
onToggleState: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
function TimerItem({ item, onToggleState, onEdit, onDelete }: TimerItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="card customcommand" style={{ marginBottom: '3px' }}>
|
||||
<header className="card-header">
|
||||
<div className="card-header-title">
|
||||
{item.enabled ? (
|
||||
<code>{item.name}</code>
|
||||
) : (
|
||||
<span className="reward-disabled">
|
||||
<code>{item.name}</code>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
className="card-header-icon"
|
||||
aria-label="expand"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<span className={expanded ? 'icon expand-off' : 'icon expand-on'}>
|
||||
❯
|
||||
</span>
|
||||
</a>
|
||||
</header>
|
||||
{expanded ? (
|
||||
<div className="content">
|
||||
{t('twitch.timers.messages')}:{' '}
|
||||
{item.messages.map((message) => (
|
||||
<blockquote>{message}</blockquote>
|
||||
))}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<a className="button is-small" onClick={onToggleState}>
|
||||
{item.enabled ? 'Disable' : 'Enable'}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onEdit}>
|
||||
{t('actions.edit')}
|
||||
</a>{' '}
|
||||
<a className="button is-small" onClick={onDelete}>
|
||||
{t('actions.delete')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimerModalProps {
|
||||
active: boolean;
|
||||
onConfirm: (newName: string, r: TwitchBotTimer) => void;
|
||||
onClose: () => void;
|
||||
initialData?: TwitchBotTimer;
|
||||
initialName?: string;
|
||||
title: string;
|
||||
confirmText: string;
|
||||
}
|
||||
|
||||
function TimerModal({
|
||||
active,
|
||||
onConfirm,
|
||||
onClose,
|
||||
initialName,
|
||||
initialData,
|
||||
title,
|
||||
confirmText,
|
||||
}: TimerModalProps) {
|
||||
const [name, setName] = useState(initialName ?? '');
|
||||
const [messages, setMessages] = useState(initialData?.messages ?? ['']);
|
||||
const [minDelay, setMinDelay] = useState(initialData?.minimum_delay ?? 300);
|
||||
const [minActivity, setMinActivity] = useState(
|
||||
initialData?.minimum_chat_activity ?? 5,
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const validForm = name !== '' && messages.length > 0 && messages[0] !== '';
|
||||
|
||||
const confirm = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm(name, {
|
||||
name,
|
||||
messages,
|
||||
minimum_chat_activity: 0,
|
||||
minimum_delay: 0,
|
||||
enabled: initialData?.enabled ?? false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setMessageIndex = (value: string, index: number) => {
|
||||
const newMessages = [...messages];
|
||||
newMessages[index] = value;
|
||||
setMessages(newMessages);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
active={active}
|
||||
title={title}
|
||||
showCancel={true}
|
||||
bgDismiss={true}
|
||||
confirmName={confirmText}
|
||||
confirmClass="is-success"
|
||||
confirmEnabled={validForm}
|
||||
onConfirm={() => confirm()}
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<Field name={t('twitch.timers.name')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<p className="control">
|
||||
<input
|
||||
className={name !== '' ? 'input' : 'input is-danger'}
|
||||
type="text"
|
||||
placeholder={t('twitch.timers.name-hint')}
|
||||
value={name}
|
||||
onChange={(ev) => setName(ev.target.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<Field name={t('twitch.timers.minimum-delay')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons" style={{ marginBottom: 0 }}>
|
||||
<Interval
|
||||
value={minDelay}
|
||||
onChange={setMinDelay}
|
||||
active={active}
|
||||
min={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<Field name={t('twitch.timers.minimum-activity')} horizontal>
|
||||
<div className="field-body">
|
||||
<div className="field has-addons" style={{ marginBottom: 0 }}>
|
||||
<p className="control">
|
||||
<input
|
||||
disabled={!active}
|
||||
className="input"
|
||||
type="number"
|
||||
placeholder="#"
|
||||
style={{ width: '6rem' }}
|
||||
value={minActivity ?? 0}
|
||||
onChange={(ev) => {
|
||||
const amount = parseInt(ev.target.value, 10);
|
||||
if (Number.isNaN(amount)) {
|
||||
return;
|
||||
}
|
||||
setMinActivity(amount);
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p className="control">
|
||||
<a className="button is-static">
|
||||
{t('twitch.timers.minimum-activity-post')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
<Field name={t('twitch.timers.messages')} horizontal>
|
||||
<div className="field-body">
|
||||
{messages.map((message, index) => (
|
||||
<div className="field">
|
||||
<p className="control">
|
||||
<textarea
|
||||
className={message !== '' ? 'textarea' : 'textarea is-danger'}
|
||||
placeholder={t('twitch.timers.message-help')}
|
||||
onChange={(ev) => setMessageIndex(ev.target.value, index)}
|
||||
value={message}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TwitchBotTimersPage(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
props: RouteComponentProps<unknown>,
|
||||
): React.ReactElement {
|
||||
const [timers, setTimers] = useModule(modules.twitchBotTimers);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [createModal, setCreateModal] = useState(false);
|
||||
const [showModifyTimer, setShowModifyTimer] = useState(null);
|
||||
const [timerFilter, setTimerFilter] = useState('');
|
||||
const timerFilterLC = timerFilter.toLowerCase();
|
||||
|
||||
const createTimer = (name: string, data: TwitchBotTimer): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
[name]: data,
|
||||
}),
|
||||
);
|
||||
setCreateModal(false);
|
||||
};
|
||||
|
||||
const modifyTimer = (
|
||||
oldName: string,
|
||||
newName: string,
|
||||
data: TwitchBotTimer,
|
||||
): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
[oldName]: undefined,
|
||||
[newName]: {
|
||||
...timers[oldName],
|
||||
...data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
setShowModifyTimer(null);
|
||||
};
|
||||
|
||||
const deleteTimer = (cmd: string): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
[cmd]: undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTimer = (cmd: string): void => {
|
||||
dispatch(
|
||||
setTimers({
|
||||
...timers,
|
||||
[cmd]: {
|
||||
...timers[cmd],
|
||||
enabled: !timers[cmd].enabled,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="title is-4">{t('twitch.timers.header')}</h1>
|
||||
<div className="field is-grouped">
|
||||
<p className="control">
|
||||
<button className="button" onClick={() => setCreateModal(true)}>
|
||||
{t('twitch.timers.new-timer')}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder={t('twitch.timers.search')}
|
||||
value={timerFilter}
|
||||
onChange={(ev) => setTimerFilter(ev.target.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TimerModal
|
||||
title={t('twitch.timers.new-timer')}
|
||||
confirmText={t('actions.create')}
|
||||
active={createModal}
|
||||
onConfirm={createTimer}
|
||||
onClose={() => setCreateModal(false)}
|
||||
/>
|
||||
{showModifyTimer ? (
|
||||
<TimerModal
|
||||
title={t('twitch.timers.modify-timer')}
|
||||
confirmText={t('actions.edit')}
|
||||
active={true}
|
||||
onConfirm={(newName, cmdData) =>
|
||||
modifyTimer(showModifyTimer, newName, cmdData)
|
||||
}
|
||||
initialName={showModifyTimer}
|
||||
initialData={showModifyTimer ? timers[showModifyTimer] : null}
|
||||
onClose={() => setShowModifyTimer(null)}
|
||||
/>
|
||||
) : null}
|
||||
<div className="reward-list" style={{ marginTop: '1rem' }}>
|
||||
{Object.keys(timers ?? {})
|
||||
?.filter((cmd) => cmd.toLowerCase().includes(timerFilterLC))
|
||||
.map((timer) => (
|
||||
<TimerItem
|
||||
key={timer}
|
||||
item={timer[timer]}
|
||||
onDelete={() => deleteTimer(timer)}
|
||||
onEdit={() => setShowModifyTimer(timer)}
|
||||
onToggleState={() => toggleTimer(timer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -91,7 +91,11 @@ func (m *BotTimerModule) runTimers() {
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
if now.Sub(lastTriggeredTime) < time.Duration(timer.MinimumDelay)*time.Second {
|
||||
minDelay := timer.MinimumDelay
|
||||
if minDelay < 5 {
|
||||
minDelay = 5
|
||||
}
|
||||
if now.Sub(lastTriggeredTime) < time.Duration(minDelay)*time.Second {
|
||||
continue
|
||||
}
|
||||
// Make sure chat activity is high enough
|
||||
|
|
Loading…
Reference in a new issue