diff --git a/App.tsx b/App.tsx index 3829d10..eba5291 100644 --- a/App.tsx +++ b/App.tsx @@ -10,29 +10,7 @@ import EditButton from './components/layout/EditButton'; import ConfigurationButton from './components/layout/ConfigurationButton'; import CategoryGroup from './components/layout/CategoryGroup'; import Wallpaper from './components/Wallpaper'; - -const defaultConfig: Config = { - title: 'Vision Start', - currentWallpapers: ['Abstract'], - wallpaperFrequency: '1d', - wallpaperBlur: 0, - wallpaperBrightness: 100, - wallpaperOpacity: 100, - titleSize: 'medium', - alignment: 'middle', - horizontalAlignment: 'middle', - clock: { - enabled: true, - size: 'medium', - font: 'Helvetica', - format: 'h:mm A', - }, - serverWidget: { - enabled: false, - pingFrequency: 15, - servers: [], - }, -}; +import { ConfigurationService } from './components/services/ConfigurationService'; const App: React.FC = () => { const [categories, setCategories] = useState(() => { @@ -52,21 +30,10 @@ const App: React.FC = () => { const [addingWebsite, setAddingWebsite] = useState(null); const [editingCategory, setEditingCategory] = useState(null); const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false); - const [config, setConfig] = useState(() => { - try { - const storedConfig = localStorage.getItem('config'); - if (storedConfig) { - const parsedConfig = JSON.parse(storedConfig); - return { ...defaultConfig, ...parsedConfig }; - } - } catch (error) { - console.error('Error parsing config from localStorage', error); - } - return { ...defaultConfig }; - }); + const [config, setConfig] = useState(() => ConfigurationService.loadConfig()); useEffect(() => { - localStorage.setItem('config', JSON.stringify(config)); + ConfigurationService.saveConfig(config); }, [config]); useEffect(() => { diff --git a/components/ConfigurationModal.tsx b/components/ConfigurationModal.tsx index 0a97568..009a372 100644 --- a/components/ConfigurationModal.tsx +++ b/components/ConfigurationModal.tsx @@ -1,92 +1,42 @@ import React, { useState, useRef, useEffect } from 'react'; -import ToggleSwitch from './ToggleSwitch'; -import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; -import { Server, Wallpaper } from '../types'; - -import Dropdown from './Dropdown'; +import { Config, Wallpaper } from '../types'; import { baseWallpapers } from './utils/baseWallpapers'; -import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager'; - -const REQUIRED_LOCAL_STORAGE_KEYS = ['config', 'categories', 'userWallpapers', 'wallpaperState'] as const; - -type RequiredLocalStorageKey = typeof REQUIRED_LOCAL_STORAGE_KEYS[number]; - -const safeParse = (value: string | null): unknown => { - if (value === null) { - return null; - } - - try { - return JSON.parse(value); - } catch { - return value; - } -}; - -const toStorageString = (value: unknown): string => { - return typeof value === 'string' ? value : JSON.stringify(value); -}; +import { checkChromeStorageLocalAvailable } from './utils/StorageLocalManager'; +import { ConfigurationService } from './services/ConfigurationService'; +import GeneralTab from './configuration/GeneralTab'; +import ThemeTab from './configuration/ThemeTab'; +import ClockTab from './configuration/ClockTab'; +import ServerWidgetTab from './configuration/ServerWidgetTab'; interface ConfigurationModalProps { onClose: () => void; - onSave: (config: any) => void; - currentConfig: any; - onWallpaperChange: (newConfig: Partial) => void; + onSave: (config: Config) => void; + currentConfig: Config; + onWallpaperChange: (newConfig: Partial) => void; } -const ConfigurationModal: React.FC = ({ onClose, onSave, currentConfig, onWallpaperChange }) => { - const [config, setConfig] = useState({ - ...currentConfig, - titleSize: currentConfig.titleSize || 'medium', - alignment: currentConfig.alignment || 'middle', - tileSize: currentConfig.tileSize || 'medium', - horizontalAlignment: currentConfig.horizontalAlignment || 'middle', - wallpaperBlur: currentConfig.wallpaperBlur || 0, - wallpaperBrightness: currentConfig.wallpaperBrightness || 100, - wallpaperOpacity: currentConfig.wallpaperOpacity || 100, - serverWidget: { - enabled: false, - pingFrequency: 15, - servers: [], - ...currentConfig.serverWidget, - }, - clock: { - enabled: true, - size: 'medium', - font: 'Helvetica', - format: 'h:mm A', - ...currentConfig.clock, - }, - currentWallpapers: Array.isArray(currentConfig.currentWallpapers) - ? currentConfig.currentWallpapers.filter((name: string) => typeof name === 'string') - : [], - wallpaperFrequency: currentConfig.wallpaperFrequency || '1d', - }); +const ConfigurationModal: React.FC = ({ + onClose, + onSave, + currentConfig, + onWallpaperChange, +}) => { + const [config, setConfig] = useState(currentConfig); const [activeTab, setActiveTab] = useState('general'); - const [newServerName, setNewServerName] = useState(''); - const [newServerAddress, setNewServerAddress] = useState(''); - const [newWallpaperName, setNewWallpaperName] = useState(''); - const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); const [userWallpapers, setUserWallpapers] = useState([]); const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false); + const [isVisible, setIsVisible] = useState(false); const menuRef = useRef(null); - const fileInputRef = useRef(null); const importInputRef = useRef(null); const isSaving = useRef(false); - const [isVisible, setIsVisible] = useState(false); useEffect(() => { setChromeStorageAvailable(checkChromeStorageLocalAvailable()); - const storedUserWallpapers = localStorage.getItem('userWallpapers'); - if (storedUserWallpapers) { - setUserWallpapers(JSON.parse(storedUserWallpapers)); - } + setUserWallpapers(ConfigurationService.loadUserWallpapers()); }, []); useEffect(() => { - const timer = setTimeout(() => { - setIsVisible(true); - }, 10); + const timer = setTimeout(() => setIsVisible(true), 10); return () => clearTimeout(timer); }, []); @@ -98,235 +48,67 @@ const ConfigurationModal: React.FC = ({ onClose, onSave }; }, []); - const handleClose = () => { - setIsVisible(false); - setTimeout(() => { - onClose(); - }, 250); - }; - - const handleChange = (e: React.ChangeEvent | { target: { name: string; value: string | string[] } }) => { - const { name, value } = e.target; - if (name === 'currentWallpapers') { - const wallpaperNames = Array.isArray(value) ? value : [value]; - setConfig({ ...config, currentWallpapers: wallpaperNames }); - } else if (name.startsWith('serverWidget.')) { - const field = name.split('.')[1]; - setConfig({ - ...config, - serverWidget: { ...config.serverWidget, [field]: value }, - }); - } else if (name.startsWith('clock.')) { - const field = name.split('.')[1]; - setConfig({ - ...config, - clock: { ...config.clock, [field]: value }, - }); - } else { - setConfig({ ...config, [name]: value }); - } - }; - useEffect(() => { onWallpaperChange({ currentWallpapers: config.currentWallpapers }); - // Set wallpaperState in localStorage with lastWallpaperChange datetime - localStorage.setItem('wallpaperState', JSON.stringify({ - lastWallpaperChange: new Date().toISOString(), - currentIndex: 0, - })); + ConfigurationService.resetWallpaperState(); }, [config.currentWallpapers]); - const handleClockToggleChange = (checked: boolean) => { - setConfig({ ...config, clock: { ...config.clock, enabled: checked } }); + const handleClose = () => { + setIsVisible(false); + setTimeout(onClose, 250); }; - const handleServerWidgetToggleChange = (checked: boolean) => { - setConfig({ - ...config, - serverWidget: { ...config.serverWidget, enabled: checked }, - }); + const handleConfigChange = (updates: Partial) => { + setConfig((prev) => ({ ...prev, ...updates })); }; - const handleAddServer = () => { - if (newServerName.trim() === '' || newServerAddress.trim() === '') return; - - const newServer: Server = { - id: Date.now().toString(), - name: newServerName, - address: newServerAddress, - }; - - setConfig({ - ...config, - serverWidget: { - ...config.serverWidget, - servers: [...config.serverWidget.servers, newServer], - }, - }); - - setNewServerName(''); - setNewServerAddress(''); + const handleAddWallpaper = async (name: string, url: string) => { + const newWallpaper = await ConfigurationService.addWallpaper(name, url); + const updated = [...userWallpapers, newWallpaper]; + setUserWallpapers(updated); + ConfigurationService.saveUserWallpapers(updated); + setConfig((prev) => ({ + ...prev, + currentWallpapers: [...prev.currentWallpapers, newWallpaper.name], + })); }; - const handleRemoveServer = (id: string) => { - setConfig({ - ...config, - serverWidget: { - ...config.serverWidget, - servers: config.serverWidget.servers.filter((server: Server) => server.id !== id), - }, - }); + const handleAddWallpaperFile = async (file: File) => { + const newWallpaper = await ConfigurationService.addWallpaperFile(file); + const updated = [...userWallpapers, newWallpaper]; + setUserWallpapers(updated); + ConfigurationService.saveUserWallpapers(updated); + setConfig((prev) => ({ + ...prev, + currentWallpapers: [...prev.currentWallpapers, newWallpaper.name], + })); }; - const onDragEnd = (result: any) => { - if (!result.destination) return; - - const items = Array.from(config.serverWidget.servers); - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - - setConfig({ - ...config, - serverWidget: { - ...config.serverWidget, - servers: items, - }, - }); - }; - - const handleAddWallpaper = async () => { - if (newWallpaperUrl.trim() === '') return; + const handleDeleteWallpaper = async (wallpaper: Wallpaper) => { try { - const finalName = await addWallpaperToChromeStorageLocal(newWallpaperName, newWallpaperUrl); - const newWallpaper: Wallpaper = { name: finalName }; - const updatedUserWallpapers = [...userWallpapers, newWallpaper]; - setUserWallpapers(updatedUserWallpapers); - localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); - setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] }); - setNewWallpaperName(''); - setNewWallpaperUrl(''); - } catch (error) { - alert('Error adding wallpaper. Please check the URL and try again.'); - console.error(error); - } - }; - - const handleFileUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - if (file.size > 4 * 1024 * 1024) { - alert('File size exceeds 4MB. Please choose a smaller file.'); - return; - } - const reader = new FileReader(); - reader.onload = async () => { - const base64 = reader.result as string; - if (base64.length > 4.5 * 1024 * 1024) { - alert('The uploaded image is too large. Please choose a smaller file.'); - return; - } - try { - const finalName = await addWallpaperToChromeStorageLocal(file.name, base64); - const newWallpaper: Wallpaper = { name: finalName }; - const updatedUserWallpapers = [...userWallpapers, newWallpaper]; - setUserWallpapers(updatedUserWallpapers); - localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); - setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] }); - } catch (error) { - alert('Error adding wallpaper. Please try again.'); - console.error(error); - } - }; - reader.readAsDataURL(file); - } - }; - - const handleDeleteUserWallpaper = async (wallpaper: Wallpaper) => { - try { - await removeWallpaperFromChromeStorageLocal(wallpaper.name); - const updatedUserWallpapers = userWallpapers.filter(w => w.name !== wallpaper.name); - setUserWallpapers(updatedUserWallpapers); - localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); - const newcurrentWallpapers = config.currentWallpapers.filter((name: string) => name !== wallpaper.name); - const newConfig = { ...config, currentWallpapers: newcurrentWallpapers }; - setConfig(newConfig); - onWallpaperChange({ currentWallpapers: newcurrentWallpapers }); + await ConfigurationService.deleteWallpaper(wallpaper); + const updated = userWallpapers.filter((w) => w.name !== wallpaper.name); + setUserWallpapers(updated); + ConfigurationService.saveUserWallpapers(updated); + const newCurrentWallpapers = config.currentWallpapers.filter((n) => n !== wallpaper.name); + setConfig((prev) => ({ ...prev, currentWallpapers: newCurrentWallpapers })); + onWallpaperChange({ currentWallpapers: newCurrentWallpapers }); } catch (error) { alert('Error deleting wallpaper. Please try again.'); console.error(error); } }; - const handleExportConfig = () => { - const exportPayload = { - version: 1, - exportedAt: new Date().toISOString(), - requiredLocalStorageKeys: [...REQUIRED_LOCAL_STORAGE_KEYS], - localStorage: REQUIRED_LOCAL_STORAGE_KEYS.reduce((acc, key) => { - acc[key] = safeParse(localStorage.getItem(key)); - return acc; - }, {} as Record), - }; - - const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `vision-start-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - const handleImportClick = () => { - importInputRef.current?.click(); - }; - const handleImportConfig = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; - if (!file) { - return; - } - + if (!file) return; try { - const fileContent = await file.text(); - const parsed = JSON.parse(fileContent); - const localStorageData = parsed?.localStorage && typeof parsed.localStorage === 'object' - ? parsed.localStorage - : parsed; - - if (!localStorageData || typeof localStorageData !== 'object') { - throw new Error('Invalid import file format.'); - } - - let importedAny = false; - - REQUIRED_LOCAL_STORAGE_KEYS.forEach((key) => { - if (Object.prototype.hasOwnProperty.call(localStorageData, key)) { - const rawValue = (localStorageData as Record)[key]; - localStorage.setItem(key, toStorageString(rawValue)); - importedAny = true; - } - }); - - if (!importedAny) { - throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`); - } - - const importedConfig = (localStorageData as Record).config; - const importedUserWallpapers = (localStorageData as Record).userWallpapers; - - if (importedConfig && typeof importedConfig === 'object') { - setConfig(importedConfig as typeof config); - onWallpaperChange({ currentWallpapers: (importedConfig as { currentWallpapers?: string[] }).currentWallpapers || [] }); - onSave(importedConfig); - } - - if (Array.isArray(importedUserWallpapers)) { - setUserWallpapers(importedUserWallpapers as Wallpaper[]); - } - + const { config: importedConfig, userWallpapers: importedWallpapers } = + await ConfigurationService.importConfig(file); + setConfig(importedConfig); + setUserWallpapers(importedWallpapers); + onWallpaperChange({ currentWallpapers: importedConfig.currentWallpapers || [] }); + onSave(importedConfig); alert('Configuration imported successfully. The page will reload to apply all data.'); window.location.reload(); } catch (error) { @@ -339,6 +121,13 @@ const ConfigurationModal: React.FC = ({ onClose, onSave const allWallpapers = [...baseWallpapers, ...userWallpapers]; + const tabs = [ + { id: 'general', label: 'General' }, + { id: 'theme', label: 'Theme' }, + { id: 'clock', label: 'Clock' }, + { id: 'serverWidget', label: 'Server Widget' }, + ]; + return (
= ({ onClose, onSave isVisible ? 'opacity-100' : 'opacity-0' }`} onClick={handleClose} - >
+ />
= ({ onClose, onSave

Configuration

- - - - + {tabs.map((tab) => ( + + ))}
{activeTab === 'general' && ( -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ )} - {activeTab === 'theme' && ( -
-
- - ({ - value: w.name, - label: w.name - }))} - /> -
- {Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && ( -
- - -
- )} -
- -
- - {config.wallpaperBlur}px -
-
-
- -
- - {config.wallpaperBrightness}% -
-
-
- -
- - {config.wallpaperOpacity}% -
-
- {chromeStorageAvailable && ( - <> -
-

User Wallpapers

-
- {userWallpapers.map((wallpaper) => ( -
- {wallpaper.name} - -
- ))} -
-
-
-

Add New Wallpaper

-
- setNewWallpaperName(e.target.value)} - className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" - /> -
- setNewWallpaperUrl(e.target.value)} - className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" - /> - -
-
- -
-
-
- - )} -
+ )} - {activeTab === 'clock' && ( -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ )} - {activeTab === 'serverWidget' && ( -
-
- - -
- {config.serverWidget.enabled && ( - <> -
- -
- - {config.serverWidget.pingFrequency}s -
-
-
-

Servers

- - - {(provided) => ( -
- {config.serverWidget.servers.map((server: Server, index: number) => ( - - {(provided) => ( -
-
-

{server.name}

-

{server.address}

-
- -
- )} -
- ))} - {provided.placeholder} -
- )} -
-
-
- setNewServerName(e.target.value)} - className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" - /> - setNewServerAddress(e.target.value)} - className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" - /> - -
-
- - )} -
+ )}
+
-
-
- - - -
-
- - -
+
+
+ + +
+
+ + +
+
diff --git a/components/configuration/ClockTab.tsx b/components/configuration/ClockTab.tsx new file mode 100644 index 0000000..af28a45 --- /dev/null +++ b/components/configuration/ClockTab.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import Dropdown from '../Dropdown'; +import ToggleSwitch from '../ToggleSwitch'; +import { Config } from '../../types'; + +interface ClockTabProps { + config: Config; + onChange: (updates: Partial) => void; +} + +const ClockTab: React.FC = ({ config, onChange }) => { + const updateClock = (updates: Partial) => { + onChange({ clock: { ...config.clock, ...updates } }); + }; + + return ( +
+
+ + updateClock({ enabled: checked })} + /> +
+
+ + updateClock({ size: e.target.value as string })} + options={[ + { value: 'tiny', label: 'Tiny' }, + { value: 'small', label: 'Small' }, + { value: 'medium', label: 'Medium' }, + { value: 'large', label: 'Large' }, + ]} + /> +
+
+ + updateClock({ font: e.target.value as string })} + options={[ + { value: 'Helvetica', label: 'Helvetica' }, + { value: `'Orbitron', sans-serif`, label: 'Orbitron' }, + { value: 'monospace', label: 'Monospace' }, + { value: 'cursive', label: 'Cursive' }, + ]} + /> +
+
+ + updateClock({ format: e.target.value as string })} + options={[ + { value: 'h:mm A', label: 'AM/PM' }, + { value: 'HH:mm', label: '24:00' }, + ]} + /> +
+
+ ); +}; + +export default ClockTab; diff --git a/components/configuration/GeneralTab.tsx b/components/configuration/GeneralTab.tsx new file mode 100644 index 0000000..387ed9d --- /dev/null +++ b/components/configuration/GeneralTab.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import Dropdown from '../Dropdown'; +import { Config } from '../../types'; + +interface GeneralTabProps { + config: Config; + onChange: (updates: Partial) => void; +} + +const GeneralTab: React.FC = ({ config, onChange }) => { + return ( +
+
+ + onChange({ title: e.target.value })} + className="bg-white/10 p-3 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+
+ + onChange({ titleSize: e.target.value as string })} + options={[ + { value: 'tiny', label: 'Tiny' }, + { value: 'small', label: 'Small' }, + { value: 'medium', label: 'Medium' }, + { value: 'large', label: 'Large' }, + ]} + /> +
+
+ + onChange({ alignment: e.target.value as string })} + options={[ + { value: 'top', label: 'Top' }, + { value: 'middle', label: 'Middle' }, + { value: 'bottom', label: 'Bottom' }, + ]} + /> +
+
+ + onChange({ tileSize: e.target.value as string })} + options={[ + { value: 'small', label: 'Small' }, + { value: 'medium', label: 'Medium' }, + { value: 'large', label: 'Large' }, + ]} + /> +
+
+ + onChange({ horizontalAlignment: e.target.value as string })} + options={[ + { value: 'left', label: 'Left' }, + { value: 'middle', label: 'Middle' }, + { value: 'right', label: 'Right' }, + ]} + /> +
+
+ ); +}; + +export default GeneralTab; diff --git a/components/configuration/ServerWidgetTab.tsx b/components/configuration/ServerWidgetTab.tsx new file mode 100644 index 0000000..c33e4a4 --- /dev/null +++ b/components/configuration/ServerWidgetTab.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import ToggleSwitch from '../ToggleSwitch'; +import { Config, Server } from '../../types'; + +interface ServerWidgetTabProps { + config: Config; + onChange: (updates: Partial) => void; +} + +const ServerWidgetTab: React.FC = ({ config, onChange }) => { + const [newServerName, setNewServerName] = useState(''); + const [newServerAddress, setNewServerAddress] = useState(''); + + const updateServerWidget = (updates: Partial) => { + onChange({ serverWidget: { ...config.serverWidget, ...updates } }); + }; + + const handleAddServer = () => { + if (newServerName.trim() === '' || newServerAddress.trim() === '') return; + const newServer: Server = { + id: Date.now().toString(), + name: newServerName, + address: newServerAddress, + }; + updateServerWidget({ servers: [...config.serverWidget.servers, newServer] }); + setNewServerName(''); + setNewServerAddress(''); + }; + + const handleRemoveServer = (id: string) => { + updateServerWidget({ + servers: config.serverWidget.servers.filter((s) => s.id !== id), + }); + }; + + const onDragEnd = (result: any) => { + if (!result.destination) return; + const items = Array.from(config.serverWidget.servers); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + updateServerWidget({ servers: items }); + }; + + return ( +
+
+ + updateServerWidget({ enabled: checked })} + /> +
+ {config.serverWidget.enabled && ( + <> +
+ +
+ updateServerWidget({ pingFrequency: Number(e.target.value) })} + className="w-48" + /> + {config.serverWidget.pingFrequency}s +
+
+
+

Servers

+ + + {(provided) => ( +
+ {config.serverWidget.servers.map((server: Server, index: number) => ( + + {(provided) => ( +
+
+

{server.name}

+

{server.address}

+
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+ setNewServerName(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + setNewServerAddress(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + +
+
+ + )} +
+ ); +}; + +export default ServerWidgetTab; diff --git a/components/configuration/ThemeTab.tsx b/components/configuration/ThemeTab.tsx new file mode 100644 index 0000000..3fe782b --- /dev/null +++ b/components/configuration/ThemeTab.tsx @@ -0,0 +1,228 @@ +import React, { useRef, useState } from 'react'; +import Dropdown from '../Dropdown'; +import { Config, Wallpaper } from '../../types'; + +interface ThemeTabProps { + config: Config; + onChange: (updates: Partial) => void; + userWallpapers: Wallpaper[]; + allWallpapers: Wallpaper[]; + chromeStorageAvailable: boolean; + onAddWallpaper: (name: string, url: string) => Promise; + onAddWallpaperFile: (file: File) => Promise; + onDeleteWallpaper: (wallpaper: Wallpaper) => Promise; +} + +const ThemeTab: React.FC = ({ + config, + onChange, + userWallpapers, + allWallpapers, + chromeStorageAvailable, + onAddWallpaper, + onAddWallpaperFile, + onDeleteWallpaper, +}) => { + const [newWallpaperName, setNewWallpaperName] = useState(''); + const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); + const fileInputRef = useRef(null); + + const handleAddWallpaper = async () => { + if (newWallpaperUrl.trim() === '') return; + try { + await onAddWallpaper(newWallpaperName, newWallpaperUrl); + setNewWallpaperName(''); + setNewWallpaperUrl(''); + } catch (error) { + alert('Error adding wallpaper. Please check the URL and try again.'); + console.error(error); + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + try { + await onAddWallpaperFile(file); + } catch (error: any) { + alert(error?.message || 'Error adding wallpaper. Please try again.'); + console.error(error); + } + e.target.value = ''; + }; + + return ( +
+
+ + onChange({ currentWallpapers: e.target.value as string[] })} + multiple + options={allWallpapers.map((w) => ({ value: w.name, label: w.name }))} + /> +
+ {Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && ( +
+ + onChange({ wallpaperFrequency: e.target.value as string })} + options={[ + { value: '1h', label: '1 hour' }, + { value: '3h', label: '3 hours' }, + { value: '6h', label: '6 hours' }, + { value: '12h', label: '12 hours' }, + { value: '1d', label: '1 day' }, + { value: '2d', label: '2 days' }, + ]} + /> +
+ )} +
+ +
+ onChange({ wallpaperBlur: Number(e.target.value) })} + className="w-48" + /> + {config.wallpaperBlur}px +
+
+
+ +
+ onChange({ wallpaperBrightness: Number(e.target.value) })} + className="w-48" + /> + {config.wallpaperBrightness}% +
+
+
+ +
+ onChange({ wallpaperOpacity: Number(e.target.value) })} + className="w-48" + /> + {config.wallpaperOpacity}% +
+
+ {chromeStorageAvailable && ( + <> +
+

User Wallpapers

+
+ {userWallpapers.map((wallpaper) => ( +
+ {wallpaper.name} + +
+ ))} +
+
+
+

Add New Wallpaper

+
+ setNewWallpaperName(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> +
+ setNewWallpaperUrl(e.target.value)} + className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400" + /> + +
+
+ +
+
+
+ + )} +
+ ); +}; + +export default ThemeTab; diff --git a/components/services/ConfigurationService.ts b/components/services/ConfigurationService.ts new file mode 100644 index 0000000..4a6a45b --- /dev/null +++ b/components/services/ConfigurationService.ts @@ -0,0 +1,181 @@ +import { Config, Wallpaper } from '../../types'; +import { + addWallpaperToChromeStorageLocal, + removeWallpaperFromChromeStorageLocal, +} from '../utils/StorageLocalManager'; + +const REQUIRED_LOCAL_STORAGE_KEYS = ['config', 'categories', 'userWallpapers', 'wallpaperState'] as const; +type RequiredLocalStorageKey = typeof REQUIRED_LOCAL_STORAGE_KEYS[number]; + +export const DEFAULT_CONFIG: Config = { + title: 'Vision Start', + currentWallpapers: ['Abstract'], + wallpaperFrequency: '1d', + wallpaperBlur: 0, + wallpaperBrightness: 100, + wallpaperOpacity: 100, + titleSize: 'medium', + alignment: 'middle', + horizontalAlignment: 'middle', + tileSize: 'medium', + clock: { + enabled: true, + size: 'medium', + font: 'Helvetica', + format: 'h:mm A', + }, + serverWidget: { + enabled: false, + pingFrequency: 15, + servers: [], + }, +}; + +const safeParse = (value: string | null): unknown => { + if (value === null) return null; + try { + return JSON.parse(value); + } catch { + return value; + } +}; + +const toStorageString = (value: unknown): string => + typeof value === 'string' ? value : JSON.stringify(value); + +export const ConfigurationService = { + loadConfig(): Config { + try { + const stored = localStorage.getItem('config'); + if (stored) { + const parsed = JSON.parse(stored); + return { ...DEFAULT_CONFIG, ...parsed }; + } + } catch (error) { + console.error('Error parsing config from localStorage', error); + } + return { ...DEFAULT_CONFIG }; + }, + + saveConfig(config: Config): void { + localStorage.setItem('config', JSON.stringify(config)); + }, + + loadUserWallpapers(): Wallpaper[] { + try { + const stored = localStorage.getItem('userWallpapers'); + if (stored) return JSON.parse(stored); + } catch { + // ignore + } + return []; + }, + + saveUserWallpapers(wallpapers: Wallpaper[]): void { + localStorage.setItem('userWallpapers', JSON.stringify(wallpapers)); + }, + + async addWallpaper(name: string, url: string): Promise { + const finalName = await addWallpaperToChromeStorageLocal(name, url); + return { name: finalName }; + }, + + async addWallpaperFile(file: File): Promise { + return new Promise((resolve, reject) => { + if (file.size > 4 * 1024 * 1024) { + reject(new Error('File size exceeds 4MB. Please choose a smaller file.')); + return; + } + const reader = new FileReader(); + reader.onload = async () => { + const base64 = reader.result as string; + if (base64.length > 4.5 * 1024 * 1024) { + reject(new Error('The uploaded image is too large. Please choose a smaller file.')); + return; + } + try { + const finalName = await addWallpaperToChromeStorageLocal(file.name, base64); + resolve({ name: finalName }); + } catch (error) { + reject(error); + } + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }, + + async deleteWallpaper(wallpaper: Wallpaper): Promise { + await removeWallpaperFromChromeStorageLocal(wallpaper.name); + }, + + exportConfig(): void { + const exportPayload = { + version: 1, + exportedAt: new Date().toISOString(), + requiredLocalStorageKeys: [...REQUIRED_LOCAL_STORAGE_KEYS], + localStorage: REQUIRED_LOCAL_STORAGE_KEYS.reduce( + (acc, key) => { + acc[key] = safeParse(localStorage.getItem(key)); + return acc; + }, + {} as Record, + ), + }; + + const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `vision-start-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }, + + async importConfig(file: File): Promise<{ config: Config; userWallpapers: Wallpaper[] }> { + const fileContent = await file.text(); + const parsed = JSON.parse(fileContent); + const localStorageData = + parsed?.localStorage && typeof parsed.localStorage === 'object' + ? parsed.localStorage + : parsed; + + if (!localStorageData || typeof localStorageData !== 'object') { + throw new Error('Invalid import file format.'); + } + + let importedAny = false; + REQUIRED_LOCAL_STORAGE_KEYS.forEach((key) => { + if (Object.prototype.hasOwnProperty.call(localStorageData, key)) { + const rawValue = (localStorageData as Record)[key]; + localStorage.setItem(key, toStorageString(rawValue)); + importedAny = true; + } + }); + + if (!importedAny) { + throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`); + } + + const importedConfig = (localStorageData as Record).config as Config; + const importedUserWallpapers = (localStorageData as Record) + .userWallpapers as Wallpaper[]; + + return { + config: importedConfig || { ...DEFAULT_CONFIG }, + userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [], + }; + }, + + resetWallpaperState(): void { + localStorage.setItem( + 'wallpaperState', + JSON.stringify({ + lastWallpaperChange: new Date().toISOString(), + currentIndex: 0, + }), + ); + }, +};