From 7e4d358f6026575fa1c702b8e5a76cf7266277fa Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Sun, 29 Oct 2023 17:36:23 +0100 Subject: [PATCH] feat: add user lang/theme to fatal error screen --- CHANGELOG.md | 3 +- app.go | 2 +- frontend/src/locale/en/translation.json | 2 +- frontend/src/locale/it/translation.json | 2 +- frontend/src/ui/App.tsx | 6 +- frontend/src/ui/ErrorWindow.tsx | 119 ++++++++++++----------- frontend/src/ui/components/Sidebar.tsx | 13 ++- frontend/src/ui/pages/Onboarding.tsx | 48 +++++++-- frontend/src/ui/pages/TwitchSettings.tsx | 6 +- frontend/src/ui/pages/UISettingsPage.tsx | 5 +- frontend/src/ui/theme/theme.ts | 2 + 11 files changed, 130 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc11bd5..31f4a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 - 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. diff --git a/app.go b/app.go index 90a2373..17a8796 100644 --- a/app.go +++ b/app.go @@ -117,7 +117,7 @@ func (a *App) startup(ctx context.Context) { go a.forwardLogs() // Run HTTP server - if err := a.httpServer.Listen(); err != nil { + if err = a.httpServer.Listen(); err != nil { a.showFatalError(err, "HTTP server stopped") return } diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index 49c3870..724ecca 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -98,7 +98,7 @@ "app-category": "Category", "app-oauth-redirect-url": "OAuth Redirect URLs", "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!", "bot-chat-cooldown-tip": "Global chat cooldown for commands (in seconds)" }, diff --git a/frontend/src/locale/it/translation.json b/frontend/src/locale/it/translation.json index 2287fea..362709b 100644 --- a/frontend/src/locale/it/translation.json +++ b/frontend/src/locale/it/translation.json @@ -359,7 +359,7 @@ } }, "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!", "bot-chat-cooldown-tip": "Tempo minimo di attesa tra comandi (in secondi)" }, diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 6f47a6f..46300ae 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -220,7 +220,11 @@ export default function App(): JSX.Element { // Sync UI changes on key change useEffect(() => { 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) { navigate('/setup'); diff --git a/frontend/src/ui/ErrorWindow.tsx b/frontend/src/ui/ErrorWindow.tsx index 434aacc..3aee7fb 100644 --- a/frontend/src/ui/ErrorWindow.tsx +++ b/frontend/src/ui/ErrorWindow.tsx @@ -22,6 +22,7 @@ import { DialogActions, Field, FlexRow, + getTheme, InputBox, Label, MultiToggle, @@ -43,6 +44,8 @@ const Container = styled('div', { overflow: 'hidden', height: '100vh', border: '2px solid $red10', + backgroundColor: '$gray1', + color: '$gray12', }); const ErrorHeader = styled('h1', { @@ -497,6 +500,7 @@ export default function ErrorWindow(): JSX.Element { const [recoveryDialogOpen, setRecoveryDialogOpen] = useState(false); useEffect(() => { + void i18n.changeLanguage(localStorage.getItem('language') ?? 'en'); void GetLastLogs().then((appLogs) => { setLogs(appLogs.map(processEntry).reverse()); }); @@ -509,9 +513,10 @@ export default function ErrorWindow(): JSX.Element { }, []); const fatal = logs.find((log) => log.level === 'error'); + const theme = getTheme(localStorage.getItem('theme') ?? 'dark'); return ( - + - - - - {t('pages.crash.fatal-message')} - - {fatal ? ( - <> - {fatal.message} - - {Object.keys(fatal.data) - .filter((key) => key.length > 1) - .map((key) => ( - - {key} - {fatal.data[key]} - - ))} - - - ) : null} - {t('pages.crash.action-header')} - {t('pages.crash.action-submit-line')} - {t('pages.crash.action-recover-line')} - - , - }} - /> - - - - - + +
+ + + {t('pages.crash.fatal-message')} + + {fatal ? ( + <> + {fatal.message} + + {Object.keys(fatal.data) + .filter((key) => key.length > 1) + .map((key) => ( + + {key} + {fatal.data[key]} + + ))} + + + ) : null} + {t('pages.crash.action-header')} + {t('pages.crash.action-submit-line')} + {t('pages.crash.action-recover-line')} + + , + }} + /> + + + + + - {t('pages.crash.app-log-header')} - - {logs.map((log) => ( - - ))} - - - + {t('pages.crash.app-log-header')} + + {logs.map((log) => ( + + ))} + + +
+
); } diff --git a/frontend/src/ui/components/Sidebar.tsx b/frontend/src/ui/components/Sidebar.tsx index 394a33e..913f6ab 100644 --- a/frontend/src/ui/components/Sidebar.tsx +++ b/frontend/src/ui/components/Sidebar.tsx @@ -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 }) { const { t } = useTranslation(); const resolved = useResolvedPath(url); @@ -254,10 +262,7 @@ export default function Sidebar({
- + {APPNAME} diff --git a/frontend/src/ui/pages/Onboarding.tsx b/frontend/src/ui/pages/Onboarding.tsx index 12ceba8..2d6efcc 100644 --- a/frontend/src/ui/pages/Onboarding.tsx +++ b/frontend/src/ui/pages/Onboarding.tsx @@ -31,6 +31,7 @@ import { Field, InputBox, Label, + lightMode, MultiToggle, MultiToggleItem, PageContainer, @@ -38,6 +39,7 @@ import { SectionHeader, styled, TextBlock, + themes, } from '../theme'; import { Alert } from '../theme/alert'; @@ -86,7 +88,7 @@ const HeroContainer = styled('div', { overflow: 'hidden', }); -const HeroLanguageSelector = styled('div', { +const HeroSelector = styled('div', { top: '10px', left: '10px', display: 'flex', @@ -95,7 +97,7 @@ const HeroLanguageSelector = styled('div', { zIndex: '10', }); -const LanguageItem = styled(MultiToggleItem, { +const HeroSelectorItem = styled(MultiToggleItem, { fontSize: '1rem', padding: '5px 8px', }); @@ -161,6 +163,10 @@ const StepList = styled('nav', { flexWrap: 'wrap', flexDirection: 'row', justifyContent: 'flex-start', + [`.${lightMode} &`]: { + borderBottom: '1px solid $gray6', + backgroundColor: '$gray2', + }, }); const StepName = styled('div', { @@ -180,6 +186,10 @@ const StepName = styled('div', { active: { color: '$gray12', display: 'inherit', + + [`.${lightMode} &`]: { + fontWeight: '500', + }, }, }, interaction: { @@ -403,9 +413,9 @@ function TwitchIntegrationStep() { variation={testResult.error ? 'danger' : 'default'} description={ testResult.error - ? t('pages.twitch-settings.test-failed', [ - testResult.error.message, - ]) + ? t('pages.twitch-settings.test-failed', { + error: testResult.error.message, + }) : t('pages.twitch-settings.test-succeeded') } actionText={t('form-actions.ok')} @@ -684,7 +694,7 @@ export default function OnboardingPage() { {landing ? ( - + {languages.map((lang) => ( - {lang.name} {lang.keys < maxKeys ? : null} - + ))} - + + { + void dispatch(setUiConfig({ ...uiConfig, theme: newTheme })); + localStorage.setItem('theme', newTheme); + }} + > + {themes.map((theme) => ( + + {t(`pages.uiconfig.themes.${theme}`)} + + ))} + + {animationItems} {t('pages.onboarding.welcome-header')} diff --git a/frontend/src/ui/pages/TwitchSettings.tsx b/frontend/src/ui/pages/TwitchSettings.tsx index b5063bc..f2d5b8e 100644 --- a/frontend/src/ui/pages/TwitchSettings.tsx +++ b/frontend/src/ui/pages/TwitchSettings.tsx @@ -357,9 +357,9 @@ function TwitchAPISettings() { variation={testResult.error ? 'danger' : 'default'} description={ testResult.error - ? t('pages.twitch-settings.test-failed', [ - testResult.error.message, - ]) + ? t('pages.twitch-settings.test-failed', { + error: testResult.error.message, + }) : t('pages.twitch-settings.test-succeeded') } actionText={t('form-actions.ok')} diff --git a/frontend/src/ui/pages/UISettingsPage.tsx b/frontend/src/ui/pages/UISettingsPage.tsx index 23bd2eb..1656bbd 100644 --- a/frontend/src/ui/pages/UISettingsPage.tsx +++ b/frontend/src/ui/pages/UISettingsPage.tsx @@ -13,6 +13,7 @@ import { PageHeader, PageTitle, styled, + themes, } from '../theme'; const PartialWarning = styled('small', { @@ -42,6 +43,7 @@ export default function UISettingsPage(): React.ReactElement { value={uiConfig?.language ?? i18n.resolvedLanguage} onValueChange={(value) => { void dispatch(setUiConfig({ ...uiConfig, language: value })); + localStorage.setItem('language', value); }} values={languages.map((lang) => ({ id: lang.code, @@ -68,8 +70,9 @@ export default function UISettingsPage(): React.ReactElement { value={uiConfig?.theme ?? 'dark'} onValueChange={(value) => { void dispatch(setUiConfig({ ...uiConfig, theme: value })); + localStorage.setItem('theme', value); }} - values={['dark', 'light'].map((theme) => ({ + values={themes.map((theme) => ({ id: theme, label: t(`pages.uiconfig.themes.${theme}`), }))} diff --git a/frontend/src/ui/theme/theme.ts b/frontend/src/ui/theme/theme.ts index 32956f8..ffe9238 100644 --- a/frontend/src/ui/theme/theme.ts +++ b/frontend/src/ui/theme/theme.ts @@ -76,6 +76,8 @@ export const lightMode = createTheme({ }, }); +export const themes = ['dark', 'light']; + export function getTheme(themeName: string) { switch (themeName) { case 'light':