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

feat: add user lang/theme to fatal error screen

This commit is contained in:
Ash Keel 2023-10-29 17:36:23 +01:00
parent 11e7741f83
commit 7e4d358f60
No known key found for this signature in database
GPG key ID: 53A9E9A6035DD109
11 changed files with 130 additions and 78 deletions

View file

@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added an unfinished light mode, check the UI settings - Added a light theme, can be set during onboarding or in the UI settings
- The fatal error screen now uses the user configured language and theme
- Added the ability to hide viewer count in the dashboard - Added the ability to hide viewer count in the dashboard
- Custom chat commands can now be sent as replies, whispers and announcements. Due to some API shenanigans yet to be solved, the latter two will always be sent from your main account, not the bot account (if they are different) - Custom chat commands can now be sent as replies, whispers and announcements. Due to some API shenanigans yet to be solved, the latter two will always be sent from your main account, not the bot account (if they are different)
- Added a structured RPC `twitch/bot/@send-message` for sending messages as replies, announcements and whispers. - Added a structured RPC `twitch/bot/@send-message` for sending messages as replies, announcements and whispers.

2
app.go
View file

@ -117,7 +117,7 @@ func (a *App) startup(ctx context.Context) {
go a.forwardLogs() go a.forwardLogs()
// Run HTTP server // Run HTTP server
if err := a.httpServer.Listen(); err != nil { if err = a.httpServer.Listen(); err != nil {
a.showFatalError(err, "HTTP server stopped") a.showFatalError(err, "HTTP server stopped")
return return
} }

View file

@ -98,7 +98,7 @@
"app-category": "Category", "app-category": "Category",
"app-oauth-redirect-url": "OAuth Redirect URLs", "app-oauth-redirect-url": "OAuth Redirect URLs",
"test-button": "Test connection", "test-button": "Test connection",
"test-failed": "Test failed: \"{{0}}\". Check your app client IDs and secret!", "test-failed": "Test failed: \"{{error}}\". Check your app client IDs and secret!",
"test-succeeded": "Test succeeded!", "test-succeeded": "Test succeeded!",
"bot-chat-cooldown-tip": "Global chat cooldown for commands (in seconds)" "bot-chat-cooldown-tip": "Global chat cooldown for commands (in seconds)"
}, },

View file

@ -359,7 +359,7 @@
} }
}, },
"test-button": "Test connessione", "test-button": "Test connessione",
"test-failed": "Test fallito: \"{{0}}\". \nControlla ID e segreto client dell'app!", "test-failed": "Test fallito: \"{{error}}\". \nControlla ID e segreto client dell'app!",
"test-succeeded": "Test riuscito!", "test-succeeded": "Test riuscito!",
"bot-chat-cooldown-tip": "Tempo minimo di attesa tra comandi (in secondi)" "bot-chat-cooldown-tip": "Tempo minimo di attesa tra comandi (in secondi)"
}, },

View file

