mirror of
https://github.com/handsomezhuzhu/AeroStart.git
synced 2026-02-20 20:10:15 +00:00
✨ feat: initialize AeroStart browser start page project
Implement a modern, customizable browser start page with comprehensive features: - Multi-theme support with 8 preset color schemes - Custom wallpaper system supporting images and videos with multiple fit modes - Integrated search functionality with 5 major search engines (Google, Baidu, Bing, DuckDuckGo, Bilibili) - Real-time clock component with 12/24 hour format options - Dynamic background blur effect during search for enhanced focus - Complete i18n system with English and Chinese language support - Responsive design with smooth animations and transitions - Local storage integration for persistent user preferences - Context menu system for quick settings access - Toast notification system for user feedback - Error boundary for robust error handling Tech Stack: - React 19 with TypeScript - Vite 6 for build tooling - Tailwind CSS for styling - Local storage for data persistence Project Structure: - Core components: Clock, SearchBox, SettingsModal, ThemeSettings, WallpaperManager - Utility modules: storage management, search suggestions - Context providers: Toast notifications, i18n - Type definitions and constants configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
122
utils/storage.ts
Normal file
122
utils/storage.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { UserSettings } from '../types';
|
||||
|
||||
const STORAGE_KEY = 'aerostart_settings';
|
||||
const DEBOUNCE_DELAY = 100; // 100ms debounce delay
|
||||
|
||||
// Debounce timer
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/**
|
||||
* Load user settings from Local Storage
|
||||
* @param defaultSettings Default settings
|
||||
* @returns Merged user settings
|
||||
*/
|
||||
export const loadSettings = (defaultSettings: UserSettings): UserSettings => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) {
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stored);
|
||||
// Merge default settings with stored settings to ensure new config items have default values
|
||||
return {
|
||||
...defaultSettings,
|
||||
...parsed,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
return defaultSettings;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate data size in bytes
|
||||
* @param data Data to calculate
|
||||
* @returns Data size in bytes
|
||||
*/
|
||||
const getDataSize = (data: string): number => {
|
||||
return new Blob([data]).size;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check localStorage available space
|
||||
* @param dataSize Data size to store (bytes)
|
||||
* @returns Whether there is enough space
|
||||
*/
|
||||
const checkStorageQuota = (dataSize: number): boolean => {
|
||||
// localStorage is typically limited to 5-10MB
|
||||
const QUOTA_LIMIT = 5 * 1024 * 1024; // 5MB safe limit
|
||||
|
||||
try {
|
||||
// Calculate currently used space
|
||||
let currentSize = 0;
|
||||
for (let key in localStorage) {
|
||||
if (localStorage.hasOwnProperty(key)) {
|
||||
currentSize += getDataSize(localStorage[key] + key);
|
||||
}
|
||||
}
|
||||
|
||||
return (currentSize + dataSize) < QUOTA_LIMIT;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Immediately save user settings to Local Storage (without debounce)
|
||||
* @param settings User settings
|
||||
* @throws Error when storage space is insufficient
|
||||
*/
|
||||
const saveSettingsImmediate = (settings: UserSettings): void => {
|
||||
try {
|
||||
const settingsJson = JSON.stringify(settings);
|
||||
const dataSize = getDataSize(settingsJson);
|
||||
|
||||
// Check storage quota
|
||||
if (!checkStorageQuota(dataSize)) {
|
||||
throw new Error('QUOTA_EXCEEDED');
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, settingsJson);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'QUOTA_EXCEEDED') {
|
||||
console.error('Insufficient storage space, cannot save settings');
|
||||
throw error;
|
||||
} else if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
||||
console.error('localStorage quota exceeded, cannot save settings');
|
||||
throw new Error('QUOTA_EXCEEDED');
|
||||
} else {
|
||||
console.error('Failed to save settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save user settings to Local Storage (with debounce)
|
||||
* @param settings User settings
|
||||
*/
|
||||
export const saveSettings = (settings: UserSettings): void => {
|
||||
// Clear previous timer
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
// Set new timer
|
||||
debounceTimer = setTimeout(() => {
|
||||
saveSettingsImmediate(settings);
|
||||
debounceTimer = null;
|
||||
}, DEBOUNCE_DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all stored settings
|
||||
*/
|
||||
export const clearSettings = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear settings:', error);
|
||||
}
|
||||
};
|
||||
126
utils/suggestions.ts
Normal file
126
utils/suggestions.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
let callbackCount = 0;
|
||||
|
||||
// Store and manage AbortController
|
||||
const abortControllers = new Map<string, AbortController>();
|
||||
|
||||
export const fetchSuggestions = (engine: string, query: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!query || !query.trim()) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bilibili uses fetch API (via Vite proxy)
|
||||
if (engine === 'Bilibili') {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const url = `/bilibili?term=${encodedQuery}`;
|
||||
|
||||
// Cancel previous request
|
||||
const requestKey = `${engine}-${query}`;
|
||||
if (abortControllers.has(requestKey)) {
|
||||
abortControllers.get(requestKey)?.abort();
|
||||
}
|
||||
|
||||
// Create new AbortController
|
||||
const controller = new AbortController();
|
||||
abortControllers.set(requestKey, controller);
|
||||
|
||||
fetch(url, { signal: controller.signal })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Clean up AbortController
|
||||
abortControllers.delete(requestKey);
|
||||
|
||||
try {
|
||||
// Bilibili return format: {code: 0, result: {tag: [{value: "suggestion text"}, ...]}}
|
||||
if (data.code === 0 && data.result && data.result.tag) {
|
||||
const suggestions = data.result.tag.map((item: any) => item.value);
|
||||
resolve(suggestions);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
resolve([]);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// Clean up AbortController
|
||||
abortControllers.delete(requestKey);
|
||||
|
||||
// If request was cancelled, do nothing
|
||||
if (error.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
resolve([]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Other search engines use JSONP
|
||||
const callbackName = `jsonp_cb_${Date.now()}_${callbackCount++}`;
|
||||
const script = document.createElement('script');
|
||||
let timeoutId: any;
|
||||
|
||||
const cleanup = () => {
|
||||
if ((window as any)[callbackName]) delete (window as any)[callbackName];
|
||||
if (document.body.contains(script)) document.body.removeChild(script);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
// Resolve empty on timeout to avoid breaking UI
|
||||
resolve([]);
|
||||
}, 3000);
|
||||
|
||||
(window as any)[callbackName] = (data: any) => {
|
||||
cleanup();
|
||||
try {
|
||||
if (engine === 'Google') {
|
||||
// Google (client=youtube) returns ["query", ["sug1", "sug2"], ...]
|
||||
resolve(data[1].map((item: any) => Array.isArray(item) ? item[0] : item));
|
||||
} else if (engine === 'Baidu') {
|
||||
// Baidu returns {q: "query", s: ["sug1", "sug2"]}
|
||||
resolve(data.s);
|
||||
} else if (engine === 'Bing') {
|
||||
// Bing returns ["query", ["sug1", "sug2"]]
|
||||
resolve(data[1].map((item: any) => item));
|
||||
} else if (engine === 'DuckDuckGo') {
|
||||
// DDG returns [{phrase: "sug1"}, ...]
|
||||
resolve(data.map((item: any) => item.phrase));
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
} catch (e) {
|
||||
resolve([]);
|
||||
}
|
||||
};
|
||||
|
||||
let url = '';
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
|
||||
if (engine === 'Google') {
|
||||
// client=youtube supports JSONP
|
||||
url = `https://suggestqueries.google.com/complete/search?client=youtube&q=${encodedQuery}&jsonp=${callbackName}`;
|
||||
} else if (engine === 'Baidu') {
|
||||
url = `https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=${encodedQuery}&cb=${callbackName}`;
|
||||
} else if (engine === 'Bing') {
|
||||
url = `https://api.bing.com/osjson.aspx?query=${encodedQuery}&JsonType=callback&JsonCallback=${callbackName}`;
|
||||
} else if (engine === 'DuckDuckGo') {
|
||||
url = `https://duckduckgo.com/ac/?q=${encodedQuery}&callback=${callbackName}&type=list`;
|
||||
} else {
|
||||
// Unsupported engine
|
||||
cleanup();
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
script.src = url;
|
||||
script.onerror = () => {
|
||||
cleanup();
|
||||
resolve([]);
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user