fixing wallpaper issue
This commit is contained in:
@@ -63,7 +63,7 @@ npm run dev
|
||||
* [x] Multiple Wallpapers
|
||||
* [x] Remake icons
|
||||
* [/] Increase offline compatibility (might not be possible)
|
||||
- [x] Use chrome.storage.local for user wallpapers -- this one is
|
||||
- [x] Use chrome.storage.local for uploaded wallpaper files; remote wallpapers stay as URLs
|
||||
- [ ] Use chrome.storage.local for some logos -- a bit hard
|
||||
- Some logos have CORS enabled, we can add `"<all_urls>"` to the manifest.json file and cache them on storage local
|
||||
* Dynamic Weather Widget
|
||||
|
||||
@@ -43,18 +43,16 @@ const getWallpaperUrlByName = async (name: string): Promise<string | undefined>
|
||||
JSON.parse(localStorage.getItem('userWallpapers') || '[]');
|
||||
const foundInUser = storedUserWallpapers.find((w: WallpaperType) => w.name === name);
|
||||
if (foundInUser) {
|
||||
resolved = foundInUser.url || foundInUser.base64;
|
||||
if (!resolved) {
|
||||
try {
|
||||
const wallpaperData = await getWallpaperFromChromeStorageLocal(name);
|
||||
if (wallpaperData && wallpaperData.startsWith('http')) {
|
||||
resolved = wallpaperData;
|
||||
} else {
|
||||
resolved = wallpaperData || undefined;
|
||||
}
|
||||
resolved = (await getWallpaperFromChromeStorageLocal(name)) || undefined;
|
||||
} catch (error) {
|
||||
console.error('Error getting wallpaper from chrome storage', error);
|
||||
resolved = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading userWallpapers from localStorage', error);
|
||||
resolved = undefined;
|
||||
|
||||
@@ -62,7 +62,11 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
setNewWallpaperName('');
|
||||
setNewWallpaperUrl('');
|
||||
} catch (error) {
|
||||
alert('Error adding wallpaper. Please check the URL and try again.');
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Error adding wallpaper. Please check the URL and try again.',
|
||||
);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -160,8 +164,6 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
<span className="w-12 text-right text-sm text-slate-200">{config.wallpaperOpacity}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{chromeStorageAvailable && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-slate-300 text-sm font-semibold mb-2">User Wallpapers</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -220,6 +222,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{chromeStorageAvailable && (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
@@ -255,10 +258,9 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center pt-2">
|
||||
<button
|
||||
onClick={onNextWallpaper}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Config, Wallpaper } from '../../types';
|
||||
import {
|
||||
addWallpaperToChromeStorageLocal,
|
||||
checkChromeStorageLocalAvailable,
|
||||
removeWallpaperFromChromeStorageLocal,
|
||||
} from '../utils/StorageLocalManager';
|
||||
|
||||
@@ -43,6 +44,16 @@ const safeParse = (value: string | null): unknown => {
|
||||
const toStorageString = (value: unknown): string =>
|
||||
typeof value === 'string' ? value : JSON.stringify(value);
|
||||
|
||||
const getWallpaperNameFromUrl = (url: URL): string => {
|
||||
const pathName = url.pathname.split('/').filter(Boolean).pop();
|
||||
if (!pathName) return url.hostname;
|
||||
try {
|
||||
return decodeURIComponent(pathName);
|
||||
} catch {
|
||||
return pathName;
|
||||
}
|
||||
};
|
||||
|
||||
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
|
||||
@@ -100,8 +111,18 @@ export const ConfigurationService = {
|
||||
},
|
||||
|
||||
async addWallpaper(name: string, url: string): Promise<Wallpaper> {
|
||||
const finalName = await addWallpaperToChromeStorageLocal(name, url);
|
||||
return { name: finalName };
|
||||
const trimmedUrl = url.trim();
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(trimmedUrl);
|
||||
} catch {
|
||||
throw new Error('Please enter a valid wallpaper URL.');
|
||||
}
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
throw new Error('Wallpaper URLs must use HTTP or HTTPS.');
|
||||
}
|
||||
const finalName = name.trim() || getWallpaperNameFromUrl(parsedUrl) || 'Wallpaper';
|
||||
return { name: finalName, url: parsedUrl.href };
|
||||
},
|
||||
|
||||
async addWallpaperFile(file: File): Promise<Wallpaper> {
|
||||
@@ -130,6 +151,7 @@ export const ConfigurationService = {
|
||||
},
|
||||
|
||||
async deleteWallpaper(wallpaper: Wallpaper): Promise<void> {
|
||||
if (wallpaper.url || wallpaper.base64 || !checkChromeStorageLocalAvailable()) return;
|
||||
await removeWallpaperFromChromeStorageLocal(wallpaper.name);
|
||||
},
|
||||
|
||||
|
||||
@@ -89,83 +89,46 @@ export async function removeCachedIconFromChromeStorageLocal(sourceUrl: string):
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* File uploads are stored as base64 while remote wallpapers remain URLs.
|
||||
* @param name Wallpaper name (string), used as a fallback.
|
||||
* @param url Wallpaper image URL (string) or base64 data URL.
|
||||
* @returns Promise<string> 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.
|
||||
* @throws Error if chrome.storage.local is unavailable or the wallpaper data is invalid.
|
||||
*/
|
||||
export async function addWallpaperToChromeStorageLocal(name: string, url: string): Promise<string> {
|
||||
if (!checkChromeStorageLocalAvailable()) {
|
||||
throw new Error('chrome.storage.local is not available');
|
||||
}
|
||||
|
||||
let finalName = name.trim();
|
||||
if (url.startsWith('data:')) {
|
||||
// This is a base64 encoded image from a file upload.
|
||||
// The name is the file name.
|
||||
return new Promise<void>((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));
|
||||
if (!finalName) throw new Error('A name is required for an uploaded wallpaper.');
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(new Error('chrome.storage.local is not available'));
|
||||
}
|
||||
}).then(() => name);
|
||||
}
|
||||
|
||||
// This is a URL. Let's try to fetch it.
|
||||
let parsedUrl: URL;
|
||||
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<string>((resolve, reject) => {
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(imageBlob);
|
||||
});
|
||||
parsedUrl = new URL(url);
|
||||
} catch {
|
||||
throw new Error('Please enter a valid wallpaper URL.');
|
||||
}
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
throw new Error('Wallpaper URLs must use HTTP or HTTPS.');
|
||||
}
|
||||
finalName = finalName || parsedUrl.pathname.split('/').filter(Boolean).pop() || parsedUrl.hostname;
|
||||
}
|
||||
|
||||
// If successful, use the filename from URL as the name.
|
||||
const finalName = url.substring(url.lastIndexOf('/') + 1).replace(/[?#].*$/, '') || name;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (window.chrome?.storage?.local) {
|
||||
window.chrome.storage.local.set({ [finalName]: base64 }, function () {
|
||||
if (!window.chrome?.storage?.local) {
|
||||
reject(new Error('chrome.storage.local is not available'));
|
||||
return;
|
||||
}
|
||||
window.chrome.storage.local.set({ [finalName]: 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(() => 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<void>((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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
---
|
||||
project_name: Vision Start
|
||||
date: 2026-07-08
|
||||
date: 2026-08-07
|
||||
type: general_overview
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@ The startpage is composed of widgets and a configuration panel:
|
||||
- **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`).
|
||||
- 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.
|
||||
- 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.
|
||||
- **Configuration panel** — Slide-in right-side modal with four tabs: General, Theme, Clock, Server Widget. Includes **Export** (downloads a JSON bundle of selected `localStorage` keys) and **Import** (restores from JSON and reloads the page).
|
||||
- **Edit mode** — Toggle via the top-left pencil button; reveals per-tile glass action toolbars, per-category edit buttons, and ghost glass "add" tiles.
|
||||
@@ -174,12 +174,12 @@ Storage layout (browser-side):
|
||||
|---|---|---|
|
||||
| `config` | `localStorage` | The full `Config` JSON |
|
||||
| `categories` | `localStorage` | `Category[]` JSON |
|
||||
| `userWallpapers` | `localStorage` | `Wallpaper[]` index (names) |
|
||||
| `userWallpapers` | `localStorage` | `Wallpaper[]` index (names plus URLs for remote wallpapers) |
|
||||
| `wallpaperState` | `localStorage` | `{ lastWallpaperChange, currentIndex }` for rotation |
|
||||
| `<wallpaperName>` | `chrome.storage.local` (when available) | base64 (or URL on CORS failure) image data |
|
||||
| `<wallpaperName>` | `chrome.storage.local` (when available) | Base64 image data for uploaded wallpaper files and legacy URL wallpapers |
|
||||
| `vision-start:icon:<encoded-source-url>` | `chrome.storage.local` (when available) | CORS-readable website icon data URL, capped at 256KiB |
|
||||
|
||||
Export/import bundles keys: `config`, `categories`, `userWallpapers`, `wallpaperState`. Icon cache entries are rebuildable and are not included.
|
||||
Export/import bundles keys: `config`, `categories`, `userWallpapers`, `wallpaperState`. Remote wallpaper URLs are included through `userWallpapers`; uploaded image data and rebuildable icon cache entries are not included.
|
||||
|
||||
---
|
||||
|
||||
@@ -234,7 +234,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.
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -266,4 +266,4 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 2026-07-10. Generated as a general project overview; not a coding-style guide._
|
||||
_Last updated: 2026-08-07. Generated as a general project overview; not a coding-style guide._
|
||||
|
||||
Reference in New Issue
Block a user