added import/export for theme

This commit is contained in:
PhilReact 2025-04-29 13:22:45 +03:00
parent ed7b36791a
commit 49848a7bd0
2 changed files with 163 additions and 57 deletions

View File

@ -26,7 +26,9 @@ import { darkThemeOptions } from '../../styles/theme-dark';
import { lightThemeOptions } from '../../styles/theme-light'; import { lightThemeOptions } from '../../styles/theme-light';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { rgbStringToHsva, rgbaStringToHsva } from '@uiw/color-convert'; 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';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
function detectColorFormat(color) { function detectColorFormat(color) {
@ -36,6 +38,36 @@ function detectColorFormat(color) {
return null; 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() { export default function ThemeManager() {
const { userThemes, addUserTheme, setUserTheme, currentThemeId } = const { userThemes, addUserTheme, setUserTheme, currentThemeId } =
useThemeContext(); useThemeContext();
@ -152,6 +184,38 @@ export default function ThemeManager() {
); );
}; };
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('Invalid theme format');
}
const newTheme = { ...importedTheme, id: uid.rnd() };
const updatedThemes = [...userThemes, newTheme];
addUserTheme(updatedThemes);
setUserTheme(newTheme);
} catch (error) {
console.error(error);
}
};
return ( return (
<Box p={2}> <Box p={2}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
@ -165,7 +229,16 @@ export default function ThemeManager() {
> >
Add Theme Add Theme
</Button> </Button>
<Button
sx={{
marginLeft: '20px',
}}
variant="contained"
startIcon={<AddIcon />}
onClick={importTheme}
>
Import theme
</Button>
<List> <List>
{userThemes?.map((theme, index) => ( {userThemes?.map((theme, index) => (
<ListItemButton <ListItemButton
@ -178,6 +251,9 @@ export default function ThemeManager() {
<ListItemSecondaryAction> <ListItemSecondaryAction>
{theme.id !== 'default' && ( {theme.id !== 'default' && (
<> <>
<IconButton onClick={() => exportTheme(theme)}>
<FileDownloadIcon />
</IconButton>
<IconButton onClick={() => handleEditTheme(theme.id)}> <IconButton onClick={() => handleEditTheme(theme.id)}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>

View File

@ -1,63 +1,93 @@
// @ts-nocheck // @ts-nocheck
class Semaphore { class Semaphore {
constructor(count) { constructor(count) {
this.count = count this.count = count;
this.waiting = [] this.waiting = [];
} }
acquire() { acquire() {
return new Promise(resolve => { return new Promise((resolve) => {
if (this.count > 0) { if (this.count > 0) {
this.count-- this.count--;
resolve() resolve();
} else { } else {
this.waiting.push(resolve) this.waiting.push(resolve);
} }
}) });
} }
release() { release() {
if (this.waiting.length > 0) { if (this.waiting.length > 0) {
const resolve = this.waiting.shift() const resolve = this.waiting.shift();
resolve() resolve();
} else { } else {
this.count++ this.count++;
} }
} }
} }
let semaphore = new Semaphore(1) let semaphore = new Semaphore(1);
let reader = new FileReader() let reader = new FileReader();
export const fileToBase64 = (file) => new Promise(async (resolve, reject) => { export const fileToBase64 = (file) =>
const reader = new FileReader(); // Create a new instance new Promise(async (resolve, reject) => {
await semaphore.acquire(); const reader = new FileReader(); // Create a new instance
reader.readAsDataURL(file); await semaphore.acquire();
reader.onload = () => { reader.readAsDataURL(file);
const dataUrl = reader.result; reader.onload = () => {
semaphore.release(); const dataUrl = reader.result;
if (typeof dataUrl === 'string') { semaphore.release();
resolve(dataUrl.split(',')[1]); if (typeof dataUrl === 'string') {
} else { resolve(dataUrl.split(',')[1]);
reject(new Error('Invalid data URL')); } else {
} reject(new Error('Invalid data URL'));
reader.onload = null; // Clear the handler }
reader.onerror = null; // Clear the handle reader.onload = null; // Clear the handler
}; reader.onerror = null; // Clear the handle
reader.onerror = (error) => { };
semaphore.release(); reader.onerror = (error) => {
reject(error); semaphore.release();
reader.onload = null; // Clear the handler reject(error);
reader.onerror = null; // Clear the handle reader.onload = null; // Clear the handler
}; reader.onerror = null; // Clear the handle
};
}); });
export const base64ToBlobUrl = (base64, mimeType = "image/png") => { export const base64ToBlobUrl = (base64, mimeType = 'image/png') => {
const binary = atob(base64); const binary = atob(base64);
const array = []; const array = [];
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i)); array.push(binary.charCodeAt(i));
} }
const blob = new Blob([new Uint8Array(array)], { type: mimeType }); const blob = new Blob([new Uint8Array(array)], { type: mimeType });
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
}; };
export const handleImportClick = async (fileTypes) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = fileTypes;
// Create a promise to handle file selection and reading synchronously
return await new Promise((resolve, reject) => {
fileInput.onchange = () => {
const file = fileInput.files[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result); // Resolve with the file content
};
reader.onerror = () => {
reject(new Error('Error reading file'));
};
reader.readAsText(file); // Read the file as text (Base64 string)
};
// Trigger the file input dialog
fileInput.click();
});
};