1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-20 02:00:49 +00:00

WIP Timer UI

This commit is contained in:
Ash Keel 2021-11-05 19:30:14 +01:00
parent f9538f062d
commit 08d3b68f2e
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
10 changed files with 400 additions and 32 deletions

View file

@ -62,30 +62,7 @@ export function useUserPoints(): LoyaltyStorage {
return data; 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 { export default {
useModule, useModule,
useUserPoints, useUserPoints,
useInterval,
}; };

View file

@ -6,6 +6,7 @@
"/twitch/settings": "Module configuration", "/twitch/settings": "Module configuration",
"/twitch/bot/settings": "Bot configuration", "/twitch/bot/settings": "Bot configuration",
"/twitch/bot/commands": "Bot commands", "/twitch/bot/commands": "Bot commands",
"/twitch/bot/timers": "Bot timers",
"/twitch/bot/modules": "Bot modules", "/twitch/bot/modules": "Bot modules",
"/loyalty": "Loyalty points", "/loyalty": "Loyalty points",
"/loyalty/settings": "Configuration", "/loyalty/settings": "Configuration",
@ -204,6 +205,20 @@
"new-command": "New command", "new-command": "New command",
"search": "Search by name", "search": "Search by name",
"modify-command": "Modify command" "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"
} }
} }
} }

View file

