Use AwaitableComponent for "CreateThemeDialog"

This commit is contained in:
schroda
2026-01-02 22:19:32 +01:00
parent fd856c5cbd
commit bc06d88db0
3 changed files with 201 additions and 205 deletions

View File

@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import AddCircleIcon from '@mui/icons-material/AddCircle'; import AddCircleIcon from '@mui/icons-material/AddCircle';
import { bindDialog, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks'; import { AwaitableComponent } from 'awaitable-component';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ThemeCreationDialog } from '@/features/theme/components/CreateThemeDialog.tsx'; import { ThemeCreationDialog } from '@/features/theme/components/CreateThemeDialog.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
@@ -18,29 +18,26 @@ import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.t
export const CreateThemeButton = () => { export const CreateThemeButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const popupState = usePopupState({ variant: 'popover', popupId: 'theme-creation-dialog' });
return ( return (
<> <Stack sx={{ alignItems: 'center', gap: 1, minWidth: '150px', maxWidth: '150px' }}>
<Stack sx={{ alignItems: 'center', gap: 1, minWidth: '150px', maxWidth: '150px' }}> <Button
<Button sx={{
sx={{ width: '100%',
width: '100%', height: '225px',
height: '225px', }}
}} variant="contained"
variant="contained" size="large"
size="large" onClick={() => {
{...bindTrigger(popupState)} AwaitableComponent.show(ThemeCreationDialog, { mode: 'create' });
> }}
<AddCircleIcon fontSize="large" /> >
</Button> <AddCircleIcon fontSize="large" />
<CustomTooltip title={t('settings.appearance.theme.create.title')} placement="top"> </Button>
<TypographyMaxLines sx={{ maxWidth: '100%' }}> <CustomTooltip title={t('settings.appearance.theme.create.title')} placement="top">
{t('settings.appearance.theme.create.title')} <TypographyMaxLines sx={{ maxWidth: '100%' }}>
</TypographyMaxLines> {t('settings.appearance.theme.create.title')}
</CustomTooltip> </TypographyMaxLines>
</Stack> </CustomTooltip>
{popupState.isOpen && <ThemeCreationDialog bindDialogProps={bindDialog(popupState)} mode="create" />} </Stack>
</>
); );
}; };

View File

