mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
add multi image upload
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
"@sentry/vite-plugin": "^2.16.0",
|
"@sentry/vite-plugin": "^2.16.0",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"dompurify": "^3.0.8",
|
"dompurify": "^3.0.8",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^10.18.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"million": "latest",
|
"million": "latest",
|
||||||
"motion": "^10.17.0",
|
"motion": "^10.17.0",
|
||||||
"npm": "^10.4.0",
|
"npm": "^10.4.0",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { debounce } from 'lodash';
|
||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
interface ThemeList {
|
interface ThemeList {
|
||||||
themes: string[];
|
themes: string[];
|
||||||
@@ -54,3 +55,12 @@ export const deleteTheme = async (themeName: string) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendThemeUpdate = debounce((updatedTheme: CustomTheme) => {
|
||||||
|
// Send the updated theme to the content script for live preview
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
type: 'currentTab',
|
||||||
|
info: 'UpdateThemePreview',
|
||||||
|
body: updatedTheme,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import CodeEditor from '../components/CodeEditor';
|
import CodeEditor from '../components/CodeEditor';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import ColorPicker from 'react-best-gradient-color-picker';
|
import ColorPicker from 'react-best-gradient-color-picker';
|
||||||
import { SettingsContextProvider } from '../SettingsContext';
|
|
||||||
import Accordion from '../components/Accordian';
|
import Accordion from '../components/Accordian';
|
||||||
import Switch from '../components/Switch';
|
import Switch from '../components/Switch';
|
||||||
|
import { sendThemeUpdate } from '../hooks/ThemeManagment';
|
||||||
|
|
||||||
export default function ThemeCreator() {
|
export default function ThemeCreator() {
|
||||||
const [theme, setTheme] = useState<CustomTheme>({
|
const [theme, setTheme] = useState<CustomTheme>({
|
||||||
@@ -15,15 +15,56 @@ export default function ThemeCreator() {
|
|||||||
CustomImages: []
|
CustomImages: []
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveTheme() {
|
const generateImageId = () => {
|
||||||
console.log(theme);
|
return '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const imageUrl = reader.result as string;
|
||||||
|
const imageId = generateImageId();
|
||||||
|
const variableName = `--custom-image-${theme.CustomImages.length}`;
|
||||||
|
const updatedTheme = {
|
||||||
|
...theme,
|
||||||
|
CustomImages: [...theme.CustomImages, { id: imageId, url: imageUrl, variableName }],
|
||||||
|
};
|
||||||
|
setTheme(updatedTheme);
|
||||||
|
sendThemeUpdate(updatedTheme);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveImage = (imageId: string) => {
|
||||||
|
const updatedTheme = {
|
||||||
|
...theme,
|
||||||
|
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
|
||||||
|
};
|
||||||
|
setTheme(updatedTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageVariableChange = (imageId: string, variableName: string) => {
|
||||||
|
const updatedTheme = {
|
||||||
|
...theme,
|
||||||
|
CustomImages: theme.CustomImages.map((image) =>
|
||||||
|
image.id === imageId ? { ...image, variableName } : image
|
||||||
|
),
|
||||||
|
};
|
||||||
|
setTheme(updatedTheme);
|
||||||
|
};
|
||||||
|
|
||||||
function CodeUpdate(value: string) {
|
function CodeUpdate(value: string) {
|
||||||
console.log(value);
|
const updatedTheme = { ...theme, CustomCSS: value };
|
||||||
setTheme((previousTheme) => ({ ...previousTheme, CustomCSS: value }));
|
setTheme(updatedTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sendThemeUpdate(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full min-h-[100vh] bg-zinc-100 dark:bg-zinc-800 dark:text-white transition duration-30'>
|
<div className='w-full min-h-[100vh] bg-zinc-100 dark:bg-zinc-800 dark:text-white transition duration-30'>
|
||||||
<div className='flex flex-col p-2'>
|
<div className='flex flex-col p-2'>
|
||||||
@@ -73,9 +114,26 @@ export default function ThemeCreator() {
|
|||||||
<div className='h-4'></div>
|
<div className='h-4'></div>
|
||||||
|
|
||||||
<Accordion defaultOpened title='Custom Images'>
|
<Accordion defaultOpened title='Custom Images'>
|
||||||
child
|
{theme.CustomImages.map((image, index) => (
|
||||||
|
<div key={image.id}>
|
||||||
|
<img src={image.url} alt={`Custom Image ${index + 1}`} />
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={image.variableName}
|
||||||
|
onChange={(e) => handleImageVariableChange(image.id, e.target.value)}
|
||||||
|
placeholder='CSS Variable Name'
|
||||||
|
/>
|
||||||
|
<button onClick={() => handleRemoveImage(image.id)}>Remove</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
onChange={handleImageUpload}
|
||||||
|
/>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
|
||||||
<div className='h-4'></div>
|
<div className='h-4'></div>
|
||||||
|
|
||||||
<Accordion defaultOpened title='Custom CSS'>
|
<Accordion defaultOpened title='Custom CSS'>
|
||||||
@@ -86,11 +144,9 @@ export default function ThemeCreator() {
|
|||||||
callback={CodeUpdate} />
|
callback={CodeUpdate} />
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<button onClick={saveTheme} className='w-full px-4 py-2 my-4 text-white transition bg-blue-500 rounded dark:text-white'>
|
<button onClick={() => console.log('shared!')} className='w-full px-4 py-2 my-4 text-white transition bg-blue-500 rounded dark:text-white'>
|
||||||
Save Theme
|
Share theme
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<SettingsContextProvider><></></SettingsContextProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,5 +4,11 @@ type CustomTheme = {
|
|||||||
defaultColour: string;
|
defaultColour: string;
|
||||||
CanChangeColour: boolean;
|
CanChangeColour: boolean;
|
||||||
CustomCSS: string;
|
CustomCSS: string;
|
||||||
CustomImages: string[];
|
CustomImages: CustomImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomImage = {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
variableName: string;
|
||||||
}
|
}
|
||||||
@@ -160,3 +160,61 @@ export const disableTheme = async () => {
|
|||||||
// Clear the selected theme from localforage
|
// Clear the selected theme from localforage
|
||||||
localforage.removeItem('selectedTheme');
|
localforage.removeItem('selectedTheme');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let imageData: CustomImage[] = [];
|
||||||
|
let previousTheme: CustomTheme = null;
|
||||||
|
|
||||||
|
export const UpdateThemePreview = async (updatedTheme: CustomTheme) => {
|
||||||
|
console.log(updatedTheme)
|
||||||
|
|
||||||
|
if (updatedTheme.CustomImages.length !== imageData.length) {
|
||||||
|
updatedTheme.CustomImages.forEach((image) => {
|
||||||
|
updateImage(image.id, image.url);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { CustomCSS, CustomImages, defaultColour } = updatedTheme;
|
||||||
|
|
||||||
|
// Apply custom CSS
|
||||||
|
let styleElement = document.getElementById('theme-preview-styles');
|
||||||
|
if (!styleElement) {
|
||||||
|
styleElement = document.createElement('style');
|
||||||
|
styleElement.id = 'theme-preview-styles';
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
}
|
||||||
|
styleElement.textContent = CustomCSS;
|
||||||
|
|
||||||
|
// Apply default color
|
||||||
|
if (defaultColour !== '') {
|
||||||
|
browser.storage.local.set({ selectedColor: defaultColour });
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomImages.forEach((image) => {
|
||||||
|
// @ts-expect-error - not sure why its yelling at me :(
|
||||||
|
const imageUrl = imageData[image.id];
|
||||||
|
if (imageUrl) {
|
||||||
|
document.documentElement.style.setProperty(image.variableName, `url(${imageUrl})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateImage(imageId: string, imageDataURI: string) {
|
||||||
|
// Extract base64 data from the data URI
|
||||||
|
const base64Index = imageDataURI.indexOf(',') + 1;
|
||||||
|
const imageBase64 = imageDataURI.substring(base64Index);
|
||||||
|
|
||||||
|
// Convert base64 to blob
|
||||||
|
const byteCharacters = atob(imageBase64);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
const blob = new Blob([byteArray], { type: 'image/png' });
|
||||||
|
|
||||||
|
// Convert blob to blob URL
|
||||||
|
const imageUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// @ts-expect-error - same problem 😭
|
||||||
|
imageData[imageId] = imageUrl;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
import { MenuOptionsOpen, OpenMenuOptions, OpenWhatsNewPopup, closeSettings } from '../../../SEQTA';
|
import { MenuOptionsOpen, OpenMenuOptions, OpenWhatsNewPopup, closeSettings } from '../../../SEQTA';
|
||||||
import { deleteTheme, disableTheme, downloadTheme, listThemes, setTheme } from '../../ui/Themes';
|
import { deleteTheme, disableTheme, downloadTheme, listThemes, setTheme, updateImage, UpdateThemePreview } from '../../ui/Themes';
|
||||||
import { OpenThemeCreator } from '../../ui/ThemeCreator';
|
import { OpenThemeCreator } from '../../ui/ThemeCreator';
|
||||||
|
|
||||||
export class MessageHandler {
|
export class MessageHandler {
|
||||||
@@ -44,6 +44,11 @@ export class MessageHandler {
|
|||||||
sendResponse({ status: 'success' });
|
sendResponse({ status: 'success' });
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
case 'UpdateThemePreview':
|
||||||
|
UpdateThemePreview(request.body).then(() => {
|
||||||
|
sendResponse({ status: 'success' });
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'OpenChangelog':
|
case 'OpenChangelog':
|
||||||
OpenWhatsNewPopup();
|
OpenWhatsNewPopup();
|
||||||
closeSettings();
|
closeSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user