diff --git a/App.tsx b/App.tsx index 0ab3781..917384f 100755 --- a/App.tsx +++ b/App.tsx @@ -1,21 +1,20 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import ConfigurationModal from './components/ConfigurationModal'; import ServerWidget from './components/ServerWidget'; import { DEFAULT_CATEGORIES } from './constants'; -import { Category, Website, Wallpaper, Config } from './types'; +import { Category, Website, Config } from './types'; import WebsiteEditModal from './components/WebsiteEditModal'; import CategoryEditModal from './components/CategoryEditModal'; import Header from './components/layout/Header'; import EditButton from './components/layout/EditButton'; import ConfigurationButton from './components/layout/ConfigurationButton'; import CategoryGroup from './components/layout/CategoryGroup'; - -import { baseWallpapers } from './components/utils/baseWallpapers'; +import Wallpaper from './components/Wallpaper'; const defaultConfig: Config = { title: 'Vision Start', subtitle: 'Your personal portal to the web.', - backgroundUrls: ['https://i.imgur.com/C6ynAtX.jpeg'], + currentWallpapers: ['Abstract'], wallpaperFrequency: '1d', wallpaperBlur: 0, wallpaperBrightness: 100, @@ -60,9 +59,6 @@ const App: React.FC = () => { const storedConfig = localStorage.getItem('config'); if (storedConfig) { const parsedConfig = JSON.parse(storedConfig); - if (!parsedConfig.backgroundUrls) { - parsedConfig.backgroundUrls = [parsedConfig.backgroundUrl].filter(Boolean); - } return { ...defaultConfig, ...parsedConfig }; } } catch (error) { @@ -70,62 +66,12 @@ const App: React.FC = () => { } return { ...defaultConfig }; }); - const [userWallpapers, setUserWallpapers] = useState(() => { - const storedUserWallpapers = localStorage.getItem('userWallpapers'); - return storedUserWallpapers ? JSON.parse(storedUserWallpapers) : []; - }); - const [currentWallpaper, setCurrentWallpaper] = useState(''); - - const allWallpapers = [...baseWallpapers, ...userWallpapers]; useEffect(() => { - const getFrequencyInMs = (frequency: string) => { - const value = parseInt(frequency.slice(0, -1)); - const unit = frequency.slice(-1); - if (unit === 'h') return value * 60 * 60 * 1000; - if (unit === 'd') return value * 24 * 60 * 60 * 1000; - return 24 * 60 * 60 * 1000; // Default to 1 day - }; - - const wallpaperState = JSON.parse(localStorage.getItem('wallpaperState') || '{}'); - const lastChanged = wallpaperState.lastChanged ? new Date(wallpaperState.lastChanged).getTime() : 0; - const frequency = getFrequencyInMs(config.wallpaperFrequency); - - const updateWallpaper = () => { - const availableWallpapers = allWallpapers.filter(w => config.backgroundUrls.includes(w.url || w.base64)); - if (availableWallpapers.length > 0) { - const currentWallpaperFromState = allWallpapers.find(w => w.name === wallpaperState.current); - const currentIndex = currentWallpaperFromState ? availableWallpapers.findIndex(w => w.name === currentWallpaperFromState.name) : -1; - const nextIndex = (currentIndex + 1) % availableWallpapers.length; - const newWallpaper = availableWallpapers[nextIndex]; - const newWallpaperUrl = newWallpaper.url || newWallpaper.base64; - setCurrentWallpaper(newWallpaperUrl || ''); - localStorage.setItem('wallpaperState', JSON.stringify({ current: newWallpaper.name, lastChanged: new Date().toISOString() })); - } else { - setCurrentWallpaper(''); - localStorage.removeItem('wallpaperState'); - } - }; - - const currentWallpaperDetails = allWallpapers.find(w => w.name === wallpaperState.current); - const isCurrentWallpaperValid = currentWallpaperDetails && config.backgroundUrls.includes(currentWallpaperDetails.url || currentWallpaperDetails.base64 || ''); - - if (!isCurrentWallpaperValid || Date.now() - lastChanged > frequency) { - updateWallpaper(); - } else if (currentWallpaperDetails) { - setCurrentWallpaper(currentWallpaperDetails.url || currentWallpaperDetails.base64 || ''); - } else { - // Fallback for when there's no valid wallpaper state - updateWallpaper(); - } - }, [config.backgroundUrls, config.wallpaperFrequency, allWallpapers]); - - useEffect(() => { - localStorage.setItem('categories', JSON.stringify(categories)); localStorage.setItem('config', JSON.stringify(config)); - }, [categories, config]); + }, [config]); - const handleSaveConfig = (newConfig: any) => { + const handleSaveConfig = (newConfig: Config) => { setConfig(newConfig); setIsConfigModalOpen(false); }; @@ -201,7 +147,6 @@ const App: React.FC = () => { }; const handleMoveWebsite = (website: Website, direction: 'left' | 'right') => { - const categoryIndex = categories.findIndex(c => c.id === website.categoryId); if (categoryIndex === -1) return; const category = categories[categoryIndex]; @@ -259,14 +204,13 @@ const App: React.FC = () => {
-
+ setIsEditing(!isEditing)} /> setIsConfigModalOpen(true)} /> @@ -305,11 +249,7 @@ const App: React.FC = () => { )} - {config.serverWidget.enabled && ( -
- -
- )} + {config.serverWidget.enabled && } {(editingWebsite || addingWebsite) && ( { )} {isConfigModalOpen && ( - setIsConfigModalOpen(false)} - onSave={handleSaveConfig} + onClose={() => setIsConfigModalOpen(false)} + onSave={handleSaveConfig} onWallpaperChange={handleWallpaperChange} /> )} @@ -349,4 +289,4 @@ const App: React.FC = () => { ); } -export default App; +export default App; \ No newline at end of file diff --git a/README.md b/README.md index e3b3af1..c7d5ed8 100755 --- a/README.md +++ b/README.md @@ -18,17 +18,6 @@ Vision Start is not yet available on Chrome Web Store, but it can be installed m 6. Click on "Load unpacked" and select the `vision-start` folder you extracted in step 3 7. The extension should now be installed! Just open a new tab to see it in action. -## Backgrounds - -It comes with a selection of some nice pre-defined backgrounds. You can also upload up to one image to it. - -* **Abstract** -* **Abstract Red** -* **Beach** -* **Dark** -* **Mountain** -* **Waves** - ## Features * **Customizable Website Tiles:** Add, edit, and organize your favorite websites for quick access. @@ -38,6 +27,12 @@ It comes with a selection of some nice pre-defined backgrounds. You can also upl * **Icon Library:** It uses the [Dashboard Icon library](https://dashboardicons.com/) for a better look and feel. It also supports auto-fetch for some websites. * **Future**: a long to do list :( +## Backgrounds + +It comes with a selection of some nice pre-defined backgrounds: **Abstract**, **Abstract Red**, **Beach**, **Dark**, **Mountain**, **Waves**. + +You can also upload your own images on it (or fetch it from the web). + ## Running Locally **Prerequisites:** Node.js diff --git a/components/CategoryEditModal.tsx b/components/CategoryEditModal.tsx index eb90b69..b1e1c39 100644 --- a/components/CategoryEditModal.tsx +++ b/components/CategoryEditModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Category } from '../types'; interface CategoryEditModalProps { @@ -12,10 +12,6 @@ interface CategoryEditModalProps { const CategoryEditModal: React.FC = ({ category, edit, onClose, onSave, onDelete }) => { const [name, setName] = useState(category ? category.name : ''); - const handleSave = () => { - onSave(name); - }; - const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); diff --git a/components/ConfigurationModal.tsx b/components/ConfigurationModal.tsx index f4866d0..2fa8e33 100644 --- a/components/ConfigurationModal.tsx +++ b/components/ConfigurationModal.tsx @@ -5,6 +5,7 @@ import { Server, Wallpaper } from '../types'; import Dropdown from './Dropdown'; import { baseWallpapers } from './utils/baseWallpapers'; +import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager'; interface ConfigurationModalProps { onClose: () => void; @@ -37,7 +38,9 @@ const ConfigurationModal: React.FC = ({ onClose, onSave format: 'h:mm A', ...currentConfig.clock, }, - backgroundUrls: currentConfig.backgroundUrls || [], + currentWallpapers: Array.isArray(currentConfig.currentWallpapers) + ? currentConfig.currentWallpapers.filter((name: string) => typeof name === 'string') + : [], wallpaperFrequency: currentConfig.wallpaperFrequency || '1d', }); const [activeTab, setActiveTab] = useState('general'); @@ -46,12 +49,14 @@ const ConfigurationModal: React.FC = ({ onClose, onSave const [newWallpaperName, setNewWallpaperName] = useState(''); const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); const [userWallpapers, setUserWallpapers] = useState([]); + const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false); const menuRef = useRef(null); const fileInputRef = 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)); @@ -59,7 +64,6 @@ const ConfigurationModal: React.FC = ({ onClose, onSave }, []); useEffect(() => { - // A small timeout to allow the component to mount before starting the transition const timer = setTimeout(() => { setIsVisible(true); }, 10); @@ -69,7 +73,7 @@ const ConfigurationModal: React.FC = ({ onClose, onSave useEffect(() => { return () => { if (!isSaving.current) { - onWallpaperChange({ backgroundUrls: currentConfig.backgroundUrls }); + onWallpaperChange({ currentWallpapers: currentConfig.currentWallpapers }); } }; }, []); @@ -78,12 +82,15 @@ const ConfigurationModal: React.FC = ({ onClose, onSave setIsVisible(false); setTimeout(() => { onClose(); - }, 300); // This duration should match the transition duration + }, 300); }; const handleChange = (e: React.ChangeEvent | { target: { name: string; value: string | string[] } }) => { const { name, value } = e.target; - if (name.startsWith('serverWidget.')) { + 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, @@ -101,8 +108,13 @@ const ConfigurationModal: React.FC = ({ onClose, onSave }; useEffect(() => { - onWallpaperChange({ backgroundUrls: config.backgroundUrls }); - }, [config.backgroundUrls]); + onWallpaperChange({ currentWallpapers: config.currentWallpapers }); + // Set wallpaperState in localStorage with lastWallpaperChange datetime + localStorage.setItem('wallpaperState', JSON.stringify({ + lastWallpaperChange: new Date().toISOString(), + currentIndex: 0, + })); + }, [config.currentWallpapers]); const handleClockToggleChange = (checked: boolean) => { setConfig({ ...config, clock: { ...config.clock, enabled: checked } }); @@ -162,21 +174,21 @@ const ConfigurationModal: React.FC = ({ onClose, onSave }); }; - const handleAddWallpaper = () => { - if (newWallpaperName.trim() === '' || newWallpaperUrl.trim() === '') return; - - const newWallpaper: Wallpaper = { - name: newWallpaperName, - url: newWallpaperUrl, - }; - - const updatedUserWallpapers = [...userWallpapers, newWallpaper]; - setUserWallpapers(updatedUserWallpapers); - localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); - setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, newWallpaperUrl] }); - - setNewWallpaperName(''); - setNewWallpaperUrl(''); + const handleAddWallpaper = async () => { + if (newWallpaperUrl.trim() === '') return; + 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) => { @@ -186,39 +198,43 @@ const ConfigurationModal: React.FC = ({ onClose, onSave alert('File size exceeds 4MB. Please choose a smaller file.'); return; } - const reader = new FileReader(); - reader.onload = () => { + 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; } - - const updatedUserWallpapers = userWallpapers.filter(w => !w.base64); - const newWallpaper: Wallpaper = { - name: file.name, - base64, - }; - setUserWallpapers([...updatedUserWallpapers, newWallpaper]); - localStorage.setItem('userWallpapers', JSON.stringify([...updatedUserWallpapers, newWallpaper])); - setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, base64] }); + 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 handleDeleteWallpaper = (wallpaper: Wallpaper) => { - const wallpaperIdentifier = wallpaper.url || wallpaper.base64; - const updatedUserWallpapers = userWallpapers.filter(w => (w.url || w.base64) !== wallpaperIdentifier); - setUserWallpapers(updatedUserWallpapers); - localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers)); - - const newBackgroundUrls = config.backgroundUrls.filter((url: string) => url !== wallpaperIdentifier); - - const newConfig = { ...config, backgroundUrls: newBackgroundUrls }; - setConfig(newConfig); - onWallpaperChange({ backgroundUrls: newBackgroundUrls }); + 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 }); + } catch (error) { + alert('Error deleting wallpaper. Please try again.'); + console.error(error); + } }; const allWallpapers = [...baseWallpapers, ...userWallpapers]; @@ -365,35 +381,17 @@ const ConfigurationModal: React.FC = ({ onClose, onSave
({ - value: w.url || w.base64 || '', - label: ( -
- {w.name} - {!baseWallpapers.find(bw => (bw.url || bw.base64) === (w.url || w.base64)) && ( - - )} -
- ) + options={allWallpapers.map(w => ({ + value: w.name, + label: w.name }))} />
- {Array.isArray(config.backgroundUrls) && config.backgroundUrls.length > 1 && ( + {Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
= ({ onClose, onSave {config.wallpaperOpacity}%
-
-

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" - /> - + {chromeStorageAvailable && ( + <> +
+

User Wallpapers

+
+ {userWallpapers.map((wallpaper) => ( +
+ {wallpaper.name} + +
+ ))} +
-
-
+ + )}
)} diff --git a/components/ServerWidget.tsx b/components/ServerWidget.tsx index 101ae1c..f0dbf4d 100644 --- a/components/ServerWidget.tsx +++ b/components/ServerWidget.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Server } from '../types'; import ping from './utils/jsping.js'; diff --git a/components/ToggleSwitch.tsx b/components/ToggleSwitch.tsx index 5901303..0a46434 100644 --- a/components/ToggleSwitch.tsx +++ b/components/ToggleSwitch.tsx @@ -1,4 +1,4 @@ -import React from 'react'; + interface ToggleSwitchProps { checked: boolean; diff --git a/components/Wallpaper.tsx b/components/Wallpaper.tsx new file mode 100644 index 0000000..afe9e13 --- /dev/null +++ b/components/Wallpaper.tsx @@ -0,0 +1,106 @@ + +import { useState, useEffect } from 'react'; +import { baseWallpapers } from './utils/baseWallpapers'; +import { Wallpaper as WallpaperType } from '../types'; +import { getWallpaperFromChromeStorageLocal } from './utils/StorageLocalManager'; + +interface WallpaperProps { + wallpaperNames: string[]; + blur: number; + brightness: number; + opacity: number; + wallpaperFrequency: string; +} + +const getWallpaperUrlByName = async (name: string): Promise => { + const foundInBase = baseWallpapers.find((w: WallpaperType) => w.name === name); + if (foundInBase) { + return foundInBase.url || foundInBase.base64; + } + + const userWallpapers: WallpaperType[] = JSON.parse(localStorage.getItem('userWallpapers') || '[]'); + const foundInUser = userWallpapers.find((w: WallpaperType) => w.name === name); + if (foundInUser) { + try { + const wallpaperData = await getWallpaperFromChromeStorageLocal(name); + if (wallpaperData && wallpaperData.startsWith('http')) { + return wallpaperData; + } + return wallpaperData || undefined; + } catch (error) { + console.error('Error getting wallpaper from chrome storage', error); + return undefined; + } + } + + return undefined; +}; + +const Wallpaper: React.FC = ({ wallpaperNames, blur, brightness, opacity, wallpaperFrequency }) => { + const [imageUrl, setImageUrl] = useState(undefined); + const [currentWallpaperIndex, setCurrentWallpaperIndex] = useState(0); + + // Helper to parse wallpaperFrequency string to ms + const parseFrequencyToMs = (freq: string): number => { + if (!freq) return 24 * 60 * 60 * 1000; // default 1 day + const match = freq.match(/(\d+)(h|d)/); + if (!match) return 24 * 60 * 60 * 1000; + const value = parseInt(match[1], 10); + const unit = match[2]; + if (unit === 'h') return value * 60 * 60 * 1000; + if (unit === 'd') return value * 24 * 60 * 60 * 1000; + return 24 * 60 * 60 * 1000; + }; + + useEffect(() => { + const updateWallpaper = async () => { + if (wallpaperNames.length === 0) return; + // Read wallpaperState from localStorage + const wallpaperState = JSON.parse(localStorage.getItem('wallpaperState') || '{}'); + const lastChange = wallpaperState.lastWallpaperChange ? new Date(wallpaperState.lastWallpaperChange).getTime() : 0; + const now = Date.now(); + const freqMs = parseFrequencyToMs(wallpaperFrequency); + let currentIndex = typeof wallpaperState.currentIndex === 'number' ? wallpaperState.currentIndex : 0; + + // If enough time has passed, pick a new wallpaper + if (now - lastChange >= freqMs) { + currentIndex = (currentIndex + 1) % wallpaperNames.length; + localStorage.setItem('wallpaperState', JSON.stringify({ + lastWallpaperChange: new Date().toISOString(), + currentIndex + })); + } else { + // Keep currentIndex in sync with localStorage if not updating + localStorage.setItem('wallpaperState', JSON.stringify({ + lastWallpaperChange: wallpaperState.lastWallpaperChange || new Date().toISOString(), + currentIndex + })); + } + setCurrentWallpaperIndex(currentIndex); + const wallpaperName = wallpaperNames[currentIndex]; + const url = await getWallpaperUrlByName(wallpaperName); + setImageUrl(url); + }; + updateWallpaper(); + // No timer, just run on render/dependency change + }, [wallpaperNames, wallpaperFrequency]); + + if (!imageUrl) return null; + + return ( +
+ ); +}; + +export default Wallpaper; diff --git a/components/WebsiteEditModal.tsx b/components/WebsiteEditModal.tsx index c4c02d3..6dddbea 100644 --- a/components/WebsiteEditModal.tsx +++ b/components/WebsiteEditModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Website } from '../types'; import { getWebsiteIcon } from './utils/iconService'; diff --git a/components/WebsiteTile.tsx b/components/WebsiteTile.tsx index 622c6ce..c34c54c 100755 --- a/components/WebsiteTile.tsx +++ b/components/WebsiteTile.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import { Website } from '../types'; - interface WebsiteTileProps { website: Website; isEditing: boolean; @@ -28,9 +27,9 @@ const getTileSizeClass = (size: string | undefined) => { const getIconPixelSize = (size: string | undefined): number => { switch (size) { case 'small': - return 32; + return 34; case 'medium': - return 40; + return 42; case 'large': return 48; default: @@ -92,7 +91,7 @@ const WebsiteTile: React.FC = ({ website, isEditing, onEdit, o )}
- {`${website.name} + {`${website.name}
{website.name} diff --git a/components/layout/CategoryGroup.tsx b/components/layout/CategoryGroup.tsx index d8b2a6c..1339f89 100644 --- a/components/layout/CategoryGroup.tsx +++ b/components/layout/CategoryGroup.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import WebsiteTile from '../WebsiteTile'; import { Category, Website } from '../../types'; diff --git a/components/layout/ConfigurationButton.tsx b/components/layout/ConfigurationButton.tsx index c5a0276..64bcbea 100644 --- a/components/layout/ConfigurationButton.tsx +++ b/components/layout/ConfigurationButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; + interface ConfigurationButtonProps { onClick: () => void; diff --git a/components/layout/EditButton.tsx b/components/layout/EditButton.tsx index 41506f8..d58c2c8 100644 --- a/components/layout/EditButton.tsx +++ b/components/layout/EditButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; + interface EditButtonProps { isEditing: boolean; diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index e67dcd9..8376c38 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import Clock from '../Clock'; import { Config } from '../../types'; diff --git a/components/utils/StorageLocalManager.ts b/components/utils/StorageLocalManager.ts new file mode 100644 index 0000000..1989725 --- /dev/null +++ b/components/utils/StorageLocalManager.ts @@ -0,0 +1,164 @@ +// TypeScript interface for window.chrome +declare global { + interface Window { + chrome?: { + storage?: { + local?: { + set: (items: object, callback?: () => void) => void; + get: (keys: string[] | string, callback: (items: { [key: string]: string }) => void) => void; + remove: (keys: string | string[], callback?: () => void) => void; + }; + }; + runtime?: { + lastError?: { message: string }; + }; + }; + } +} + +let isChromeStorageLocalAvailable: boolean | null = null; + + +/** + * Checks if chrome.storage.local is available and caches the result. + */ +export function checkChromeStorageLocalAvailable(): boolean { + if (isChromeStorageLocalAvailable !== null) return isChromeStorageLocalAvailable; + isChromeStorageLocalAvailable = + typeof window !== 'undefined' && + typeof window.chrome !== 'undefined' && + typeof window.chrome.storage !== 'undefined' && + typeof window.chrome.storage.local !== 'undefined'; + return isChromeStorageLocalAvailable; +} + +/** + * Adds a new wallpaper to chrome.storage.local. + * If the URL is fetchable, it will be stored as base64 and the name will be derived from the URL. + * If the URL is not fetchable (e.g., CORS), it will be stored as a URL and the provided name will be used. + * @param name Wallpaper name (string), used as a fallback. + * @param url Wallpaper image URL (string) or base64 data URL. + * @returns Promise The name under which the wallpaper was stored. + * @throws Error if chrome.storage.local is unavailable or if a name is not provided for a non-fetchable URL. + */ +export async function addWallpaperToChromeStorageLocal(name: string, url: string): Promise { + if (!checkChromeStorageLocalAvailable()) { + throw new Error('chrome.storage.local is not available'); + } + + if (url.startsWith('data:')) { + // This is a base64 encoded image from a file upload. + // The name is the file name. + return new Promise((resolve, reject) => { + if (window.chrome?.storage?.local) { + window.chrome.storage.local.set({ [name]: url }, function () { + if (window.chrome?.runtime?.lastError) { + reject(new Error(window.chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + } else { + reject(new Error('chrome.storage.local is not available')); + } + }).then(() => name); + } + + // This is a URL. Let's try to fetch it. + try { + const response = await fetch(url); + if (!response.ok) throw new Error('Failed to fetch image'); + const imageBlob = await response.blob(); + const reader = new FileReader(); + const base64 = await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(imageBlob); + }); + + // If successful, use the filename from URL as the name. + const finalName = url.substring(url.lastIndexOf('/') + 1).replace(/[?#].*$/, '') || name; + return new Promise((resolve, reject) => { + if (window.chrome?.storage?.local) { + window.chrome.storage.local.set({ [finalName]: base64 }, function () { + if (window.chrome?.runtime?.lastError) { + reject(new Error(window.chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + } else { + reject(new Error('chrome.storage.local is not available')); + } + }).then(() => finalName); + } catch (error) { + // If fetch fails (e.g., CORS), store the URL directly with the user-provided name. + console.warn('Could not fetch wallpaper, storing URL instead. Error:', error); + if (!name) { + throw new Error("A name for the wallpaper is required when the URL can't be accessed."); + } + return new Promise((resolve, reject) => { + if (window.chrome?.storage?.local) { + window.chrome.storage.local.set({ [name]: url }, function () { + if (window.chrome?.runtime?.lastError) { + reject(new Error(window.chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + } else { + reject(new Error('chrome.storage.local is not available')); + } + }).then(() => name); + } +} + +/** + * Gets a specific wallpaper from chrome.storage.local by name. + * @param name Wallpaper name (string) + * @returns Promise (base64 string or null) + * @throws Error if chrome.storage.local is unavailable + */ +export async function getWallpaperFromChromeStorageLocal(name: string): Promise { + if (!checkChromeStorageLocalAvailable()) { + throw new Error('chrome.storage.local is not available'); + } + return new Promise((resolve, reject) => { + if (window.chrome?.storage?.local) { + window.chrome.storage.local.get([name], function (result: { [key: string]: string }) { + if (window.chrome?.runtime?.lastError) { + reject(new Error(window.chrome.runtime.lastError.message)); + } else { + resolve(result[name] || null); + } + }); + } else { + reject(new Error('chrome.storage.local is not available')); + } + }); +} + +/** + * Removes a wallpaper from chrome.storage.local by name. + * @param name Wallpaper name (string) + * @returns Promise + * @throws Error if chrome.storage.local is unavailable + */ +export async function removeWallpaperFromChromeStorageLocal(name: string): Promise { + if (!checkChromeStorageLocalAvailable()) { + throw new Error('chrome.storage.local is not available'); + } + return new Promise((resolve, reject) => { + if (window.chrome?.storage?.local) { + window.chrome.storage.local.remove(name, function () { + if (window.chrome?.runtime?.lastError) { + reject(new Error(window.chrome.runtime.lastError.message)); + } else { + resolve(); + } + }); + } else { + reject(new Error('chrome.storage.local is not available')); + } + }); +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 252d1dc..ae1a866 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,4 +8,15 @@ export default { extend: {}, }, plugins: [], + safelist: [ + 'w-[24px]', 'h-[24px]', + 'w-[28px]', 'h-[28px]', + 'w-[32px]', 'h-[32px]', + 'w-[34px]', 'h-[34px]', + 'w-[36px]', 'h-[36px]', + 'w-[40px]', 'h-[40px]', + 'w-[42px]', 'h-[42px]', + 'w-[48px]', 'h-[48px]', + // add any other sizes you use + ], } \ No newline at end of file diff --git a/types.ts b/types.ts index ecc3f1f..4f6ca9d 100755 --- a/types.ts +++ b/types.ts @@ -27,7 +27,7 @@ export interface Wallpaper { export interface Config { title: string; subtitle: string; - backgroundUrls: string[]; + currentWallpapers: string[]; wallpaperFrequency: string; wallpaperBlur: number; wallpaperBrightness: number; diff --git a/waves.jpg b/waves.jpg deleted file mode 100644 index 129f7fd..0000000 Binary files a/waves.jpg and /dev/null differ