mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
fix: building working, (lots of bugs)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import React, { forwardRef, ForwardRefExoticComponent, RefAttributes, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { deleteTheme, disableTheme, getDownloadedThemes, listThemes, sendThemeUpdate, setTheme } from '../hooks/ThemeManagment';
|
||||
import { DeleteDownloadedTheme } from '../pages/Store';
|
||||
import { ThemeCover } from './ThemeCover';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { CustomTheme, DownloadedTheme } from '../types/CustomThemes';
|
||||
import { useSettingsContext } from '../SettingsContext';
|
||||
import { SettingsState } from '../types/AppProps';
|
||||
import { InstallTheme } from '../../seqta/ui/themes/downloadTheme';
|
||||
import SpinnerIcon from './LoadingSpinner';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import useVisibility from './useVisibility';
|
||||
import { debounce } from 'lodash';
|
||||
import { Mutex } from '../../seqta/utils/mutex';
|
||||
|
||||
interface ThemeSelectorProps {
|
||||
isEditMode: boolean;
|
||||
ref: React.Ref<any>;
|
||||
}
|
||||
|
||||
const ThemeSelector: ForwardRefExoticComponent<Omit<ThemeSelectorProps, "ref"> & RefAttributes<any>> = forwardRef(({ isEditMode = false }, ref) => {
|
||||
const [themes, setThemes] = useState<Omit<CustomTheme, 'CustomImages'>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [tempTheme, setTempTheme] = useState<any>(null);
|
||||
const { settingsState, setSettingsState } = useSettingsContext();
|
||||
const [elementRef, isVisible] = useVisibility({
|
||||
root: null, // Use the viewport as the root
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1, // 10% of the element needs to be visible
|
||||
});
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
const setSelectedTheme = (themeId: string) => {
|
||||
setSettingsState((prevState: SettingsState) => ({
|
||||
...prevState,
|
||||
selectedTheme: themeId,
|
||||
}));
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
disableTheme: async () => {
|
||||
await disableTheme();
|
||||
setSelectedTheme('');
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const handleThemeChange = async () => {
|
||||
//await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
fetchThemes();
|
||||
};
|
||||
|
||||
window.addEventListener('message', (message) => {
|
||||
if (message.data.type === 'themeChanged') {
|
||||
handleThemeChange();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', (message) => {
|
||||
if (message.data.type === 'themeChanged') {
|
||||
handleThemeChange();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: any;
|
||||
if (isVisible) {
|
||||
intervalId = setInterval(fetchThemes, 2000);
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
const fetchThemes = async () => {
|
||||
try {
|
||||
const { themes, selectedTheme } = await listThemes();
|
||||
let tempDownloadedThemes = await getDownloadedThemes();
|
||||
|
||||
setThemes(themes);
|
||||
setSelectedTheme(selectedTheme ? selectedTheme : '');
|
||||
|
||||
const matchingThemes = themes.filter(theme =>
|
||||
tempDownloadedThemes.some(downloadedTheme => downloadedTheme.id === theme.id)
|
||||
);
|
||||
|
||||
if (matchingThemes.length > 0) {
|
||||
matchingThemes.forEach((theme) => {
|
||||
DeleteDownloadedTheme(theme.id);
|
||||
tempDownloadedThemes = tempDownloadedThemes.filter(downloadedTheme => downloadedTheme.id !== theme.id);
|
||||
})
|
||||
}
|
||||
|
||||
tempDownloadedThemes.forEach(async (theme) => {
|
||||
await sendThemeUpdate(theme as DownloadedTheme, true, false)
|
||||
DeleteDownloadedTheme(theme.id);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching themes:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchThemes();
|
||||
}, []);
|
||||
|
||||
const handleThemeSelect = useCallback(
|
||||
async (themeId: string) => {
|
||||
const unlock = await mutex.lock();
|
||||
try {
|
||||
if (themeId === settingsState.selectedTheme) {
|
||||
await disableTheme();
|
||||
setSelectedTheme('');
|
||||
} else {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId);
|
||||
if (selectedTheme) {
|
||||
await setTheme(selectedTheme.id);
|
||||
setSelectedTheme(themeId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
},
|
||||
[settingsState.selectedTheme, themes]
|
||||
);
|
||||
|
||||
const handleThemeSelectDebounced = useCallback(
|
||||
debounce(handleThemeSelect, 100),
|
||||
[handleThemeSelect]
|
||||
);
|
||||
|
||||
const handleThemeDelete = useCallback(
|
||||
async (themeId: string) => {
|
||||
try {
|
||||
await deleteTheme(themeId);
|
||||
setThemes((prevThemes) => prevThemes.filter((theme) => theme.id !== themeId));
|
||||
if (themeId === settingsState.selectedTheme) {
|
||||
setSelectedTheme('')
|
||||
disableTheme();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting theme:', error);
|
||||
}
|
||||
},
|
||||
[settingsState.selectedTheme]
|
||||
);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file: File = e.dataTransfer.files[0];
|
||||
const reader: FileReader = new FileReader();
|
||||
|
||||
reader.onload = async (event: ProgressEvent<FileReader>) => {
|
||||
try {
|
||||
const result: any = JSON.parse(event.target!.result as string);
|
||||
try {
|
||||
setTempTheme(result);
|
||||
await InstallTheme(result);
|
||||
await fetchThemes();
|
||||
setTempTheme(null);
|
||||
} catch(error) {
|
||||
toast.error('Invalid file type. Please upload a valid theme file.');
|
||||
setTempTheme(null);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error parsing file. Please upload a valid JSON theme file.');
|
||||
setTempTheme(null);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='text-center'>Loading themes...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className={`my-3 w-full`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className={`${isDragging ? 'opacity-100' : 'opacity-0'} transition pointer-events-none absolute w-full p-2 z-50`}>
|
||||
<div className='sticky w-full h-64 bg-white shadow-xl dark:bg-zinc-900 top-5 dark:text-white rounded-xl outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700'>
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<svg height="48" width="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="currentColor">
|
||||
<path d="M44,31a1,1,0,0,0-1,1v8a3,3,0,0,1-3,3H8a3,3,0,0,1-3-3V32a1,1,0,0,0-2,0v8a5.006,5.006,0,0,0,5,5H40a5.006,5.006,0,0,0,5-5V32A1,1,0,0,0,44,31Z" fill="currentColor"/>
|
||||
<path d="M23.2,33.6a1,1,0,0,0,1.6,0l9-12A1,1,0,0,0,33,20H26V5a2,2,0,0,0-4,0V20H15a1,1,0,0,0-.8,1.6Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
<span className='text-lg'>Import Theme</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="pb-2 text-lg font-bold">Themes</h2>
|
||||
<div className="flex flex-col gap-2 px-1">
|
||||
|
||||
{themes.map((theme) => (
|
||||
<ThemeCover
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
isSelected={theme.id === settingsState.selectedTheme}
|
||||
isEditMode={isEditMode}
|
||||
onThemeSelect={handleThemeSelectDebounced}
|
||||
onThemeDelete={handleThemeDelete}
|
||||
/>
|
||||
))}
|
||||
|
||||
{tempTheme && (
|
||||
<div className="flex justify-center w-full bg-gray-200 rounded-xl dark:bg-zinc-700/50 place-items-center aspect-theme animate-pulse">
|
||||
<SpinnerIcon className='opacity-50' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ themes.length > 0 && <div
|
||||
id="divider"
|
||||
className="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"
|
||||
></div>}
|
||||
|
||||
<a
|
||||
href={browser.runtime.getURL('interface/index.html#store')}
|
||||
target="_blank"
|
||||
className="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span className="text-xl font-IconFamily">{'\uecc5'}</span>
|
||||
<span className="ml-2">Theme Store</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator' })}
|
||||
className="flex items-center justify-center w-full transition aspect-theme 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 your own</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ThemeSelector;
|
||||
Reference in New Issue
Block a user