Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
551bff1e5b | ||
|
|
d391dc7135 |
@@ -47,39 +47,72 @@ const blobToDataUrl = (blob: Blob): Promise<string> =>
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const TRUSTED_TLDS = new Set([
|
||||
'com',
|
||||
'org',
|
||||
'net',
|
||||
'gov',
|
||||
'edu',
|
||||
'io',
|
||||
'co',
|
||||
'dev',
|
||||
'app',
|
||||
'me',
|
||||
'ai',
|
||||
'info',
|
||||
'br',
|
||||
'uk',
|
||||
'de',
|
||||
]);
|
||||
|
||||
const isTrustedTld = (hostname: string): boolean => {
|
||||
if (!hostname || !hostname.includes('.')) return false;
|
||||
const parts = hostname.toLowerCase().split('.');
|
||||
const lastPart = parts[parts.length - 1];
|
||||
return TRUSTED_TLDS.has(lastPart);
|
||||
};
|
||||
|
||||
const getIconFetchSource = (iconUrl: string): string | null => {
|
||||
try {
|
||||
const url = new URL(iconUrl);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
||||
if (
|
||||
(url.hostname === 'www.google.com' || url.hostname === 'google.com') &&
|
||||
url.pathname.startsWith('/s2/favicons')
|
||||
) {
|
||||
const domain = url.searchParams.get('domain');
|
||||
if (domain) {
|
||||
let cleanHost = domain.trim().replace(/^https?:\/\//i, '');
|
||||
try {
|
||||
cleanHost = new URL(`https://${cleanHost}`).hostname;
|
||||
} catch {
|
||||
// ignore parsing error
|
||||
}
|
||||
if (cleanHost && isTrustedTld(cleanHost)) {
|
||||
return `https://icon.horse/icon/${encodeURIComponent(cleanHost)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function getWebsiteIcon(rawUrl: string): Promise<string> {
|
||||
let targetUrl = rawUrl.trim();
|
||||
if (targetUrl && !/^https?:\/\//i.test(targetUrl)) {
|
||||
targetUrl = `https://${targetUrl}`;
|
||||
}
|
||||
const trimmed = rawUrl.trim();
|
||||
const hasProtocol = /^https?:\/\//i.test(trimmed);
|
||||
const targetUrl = hasProtocol ? trimmed : `https://${trimmed}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl);
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
const appleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]');
|
||||
if (appleTouchIcon) {
|
||||
const href = appleTouchIcon.getAttribute('href');
|
||||
if (href) {
|
||||
return new URL(href, targetUrl).href;
|
||||
const parsed = new URL(targetUrl);
|
||||
if (!isTrustedTld(parsed.hostname)) {
|
||||
if (!hasProtocol) {
|
||||
return `http://${parsed.host}/favicon.ico`;
|
||||
}
|
||||
return `${parsed.origin}/favicon.ico`;
|
||||
}
|
||||
|
||||
const iconLink = doc.querySelector('link[rel="icon"][type="image/png"]') || doc.querySelector('link[rel="icon"]');
|
||||
if (iconLink) {
|
||||
const href = iconLink.getAttribute('href');
|
||||
if (href) {
|
||||
return new URL(href, targetUrl).href;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching and parsing HTML for icon:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname = new URL(targetUrl).hostname;
|
||||
return `https://www.google.com/s2/favicons?domain=${hostname}&sz=128`;
|
||||
return `https://www.google.com/s2/favicons?domain=${parsed.hostname}&sz=128`;
|
||||
} catch {
|
||||
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(rawUrl)}&sz=128`;
|
||||
}
|
||||
@@ -125,8 +158,11 @@ async function cacheWebsiteIcon(iconUrl: string): Promise<string | null> {
|
||||
const cachedIcon = await getCachedWebsiteIcon(iconUrl);
|
||||
if (cachedIcon) return cachedIcon;
|
||||
|
||||
const fetchSource = getIconFetchSource(iconUrl);
|
||||
if (!fetchSource) return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(iconUrl, { mode: 'cors' });
|
||||
const response = await fetch(fetchSource);
|
||||
if (!response.ok || response.type === 'opaque') return null;
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
+3
-3
@@ -58,13 +58,13 @@ The startpage is composed of widgets and a configuration panel:
|
||||
- **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.
|
||||
- **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 for trusted public TLDs as a Google S2 favicon URL derived from the site's hostname (no HTML fetching — the S2 URL is used purely as an `<img>` source). Non-trusted TLDs (e.g. local TLDs, IP addresses) fetch `${origin}/favicon.ico` directly. Tiles opportunistically cache CORS-readable icon responses as data URLs in `chrome.storage.local` and retain the original URL when caching is unavailable. Cache population fetches trusted TLDs through a CORS-open favicon service (`icon.horse`) because Google's S2 endpoint 301-redirects without `Access-Control-Allow-Origin`, which would block cross-origin `fetch`.
|
||||
- **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.
|
||||
- **Liquid glass design language** — Soft translucent surfaces, restrained edge highlights, moderate backdrop blur, soft shadows, cyan focus states, and iOS-like easing tokens (`ease-ios`, `ease-spring`, `ease-liquid`) defined in `index.css`.
|
||||
|
||||
Performance notes:
|
||||
- Website tile icons check a deterministic `vision-start:icon:<encoded-source-url>` entry in `chrome.storage.local` before loading the external URL. Cache population uses ordinary CORS-enabled `fetch`, deduplicates in-flight requests, stores only `image/*` responses up to 256KiB, and never intercepts or proxies outside requests.
|
||||
- Website tile icons check a deterministic `vision-start:icon:<encoded-source-url>` entry in `chrome.storage.local` before loading the external URL. Cache population fetches Google S2 favicon URLs for trusted public TLDs through `https://icon.horse/icon/<host>` (a CORS-open favicon service), while non-trusted TLDs and direct icon URLs render directly in `<img>` tags without triggering background `fetch` calls. It deduplicates in-flight requests, stores only `image/*` responses up to 256KiB, and never intercepts or proxies outside requests.
|
||||
- Modals (`ConfigurationModal`, `WebsiteEditModal`, `CategoryEditModal`) are code-split via `React.lazy` + `Suspense` and only loaded when opened. `ConfigurationModal` is the heaviest chunk (it pulls in `@hello-pangea/dnd` via `ServerWidgetTab`); the rest of `@hello-pangea/dnd` is isolated from the initial load. `WebsiteEditModal` and `CategoryEditModal` share a common `ModalShell` component.
|
||||
- `WebsiteTile` and `CategoryGroup` are wrapped in `React.memo`; `App.tsx` handlers are `useCallback`-stabilized and pure alignment helpers are hoisted to module scope, so opening a modal / toggling edit no longer re-renders every tile.
|
||||
- `Clock` updates on the minute boundary (one `setTimeout` → `setInterval(60_000)`) instead of every second.
|
||||
@@ -245,7 +245,7 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
||||
- **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`, 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.
|
||||
- **Website icon cache** is persistent only in `chrome.storage.local`; it is rebuilt from website icon URLs after configuration import. Only `image/*` responses no larger than 256KiB are cached. Trusted public TLDs use Google S2 and the CORS-open `icon.horse` service for favicon caching, while non-trusted TLDs (local TLDs, IP addresses, custom domains) fetch `${origin}/favicon.ico` directly. No site HTML is ever fetched for icon discovery.
|
||||
- **`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.).
|
||||
- **Project guidance note** in `PROJECT.md`: do not use `npm run dev` for real verification — use `npm run build`.
|
||||
|
||||
Reference in New Issue
Block a user