diff --git a/App.tsx b/App.tsx index e58c0e7..b2b5cbb 100644 --- a/App.tsx +++ b/App.tsx @@ -39,6 +39,12 @@ const getHorizontalAlignmentClass = (alignment: string) => { } }; +const getRandomWallpaperIndex = (wallpaperCount: number, currentIndex: number): number => { + if (wallpaperCount <= 1) return 0; + const offset = Math.floor(Math.random() * (wallpaperCount - 1)) + 1; + return (currentIndex + offset) % wallpaperCount; +}; + const App: React.FC = () => { const [categories, setCategories] = useState(() => { try { @@ -81,23 +87,23 @@ const App: React.FC = () => { setConfig(prev => ({ ...prev, ...newConfig })); }, []); - const handleNextWallpaper = useCallback(() => { + const handleRandomWallpaper = useCallback(() => { const names = config.currentWallpapers; if (names.length === 0) return; try { const state = JSON.parse(localStorage.getItem('wallpaperState') || '{}'); const current = typeof state.currentIndex === 'number' ? state.currentIndex : 0; const safeCurrent = current < 0 || current >= names.length ? 0 : current; - const nextIndex = (safeCurrent + 1) % names.length; + const randomIndex = getRandomWallpaperIndex(names.length, safeCurrent); localStorage.setItem( 'wallpaperState', JSON.stringify({ lastWallpaperChange: new Date().toISOString(), - currentIndex: nextIndex, + currentIndex: randomIndex, }), ); } catch (error) { - console.error('Error advancing wallpaper state', error); + console.error('Error randomizing wallpaper state', error); } setWallpaperVersion(v => v + 1); }, [config.currentWallpapers]); @@ -282,7 +288,7 @@ const App: React.FC = () => { onClose={() => setIsConfigModalOpen(false)} onSave={handleSaveConfig} onWallpaperChange={handleWallpaperChange} - onNextWallpaper={handleNextWallpaper} + onRandomWallpaper={handleRandomWallpaper} /> )} diff --git a/components/ConfigurationModal.tsx b/components/ConfigurationModal.tsx index a868de3..42da019 100644 --- a/components/ConfigurationModal.tsx +++ b/components/ConfigurationModal.tsx @@ -13,7 +13,7 @@ interface ConfigurationModalProps { onSave: (config: Config) => void; currentConfig: Config; onWallpaperChange: (newConfig: Partial) => void; - onNextWallpaper: () => void; + onRandomWallpaper: () => void; } const ConfigurationModal: React.FC = ({ @@ -21,7 +21,7 @@ const ConfigurationModal: React.FC = ({ onSave, currentConfig, onWallpaperChange, - onNextWallpaper, + onRandomWallpaper, }) => { const [config, setConfig] = useState(currentConfig); const [activeTab, setActiveTab] = useState('general'); @@ -177,7 +177,7 @@ const ConfigurationModal: React.FC = ({ onAddWallpaper={handleAddWallpaper} onAddWallpaperFile={handleAddWallpaperFile} onDeleteWallpaper={handleDeleteWallpaper} - onNextWallpaper={onNextWallpaper} + onRandomWallpaper={onRandomWallpaper} /> )} {activeTab === 'clock' && ( diff --git a/components/Wallpaper.tsx b/components/Wallpaper.tsx index 003cf12..8ffacd4 100644 --- a/components/Wallpaper.tsx +++ b/components/Wallpaper.tsx @@ -27,6 +27,12 @@ const parseFrequencyToMs = (freq: string): number => { return Math.min(MAX_WALLPAPER_FREQUENCY_MS, Math.max(MIN_WALLPAPER_FREQUENCY_MS, frequencyMs)); }; +const getRandomWallpaperIndex = (wallpaperCount: number, currentIndex: number): number => { + if (wallpaperCount <= 1) return 0; + const offset = Math.floor(Math.random() * (wallpaperCount - 1)) + 1; + return (currentIndex + offset) % wallpaperCount; +}; + const wallpaperUrlCache = new Map(); const getWallpaperUrlByName = async (name: string): Promise => { @@ -91,7 +97,7 @@ const Wallpaper: React.FC = ({ wallpaperNames, blur, brightness, const shouldRotate = now - lastChange >= freqMs; let resolvedIndex = shouldRotate - ? (storedIndex + 1) % wallpaperNames.length + ? getRandomWallpaperIndex(wallpaperNames.length, storedIndex) : storedIndex; const tried = new Set(); diff --git a/components/configuration/ThemeTab.tsx b/components/configuration/ThemeTab.tsx index ae90a9f..042f8ab 100644 --- a/components/configuration/ThemeTab.tsx +++ b/components/configuration/ThemeTab.tsx @@ -11,7 +11,7 @@ interface ThemeTabProps { onAddWallpaper: (name: string, url: string) => Promise; onAddWallpaperFile: (file: File) => Promise; onDeleteWallpaper: (wallpaper: Wallpaper) => Promise; - onNextWallpaper: () => void; + onRandomWallpaper: () => void; } type RangeStyle = React.CSSProperties & { '--range-progress': string }; @@ -48,7 +48,7 @@ const ThemeTab: React.FC = ({ onAddWallpaper, onAddWallpaperFile, onDeleteWallpaper, - onNextWallpaper, + onRandomWallpaper, }) => { const [newWallpaperName, setNewWallpaperName] = useState(''); const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); @@ -263,7 +263,7 @@ const ThemeTab: React.FC = ({
diff --git a/project-context.md b/project-context.md index 89487fb..dde6c0c 100644 --- a/project-context.md +++ b/project-context.md @@ -55,7 +55,7 @@ The startpage is composed of widgets and a configuration panel: - **Clock** — Optional header clock with selectable size, font, and 12h/24h format. - **Title** — Optional big header title (text + size configurable). - **Server Status Widget** — Bottom-center glass pill that periodically "pings" configured server addresses and shows online/offline indicators. Ping uses an image-load trick (`components/utils/jsping.js`) with a 5s timeout, at a configurable frequency. -- **Wallpaper background** — Fullscreen background image with adjustable blur, brightness, and opacity, rendered behind a soft readability layer for the liquid-glass UI. Supports rotating through multiple wallpapers at an hourly cadence selected by slider (`1h`–`48h`). +- **Wallpaper background** — Fullscreen background image with adjustable blur, brightness, and opacity, rendered behind a soft readability layer for the liquid-glass UI. Randomly rotates through multiple wallpapers at an hourly cadence selected by slider (`1h`–`48h`). - Built-in wallpapers: Abstract, Abstract Red, Beach, Dark, Mountain, Waves (`components/utils/baseWallpapers.ts`). - User wallpapers: upload from a file (≤4MB, ≤4.5MB base64) or add by URL. Remote URLs remain lightweight entries in the `userWallpapers` index; uploaded image data is stored in `chrome.storage.local` when available. - **Icon library, auto-fetch & cache** — Website icons can be picked from the [Dashboard Icons](https://dashboardicons.com/) library (metadata pre-downloaded to `public/icon-metadata.json`) or auto-fetched from the target site's `apple-touch-icon`/`icon` link tags, with a fallback to Google's S2 favicon service. Tiles opportunistically cache CORS-readable icon responses as data URLs in `chrome.storage.local` and retain the original URL when caching is unavailable. @@ -235,7 +235,7 @@ External assets fetched at build time by `scripts/prepare_release.sh`: - **`EditModal.tsx` has been removed.** It was a legacy drag-and-drop editor that imported non-existent `lucide-react` and `./IconPicker`; it was never wired into `App.tsx`. Use `WebsiteEditModal.tsx` / `CategoryEditModal.tsx` instead. - **`@hello-pangea/dnd`** is used only in `ServerWidgetTab.tsx` (server reorder), which itself is imported by the lazy-loaded `ConfigurationModal`, so it lives in a separate chunk and is absent from the initial page load. `WebsiteTile` moves tiles within their own category via simple left/right buttons (no cross-category movement), not drag-and-drop. - **Chrome storage is optional.** `StorageLocalManager` checks availability once (`checkChromeStorageLocalAvailable`) and caches it. Remote wallpaper URLs work without it because their URLs live in `userWallpapers`; file upload is gated off when unavailable, and `addWallpaperToChromeStorageLocal` throws if called without it. -- **Wallpaper rotation** is time-based, evaluated on render/mount rather than via a timer. It reads `wallpaperState` from `localStorage`, advances the index if the frequency window has elapsed, and writes it back; the frequency is clamped to 1–48 hours, while older saved values like `1d`/`2d` still resolve to their hour equivalents. The renderer clamps `currentIndex` to the valid range of the current selection and walks the list forward to find a wallpaper whose data actually resolves (so deleting the currently-displayed wallpaper, or shrinking the selection, never leaves the background blank); if no wallpaper resolves, the background layer is hidden. When the selection becomes empty, `wallpaperState` is reset and the background is hidden. A manual "Next Wallpaper" button in the Theme tab advances `currentIndex` (with wraparound) and bumps a `wallpaperVersion` nonce in `App.tsx` that retriggers the renderer. +- **Wallpaper rotation** is time-based, evaluated on render/mount rather than via a timer. It reads `wallpaperState` from `localStorage`, selects a random non-current wallpaper when the frequency window has elapsed, and writes its index back; the frequency is clamped to 1–48 hours, while older saved values like `1d`/`2d` still resolve to their hour equivalents. The renderer clamps `currentIndex` to the valid range of the current selection and walks the list forward to find a wallpaper whose data actually resolves (so deleting the currently-displayed wallpaper, or shrinking the selection, never leaves the background blank); if no wallpaper resolves, the background layer is hidden. When the selection becomes empty, `wallpaperState` is reset and the background is hidden. A manual "Random Wallpaper" button in the Theme tab also picks a random non-current wallpaper and bumps a `wallpaperVersion` nonce in `App.tsx` that retriggers the renderer. - **Icon picker** in `WebsiteEditModal` loads `/icon-metadata.json` at runtime and expands each icon's `colors` into duplicate-name entries so color variants are searchable. - **Website icon cache** is persistent only in `chrome.storage.local`; it is rebuilt from website icon URLs after configuration import. Only CORS-readable `http`/`https` image responses no larger than 256KiB are cached. - **`tsconfig.json` does not emit JS** (`noEmit: true`, bundler resolution); Vite handles all transpilation.