1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

feat: add UI config object, wip ui settings page

This commit is contained in:
Ash Keel 2022-12-16 11:10:43 +01:00
parent 1c5db13b0d
commit d7586e9bb9
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
25 changed files with 230 additions and 42 deletions

View file

@ -16,6 +16,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-radio-group": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toggle": "^1.0.1",
@ -953,6 +954,28 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.0.tgz",
"integrity": "sha512-7rrkZCXu0Q7oC0MxCm497X1DdV/tI78oNIGXA8sDbCkboiTkuLSe728zCCpRYHw+9PifHIx86nsbITPEq5yijg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-roving-focus": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
@ -5817,6 +5840,24 @@
"@radix-ui/react-slot": "1.0.1"
}
},
"@radix-ui/react-radio-group": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.0.tgz",
"integrity": "sha512-7rrkZCXu0Q7oC0MxCm497X1DdV/tI78oNIGXA8sDbCkboiTkuLSe728zCCpRYHw+9PifHIx86nsbITPEq5yijg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-roving-focus": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
}
},
"@radix-ui/react-roving-focus": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",

View file

@ -11,6 +11,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-radio-group": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toggle": "^1.0.1",

View file

@ -1 +1 @@
b120abf6c619728b1d1a33e9dda709c0
1e088af20465ec91bc3495c250e91b1a

View file

