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:
parent
11e7741f83
commit
7e4d358f60
11 changed files with 130 additions and 78 deletions
|
@ -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.
|
||||
|
|
2
app.go
2
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
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
},
|
||||
|
|
|
@ -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)"
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
<Container>
|
||||
<Container id="app-container" className={theme}>
|
||||
<ReportDialog
|
||||
open={reportDialogOpen}
|
||||
onOpenChange={setReportDialogOpen}
|
||||
|
@ -541,8 +546,9 @@ export default function ErrorWindow(): JSX.Element {
|
|||
))}
|
||||
</MultiToggle>
|
||||
</LanguageSelector>
|
||||
<Scrollbar vertical={true} viewport={{ flex: '1', maxHeight: '100vh' }}>
|
||||
<div style={{ width: '100vw' }}>
|
||||
<PageContainer>
|
||||
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
|
||||
<PageHeader>
|
||||
<TextBlock>{t('pages.crash.fatal-message')}</TextBlock>
|
||||
</PageHeader>
|
||||
|
@ -595,8 +601,9 @@ export default function ErrorWindow(): JSX.Element {
|
|||
<LogItem key={log.time.toString()} data={log} />
|
||||
))}
|
||||
</LogContainer>
|
||||
</Scrollbar>
|
||||
</PageContainer>
|
||||
</div>
|
||||
</Scrollbar>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
<Header>
|
||||
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
|
||||
<AppName>
|
||||
<img
|
||||
src={logo as string}
|
||||
style={{ height: '28px', marginBottom: '-2px' }}
|
||||
/>
|
||||
<AppLogo src={logo as string} />
|
||||
{APPNAME}
|
||||
</AppName>
|
||||
<VersionLabel>
|
||||
|
|
|
@ -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() {
|
|||
<TopBanner>
|
||||
{landing ? (
|
||||
<HeroContainer>
|
||||
<HeroLanguageSelector>
|
||||
<HeroSelector>
|
||||
<MultiToggle
|
||||
value={uiConfig?.language ?? i18n.resolvedLanguage}
|
||||
type="single"
|
||||
|
@ -692,10 +702,11 @@ export default function OnboardingPage() {
|
|||
void dispatch(
|
||||
setUiConfig({ ...uiConfig, language: newLang }),
|
||||
);
|
||||
localStorage.setItem('language', newLang);
|
||||
}}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<LanguageItem
|
||||
<HeroSelectorItem
|
||||
key={lang.code}
|
||||
aria-label={lang.name}
|
||||
value={lang.code}
|
||||
|
@ -707,10 +718,29 @@ export default function OnboardingPage() {
|
|||
>
|
||||
{lang.name}
|
||||
{lang.keys < maxKeys ? <ExclamationTriangleIcon /> : null}
|
||||
</LanguageItem>
|
||||
</HeroSelectorItem>
|
||||
))}
|
||||
</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>
|
||||
<HeroTitle>{t('pages.onboarding.welcome-header')}</HeroTitle>
|
||||
<HeroContent>
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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}`),
|
||||
}))}
|
||||
|
|
|
@ -76,6 +76,8 @@ export const lightMode = createTheme({
|
|||
},
|
||||
});
|
||||
|
||||
export const themes = ['dark', 'light'];
|
||||
|
||||
export function getTheme(themeName: string) {
|
||||
switch (themeName) {
|
||||
case 'light':
|
||||
|
|
Loading…
Reference in a new issue