mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
add theme saving (This took hours)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { CustomTheme } from '../types/CustomThemes';
|
||||
|
||||
type ThemeCoverProps = {
|
||||
theme: Omit<CustomTheme, 'CustomImages'>;
|
||||
isSelected: boolean;
|
||||
isEditMode: boolean;
|
||||
onThemeSelect: (themeId: string) => void;
|
||||
onThemeDelete: (themeId: string) => void;
|
||||
};
|
||||
|
||||
export const ThemeCover: React.FC<ThemeCoverProps> = ({
|
||||
theme,
|
||||
isSelected,
|
||||
isEditMode,
|
||||
onThemeSelect,
|
||||
onThemeDelete,
|
||||
}) => {
|
||||
const handleThemeClick = () => {
|
||||
onThemeSelect(theme.id);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onThemeDelete(theme.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`relative w-full h-16 flex justify-center items-center rounded-lg bg-zinc-700 transition ring dark:ring-white ring-zinc-300 ${
|
||||
isSelected ? 'dark:ring-2 ring-4' : 'ring-0'
|
||||
}`}
|
||||
onClick={handleThemeClick}
|
||||
>
|
||||
{isEditMode && (
|
||||
<div
|
||||
className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onClick={handleDeleteClick}
|
||||
>
|
||||
<div className="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative top-0 z-10 flex justify-center w-full h-full overflow-hidden text-white transition rounded-lg group place-items-center bg-zinc-200 dark:bg-zinc-900">
|
||||
{/* Render theme cover image or placeholder */}
|
||||
{/* {theme.CustomImages.length > 0 ? (
|
||||
<img
|
||||
src={URL.createObjectURL(theme.CustomImages[0].blob)}
|
||||
alt={theme.name}
|
||||
className="absolute inset-0 z-0 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 z-0 bg-gray-300 rounded-lg"></div>
|
||||
)} */}
|
||||
<div className="z-10">{theme.name}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,177 +1,106 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import themesList from '../assets/themes';
|
||||
import { listThemes, disableTheme, downloadTheme, setTheme, deleteTheme } from "../hooks/ThemeManagment";
|
||||
import Browser from "webextension-polyfill";
|
||||
|
||||
interface Theme {
|
||||
name: string;
|
||||
url: string;
|
||||
isDownloaded: boolean;
|
||||
isLoading: boolean;
|
||||
coverImage: JSX.Element;
|
||||
}
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { listThemes, deleteTheme, setTheme, disableTheme } from '../hooks/ThemeManagment';
|
||||
import { ThemeCover } from './ThemeCover';
|
||||
import Browser from 'webextension-polyfill';
|
||||
import { CustomTheme } from '../types/CustomThemes';
|
||||
|
||||
interface ThemeSelectorProps {
|
||||
selectedType: "background" | "theme";
|
||||
setSelectedType: (type: "background" | "theme") => void;
|
||||
setSelectedType: React.Dispatch<React.SetStateAction<'background' | 'theme'>>;
|
||||
selectedType: 'background' | 'theme';
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
const ThemeSelector = ({ selectedType, setSelectedType, isEditMode }: ThemeSelectorProps) => {
|
||||
const [themes, setThemes] = useState<Theme[]>([]);
|
||||
const [enabledThemeName, setEnabledThemeName] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const initializeThemes = async () => {
|
||||
const downloaded = (await listThemes());
|
||||
const initializedThemes = themesList.map(theme => ({
|
||||
...theme,
|
||||
isDownloaded: downloaded.themes.includes(theme.name),
|
||||
isLoading: false
|
||||
}));
|
||||
|
||||
if (downloaded.selectedTheme !== '') {
|
||||
setEnabledThemeName(downloaded.selectedTheme);
|
||||
}
|
||||
|
||||
initializedThemes.sort((a, b) => Number(b.isDownloaded) - Number(a.isDownloaded));
|
||||
|
||||
setThemes(initializedThemes);
|
||||
};
|
||||
|
||||
initializeThemes();
|
||||
}, []);
|
||||
|
||||
const handleDeleteTheme = async (themeName: string) => {
|
||||
await deleteTheme(themeName);
|
||||
setThemes(prevThemes => {
|
||||
// Update the theme's isDownloaded property to false
|
||||
const updatedThemes = prevThemes.map(theme => {
|
||||
if (theme.name === themeName) {
|
||||
return { ...theme, isDownloaded: false };
|
||||
}
|
||||
return theme;
|
||||
});
|
||||
|
||||
// Sort themes so non-downloaded ones appear last
|
||||
updatedThemes.sort((a, b) => Number(b.isDownloaded) - Number(a.isDownloaded));
|
||||
|
||||
return updatedThemes;
|
||||
});
|
||||
|
||||
// Reset the enabled theme name if the deleted theme was the currently enabled one
|
||||
if (enabledThemeName === themeName) {
|
||||
setEnabledThemeName('');
|
||||
setSelectedType('background');
|
||||
}
|
||||
};
|
||||
|
||||
const handleThemeAction = async (themeName: string, themeURL: string) => {
|
||||
const startLoading = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isLoading: true } : theme
|
||||
))
|
||||
);
|
||||
|
||||
// Stop loading for the selected theme.
|
||||
const stopLoading = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isLoading: false } : theme
|
||||
))
|
||||
);
|
||||
|
||||
// Update the theme as downloaded.
|
||||
const markAsDownloaded = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isDownloaded: true } : theme
|
||||
))
|
||||
);
|
||||
|
||||
startLoading(themeName);
|
||||
|
||||
// Early return if theme is not found.
|
||||
const theme = themes.find(t => t.name === themeName);
|
||||
if (!theme) {
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is downloaded and is the currently enabled theme, disable it.
|
||||
if (theme.isDownloaded && themeName === enabledThemeName) {
|
||||
await disableTheme();
|
||||
setEnabledThemeName('');
|
||||
setSelectedType('background');
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is downloaded but not enabled, enable it.
|
||||
if (theme.isDownloaded && themeName !== enabledThemeName) {
|
||||
await setTheme(themeName, themeURL);
|
||||
setEnabledThemeName(themeName);
|
||||
setSelectedType('theme');
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is not downloaded, download and enable it.
|
||||
if (!theme.isDownloaded) {
|
||||
await downloadTheme(themeName, themeURL);
|
||||
markAsDownloaded(themeName);
|
||||
setSelectedType('theme');
|
||||
setEnabledThemeName(themeName);
|
||||
}
|
||||
|
||||
stopLoading(themeName);
|
||||
};
|
||||
const ThemeSelector: React.FC<ThemeSelectorProps> = ({
|
||||
setSelectedType,
|
||||
selectedType,
|
||||
isEditMode,
|
||||
}) => {
|
||||
const [themes, setThemes] = useState<Omit<CustomTheme, 'CustomImages'>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [selectedThemeId, setSelectedThemeId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedType === 'background') {
|
||||
setEnabledThemeName('');
|
||||
setSelectedThemeId(null);
|
||||
}
|
||||
}, [selectedType]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchThemes = async () => {
|
||||
try {
|
||||
const { themes, selectedTheme } = await listThemes();
|
||||
|
||||
console.log(await listThemes());
|
||||
|
||||
setThemes(themes);
|
||||
setSelectedThemeId(selectedTheme ? selectedTheme : null);
|
||||
} catch (error) {
|
||||
console.error('Error fetching themes:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchThemes();
|
||||
}, []);
|
||||
|
||||
const handleThemeSelect = useCallback(
|
||||
async (themeId: string) => {
|
||||
if (themeId === selectedThemeId) {
|
||||
await disableTheme();
|
||||
setSelectedThemeId(null);
|
||||
setSelectedType('background');
|
||||
} else {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
||||
if (selectedTheme) {
|
||||
await setTheme(selectedTheme.id);
|
||||
setSelectedThemeId(themeId);
|
||||
setSelectedType('theme');
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedThemeId, themes, setSelectedType]
|
||||
);
|
||||
|
||||
const handleThemeDelete = useCallback(
|
||||
async (themeId: string) => {
|
||||
try {
|
||||
await deleteTheme(themeId);
|
||||
setThemes((prevThemes) => prevThemes.filter((theme) => theme.id !== themeId));
|
||||
if (themeId === selectedThemeId) {
|
||||
setSelectedThemeId(null);
|
||||
setSelectedType('background');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting theme:', error);
|
||||
}
|
||||
},
|
||||
[selectedThemeId, setSelectedType]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading themes...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
{(isEditMode ? themes.some(theme => theme.isDownloaded) : themes.length > 0) && (
|
||||
<h2 className="pb-2 text-lg font-bold">Themes</h2>
|
||||
)}
|
||||
<h2 className="pb-2 text-lg font-bold">Themes</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
{themes
|
||||
.filter(theme => !isEditMode || theme.isDownloaded) // Only show downloaded themes in edit mode
|
||||
.map((theme) => (
|
||||
<button
|
||||
key={theme.name}
|
||||
className={`relative w-full h-16 flex justify-center items-center rounded-lg bg-zinc-700 transition ring dark:ring-white ring-zinc-300 ${enabledThemeName == theme.name && selectedType == "theme" ? 'dark:ring-2 ring-4' : 'ring-0'}`}
|
||||
onClick={() => handleThemeAction(theme.name, theme.url)}
|
||||
disabled={theme.isLoading}
|
||||
>
|
||||
{isEditMode && (
|
||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteTheme(theme.name); }}>
|
||||
<div className="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`relative transition rounded-lg overflow-hidden top-0 z-10 flex justify-center w-full h-full text-white group place-items-center ${ theme.isDownloaded ? '' : 'hover:bg-black/20'}`}>
|
||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
||||
{ theme.isDownloaded || theme.isLoading ? '' : ''}
|
||||
</span>
|
||||
|
||||
{ theme.isLoading &&
|
||||
<div className="z-10 inline-block w-6 h-6 border-4 border-current rounded-full animate-spin border-t-transparent" role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div> }
|
||||
|
||||
</div>
|
||||
<div className="absolute inset-0 z-0 overflow-hidden rounded-lg">
|
||||
{theme.coverImage}
|
||||
</div>
|
||||
</button>
|
||||
{themes.map((theme) => (
|
||||
<ThemeCover
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
isSelected={theme.id === selectedThemeId}
|
||||
isEditMode={isEditMode}
|
||||
onThemeSelect={handleThemeSelect}
|
||||
onThemeDelete={handleThemeDelete}
|
||||
/>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => Browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator' })}
|
||||
className="flex items-center justify-center w-full h-16 transition rounded-lg bg-zinc-700">
|
||||
className="flex items-center justify-center w-full h-16 transition rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span className="text-xl font-IconFamily">{'\uec60'}</span>
|
||||
<span className="ml-2">Create Theme</span>
|
||||
</button>
|
||||
@@ -180,4 +109,4 @@ const ThemeSelector = ({ selectedType, setSelectedType, isEditMode }: ThemeSelec
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ThemeSelector);
|
||||
export default ThemeSelector;
|
||||
@@ -1,9 +1,6 @@
|
||||
import { debounce } from 'lodash';
|
||||
import browser from 'webextension-polyfill'
|
||||
interface ThemeList {
|
||||
themes: string[];
|
||||
selectedTheme: string;
|
||||
}
|
||||
import { CustomTheme, ThemeList } from '../types/CustomThemes';
|
||||
|
||||
export const downloadTheme = async (themeName: string, themeURL: string) => {
|
||||
// send message to the background script
|
||||
@@ -17,23 +14,32 @@ export const downloadTheme = async (themeName: string, themeURL: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
export const setTheme = async (themeName: string, themeURL: string) => {
|
||||
export const setTheme = async (themeID: string) => {
|
||||
// send message to the background script
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'SetTheme',
|
||||
body: {
|
||||
themeName: themeName,
|
||||
themeURL: themeURL
|
||||
themeID: themeID
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const listThemes = async () => {
|
||||
export const listThemes = async (): Promise<ThemeList> => {
|
||||
// send message to the background script
|
||||
const response: ThemeList = await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'ListThemes'
|
||||
const response: ThemeList = await new Promise((resolve, reject) => {
|
||||
browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'ListThemes'
|
||||
}).then((response) => {
|
||||
if (response) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error('Failed to get response'));
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -46,20 +52,22 @@ export const disableTheme = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTheme = async (themeName: string) => {
|
||||
export const deleteTheme = async (themeID: string) => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'DeleteTheme',
|
||||
body: {
|
||||
themeName: themeName
|
||||
themeID: themeID
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const sendThemeUpdate = debounce((updatedTheme: CustomTheme) => {
|
||||
export const sendThemeUpdate = debounce((updatedTheme: CustomTheme, saveTheme?: boolean) => {
|
||||
// Create a copy of the updatedTheme object
|
||||
const updatedThemeCopy: CustomTheme = { ...updatedTheme };
|
||||
|
||||
saveTheme = saveTheme || false;
|
||||
|
||||
// Convert image blobs to base64
|
||||
const base64ConversionPromises = updatedThemeCopy.CustomImages.map(async (image) => {
|
||||
const base64 = await blobToBase64(image.blob);
|
||||
@@ -76,6 +84,7 @@ export const sendThemeUpdate = debounce((updatedTheme: CustomTheme) => {
|
||||
type: 'currentTab',
|
||||
info: 'UpdateThemePreview',
|
||||
body: updatedThemeCopy,
|
||||
save: saveTheme,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -96,4 +105,19 @@ const blobToBase64 = (blob: Blob): Promise<string> => {
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
export const enableCurrentTheme = async () => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'EnableCurrentTheme',
|
||||
});
|
||||
};
|
||||
|
||||
export const saveUpdatedTheme = async (updatedTheme: CustomTheme) => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'SaveTheme',
|
||||
body: updatedTheme,
|
||||
});
|
||||
};
|
||||
@@ -9,7 +9,7 @@ const Themes: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
listThemes().then(themes => {
|
||||
if (themes.selectedTheme) {
|
||||
if (themes?.selectedTheme) {
|
||||
setSelectedType('theme');
|
||||
} else {
|
||||
setSelectedType('background');
|
||||
@@ -18,12 +18,12 @@ const Themes: FC = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-0.5">
|
||||
<button className="absolute top-12 z-20 right-0 p-2 text-[0.8rem] text-blue-500" onClick={() => setIsEditMode(!isEditMode)}>
|
||||
{isEditMode ? 'Done' : 'Edit'}
|
||||
</button>
|
||||
<BackgroundSelector setSelectedType={setSelectedType} selectedType={selectedType} isEditMode={isEditMode} />
|
||||
<ThemeSelector setSelectedType={setSelectedType} selectedType={selectedType} isEditMode={isEditMode} />
|
||||
<ThemeSelector selectedType={selectedType} setSelectedType={setSelectedType} isEditMode={isEditMode} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { sendThemeUpdate } from '../hooks/ThemeManagment';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import localforage from 'localforage';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { CustomTheme } from '../types/CustomThemes';
|
||||
|
||||
function ThemeCreator({ themeID }: { themeID?: string }) {
|
||||
const [theme, setTheme] = useState<CustomTheme>({
|
||||
@@ -77,24 +78,8 @@ function ThemeCreator({ themeID }: { themeID?: string }) {
|
||||
}));
|
||||
}
|
||||
|
||||
const saveTheme = async () => {
|
||||
try {
|
||||
await localforage.setItem(theme.id, theme);
|
||||
await localforage.getItem('customThemes').then((themes: unknown) => {
|
||||
const themeList = themes as string[] | null;
|
||||
if (themeList) {
|
||||
if (!themeList.includes(theme.id)) {
|
||||
themeList.push(theme.id);
|
||||
localforage.setItem('customThemes', themeList);
|
||||
}
|
||||
} else {
|
||||
localforage.setItem('customThemes', [theme.id]);
|
||||
}
|
||||
});
|
||||
console.log('Theme saved successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error saving theme:', error);
|
||||
}
|
||||
const saveTheme = () => {
|
||||
sendThemeUpdate(theme, true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type CustomTheme = {
|
||||
export type CustomTheme = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -8,8 +8,23 @@ type CustomTheme = {
|
||||
CustomImages: CustomImage[];
|
||||
}
|
||||
|
||||
type CustomImage = {
|
||||
export type CustomImage = {
|
||||
id: string;
|
||||
blob: Blob;
|
||||
variableName: string;
|
||||
}
|
||||
|
||||
export type CustomImageBase64 = {
|
||||
id: string;
|
||||
url: string;
|
||||
variableName: string;
|
||||
}
|
||||
|
||||
export type CustomThemeBase64 = Omit<CustomTheme, 'CustomImages'> & {
|
||||
CustomImages: CustomImageBase64[];
|
||||
}
|
||||
|
||||
export type ThemeList = {
|
||||
themes: Omit<CustomTheme, 'CustomImages'>[];
|
||||
selectedTheme: string;
|
||||
}
|
||||
Reference in New Issue
Block a user