mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-14 03:51:23 +00:00
added import/export for theme
This commit is contained in:
parent
ed7b36791a
commit
49848a7bd0
@ -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>
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user