@ -1,4 +1,7 @@
{
"$meta": {
"language-name": "English"
},
"menu": {
"sections": {
"monitor": "Monitor",
@ -11,7 +14,8 @@
"dashboard": "Dashboard"
},
"strimertul": {
"settings": "Server settings"
"settings": "Server settings",
"ui-config": "User interface"
},
"twitch": {
"configuration": "Configuration",
@ -253,7 +257,14 @@
"not-live": "Offline / Not streaming"
},
"onboarding": {
"welcome-header": "Welcome to {{APPNAME}}"
"welcome-header": "Welcome to {{APPNAME}}",
"welcome-p1": "It looks like this is the first time you started {{APPNAME}}. You can click the \"Get started\" button at the bottom to set things up one by one, or just skip everything and configure things manually later.",
"welcome-continue-button": "Get started",
"skip-button": "Skip onboarding"
},
"uiconfig": {
"title": "User interface settings",
"language": "Language"
}
},
"form-actions": {

View file

@ -1,4 +1,7 @@
{
"$meta": {
"language-name": "Italiano"
},
"form-actions": {
"save": "Salva",
"saving": "Sto salvando...",

View file

@ -0,0 +1,34 @@
import { ResourceKey } from 'i18next';
import en from './en/translation.json';
import it from './it/translation.json';
function countKeys(res: ResourceKey): number {
if (typeof res === 'string') {
return 1;
}
return Object.values(res).reduce<number>(
(acc: number, k: ResourceKey) => acc + countKeys(k),
0,
);
}
interface LanguageMeta {
'language-name': string;
}
export const resources = {
en: {
translation: en,
},
it: {
translation: it,
},
} as const;
export const languages = Object.entries(resources).map(([code, lang]) => ({
code,
name: (lang.translation.$meta as LanguageMeta)['language-name'] || code,
keys: countKeys(lang),
}));
export default resources;

View file

@ -1,19 +1,10 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { APPNAME } from '~/ui/theme';
import en from './en/translation.json';
import it from './it/translation.json';
import { resources } from './languages';
void i18n.use(initReactI18next).init({
resources: {
en: {
translation: en,
},
it: {
translation: it,
},
},
resources,
lng: navigator.language,
fallbackLng: 'en',
interpolation: {

View file

@ -230,6 +230,14 @@ export const modules = {
state.loyalty.redeemQueue = payload;
},
),
uiConfig: makeModule(
'ui/settings',
(state) => state.uiConfig,
(state, { payload }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.uiConfig = payload;
},
),
};
export const createRedeem = createAsyncThunk(
@ -280,6 +288,7 @@ const initialState: APIState = {
twitchBotConfig: null,
loyaltyConfig: null,
},
uiConfig: null,
requestStatus: {},
};

View file

@ -143,6 +143,11 @@ export interface LoyaltyRedeem {
request_text: string;
}
export interface UISettings {
onboardingDone: boolean;
language: string;
}
export enum ConnectionStatus {
NotConnected,
AuthenticationNeeded,
@ -176,5 +181,6 @@ export interface APIState {
twitchBotConfig: TwitchBotConfig;
loyaltyConfig: LoyaltyConfig;
};
uiConfig: UISettings;
requestStatus: Record<string, RequestStatus>;
}

View file

@ -1,6 +1,7 @@
import { configureStore } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import { EqualityFn, useDispatch, useSelector } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import apiReducer from './api/reducer';
import loggingReducer from './logging/reducer';
@ -19,5 +20,9 @@ const store = configureStore({
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: <Selected = unknown>(
selector: (state: RootState) => Selected,
equalityFn?: EqualityFn<Selected> | undefined,
) => Selected = useSelector;
export default store;

View file

@ -1,9 +1,10 @@
import {
ChatBubbleIcon,
CodeIcon,
DashboardIcon,
FrameIcon,
GearIcon,
MixerHorizontalIcon,
MixIcon,
StarIcon,
TableIcon,
TimerIcon,
@ -11,7 +12,6 @@ import {
import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime';
import { t } from 'i18next';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import {
@ -21,14 +21,16 @@ import {
} from '@wailsapp/go/main/App';
import { main } from '@wailsapp/go/models';
import { RootState, useAppDispatch } from '~/store';
import { useAppDispatch, useAppSelector } from '~/store';
import { createWSClient, useAuthBypass } from '~/store/api/reducer';
import { ConnectionStatus } from '~/store/api/types';
import loggingReducer from '~/store/logging/reducer';
// @ts-expect-error Asset import
import spinner from '~/assets/icon-loading.svg';
import LogViewer from './components/LogViewer';
import Sidebar, { RouteSection } from './components/Sidebar';
import Scrollbar from './components/utils/Scrollbar';
import AuthDialog from './pages/AuthDialog';
import TwitchBotCommandsPage from './pages/BotCommands';
import TwitchBotTimersPage from './pages/BotTimers';
@ -38,13 +40,12 @@ import DebugPage from './pages/Debug';
import LoyaltyConfigPage from './pages/LoyaltyConfig';
import LoyaltyQueuePage from './pages/LoyaltyQueue';
import LoyaltyRewardsPage from './pages/LoyaltyRewards';
import OnboardingPage from './pages/Onboarding';
import ServerSettingsPage from './pages/ServerSettings';
import StrimertulPage from './pages/Strimertul';
import TwitchSettingsPage from './pages/TwitchSettings';
import UISettingsPage from './pages/UISettingsPage';
import { styled } from './theme';
import Scrollbar from './components/utils/Scrollbar';
import LogViewer from './components/LogViewer';
import OnboardingPage from './pages/Onboarding';
const LoadingDiv = styled('div', {
display: 'flex',
@ -87,7 +88,12 @@ const sections: RouteSection[] = [
{
title: 'menu.pages.strimertul.settings',
url: '/http',
icon: <GearIcon />,
icon: <CodeIcon />,
},
{
title: 'menu.pages.strimertul.ui-config',
url: '/ui-config',
icon: <MixIcon />,
},
],
},
@ -160,10 +166,9 @@ const PageWrapper = styled('div', {
export default function App(): JSX.Element {
const [ready, setReady] = useState(false);
const client = useSelector((state: RootState) => state.api.client);
const connected = useSelector(
(state: RootState) => state.api.connectionStatus,
);
const client = useAppSelector((state) => state.api.client);
const uiConfig = useAppSelector((state) => state.api.uiConfig);
const connected = useAppSelector((state) => state.api.connectionStatus);
const dispatch = useAppDispatch();
const location = useLocation();
const navigate = useNavigate();
@ -212,10 +217,12 @@ export default function App(): JSX.Element {
}
}, [ready, connected]);
// TODO: Only do this when
const onboardingDone = uiConfig?.onboardingDone;
useEffect(() => {
navigate('/setup');
}, []);
if (!onboardingDone) {
navigate('/setup');
}
}, [ready, onboardingDone]);
if (connected === ConnectionStatus.NotConnected) {
return <Loading message={t('special.loading')} />;
@ -244,6 +251,7 @@ export default function App(): JSX.Element {
<Route path="/about" element={<StrimertulPage />} />
<Route path="/debug" element={<DebugPage />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/ui-config" element={<UISettingsPage />} />
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
<Route
path="/twitch/bot/commands"

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getInterval } from '~/lib/time-utils';
import { ComboBox, FlexRow, InputBox } from '../theme';
import { ComboBox, FlexRow, InputBox } from '../../theme';
export interface TimeUnit {
multiplier: number;

View file

@ -1,7 +1,7 @@
import { Cross2Icon } from '@radix-ui/react-icons';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button, FlexRow, Textarea } from '../theme';
import { Button, FlexRow, Textarea } from '../../theme';
export interface MultiInputProps {
placeholder?: string;

View file

@ -0,0 +1,31 @@
import React, { ReactElement } from 'react';
import { Root, Item, Indicator } from '@radix-ui/react-radio-group';
export interface RadioGroupProps {
label: string;
selected?: string;
values: {
id: string;
label: string | ReactElement;
}[];
default?: string;
}
export default function RadioGroup(props: RadioGroupProps) {
return (
<Root
defaultValue={props.default}
value={props.selected}
aria-label={props.label}
>
{props.values.map(({ id, label }) => (
<div key={id} style={{ display: 'flex', alignItems: 'center' }}>
<Item value="default" id={`r${id}`}>
<Indicator />
</Item>
<label htmlFor={`r${id}`}>{label}</label>
</div>
))}
</Root>
);
}

View file

@ -8,8 +8,8 @@ import { modules } from '~/store/api/reducer';
import { TwitchBotTimer } from '~/store/api/types';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import Interval, { hours, minutes } from '../components/Interval';
import MultiInput from '../components/MultiInput';
import Interval, { hours, minutes } from '../components/forms/Interval';
import MultiInput from '../components/forms/MultiInput';
import {
Button,
Dialog,

View file

@ -4,7 +4,7 @@ import { CheckIcon } from '@radix-ui/react-icons';
import { useModule, useStatus } from '~/lib/react-utils';
import apiReducer, { modules } from '~/store/api/reducer';
import { useAppDispatch } from '~/store';
import MultiInput from '../components/MultiInput';
import MultiInput from '../components/forms/MultiInput';
import {
Checkbox,
CheckboxIndicator,
@ -20,7 +20,7 @@ import {
TabList,
TextBlock,
} from '../theme';
import SaveButton from '../components/utils/SaveButton';
import SaveButton from '../components/forms/SaveButton';
export default function ChatAlertsPage(): React.ReactElement {
const { t } = useTranslation();

View file

@ -17,8 +17,8 @@ import {
InputBox,
FieldNote,
} from '../theme';
import SaveButton from '../components/utils/SaveButton';
import Interval from '../components/Interval';
import SaveButton from '../components/forms/SaveButton';
import Interval from '../components/forms/Interval';
export default function LoyaltySettingsPage(): React.ReactElement {
const { t } = useTranslation();

View file

@ -7,7 +7,7 @@ import { modules } from '~/store/api/reducer';
import { LoyaltyGoal, LoyaltyReward } from '~/store/api/types';
import AlertContent from '../components/AlertContent';
import DialogContent from '../components/DialogContent';
import Interval from '../components/Interval';
import Interval from '../components/forms/Interval';
import {
Button,
Checkbox,

View file

@ -5,7 +5,7 @@ import { useAppDispatch } from '~/store';
import apiReducer, { modules } from '~/store/api/reducer';
import AlertContent from '../components/AlertContent';
import RevealLink from '../components/utils/RevealLink';
import SaveButton from '../components/utils/SaveButton';
import SaveButton from '../components/forms/SaveButton';
import {
Field,
FieldNote,

View file

@ -12,7 +12,7 @@ import apiReducer, { modules } from '~/store/api/reducer';
import BrowserLink from '../components/BrowserLink';
import DefinitionTable from '../components/DefinitionTable';
import RevealLink from '../components/utils/RevealLink';
import SaveButton from '../components/utils/SaveButton';
import SaveButton from '../components/forms/SaveButton';
import {
Button,
ButtonGroup,

View file

@ -0,0 +1,48 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useModule } from '~/lib/react-utils';
import { languages } from '~/locale/languages';
import { modules } from '~/store/api/reducer';
import RadioGroup from '../components/forms/RadioGroup';
import { Field, Label, PageContainer, PageHeader, PageTitle } from '../theme';
export default function UISettingsPage(): React.ReactElement {
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
const [t, i18n] = useTranslation();
const maxKeys = languages.reduce(
(current, it) => Math.max(current, it.keys),
0,
);
return (
<PageContainer>
<PageHeader css={{ paddingBottom: '1rem' }}>
<PageTitle>{t('pages.uiconfig.title')}</PageTitle>
</PageHeader>
<Field size="fullWidth">
<Label htmlFor="bind">{t('pages.uiconfig.language')}</Label>
<RadioGroup
label={t('pages.uiconfig.language')}
default={i18n.resolvedLanguage}
selected={uiConfig?.language ?? i18n.resolvedLanguage}
values={languages.map((lang) => ({
id: lang.code,
label: (
<span>
{lang.name}{' '}
{lang.keys < maxKeys ? (
<small>
Partial translation (
{((lang.keys / maxKeys) * 100).toFixed(1)}% - {lang.keys}/
{maxKeys})
</small>
) : null}
</span>
),
}))}
/>
</Field>
</PageContainer>
);
}

View file

@ -2,8 +2,8 @@ import * as UnstyledLabel from '@radix-ui/react-label';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import * as ToggleGroup from '@radix-ui/react-toggle-group';
import { styled, theme } from './theme';
import ControlledInput from '../components/utils/ControlledInput';
import PasswordField from '../components/utils/PasswordField';
import ControlledInput from '../components/forms/ControlledInput';
import PasswordField from '../components/forms/PasswordField';
export const Field = styled('fieldset', {
all: 'unset',