From c9dafd76d138050cba626ed95993bc46e157a35e Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Fri, 10 Jul 2026 20:11:40 -0300 Subject: [PATCH] adding extra theme options --- components/Wallpaper.tsx | 15 +++--- components/configuration/ThemeTab.tsx | 51 +++++++++++++++------ components/services/ConfigurationService.ts | 32 +++++++++++-- project-context.md | 14 +++--- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/components/Wallpaper.tsx b/components/Wallpaper.tsx index b98cc65..cd4082e 100644 --- a/components/Wallpaper.tsx +++ b/components/Wallpaper.tsx @@ -13,15 +13,18 @@ interface WallpaperProps { wallpaperVersion: number; } +const MIN_WALLPAPER_FREQUENCY_MS = 60 * 60 * 1000; +const MAX_WALLPAPER_FREQUENCY_MS = 48 * 60 * 60 * 1000; +const DEFAULT_WALLPAPER_FREQUENCY_MS = 24 * 60 * 60 * 1000; + 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; + if (!freq) return DEFAULT_WALLPAPER_FREQUENCY_MS; + const match = freq.match(/^(\d+)(h|d)$/); + if (!match) return DEFAULT_WALLPAPER_FREQUENCY_MS; 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; + const frequencyMs = unit === 'd' ? value * 24 * 60 * 60 * 1000 : value * 60 * 60 * 1000; + return Math.min(MAX_WALLPAPER_FREQUENCY_MS, Math.max(MIN_WALLPAPER_FREQUENCY_MS, frequencyMs)); }; const wallpaperUrlCache = new Map(); diff --git a/components/configuration/ThemeTab.tsx b/components/configuration/ThemeTab.tsx index 0a87d57..33223bb 100644 --- a/components/configuration/ThemeTab.tsx +++ b/components/configuration/ThemeTab.tsx @@ -21,6 +21,24 @@ const getRangeStyle = (value: number, min: number, max: number): RangeStyle => { return { '--range-progress': `${progress}%` }; }; +const MIN_WALLPAPER_FREQUENCY_HOURS = 1; +const MAX_WALLPAPER_FREQUENCY_HOURS = 48; +const DEFAULT_WALLPAPER_FREQUENCY_HOURS = 24; + +const clampWallpaperFrequencyHours = (hours: number): number => + Math.min(MAX_WALLPAPER_FREQUENCY_HOURS, Math.max(MIN_WALLPAPER_FREQUENCY_HOURS, Math.round(hours))); + +const getWallpaperFrequencyHours = (frequency: string): number => { + const match = frequency.match(/^(\d+)(h|d)$/); + if (!match) return DEFAULT_WALLPAPER_FREQUENCY_HOURS; + const value = Number(match[1]); + const hours = match[2] === 'd' ? value * 24 : value; + return clampWallpaperFrequencyHours(hours); +}; + +const formatWallpaperFrequency = (hours: number): string => + `${hours} ${hours === 1 ? 'hour' : 'hours'}`; + const ThemeTab: React.FC = ({ config, onChange, @@ -35,6 +53,7 @@ const ThemeTab: React.FC = ({ const [newWallpaperName, setNewWallpaperName] = useState(''); const [newWallpaperUrl, setNewWallpaperUrl] = useState(''); const fileInputRef = useRef(null); + const wallpaperFrequencyHours = getWallpaperFrequencyHours(config.wallpaperFrequency); const handleAddWallpaper = async () => { if (newWallpaperUrl.trim() === '') return; @@ -75,19 +94,25 @@ const ThemeTab: React.FC = ({ {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({ wallpaperFrequency: `${Number(e.target.value)}h` })} + className="liquid-range" + style={getRangeStyle( + wallpaperFrequencyHours, + MIN_WALLPAPER_FREQUENCY_HOURS, + MAX_WALLPAPER_FREQUENCY_HOURS, + )} + /> + + {formatWallpaperFrequency(wallpaperFrequencyHours)} + +
)}
diff --git a/components/services/ConfigurationService.ts b/components/services/ConfigurationService.ts index e7fed59..aed34c2 100644 --- a/components/services/ConfigurationService.ts +++ b/components/services/ConfigurationService.ts @@ -43,13 +43,37 @@ const safeParse = (value: string | null): unknown => { const toStorageString = (value: unknown): string => typeof value === 'string' ? value : JSON.stringify(value); +const isPlainObject = (v: unknown): v is Record => + typeof v === 'object' && v !== null && !Array.isArray(v); + +const deepMerge = (base: T, stored: unknown): T => { + if (isPlainObject(base)) { + const result: Record = { ...(base as Record) }; + const storedObj = isPlainObject(stored) ? stored : {}; + for (const key of Object.keys(result)) { + if (Object.prototype.hasOwnProperty.call(storedObj, key)) { + result[key] = deepMerge(result[key], storedObj[key]); + } + } + return result as T; + } + if (Array.isArray(base)) { + return (Array.isArray(stored) ? stored : base) as T; + } + if (stored === null) return base; + return typeof stored === typeof base ? (stored as T) : base; +}; + +export const normalizeConfig = (stored: unknown): Config => + deepMerge(DEFAULT_CONFIG, stored); + export const ConfigurationService = { loadConfig(): Config { try { const stored = localStorage.getItem('config'); if (stored) { const parsed = JSON.parse(stored); - return { ...DEFAULT_CONFIG, ...parsed }; + return normalizeConfig(parsed); } } catch (error) { console.error('Error parsing config from localStorage', error); @@ -159,12 +183,12 @@ export const ConfigurationService = { throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`); } - const importedConfig = (localStorageData as Record).config as Config; + const importedConfig = (localStorageData as Record).config; const importedUserWallpapers = (localStorageData as Record) - .userWallpapers as Wallpaper[]; + .userWallpapers; return { - config: importedConfig || { ...DEFAULT_CONFIG }, + config: normalizeConfig(importedConfig), userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [], }; }, diff --git a/project-context.md b/project-context.md index cf0e726..99f616c 100644 --- a/project-context.md +++ b/project-context.md @@ -1,6 +1,6 @@ --- project_name: Vision Start -date: 2026-07-03 +date: 2026-07-08 type: general_overview --- @@ -54,7 +54,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 a cadence (`1h`–`2d`). +- **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`). - 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; stored in `chrome.storage.local` when available, falling back to storing the URL directly on CORS failure. - **Icon library & auto-fetch** — 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. @@ -109,7 +109,7 @@ vision-start/ │ │ │ ├── configuration/ │ │ ├── GeneralTab.tsx # Title, sizes, alignment, tile size -│ │ ├── ThemeTab.tsx # Background selection, wallpaper mgmt, blur/brightness/opacity +│ │ ├── ThemeTab.tsx # Background selection, wallpaper cadence, wallpaper mgmt, blur/brightness/opacity │ │ ├── ClockTab.tsx # Clock enable/size/font/format │ │ └── ServerWidgetTab.tsx # Server widget enable/ping/servers (drag-to-reorder) │ │ @@ -158,9 +158,9 @@ The shape of all persisted data lives in `types.ts`: - **`Server`** — `id`, `name`, `address` - **`Category`** — `id`, `name`, `websites: Website[]` - **`Wallpaper`** — `name`, optional `url` or `base64` -- **`Config`** — Everything else: title, wallpaper list + frequency/blur/brightness/opacity, titleSize, vertical & horizontal alignment, tileSize, `clock {enabled, size, font, format}`, `serverWidget {enabled, pingFrequency, servers}` +- **`Config`** — Everything else: title, wallpaper list + hourly frequency/blur/brightness/opacity, titleSize, vertical & horizontal alignment, tileSize, `clock {enabled, size, font, format}`, `serverWidget {enabled, pingFrequency, servers}` -`ConfigurationService` (`components/services/ConfigurationService.ts`) is the source of truth for default config and persistence helpers. +`ConfigurationService` (`components/services/ConfigurationService.ts`) is the source of truth for default config and persistence helpers. Config loaded from `localStorage` (and imported from export files) is run through `normalizeConfig()`, a schema-driven deep merge against `DEFAULT_CONFIG`: nested blocks (`clock`, `serverWidget`) inherit new sub-fields added to defaults, stored values with the wrong type fall back to defaults, and keys absent from the default schema are pruned. The merge is self-healing — `App.tsx`'s persist-`useEffect` writes the normalized shape back to `localStorage` on first run. Storage layout (browser-side): @@ -224,7 +224,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. When unavailable (e.g., running as a plain web page), wallpaper upload/delete flows are gated off and `addWallpaperToChromeStorageLocal` throws. -- **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 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`, 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. - **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. - **`tsconfig.json` does not emit JS** (`noEmit: true`, bundler resolution); Vite handles all transpilation. - **`tailwind.config.js` safelists** a set of `w-[Npx]/h-[Npx]` classes because `WebsiteTile` generates tailwind classes dynamically from `tileSize` (`w-[42px]`, etc.). @@ -253,4 +253,4 @@ External assets fetched at build time by `scripts/prepare_release.sh`: --- -_Last updated: 2026-07-03. Generated as a general project overview; not a coding-style guide._ +_Last updated: 2026-07-08. Generated as a general project overview; not a coding-style guide._