@@ -11,7 +11,6 @@ import { useState } from 'react';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { bindDialog } from 'material-ui-popup-state/hooks';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
@@ -19,6 +18,7 @@ import DialogActions from '@mui/material/DialogActions';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import { jsonrepair } from 'jsonrepair'; import { jsonrepair } from 'jsonrepair';
import { AwaitableComponentProps } from 'awaitable-component';
import { getErrorMessage, jsonSaveParse } from '@/lib/HelperFunctions.ts'; import { getErrorMessage, jsonSaveParse } from '@/lib/HelperFunctions.ts';
import { AppTheme, isThemeNameUnique } from '@/features/theme/services/AppThemes.ts'; import { AppTheme, isThemeNameUnique } from '@/features/theme/services/AppThemes.ts';
import { import {
@@ -91,11 +91,13 @@ const dialogModeToTranslationKey: Record<
}; };
export const ThemeCreationDialog = ({ export const ThemeCreationDialog = ({
bindDialogProps,
mode, mode,
appTheme = baseCustomTheme, appTheme = baseCustomTheme,
}: { isVisible,
bindDialogProps: ReturnType<typeof bindDialog>; onExitComplete,
onSubmit,
onDismiss,
}: AwaitableComponentProps<void> & {
mode: DialogMode; mode: DialogMode;
appTheme?: AppTheme; appTheme?: AppTheme;
}) => { }) => {
@@ -117,7 +119,7 @@ export const ThemeCreationDialog = ({
const [didThemeChange, setDidThemeChange] = useState(mode === 'create'); const [didThemeChange, setDidThemeChange] = useState(mode === 'create');
return ( return (
<Dialog {...bindDialogProps} fullWidth maxWidth="md"> <Dialog open={isVisible} onTransitionExited={onExitComplete} fullWidth maxWidth="md">
<DialogTitle>{t(dialogModeToTranslationKey[mode].title)}</DialogTitle> <DialogTitle>{t(dialogModeToTranslationKey[mode].title)}</DialogTitle>
<DialogContent> <DialogContent>
{loading && <LoadingPlaceholder />} {loading && <LoadingPlaceholder />}
@@ -212,12 +214,12 @@ export const ThemeCreationDialog = ({
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={bindDialogProps.onClose} color="primary"> <Button autoFocus onClick={() => onDismiss()} color="primary">
{t('global.button.cancel')} {t('global.button.cancel')}
</Button> </Button>
<Button <Button
disabled={invalidTheme || invalidName || isCreating || !didThemeChange || !theme.id.length} disabled={invalidTheme || invalidName || isCreating || !didThemeChange || !theme.id.length}
onClick={(e) => { onClick={() => {
makeToast(t(dialogModeToTranslationKey[mode].action, { theme: theme.getName() }), 'info'); makeToast(t(dialogModeToTranslationKey[mode].action, { theme: theme.getName() }), 'info');
setIsCreating(true); setIsCreating(true);
@@ -227,7 +229,7 @@ export const ThemeCreationDialog = ({
t(dialogModeToTranslationKey[mode].success, { theme: theme.getName() }), t(dialogModeToTranslationKey[mode].success, { theme: theme.getName() }),
'success', 'success',
); );
bindDialogProps.onClose(e); onSubmit();
}) })
.catch((updateError) => .catch((updateError) =>
makeToast( makeToast(

View File

@@ -17,7 +17,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { bindDialog, usePopupState } from 'material-ui-popup-state/hooks'; import { AwaitableComponent } from 'awaitable-component';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { useAppThemeContext } from '@/features/theme/AppThemeContext.tsx'; import { useAppThemeContext } from '@/features/theme/AppThemeContext.tsx';
import { AppTheme } from '@/features/theme/services/AppThemes.ts'; import { AppTheme } from '@/features/theme/services/AppThemes.ts';
@@ -40,8 +40,6 @@ export const ThemePreview = ({ appTheme, onDelete }: { appTheme: AppTheme; onDel
const theme = useTheme(); const theme = useTheme();
const { themeMode, setAppTheme, appTheme: activeAppTheme, shouldUsePureBlackMode } = useAppThemeContext(); const { themeMode, setAppTheme, appTheme: activeAppTheme, shouldUsePureBlackMode } = useAppThemeContext();
const popupState = usePopupState({ variant: 'popover', popupId: `theme-edit-dialog-${appTheme.id}` });
const isSelected = appTheme.id === activeAppTheme; const isSelected = appTheme.id === activeAppTheme;
const muiTheme = useMemo( const muiTheme = useMemo(
@@ -50,191 +48,190 @@ export const ThemePreview = ({ appTheme, onDelete }: { appTheme: AppTheme; onDel
); );
return ( return (
<> <Stack
<Stack sx={{
sx={{ justifyContent: 'center',
justifyContent: 'center', alignItems: 'center',
alignItems: 'center', minWidth: '150px',
minWidth: '150px', maxWidth: '150px',
maxWidth: '150px', gap: 1,
gap: 1, }}
}} >
> <ThemeProvider theme={muiTheme}>
<ThemeProvider theme={muiTheme}> <Card
<Card sx={{
width: '100%',
height: '225px',
position: 'relative',
}}
>
<Box
sx={{ sx={{
width: '100%', position: 'absolute',
height: '225px', top: 0,
position: 'relative', right: 0,
bottom: 0,
left: 0,
border: '4px solid',
borderColor: isSelected ? muiTheme.palette.primary.light : muiTheme.palette.grey['100'],
zIndex: 1,
pointerEvents: 'none',
borderRadius: 1,
...theme.applyStyles('dark', {
borderColor: isSelected ? muiTheme.palette.primary.light : muiTheme.palette.grey['900'],
}),
}}
/>
<CardActionArea
sx={{
height: '100%',
backgroundColor: 'background.default',
}}
onClick={() => {
const needToLoadFonts = ThemeFontLoader.hasMissing(appTheme.muiTheme);
if (!needToLoadFonts) {
setAppTheme(appTheme.id);
return;
}
makeToast(t('settings.appearance.theme.select.fonts.loading'), 'info');
ThemeFontLoader.load(appTheme.muiTheme)
.then(() => setAppTheme(appTheme.id))
.catch((e) =>
makeToast(
t('settings.appearance.theme.select.fonts.error'),
'error',
getErrorMessage(e),
),
);
}} }}
> >
<Box <Stack sx={{ height: '100%', m: 0 }}>
sx={{ <Stack sx={{ height: '100%', gap: 2, p: 2 }}>
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
border: '4px solid',
borderColor: isSelected ? muiTheme.palette.primary.light : muiTheme.palette.grey['100'],
zIndex: 1,
pointerEvents: 'none',
borderRadius: 1,
...theme.applyStyles('dark', {
borderColor: isSelected
? muiTheme.palette.primary.light
: muiTheme.palette.grey['900'],
}),
}}
/>
<CardActionArea
sx={{
height: '100%',
backgroundColor: 'background.default',
}}
onClick={() => {
const needToLoadFonts = ThemeFontLoader.hasMissing(appTheme.muiTheme);
if (!needToLoadFonts) {
setAppTheme(appTheme.id);
return;
}
makeToast(t('settings.appearance.theme.select.fonts.loading'), 'info');
ThemeFontLoader.load(appTheme.muiTheme)
.then(() => setAppTheme(appTheme.id))
.catch((e) =>
makeToast(
t('settings.appearance.theme.select.fonts.error'),
'error',
getErrorMessage(e),
),
);
}}
>
<Stack sx={{ height: '100%', m: 0 }}>
<Stack sx={{ height: '100%', gap: 2, p: 2 }}>
<Stack
sx={{
maxHeight: '20px',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
}}
>
<Box
sx={{
width: '65%',
height: '20px',
backgroundColor: 'primary.dark',
borderRadius: 1,
}}
/>
<Stack
sx={{
alignItems: 'center',
gap: 1,
mt: -0.25,
}}
>
{isSelected && (
<CheckCircleIcon
sx={{
visibility: isSelected ? 'visible' : 'hidden',
color: 'primary.light',
}}
/>
)}
{!isSelected && appTheme.isCustom && (
<IconButton
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete();
}}
component="div"
size="small"
sx={{ mt: -0.5 }}
>
<CustomTooltip title={t('global.button.delete')}>
<DeleteIcon />
</CustomTooltip>
</IconButton>
)}
{appTheme.isCustom && (
<IconButton
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
popupState.open();
}}
component="div"
size="small"
>
<CustomTooltip title={t('global.button.edit')}>
<EditIcon />
</CustomTooltip>
</IconButton>
)}
</Stack>
</Stack>
<Box
sx={{
width: '55%',
height: '65%',
backgroundColor: 'background.paper',
p: 1,
borderRadius: 1,
}}
>
<Stack
sx={{
width: 'max-content',
flexDirection: 'row',
overflow: 'hidden',
borderRadius: 1,
}}
>
<ThemePreviewBadge sx={{ backgroundColor: 'primary.main' }} />
<ThemePreviewBadge sx={{ backgroundColor: 'secondary.main' }} />
</Stack>
</Box>
</Stack>
<Stack <Stack
sx={{ sx={{
height: '25%', maxHeight: '20px',
backgroundColor: 'background.paper',
alignItems: 'center',
flexDirection: 'row', flexDirection: 'row',
gap: 2, justifyContent: 'space-between',
p: 2, alignItems: 'flex-start',
}} }}
> >
<Box <Box
sx={{ sx={{
width: '20px', width: '65%',
height: '20px',
borderRadius: '50%',
backgroundColor: 'primary.main',
}}
/>
<Box
sx={{
flexGrow: 1,
height: '20px', height: '20px',
backgroundColor: 'primary.dark',
borderRadius: 1, borderRadius: 1,
backgroundColor: 'primary.light',
}} }}
/> />
<Stack
sx={{
alignItems: 'center',
gap: 1,
mt: -0.25,
}}
>
{isSelected && (
<CheckCircleIcon
sx={{
visibility: isSelected ? 'visible' : 'hidden',
color: 'primary.light',
}}
/>
)}
{!isSelected && appTheme.isCustom && (
<IconButton
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete();
}}
component="div"
size="small"
sx={{ mt: -0.5 }}
>
<CustomTooltip title={t('global.button.delete')}>
<DeleteIcon />
</CustomTooltip>
</IconButton>
)}
{appTheme.isCustom && (
<IconButton
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
AwaitableComponent.show(ThemeCreationDialog, {
mode: 'edit',
appTheme,
});
}}
component="div"
size="small"
>
<CustomTooltip title={t('global.button.edit')}>
<EditIcon />
</CustomTooltip>
</IconButton>
)}
</Stack>
</Stack> </Stack>
<Box
sx={{
width: '55%',
height: '65%',
backgroundColor: 'background.paper',
p: 1,
borderRadius: 1,
}}
>
<Stack
sx={{
width: 'max-content',
flexDirection: 'row',
overflow: 'hidden',
borderRadius: 1,
}}
>
<ThemePreviewBadge sx={{ backgroundColor: 'primary.main' }} />
<ThemePreviewBadge sx={{ backgroundColor: 'secondary.main' }} />
</Stack>
</Box>
</Stack> </Stack>
</CardActionArea> <Stack
</Card> sx={{
</ThemeProvider> height: '25%',
<CustomTooltip title={getName()} placement="top"> backgroundColor: 'background.paper',
<TypographyMaxLines sx={{ maxWidth: '100%' }}>{getName()}</TypographyMaxLines> alignItems: 'center',
</CustomTooltip> flexDirection: 'row',
</Stack> gap: 2,
<ThemeCreationDialog bindDialogProps={bindDialog(popupState)} mode="edit" appTheme={appTheme} /> p: 2,
</> }}
>
<Box
sx={{
width: '20px',
height: '20px',
borderRadius: '50%',
backgroundColor: 'primary.main',
}}
/>
<Box
sx={{
flexGrow: 1,
height: '20px',
borderRadius: 1,
backgroundColor: 'primary.light',
}}
/>
</Stack>
</Stack>
</CardActionArea>
</Card>
</ThemeProvider>
<CustomTooltip title={getName()} placement="top">
<TypographyMaxLines sx={{ maxWidth: '100%' }}>{getName()}</TypographyMaxLines>
</CustomTooltip>
</Stack>
); );
}; };