general fixes & enhancements
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
// TypeScript interface for window.chrome
|
||||
import { getFileNameFromUrl } from './urlUtils';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
chrome?: {
|
||||
@@ -23,7 +25,6 @@ const ICON_CACHE_KEY_PREFIX = 'vision-start:icon:';
|
||||
const getIconCacheKey = (sourceUrl: string): string =>
|
||||
`${ICON_CACHE_KEY_PREFIX}${encodeURIComponent(sourceUrl)}`;
|
||||
|
||||
|
||||
/**
|
||||
* Checks if chrome.storage.local is available and caches the result.
|
||||
*/
|
||||
@@ -37,54 +38,60 @@ export function checkChromeStorageLocalAvailable(): boolean {
|
||||
return isChromeStorageLocalAvailable;
|
||||
}
|
||||
|
||||
export async function getCachedIconFromChromeStorageLocal(sourceUrl: string): Promise<string | null> {
|
||||
if (!checkChromeStorageLocalAvailable()) return null;
|
||||
type StorageResult = { [key: string]: string };
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
const chromeLocalCall = <T>(
|
||||
operation: (callback: (result: T) => void) => void,
|
||||
): Promise<T> =>
|
||||
new Promise<T>((resolve, reject) => {
|
||||
if (!window.chrome?.storage?.local) {
|
||||
resolve(null);
|
||||
reject(new Error('chrome.storage.local is not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getIconCacheKey(sourceUrl);
|
||||
window.chrome.storage.local.get([key], function (result: { [key: string]: string }) {
|
||||
operation((result) => {
|
||||
if (window.chrome?.runtime?.lastError) {
|
||||
resolve(null);
|
||||
return;
|
||||
reject(new Error(window.chrome.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
resolve(result[key] || null);
|
||||
});
|
||||
});
|
||||
|
||||
export async function getCachedIconFromChromeStorageLocal(sourceUrl: string): Promise<string | null> {
|
||||
if (!checkChromeStorageLocalAvailable()) return null;
|
||||
try {
|
||||
const key = getIconCacheKey(sourceUrl);
|
||||
const result = await chromeLocalCall<StorageResult>((cb) =>
|
||||
window.chrome?.storage?.local?.get([key], cb),
|
||||
);
|
||||
return result[key] || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveCachedIconToChromeStorageLocal(sourceUrl: string, dataUrl: string): Promise<boolean> {
|
||||
if (!checkChromeStorageLocalAvailable()) return false;
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
if (!window.chrome?.storage?.local) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.chrome.storage.local.set({ [getIconCacheKey(sourceUrl)]: dataUrl }, function () {
|
||||
resolve(!window.chrome?.runtime?.lastError);
|
||||
});
|
||||
});
|
||||
try {
|
||||
await chromeLocalCall<void>((cb) =>
|
||||
window.chrome?.storage?.local?.set({ [getIconCacheKey(sourceUrl)]: dataUrl }, cb),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCachedIconFromChromeStorageLocal(sourceUrl: string): Promise<boolean> {
|
||||
if (!checkChromeStorageLocalAvailable()) return false;
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
if (!window.chrome?.storage?.local) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.chrome.storage.local.remove(getIconCacheKey(sourceUrl), function () {
|
||||
resolve(!window.chrome?.runtime?.lastError);
|
||||
});
|
||||
});
|
||||
try {
|
||||
await chromeLocalCall<void>((cb) =>
|
||||
window.chrome?.storage?.local?.remove(getIconCacheKey(sourceUrl), cb),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,22 +120,13 @@ export async function addWallpaperToChromeStorageLocal(name: string, url: string
|
||||
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;
|
||||
finalName = finalName || getFileNameFromUrl(parsedUrl);
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}).then(() => finalName);
|
||||
await chromeLocalCall<void>((cb) =>
|
||||
window.chrome?.storage?.local?.set({ [finalName]: url }, cb),
|
||||
);
|
||||
return finalName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,19 +139,10 @@ export async function getWallpaperFromChromeStorageLocal(name: string): Promise<
|
||||
if (!checkChromeStorageLocalAvailable()) {
|
||||
throw new Error('chrome.storage.local is not available');
|
||||
}
|
||||
return new Promise<string | null>((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'));
|
||||
}
|
||||
});
|
||||
const result = await chromeLocalCall<StorageResult>((cb) =>
|
||||
window.chrome?.storage?.local?.get([name], cb),
|
||||
);
|
||||
return result[name] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,17 +155,7 @@ export async function removeWallpaperFromChromeStorageLocal(name: string): Promi
|
||||
if (!checkChromeStorageLocalAvailable()) {
|
||||
throw new Error('chrome.storage.local is not available');
|
||||
}
|
||||
return new Promise<void>((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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
await chromeLocalCall<void>((cb) =>
|
||||
window.chrome?.storage?.local?.remove(name, cb),
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,19 @@ import {
|
||||
} from './StorageLocalManager';
|
||||
|
||||
const MAX_CACHED_ICON_BYTES = 256 * 1024;
|
||||
const MAX_RESOLVED_ICONS = 50;
|
||||
const resolvedIconCache = new Map<string, string>();
|
||||
const iconCacheLookups = new Map<string, Promise<string | null>>();
|
||||
const iconCacheRequests = new Map<string, Promise<string | null>>();
|
||||
|
||||
const rememberResolvedIcon = (iconUrl: string, dataUrl: string): void => {
|
||||
resolvedIconCache.set(iconUrl, dataUrl);
|
||||
if (resolvedIconCache.size > MAX_RESOLVED_ICONS) {
|
||||
const oldest = resolvedIconCache.keys().next().value;
|
||||
if (oldest !== undefined) resolvedIconCache.delete(oldest);
|
||||
}
|
||||
};
|
||||
|
||||
const isDataUrl = (value: string): boolean => value.startsWith('data:');
|
||||
|
||||
const isCacheableIconUrl = (value: string): boolean => {
|
||||
@@ -38,9 +47,14 @@ const blobToDataUrl = (blob: Blob): Promise<string> =>
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
async function getWebsiteIcon(url: string): Promise<string> {
|
||||
async function getWebsiteIcon(rawUrl: string): Promise<string> {
|
||||
let targetUrl = rawUrl.trim();
|
||||
if (targetUrl && !/^https?:\/\//i.test(targetUrl)) {
|
||||
targetUrl = `https://${targetUrl}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(targetUrl);
|
||||
const html = await response.text();
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
@@ -48,7 +62,7 @@ async function getWebsiteIcon(url: string): Promise<string> {
|
||||
if (appleTouchIcon) {
|
||||
const href = appleTouchIcon.getAttribute('href');
|
||||
if (href) {
|
||||
return new URL(href, url).href;
|
||||
return new URL(href, targetUrl).href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,15 +70,19 @@ async function getWebsiteIcon(url: string): Promise<string> {
|
||||
if (iconLink) {
|
||||
const href = iconLink.getAttribute('href');
|
||||
if (href) {
|
||||
return new URL(href, url).href;
|
||||
return new URL(href, targetUrl).href;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching and parsing HTML for icon:', error);
|
||||
}
|
||||
|
||||
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=128`;
|
||||
try {
|
||||
const hostname = new URL(targetUrl).hostname;
|
||||
return `https://www.google.com/s2/favicons?domain=${hostname}&sz=128`;
|
||||
} catch {
|
||||
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(rawUrl)}&sz=128`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCachedWebsiteIcon(iconUrl: string): Promise<string | null> {
|
||||
@@ -80,7 +98,7 @@ async function getCachedWebsiteIcon(iconUrl: string): Promise<string | null> {
|
||||
const lookup = getCachedIconFromChromeStorageLocal(iconUrl)
|
||||
.then((cachedIcon) => {
|
||||
if (isValidCachedIcon(cachedIcon)) {
|
||||
resolvedIconCache.set(iconUrl, cachedIcon);
|
||||
rememberResolvedIcon(iconUrl, cachedIcon);
|
||||
return cachedIcon;
|
||||
}
|
||||
return null;
|
||||
@@ -121,7 +139,7 @@ async function cacheWebsiteIcon(iconUrl: string): Promise<string | null> {
|
||||
}
|
||||
|
||||
const dataUrl = await blobToDataUrl(blob);
|
||||
resolvedIconCache.set(iconUrl, dataUrl);
|
||||
rememberResolvedIcon(iconUrl, dataUrl);
|
||||
await saveCachedIconToChromeStorageLocal(iconUrl, dataUrl);
|
||||
return dataUrl;
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
export const SIZE_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'tiny', label: 'Tiny' },
|
||||
{ value: 'small', label: 'Small' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'large', label: 'Large' },
|
||||
];
|
||||
|
||||
export const TILE_SIZE_CLASSES: Record<string, string> = {
|
||||
small: 'w-28 h-28',
|
||||
medium: 'w-32 h-32',
|
||||
large: 'w-36 h-36',
|
||||
};
|
||||
|
||||
export const getTileSizeClass = (size: string | undefined): string =>
|
||||
TILE_SIZE_CLASSES[size ?? ''] ?? 'w-32 h-32';
|
||||
|
||||
export const ICON_PIXEL_SIZES: Record<string, number> = {
|
||||
small: 34,
|
||||
medium: 42,
|
||||
large: 48,
|
||||
};
|
||||
|
||||
export const getIconPixelSize = (size: string | undefined): number =>
|
||||
ICON_PIXEL_SIZES[size ?? ''] ?? 40;
|
||||
|
||||
export const ICON_LOADING_PIXEL_SIZES: Record<string, number> = {
|
||||
small: 24,
|
||||
medium: 32,
|
||||
large: 40,
|
||||
};
|
||||
|
||||
export const getIconLoadingPixelSize = (size: string | undefined): number =>
|
||||
ICON_LOADING_PIXEL_SIZES[size ?? ''] ?? 32;
|
||||
|
||||
export const CLOCK_SIZE_CLASSES: Record<string, string> = {
|
||||
tiny: 'text-3xl',
|
||||
small: 'text-4xl',
|
||||
medium: 'text-5xl',
|
||||
large: 'text-6xl',
|
||||
};
|
||||
|
||||
export const getClockSizeClass = (size: string): string =>
|
||||
CLOCK_SIZE_CLASSES[size] ?? 'text-5xl';
|
||||
|
||||
export const TITLE_SIZE_CLASSES: Record<string, string> = {
|
||||
tiny: 'text-4xl',
|
||||
small: 'text-5xl',
|
||||
medium: 'text-6xl',
|
||||
large: 'text-7xl',
|
||||
};
|
||||
|
||||
export const getTitleSizeClass = (size: string): string =>
|
||||
TITLE_SIZE_CLASSES[size] ?? 'text-6xl';
|
||||
|
||||
export const ALIGNMENT_CLASSES: Record<string, string> = {
|
||||
top: 'justify-start',
|
||||
left: 'justify-start',
|
||||
middle: 'justify-center',
|
||||
bottom: 'justify-end',
|
||||
right: 'justify-end',
|
||||
};
|
||||
|
||||
export const getAlignmentClass = (alignment: string): string =>
|
||||
ALIGNMENT_CLASSES[alignment] ?? 'justify-center';
|
||||
@@ -0,0 +1,9 @@
|
||||
export const getFileNameFromUrl = (url: URL): string => {
|
||||
const pathName = url.pathname.split('/').filter(Boolean).pop();
|
||||
if (!pathName) return url.hostname;
|
||||
try {
|
||||
return decodeURIComponent(pathName);
|
||||
} catch {
|
||||
return pathName;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
export const MIN_WALLPAPER_FREQUENCY_HOURS = 1;
|
||||
export const MAX_WALLPAPER_FREQUENCY_HOURS = 48;
|
||||
export const DEFAULT_WALLPAPER_FREQUENCY_HOURS = 24;
|
||||
|
||||
export const getWallpaperFrequencyHours = (frequency: string): number => {
|
||||
if (!frequency) return DEFAULT_WALLPAPER_FREQUENCY_HOURS;
|
||||
const match = frequency.match(/^(\d+)(h|d)$/);
|
||||
if (!match) return DEFAULT_WALLPAPER_FREQUENCY_HOURS;
|
||||
const value = parseInt(match[1], 10);
|
||||
const hours = match[2] === 'd' ? value * 24 : value;
|
||||
return Math.min(MAX_WALLPAPER_FREQUENCY_HOURS, Math.max(MIN_WALLPAPER_FREQUENCY_HOURS, hours));
|
||||
};
|
||||
|
||||
export const getWallpaperFrequencyMs = (frequency: string): number =>
|
||||
getWallpaperFrequencyHours(frequency) * 60 * 60 * 1000;
|
||||
|
||||
export const formatWallpaperFrequency = (hours: number): string =>
|
||||
`${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||
|
||||
export 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;
|
||||
};
|
||||
Reference in New Issue
Block a user