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; } const ThemeSelector: ForwardRefExoticComponent & RefAttributes> = forwardRef(({ isEditMode = false }, ref) => { const [themes, setThemes] = useState[]>([]); const [isLoading, setIsLoading] = useState(true); const [isDragging, setIsDragging] = useState(false); const [tempTheme, setTempTheme] = useState(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) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = () => { setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file: File = e.dataTransfer.files[0]; const reader: FileReader = new FileReader(); reader.onload = async (event: ProgressEvent) => { 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
Loading themes...
; } return (
Import Theme

Themes

{themes.map((theme) => ( ))} {tempTheme && (
)} { themes.length > 0 &&
} {'\uecc5'} Theme Store
); }); export default ThemeSelector;