@ -220,7 +220,11 @@ export default function App(): JSX.Element {
// Sync UI changes on key change // Sync UI changes on key change
useEffect(() => { useEffect(() => {
if (uiConfig?.language) { if (uiConfig?.language) {
void i18n.changeLanguage(uiConfig?.language ?? 'en'); void i18n.changeLanguage(uiConfig.language ?? 'en');
localStorage.setItem('language', uiConfig.language);
}
if (uiConfig?.theme) {
localStorage.setItem('theme', uiConfig.theme);
} }
if (!uiConfig?.onboardingDone) { if (!uiConfig?.onboardingDone) {
navigate('/setup'); navigate('/setup');

View file

@ -22,6 +22,7 @@ import {
DialogActions, DialogActions,
Field, Field,
FlexRow, FlexRow,
getTheme,
InputBox, InputBox,
Label, Label,
MultiToggle, MultiToggle,
@ -43,6 +44,8 @@ const Container = styled('div', {
overflow: 'hidden', overflow: 'hidden',
height: '100vh', height: '100vh',
border: '2px solid $red10', border: '2px solid $red10',
backgroundColor: '$gray1',
color: '$gray12',
}); });
const ErrorHeader = styled('h1', { const ErrorHeader = styled('h1', {
@ -497,6 +500,7 @@ export default function ErrorWindow(): JSX.Element {
const [recoveryDialogOpen, setRecoveryDialogOpen] = useState(false); const [recoveryDialogOpen, setRecoveryDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
void i18n.changeLanguage(localStorage.getItem('language') ?? 'en');
void GetLastLogs().then((appLogs) => { void GetLastLogs().then((appLogs) => {
setLogs(appLogs.map(processEntry).reverse()); setLogs(appLogs.map(processEntry).reverse());
}); });
@ -509,9 +513,10 @@ export default function ErrorWindow(): JSX.Element {
}, []); }, []);
const fatal = logs.find((log) => log.level === 'error'); const fatal = logs.find((log) => log.level === 'error');
const theme = getTheme(localStorage.getItem('theme') ?? 'dark');
return ( return (
<Container> <Container id="app-container" className={theme}>
<ReportDialog <ReportDialog
open={reportDialogOpen} open={reportDialogOpen}
onOpenChange={setReportDialogOpen} onOpenChange={setReportDialogOpen}
@ -541,62 +546,64 @@ export default function ErrorWindow(): JSX.Element {
))} ))}
</MultiToggle> </MultiToggle>
</LanguageSelector> </LanguageSelector>
<PageContainer> <Scrollbar vertical={true} viewport={{ flex: '1', maxHeight: '100vh' }}>
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}> <div style={{ width: '100vw' }}>
<PageHeader> <PageContainer>
<TextBlock>{t('pages.crash.fatal-message')}</TextBlock> <PageHeader>
</PageHeader> <TextBlock>{t('pages.crash.fatal-message')}</TextBlock>
{fatal ? ( </PageHeader>
<> {fatal ? (
<ErrorHeader>{fatal.message}</ErrorHeader> <>
<ErrorDetails> <ErrorHeader>{fatal.message}</ErrorHeader>
{Object.keys(fatal.data) <ErrorDetails>
.filter((key) => key.length > 1) {Object.keys(fatal.data)
.map((key) => ( .filter((key) => key.length > 1)
<Fragment key={key}> .map((key) => (
<ErrorDetailKey>{key}</ErrorDetailKey> <Fragment key={key}>
<ErrorDetailValue>{fatal.data[key]}</ErrorDetailValue> <ErrorDetailKey>{key}</ErrorDetailKey>
</Fragment> <ErrorDetailValue>{fatal.data[key]}</ErrorDetailValue>
))} </Fragment>
</ErrorDetails> ))}
</> </ErrorDetails>
) : null} </>
<SectionHeader>{t('pages.crash.action-header')}</SectionHeader> ) : null}
<TextBlock>{t('pages.crash.action-submit-line')}</TextBlock> <SectionHeader>{t('pages.crash.action-header')}</SectionHeader>
<TextBlock>{t('pages.crash.action-recover-line')}</TextBlock> <TextBlock>{t('pages.crash.action-submit-line')}</TextBlock>
<TextBlock> <TextBlock>{t('pages.crash.action-recover-line')}</TextBlock>
<Trans <TextBlock>
t={t} <Trans
i18nKey="pages.crash.action-log-line" t={t}
values={{ i18nKey="pages.crash.action-log-line"
A: 'strimertul.log', values={{
B: 'strimertul-panic.log', A: 'strimertul.log',
}} B: 'strimertul-panic.log',
components={{ }}
m: <Mono />, components={{
}} m: <Mono />,
/> }}
</TextBlock> />
<FlexRow align="left" spacing={1} css={{ paddingTop: '0.5rem' }}> </TextBlock>
<Button <FlexRow align="left" spacing={1} css={{ paddingTop: '0.5rem' }}>
variation={'danger'} <Button
onClick={() => setReportDialogOpen(true)} variation={'danger'}
> onClick={() => setReportDialogOpen(true)}
{t('pages.crash.button-report')} >
</Button> {t('pages.crash.button-report')}
<Button onClick={() => setRecoveryDialogOpen(true)}> </Button>
{t('pages.crash.button-recovery')} <Button onClick={() => setRecoveryDialogOpen(true)}>
</Button> {t('pages.crash.button-recovery')}
</FlexRow> </Button>
</FlexRow>
<MiniHeader>{t('pages.crash.app-log-header')}</MiniHeader> <MiniHeader>{t('pages.crash.app-log-header')}</MiniHeader>
<LogContainer> <LogContainer>
{logs.map((log) => ( {logs.map((log) => (
<LogItem key={log.time.toString()} data={log} /> <LogItem key={log.time.toString()} data={log} />
))} ))}
</LogContainer> </LogContainer>
</Scrollbar> </PageContainer>
</PageContainer> </div>
</Scrollbar>
</Container> </Container>
); );
} }

View file

@ -144,6 +144,14 @@ const MenuLink = styled(Link, {
}, },
}); });
const AppLogo = styled('img', {
height: '28px',
marginBottom: '-2px',
[`.${lightMode} &`]: {
filter: 'invert(1)',
},
});
function SidebarLink({ route: { title, url, icon } }: { route: Route }) { function SidebarLink({ route: { title, url, icon } }: { route: Route }) {
const { t } = useTranslation(); const { t } = useTranslation();
const resolved = useResolvedPath(url); const resolved = useResolvedPath(url);
@ -254,10 +262,7 @@ export default function Sidebar({
<Header> <Header>
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}> <AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
<AppName> <AppName>
<img <AppLogo src={logo as string} />
src={logo as string}
style={{ height: '28px', marginBottom: '-2px' }}
/>
{APPNAME} {APPNAME}
</AppName> </AppName>
<VersionLabel> <VersionLabel>

View file

@ -31,6 +31,7 @@ import {
Field, Field,
InputBox, InputBox,
Label, Label,
lightMode,
MultiToggle, MultiToggle,
MultiToggleItem, MultiToggleItem,
PageContainer, PageContainer,
@ -38,6 +39,7 @@ import {
SectionHeader, SectionHeader,
styled, styled,
TextBlock, TextBlock,
themes,
} from '../theme'; } from '../theme';
import { Alert } from '../theme/alert'; import { Alert } from '../theme/alert';
@ -86,7 +88,7 @@ const HeroContainer = styled('div', {
overflow: 'hidden', overflow: 'hidden',
}); });
const HeroLanguageSelector = styled('div', { const HeroSelector = styled('div', {
top: '10px', top: '10px',
left: '10px', left: '10px',
display: 'flex', display: 'flex',
@ -95,7 +97,7 @@ const HeroLanguageSelector = styled('div', {
zIndex: '10', zIndex: '10',
}); });
const LanguageItem = styled(MultiToggleItem, { const HeroSelectorItem = styled(MultiToggleItem, {
fontSize: '1rem', fontSize: '1rem',
padding: '5px 8px', padding: '5px 8px',
}); });
@ -161,6 +163,10 @@ const StepList = styled('nav', {
flexWrap: 'wrap', flexWrap: 'wrap',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-start', justifyContent: 'flex-start',
[`.${lightMode} &`]: {
borderBottom: '1px solid $gray6',
backgroundColor: '$gray2',
},
}); });
const StepName = styled('div', { const StepName = styled('div', {
@ -180,6 +186,10 @@ const StepName = styled('div', {
active: { active: {
color: '$gray12', color: '$gray12',
display: 'inherit', display: 'inherit',
[`.${lightMode} &`]: {
fontWeight: '500',
},
}, },
}, },
interaction: { interaction: {
@ -403,9 +413,9 @@ function TwitchIntegrationStep() {
variation={testResult.error ? 'danger' : 'default'} variation={testResult.error ? 'danger' : 'default'}
description={ description={
testResult.error testResult.error
? t('pages.twitch-settings.test-failed', [ ? t('pages.twitch-settings.test-failed', {
testResult.error.message, error: testResult.error.message,
]) })
: t('pages.twitch-settings.test-succeeded') : t('pages.twitch-settings.test-succeeded')
} }
actionText={t('form-actions.ok')} actionText={t('form-actions.ok')}
@ -684,7 +694,7 @@ export default function OnboardingPage() {
<TopBanner> <TopBanner>
{landing ? ( {landing ? (
<HeroContainer> <HeroContainer>
<HeroLanguageSelector> <HeroSelector>
<MultiToggle <MultiToggle
value={uiConfig?.language ?? i18n.resolvedLanguage} value={uiConfig?.language ?? i18n.resolvedLanguage}
type="single" type="single"
@ -692,10 +702,11 @@ export default function OnboardingPage() {
void dispatch( void dispatch(
setUiConfig({ ...uiConfig, language: newLang }), setUiConfig({ ...uiConfig, language: newLang }),
); );
localStorage.setItem('language', newLang);
}} }}
> >
{languages.map((lang) => ( {languages.map((lang) => (
<LanguageItem <HeroSelectorItem
key={lang.code} key={lang.code}
aria-label={lang.name} aria-label={lang.name}
value={lang.code} value={lang.code}
@ -707,10 +718,29 @@ export default function OnboardingPage() {
> >
{lang.name} {lang.name}
{lang.keys < maxKeys ? <ExclamationTriangleIcon /> : null} {lang.keys < maxKeys ? <ExclamationTriangleIcon /> : null}
</LanguageItem> </HeroSelectorItem>
))} ))}
</MultiToggle> </MultiToggle>
</HeroLanguageSelector>
<MultiToggle
value={uiConfig?.theme ?? 'dark'}
type="single"
onValueChange={(newTheme) => {
void dispatch(setUiConfig({ ...uiConfig, theme: newTheme }));
localStorage.setItem('theme', newTheme);
}}
>
{themes.map((theme) => (
<HeroSelectorItem
key={theme}
value={theme}
aria-label={t(`pages.uiconfig.themes.${theme}`)}
>
{t(`pages.uiconfig.themes.${theme}`)}
</HeroSelectorItem>
))}
</MultiToggle>
</HeroSelector>
<HeroAnimation>{animationItems}</HeroAnimation> <HeroAnimation>{animationItems}</HeroAnimation>
<HeroTitle>{t('pages.onboarding.welcome-header')}</HeroTitle> <HeroTitle>{t('pages.onboarding.welcome-header')}</HeroTitle>
<HeroContent> <HeroContent>

View file

@ -357,9 +357,9 @@ function TwitchAPISettings() {
variation={testResult.error ? 'danger' : 'default'} variation={testResult.error ? 'danger' : 'default'}
description={ description={
testResult.error testResult.error
? t('pages.twitch-settings.test-failed', [ ? t('pages.twitch-settings.test-failed', {
testResult.error.message, error: testResult.error.message,
]) })
: t('pages.twitch-settings.test-succeeded') : t('pages.twitch-settings.test-succeeded')
} }
actionText={t('form-actions.ok')} actionText={t('form-actions.ok')}

View file

@ -13,6 +13,7 @@ import {
PageHeader, PageHeader,
PageTitle, PageTitle,
styled, styled,
themes,
} from '../theme'; } from '../theme';
const PartialWarning = styled('small', { const PartialWarning = styled('small', {
@ -42,6 +43,7 @@ export default function UISettingsPage(): React.ReactElement {
value={uiConfig?.language ?? i18n.resolvedLanguage} value={uiConfig?.language ?? i18n.resolvedLanguage}
onValueChange={(value) => { onValueChange={(value) => {
void dispatch(setUiConfig({ ...uiConfig, language: value })); void dispatch(setUiConfig({ ...uiConfig, language: value }));
localStorage.setItem('language', value);
}} }}
values={languages.map((lang) => ({ values={languages.map((lang) => ({
id: lang.code, id: lang.code,
@ -68,8 +70,9 @@ export default function UISettingsPage(): React.ReactElement {
value={uiConfig?.theme ?? 'dark'} value={uiConfig?.theme ?? 'dark'}
onValueChange={(value) => { onValueChange={(value) => {
void dispatch(setUiConfig({ ...uiConfig, theme: value })); void dispatch(setUiConfig({ ...uiConfig, theme: value }));
localStorage.setItem('theme', value);
}} }}
values={['dark', 'light'].map((theme) => ({ values={themes.map((theme) => ({
id: theme, id: theme,
label: t(`pages.uiconfig.themes.${theme}`), label: t(`pages.uiconfig.themes.${theme}`),
}))} }))}

View file

@ -76,6 +76,8 @@ export const lightMode = createTheme({
}, },
}); });
export const themes = ['dark', 'light'];
export function getTheme(themeName: string) { export function getTheme(themeName: string) {
switch (themeName) { switch (themeName) {
case 'light': case 'light':