mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-07-22 20:26:50 +00:00
added theme manager
This commit is contained in:
@@ -110,7 +110,6 @@ export const BuyQortInformation = ({ balance }) => {
|
||||
</Typography>
|
||||
<List
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default,
|
||||
maxWidth: 360,
|
||||
width: '100%',
|
||||
}}
|
||||
@@ -118,21 +117,13 @@ export const BuyQortInformation = ({ balance }) => {
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon
|
||||
sx={{
|
||||
color: theme.palette.primary.dark,
|
||||
}}
|
||||
/>
|
||||
<RadioButtonCheckedIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Create transactions on the Qortal Blockchain" />
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon
|
||||
sx={{
|
||||
color: theme.palette.primary.dark,
|
||||
}}
|
||||
/>
|
||||
<RadioButtonCheckedIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Having at least 4 QORT in your balance allows you to send chat messages at near instant speed." />
|
||||
</ListItem>
|
||||
|
@@ -48,6 +48,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import ErrorBoundary from '../../common/ErrorBoundary';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { getFee } from '../../background';
|
||||
export const requestQueuePromos = new RequestQueueWithPromise(20);
|
||||
|
||||
export function utf8ToBase64(inputString: string): string {
|
||||
|
@@ -18,6 +18,7 @@ import { TransitionProps } from '@mui/material/transitions';
|
||||
import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material';
|
||||
import { enabledDevModeAtom } from '../../atoms/global';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import ThemeManager from '../Theme/ThemeManager';
|
||||
|
||||
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
|
||||
padding: 8,
|
||||
@@ -185,6 +186,7 @@ export const Settings = ({ address, open, setOpen }) => {
|
||||
label="Enable dev mode"
|
||||
/>
|
||||
)}
|
||||
<ThemeManager />
|
||||
</Box>
|
||||
</Dialog>
|
||||
</Fragment>
|
||||
|
@@ -6,57 +6,129 @@ import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
||||
import { darkTheme } from '../../styles/theme-dark';
|
||||
import { lightTheme } from '../../styles/theme-light';
|
||||
import {
|
||||
ThemeProvider as MuiThemeProvider,
|
||||
createTheme,
|
||||
} from '@mui/material/styles';
|
||||
import { lightThemeOptions } from '../../styles/theme-light';
|
||||
import { darkThemeOptions } from '../../styles/theme-dark';
|
||||
|
||||
const defaultTheme = {
|
||||
id: 'default',
|
||||
name: 'Default Theme',
|
||||
light: lightThemeOptions.palette,
|
||||
dark: darkThemeOptions.palette,
|
||||
};
|
||||
|
||||
const ThemeContext = createContext({
|
||||
themeMode: 'light',
|
||||
toggleTheme: () => {},
|
||||
userThemes: [defaultTheme],
|
||||
addUserTheme: (themes) => {},
|
||||
setUserTheme: (theme) => {},
|
||||
currentThemeId: 'default',
|
||||
});
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [themeMode, setThemeMode] = useState('light');
|
||||
const [userThemes, setUserThemes] = useState([defaultTheme]);
|
||||
const [currentThemeId, setCurrentThemeId] = useState('default');
|
||||
|
||||
const theme = useMemo(
|
||||
() => (themeMode === 'light' ? lightTheme : darkTheme),
|
||||
[themeMode]
|
||||
);
|
||||
const currentTheme =
|
||||
userThemes.find((theme) => theme.id === currentThemeId) || defaultTheme;
|
||||
|
||||
const muiTheme = useMemo(() => {
|
||||
if (themeMode === 'light') {
|
||||
return createTheme({
|
||||
...lightThemeOptions,
|
||||
palette: {
|
||||
...currentTheme.light,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return createTheme({
|
||||
...lightThemeOptions,
|
||||
palette: {
|
||||
...currentTheme.dark,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [themeMode, currentTheme]);
|
||||
|
||||
const saveSettings = (
|
||||
themes = userThemes,
|
||||
mode = themeMode,
|
||||
themeId = currentThemeId
|
||||
) => {
|
||||
localStorage.setItem(
|
||||
'saved_ui_theme',
|
||||
JSON.stringify({
|
||||
mode,
|
||||
userThemes: themes,
|
||||
currentThemeId: themeId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
setThemeMode((prevMode) => {
|
||||
const newMode = prevMode === 'light' ? 'dark' : 'light';
|
||||
|
||||
const themeProperties = {
|
||||
mode: newMode,
|
||||
};
|
||||
|
||||
localStorage.setItem('saved_ui_theme', JSON.stringify(themeProperties));
|
||||
|
||||
setThemeMode((prev) => {
|
||||
const newMode = prev === 'light' ? 'dark' : 'light';
|
||||
saveSettings(userThemes, newMode, currentThemeId);
|
||||
return newMode;
|
||||
});
|
||||
};
|
||||
|
||||
const getSavedTheme = useCallback(async () => {
|
||||
try {
|
||||
const themeProperties = JSON.parse(
|
||||
localStorage.getItem(`saved_ui_theme`) || '{}'
|
||||
);
|
||||
const addUserTheme = (themes) => {
|
||||
setUserThemes(themes);
|
||||
saveSettings(themes);
|
||||
};
|
||||
|
||||
const theme = themeProperties?.mode || 'light';
|
||||
setThemeMode(theme);
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
const setUserTheme = (theme) => {
|
||||
if (theme.id === 'default') {
|
||||
setCurrentThemeId('default');
|
||||
saveSettings(userThemes, themeMode, 'default');
|
||||
} else {
|
||||
setCurrentThemeId(theme.id);
|
||||
saveSettings(userThemes, themeMode, theme.id);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = useCallback(() => {
|
||||
const saved = localStorage.getItem('saved_ui_theme');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed.mode === 'light' || parsed.mode === 'dark')
|
||||
setThemeMode(parsed.mode);
|
||||
if (Array.isArray(parsed.userThemes)) {
|
||||
const filteredThemes = parsed.userThemes.filter(
|
||||
(theme) => theme.id !== 'default'
|
||||
);
|
||||
setUserThemes([defaultTheme, ...filteredThemes]);
|
||||
}
|
||||
if (parsed.currentThemeId) setCurrentThemeId(parsed.currentThemeId);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved_ui_theme:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getSavedTheme();
|
||||
}, [getSavedTheme]);
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
|
||||
<MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
themeMode,
|
||||
toggleTheme,
|
||||
userThemes,
|
||||
addUserTheme,
|
||||
setUserTheme,
|
||||
currentThemeId,
|
||||
}}
|
||||
>
|
||||
<MuiThemeProvider theme={muiTheme}>{children}</MuiThemeProvider>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
309
src/components/Theme/ThemeManager.tsx
Normal file
309
src/components/Theme/ThemeManager.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { 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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
// Emergency fallback
|
||||
setUserTheme({
|
||||
light: lightThemeOptions,
|
||||
dark: darkThemeOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyTheme = (theme) => {
|
||||
setUserTheme(theme);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Box
|
||||
mb={2}
|
||||
{...{ 'data-color-mode': mode === 'dark' ? 'dark' : 'light' }}
|
||||
>
|
||||
<Typography variant="body2" mb={1}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Sketch
|
||||
key={`${mode}-${fieldPath}`}
|
||||
color={color}
|
||||
onChange={(color) => handleColorChange(mode, fieldPath, color)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Theme Manager
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddTheme}
|
||||
>
|
||||
Add Theme
|
||||
</Button>
|
||||
|
||||
<List>
|
||||
{userThemes?.map((theme, index) => (
|
||||
<ListItemButton
|
||||
key={theme?.id || index}
|
||||
selected={theme?.id === currentThemeId}
|
||||
>
|
||||
<ListItemText
|
||||
primary={`${theme?.name || `Theme ${index + 1}`} ${theme?.id === currentThemeId ? '(Current)' : ''}`}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{theme.id !== 'default' && (
|
||||
<>
|
||||
<IconButton onClick={() => handleEditTheme(theme.id)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleDeleteTheme(theme.id)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<IconButton onClick={() => handleApplyTheme(theme)}>
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Dialog
|
||||
open={openEditor}
|
||||
onClose={() => setOpenEditor(false)}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
>
|
||||
<DialogTitle>
|
||||
{themeDraft.id ? 'Edit Theme' : 'Add New Theme'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
inputRef={nameInputRef}
|
||||
margin="dense"
|
||||
label="Theme Name"
|
||||
fullWidth
|
||||
value={themeDraft.name}
|
||||
onChange={(e) =>
|
||||
setThemeDraft((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(e, newValue) => setCurrentTab(newValue)}
|
||||
sx={{ mt: 2, mb: 2 }}
|
||||
>
|
||||
<Tab label="Light" value="light" />
|
||||
<Tab label="Dark" value="dark" />
|
||||
</Tabs>
|
||||
|
||||
<Box>
|
||||
{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
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenEditor(false)}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!themeDraft.name}
|
||||
onClick={handleSaveTheme}
|
||||
variant="contained"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
39
src/components/Theme/themeManager.css
Normal file
39
src/components/Theme/themeManager.css
Normal file
@@ -0,0 +1,39 @@
|
||||
[data-color-mode*='dark'] .w-color-sketch {
|
||||
--sketch-background: #323232 !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-swatch {
|
||||
--sketch-swatch-border-top: 1px solid #525252 !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-block {
|
||||
--block-background-color: #323232 !important;
|
||||
--block-box-shadow: rgb(0 0 0 / 10%) 0 1px !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-editable-input {
|
||||
--editable-input-label-color: #757575 !important;
|
||||
--editable-input-box-shadow: #616161 0px 0px 0px 1px inset !important;
|
||||
--editable-input-color: #bbb !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-github {
|
||||
--github-border: 1px solid rgba(0, 0, 0, 0.2) !important;
|
||||
--github-background-color: #323232 !important;
|
||||
--github-box-shadow: rgb(0 0 0 / 15%) 0px 3px 12px !important;
|
||||
--github-arrow-border-color: rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-compact {
|
||||
--compact-background-color: #323232 !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-material {
|
||||
--material-background-color: #323232 !important;
|
||||
--material-border-bottom-color: #707070 !important;
|
||||
}
|
||||
|
||||
[data-color-mode*='dark'] .w-color-alpha {
|
||||
--alpha-pointer-background-color: #6a6a6a !important;
|
||||
--alpha-pointer-box-shadow: rgb(0 0 0 / 37%) 0px 1px 4px 0px !important;
|
||||
}
|
Reference in New Issue
Block a user