diff --git a/package.json b/package.json index c43aca56..ad5d7839 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@sentry/vite-plugin": "^2.16.0", "@types/color": "^3.0.6", "@types/dompurify": "^3.0.5", + "@types/lodash": "^4.17.0", "@types/node": "^20.11.30", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", @@ -55,6 +56,7 @@ "dompurify": "^3.0.8", "framer-motion": "^10.18.0", "localforage": "^1.10.0", + "lodash": "^4.17.21", "million": "latest", "motion": "^10.17.0", "npm": "^10.4.0", diff --git a/src/interface/hooks/ThemeManagment.tsx b/src/interface/hooks/ThemeManagment.tsx index c3d43c02..147977f3 100644 --- a/src/interface/hooks/ThemeManagment.tsx +++ b/src/interface/hooks/ThemeManagment.tsx @@ -1,3 +1,4 @@ +import { debounce } from 'lodash'; import browser from 'webextension-polyfill' interface ThemeList { themes: string[]; @@ -53,4 +54,13 @@ export const deleteTheme = async (themeName: string) => { themeName: themeName } }); -} \ No newline at end of file +} + +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); \ No newline at end of file diff --git a/src/interface/pages/ThemeCreator.tsx b/src/interface/pages/ThemeCreator.tsx index 1e652150..11298606 100644 --- a/src/interface/pages/ThemeCreator.tsx +++ b/src/interface/pages/ThemeCreator.tsx @@ -1,9 +1,9 @@ import CodeEditor from '../components/CodeEditor'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import ColorPicker from 'react-best-gradient-color-picker'; -import { SettingsContextProvider } from '../SettingsContext'; import Accordion from '../components/Accordian'; import Switch from '../components/Switch'; +import { sendThemeUpdate } from '../hooks/ThemeManagment'; export default function ThemeCreator() { const [theme, setTheme] = useState({ @@ -15,15 +15,56 @@ export default function ThemeCreator() { CustomImages: [] }); - function saveTheme() { - console.log(theme); - } + const generateImageId = () => { + return '_' + Math.random().toString(36).substr(2, 9); + }; + + const handleImageUpload = (event: React.ChangeEvent) => { + 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) { - console.log(value); - setTheme((previousTheme) => ({ ...previousTheme, CustomCSS: value })); + const updatedTheme = { ...theme, CustomCSS: value }; + setTheme(updatedTheme); } + useEffect(() => { + sendThemeUpdate(theme); + }, [theme]); + return (
@@ -73,8 +114,25 @@ export default function ThemeCreator() {
- child + {theme.CustomImages.map((image, index) => ( +
+ {`Custom + handleImageVariableChange(image.id, e.target.value)} + placeholder='CSS Variable Name' + /> + +
+ ))} +
+
@@ -86,11 +144,9 @@ export default function ThemeCreator() { callback={CodeUpdate} /> - - - <>
); diff --git a/src/interface/types/CustomThemes.ts b/src/interface/types/CustomThemes.ts index ea08866f..6d274a5d 100644 --- a/src/interface/types/CustomThemes.ts +++ b/src/interface/types/CustomThemes.ts @@ -4,5 +4,11 @@ type CustomTheme = { defaultColour: string; CanChangeColour: boolean; CustomCSS: string; - CustomImages: string[]; + CustomImages: CustomImage[]; +} + +type CustomImage = { + id: string; + url: string; + variableName: string; } \ No newline at end of file diff --git a/src/seqta/ui/Themes.ts b/src/seqta/ui/Themes.ts index 7762ded8..b48afced 100644 --- a/src/seqta/ui/Themes.ts +++ b/src/seqta/ui/Themes.ts @@ -159,4 +159,62 @@ export const disableTheme = async () => { // Clear the selected theme from localforage localforage.removeItem('selectedTheme'); -}; \ No newline at end of file +}; + +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; +} \ No newline at end of file diff --git a/src/seqta/utils/listeners/MessageListener.ts b/src/seqta/utils/listeners/MessageListener.ts index b8d2224f..448e8f08 100644 --- a/src/seqta/utils/listeners/MessageListener.ts +++ b/src/seqta/utils/listeners/MessageListener.ts @@ -1,7 +1,7 @@ import browser from 'webextension-polyfill' 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'; export class MessageHandler { @@ -44,6 +44,11 @@ export class MessageHandler { sendResponse({ status: 'success' }); }); return true; + case 'UpdateThemePreview': + UpdateThemePreview(request.body).then(() => { + sendResponse({ status: 'success' }); + }); + break; case 'OpenChangelog': OpenWhatsNewPopup(); closeSettings();