import { useState, useRef, useEffect } from 'react'; import { Box, Button, IconButton, Typography, Dialog, DialogTitle, DialogContent, DialogActions, List, ListItemText, ListItemSecondaryAction, TextField, Tabs, Tab, ListItemButton, } from '@mui/material'; import { Sketch } from '@uiw/react-color'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import AddIcon from '@mui/icons-material/Add'; import CheckIcon from '@mui/icons-material/Check'; import { useThemeContext } from './ThemeContext'; import { darkThemeOptions } from '../../styles/theme-dark'; import { lightThemeOptions } from '../../styles/theme-light'; import ShortUniqueId from 'short-unique-id'; import { rgbStringToHsva, rgbaStringToHsva } from '@uiw/color-convert'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; import { handleImportClick } from '../../utils/fileReading'; import { useTranslation } from 'react-i18next'; const uid = new ShortUniqueId({ length: 8 }); function detectColorFormat(color) { if (typeof color !== 'string') return null; if (color.startsWith('rgba')) return 'rgba'; if (color.startsWith('rgb')) return 'rgb'; return null; } const validateTheme = (theme) => { if (typeof theme !== 'object' || !theme) return false; if (typeof theme.name !== 'string') return false; if (!theme.light || typeof theme.light !== 'object') return false; if (!theme.dark || typeof theme.dark !== 'object') return false; // Optional: deeper checks on structure const requiredKeys = [ 'primary', 'secondary', 'background', 'text', 'border', 'other', ]; for (const mode of ['light', 'dark']) { const modeTheme = theme[mode]; if (modeTheme.mode !== mode) return false; for (const key of requiredKeys) { if (!modeTheme[key] || typeof modeTheme[key] !== 'object') { return false; } } } return true; }; export default function ThemeManager() { const { userThemes, addUserTheme, setUserTheme, currentThemeId } = useThemeContext(); const [openEditor, setOpenEditor] = useState(false); const [themeDraft, setThemeDraft] = useState({ id: '', name: '', light: {}, dark: {}, }); const [currentTab, setCurrentTab] = useState('light'); const nameInputRef = useRef(null); const { t } = useTranslation(['auth', 'core', 'group']); useEffect(() => { if (openEditor && nameInputRef.current) { nameInputRef.current.focus(); } }, [openEditor]); const handleAddTheme = () => { setThemeDraft({ id: '', name: '', light: structuredClone(lightThemeOptions.palette), dark: structuredClone(darkThemeOptions.palette), }); setOpenEditor(true); }; const handleEditTheme = (themeId) => { const themeToEdit = userThemes.find((theme) => theme.id === themeId); if (themeToEdit) { setThemeDraft({ ...themeToEdit }); setOpenEditor(true); } }; const handleSaveTheme = () => { if (themeDraft.id) { const updatedThemes = [...userThemes]; const index = updatedThemes.findIndex( (theme) => theme.id === themeDraft.id ); if (index !== -1) { updatedThemes[index] = themeDraft; addUserTheme(updatedThemes); } } else { const newTheme = { ...themeDraft, id: uid.rnd() }; const updatedThemes = [...userThemes, newTheme]; addUserTheme(updatedThemes); setUserTheme(newTheme, updatedThemes); } setOpenEditor(false); }; const handleDeleteTheme = (id) => { const updatedThemes = userThemes.filter((theme) => theme.id !== id); addUserTheme(updatedThemes); if (id === currentThemeId) { // Find the default theme object in the list const defaultTheme = updatedThemes.find( (theme) => theme.id === 'default' ); if (defaultTheme) { setUserTheme(defaultTheme, updatedThemes); } else { // Emergency fallback setUserTheme( { light: lightThemeOptions, dark: darkThemeOptions, }, updatedThemes ); } } }; const handleApplyTheme = (theme) => { setUserTheme(theme, null); }; const handleColorChange = (mode, fieldPath, color) => { setThemeDraft((prev) => { const updated = { ...prev }; const paths = fieldPath.split('.'); updated[mode][paths[0]][paths[1]] = color.hex; return updated; }); }; const renderColorPicker = (mode, label, fieldPath, currentValue) => { let color = currentValue || '#ffffff'; const format = detectColorFormat(currentValue); if (format === 'rgba') { color = rgbaStringToHsva(currentValue); } else if (format === 'rgb') { color = rgbStringToHsva(currentValue); } return ( {label} handleColorChange(mode, fieldPath, color)} /> ); }; const exportTheme = async (theme) => { try { const copyTheme = structuredClone(theme); delete copyTheme.id; const fileName = `ui_theme_${theme.name}.json`; const blob = new Blob([JSON.stringify(copyTheme, null, 2)], { type: 'application/json', }); await saveFileToDiskGeneric(blob, fileName); } catch (error) { console.error(error); } }; const importTheme = async (theme) => { try { const fileContent = await handleImportClick('.json'); const importedTheme = JSON.parse(fileContent); if (!validateTheme(importedTheme)) { throw new Error( t('core:message.generic.invalid_theme_format', { postProcess: 'capitalizeFirstChar', }) ); } const newTheme = { ...importedTheme, id: uid.rnd() }; const updatedThemes = [...userThemes, newTheme]; addUserTheme(updatedThemes); setUserTheme(newTheme, updatedThemes); } catch (error) { console.error(error); } }; return ( {t('core:theme.manager', { postProcess: 'capitalizeFirstChar' })} {userThemes?.map((theme, index) => ( {theme.id !== 'default' && ( <> exportTheme(theme)}> handleEditTheme(theme.id)}> handleDeleteTheme(theme.id)}> )} handleApplyTheme(theme)}> ))} setOpenEditor(false)} fullWidth maxWidth="md" > {themeDraft.id ? t('core:action.edit_theme', { postProcess: 'capitalizeFirstChar', }) : t('core:action.new.theme', { postProcess: 'capitalizeFirstChar', })} setThemeDraft((prev) => ({ ...prev, name: e.target.value })) } /> setCurrentTab(newValue)} sx={{ mt: 2, mb: 2 }} > {renderColorPicker( currentTab, 'Primary Main', 'primary.main', themeDraft[currentTab]?.primary?.main )} {renderColorPicker( currentTab, 'Primary Dark', 'primary.dark', themeDraft[currentTab]?.primary?.dark )} {renderColorPicker( currentTab, 'Primary Light', 'primary.light', themeDraft[currentTab]?.primary?.light )} {renderColorPicker( currentTab, 'Secondary Main', 'secondary.main', themeDraft[currentTab]?.secondary?.main )} {renderColorPicker( currentTab, 'Background Default', 'background.default', themeDraft[currentTab]?.background?.default )} {renderColorPicker( currentTab, 'Background Paper', 'background.paper', themeDraft[currentTab]?.background?.paper )} {renderColorPicker( currentTab, 'Background Surface', 'background.surface', themeDraft[currentTab]?.background?.surface )} {renderColorPicker( currentTab, 'Text Primary', 'text.primary', themeDraft[currentTab]?.text?.primary )} {renderColorPicker( currentTab, 'Text Secondary', 'text.secondary', themeDraft[currentTab]?.text?.secondary )} {renderColorPicker( currentTab, 'Border Main', 'border.main', themeDraft[currentTab]?.border?.main )} {renderColorPicker( currentTab, 'Border Subtle', 'border.subtle', themeDraft[currentTab]?.border?.subtle )} {renderColorPicker( currentTab, 'Positive', 'other.positive', themeDraft[currentTab]?.other?.positive )} {renderColorPicker( currentTab, 'Danger', 'other.danger', themeDraft[currentTab]?.other?.danger )} {renderColorPicker( currentTab, 'Unread', 'other.unread', themeDraft[currentTab]?.other?.unread )} ); }