From 49848a7bd095b8381022c476db71604b5db09f35 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 29 Apr 2025 13:22:45 +0300 Subject: [PATCH] added import/export for theme --- src/components/Theme/ThemeManager.tsx | 80 ++++++++++++++- src/utils/fileReading/index.ts | 140 ++++++++++++++++---------- 2 files changed, 163 insertions(+), 57 deletions(-) diff --git a/src/components/Theme/ThemeManager.tsx b/src/components/Theme/ThemeManager.tsx index 2043eac..eca0e9f 100644 --- a/src/components/Theme/ThemeManager.tsx +++ b/src/components/Theme/ThemeManager.tsx @@ -26,7 +26,9 @@ 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'; const uid = new ShortUniqueId({ length: 8 }); function detectColorFormat(color) { @@ -36,6 +38,36 @@ function detectColorFormat(color) { 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(); @@ -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 ( @@ -165,7 +229,16 @@ export default function ThemeManager() { > Add Theme - + {userThemes?.map((theme, index) => ( {theme.id !== 'default' && ( <> + exportTheme(theme)}> + + handleEditTheme(theme.id)}> diff --git a/src/utils/fileReading/index.ts b/src/utils/fileReading/index.ts index a6295c8..a04435c 100644 --- a/src/utils/fileReading/index.ts +++ b/src/utils/fileReading/index.ts @@ -1,63 +1,93 @@ // @ts-nocheck class Semaphore { - constructor(count) { - this.count = count - this.waiting = [] - } - acquire() { - return new Promise(resolve => { - if (this.count > 0) { - this.count-- - resolve() - } else { - this.waiting.push(resolve) - } - }) - } - release() { - if (this.waiting.length > 0) { - const resolve = this.waiting.shift() - resolve() - } else { - this.count++ - } - } + constructor(count) { + this.count = count; + this.waiting = []; + } + acquire() { + return new Promise((resolve) => { + if (this.count > 0) { + this.count--; + resolve(); + } else { + this.waiting.push(resolve); + } + }); + } + release() { + if (this.waiting.length > 0) { + const resolve = this.waiting.shift(); + resolve(); + } else { + this.count++; + } + } } -let semaphore = new Semaphore(1) -let reader = new FileReader() +let semaphore = new Semaphore(1); +let reader = new FileReader(); -export const fileToBase64 = (file) => new Promise(async (resolve, reject) => { - const reader = new FileReader(); // Create a new instance - await semaphore.acquire(); - reader.readAsDataURL(file); - reader.onload = () => { - const dataUrl = reader.result; - semaphore.release(); - if (typeof dataUrl === 'string') { - resolve(dataUrl.split(',')[1]); - } else { - reject(new Error('Invalid data URL')); - } - reader.onload = null; // Clear the handler - reader.onerror = null; // Clear the handle - }; - reader.onerror = (error) => { - semaphore.release(); - reject(error); - reader.onload = null; // Clear the handler - reader.onerror = null; // Clear the handle - }; +export const fileToBase64 = (file) => + new Promise(async (resolve, reject) => { + const reader = new FileReader(); // Create a new instance + await semaphore.acquire(); + reader.readAsDataURL(file); + reader.onload = () => { + const dataUrl = reader.result; + semaphore.release(); + if (typeof dataUrl === 'string') { + resolve(dataUrl.split(',')[1]); + } else { + reject(new Error('Invalid data URL')); + } + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; + reader.onerror = (error) => { + semaphore.release(); + reject(error); + reader.onload = null; // Clear the handler + reader.onerror = null; // Clear the handle + }; }); - -export const base64ToBlobUrl = (base64, mimeType = "image/png") => { - const binary = atob(base64); - const array = []; - for (let i = 0; i < binary.length; i++) { - array.push(binary.charCodeAt(i)); - } - const blob = new Blob([new Uint8Array(array)], { type: mimeType }); - return URL.createObjectURL(blob); - }; \ No newline at end of file +export const base64ToBlobUrl = (base64, mimeType = 'image/png') => { + const binary = atob(base64); + const array = []; + for (let i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + const blob = new Blob([new Uint8Array(array)], { type: mimeType }); + 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(); + }); +};