@ -147,6 +147,13 @@ export const modules = {
state.moduleConfigs.twitchBotConfig = payload; state.moduleConfigs.twitchBotConfig = payload;
}, },
), ),
twitchBotModulesConfig: makeModule(
'twitch/bot-modules/config',
(state) => state.twitchBot?.modules,
(state, { payload }) => {
state.twitchBot.modules = payload;
},
),
twitchBotCommands: makeModule( twitchBotCommands: makeModule(
'twitch/bot-custom-commands', 'twitch/bot-custom-commands',
(state) => state.twitchBot?.commands, (state) => state.twitchBot?.commands,
@ -154,6 +161,13 @@ export const modules = {
state.twitchBot.commands = payload; state.twitchBot.commands = payload;
}, },
), ),
twitchBotTimers: makeModule(
'twitch/bot-modules/timers/config',
(state) => state.twitchBot?.timers,
(state, { payload }) => {
state.twitchBot.timers = payload;
},
),
stulbeConfig: makeModule( stulbeConfig: makeModule(
'stulbe/config', 'stulbe/config',
(state) => state.moduleConfigs?.stulbeConfig, (state) => state.moduleConfigs?.stulbeConfig,
@ -229,6 +243,8 @@ const initialState: APIState = {
}, },
twitchBot: { twitchBot: {
commands: null, commands: null,
modules: null,
timers: null,
}, },
moduleConfigs: { moduleConfigs: {
moduleConfig: null, moduleConfig: null,

View file

@ -30,6 +30,10 @@ interface TwitchBotConfig {
chat_history: number; chat_history: number;
} }
interface TwitchModulesConfig {
enable_timers: boolean;
}
type AccessLevelType = 'everyone' | 'vip' | 'moderators' | 'streamer'; type AccessLevelType = 'everyone' | 'vip' | 'moderators' | 'streamer';
export interface TwitchBotCustomCommand { export interface TwitchBotCustomCommand {
@ -57,6 +61,18 @@ interface LoyaltyConfig {
banlist: string[]; 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 { export interface LoyaltyPointsEntry {
points: number; points: number;
} }
@ -105,6 +121,8 @@ export interface APIState {
}; };
twitchBot: { twitchBot: {
commands: TwitchBotCustomCommands; commands: TwitchBotCustomCommands;
modules: TwitchModulesConfig;
timers: TwitchBotTimersConfig;
}; };
moduleConfigs: { moduleConfigs: {
moduleConfig: ModuleConfig; moduleConfig: ModuleConfig;

View file

@ -22,6 +22,7 @@ import TwitchBotCommandsPage from './pages/twitch/Commands';
import TwitchBotModulesPage from './pages/twitch/Modules'; import TwitchBotModulesPage from './pages/twitch/Modules';
import StulbeConfigPage from './pages/stulbe/Config'; import StulbeConfigPage from './pages/stulbe/Config';
import StulbeWebhooksPage from './pages/stulbe/Webhook'; import StulbeWebhooksPage from './pages/stulbe/Webhook';
import TwitchBotTimersPage from './pages/twitch/Timers';
interface RouteItem { interface RouteItem {
name?: string; name?: string;
@ -38,6 +39,7 @@ const menu: RouteItem[] = [
{ route: '/twitch/settings' }, { route: '/twitch/settings' },
{ route: '/twitch/bot/settings' }, { route: '/twitch/bot/settings' },
{ route: '/twitch/bot/commands' }, { route: '/twitch/bot/commands' },
{ route: '/twitch/bot/timers' },
{ route: '/twitch/bot/modules' }, { route: '/twitch/bot/modules' },
], ],
}, },
@ -127,6 +129,7 @@ export default function App(): React.ReactElement {
<TwitchSettingsPage path="settings" /> <TwitchSettingsPage path="settings" />
<TwitchBotSettingsPage path="bot/settings" /> <TwitchBotSettingsPage path="bot/settings" />
<TwitchBotCommandsPage path="bot/commands" /> <TwitchBotCommandsPage path="bot/commands" />
<TwitchBotTimersPage path="bot/timers" />
<TwitchBotModulesPage path="bot/modules" /> <TwitchBotModulesPage path="bot/modules" />
</TwitchPage> </TwitchPage>
<LoyaltyPage path="loyalty"> <LoyaltyPage path="loyalty">

View file

@ -1,19 +1,29 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useInterval } from '../../lib/react-utils'; import { getInterval } from '../../lib/time-utils';
export interface IntervalProps { export interface IntervalProps {
active: boolean; active: boolean;
value: number; value: number;
min?: number;
onChange?: (value: number) => void; onChange?: (value: number) => void;
} }
function Interval({ active, value, onChange }: IntervalProps) { function Interval({ active, value, min, onChange }: IntervalProps) {
const { t } = useTranslation(); 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(() => { useEffect(() => {
onChange(valueNum); const seconds = num * mult;
}, [valueNum]); if (min && seconds < min) {
setNum(5);
setMult(1);
}
onChange(Math.max(min ?? 0, seconds));
}, [num, mult]);
return ( return (
<> <>
@ -24,6 +34,7 @@ function Interval({ active, value, onChange }: IntervalProps) {
type="number" type="number"
placeholder="#" placeholder="#"
value={num ?? ''} value={num ?? ''}
style={{ width: '6em' }}
onChange={(ev) => { onChange={(ev) => {
const intNum = parseInt(ev.target.value, 10); const intNum = parseInt(ev.target.value, 10);
if (Number.isNaN(intNum)) { if (Number.isNaN(intNum)) {

View file

@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import prettyTime from 'pretty-ms'; import prettyTime from 'pretty-ms';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useInterval, useModule } from '../../../lib/react-utils'; import { useModule } from '../../../lib/react-utils';
import { RootState } from '../../../store'; import { RootState } from '../../../store';
import { createRedeem, modules } from '../../../store/api/reducer'; import { createRedeem, modules } from '../../../store/api/reducer';
import Modal from '../../components/Modal'; import Modal from '../../components/Modal';

View file

@ -122,7 +122,12 @@ export default function LoyaltySettingPage(
{t('loyalty.config.points-every')} {t('loyalty.config.points-every')}
</a> </a>
</p> </p>
<Interval value={interval} onChange={setInterval} active={active} /> <Interval
value={interval}
onChange={setInterval}
active={active}
min={5}
/>
</div> </div>
</Field> </Field>
<Field name={t('loyalty.config.bonus-points')}> <Field name={t('loyalty.config.bonus-points')}>

View 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>
</>
);
}

View file

@ -91,7 +91,11 @@ func (m *BotTimerModule) runTimers() {
if !ok { if !ok {
continue 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 continue
} }
// Make sure chat activity is high enough // Make sure chat activity is high enough