♻️ refactor: reorganize project structure and centralize search engine config

- Move all source files from root to src/ directory for better organization
- Create src/config/searchEngines.ts to centralize search engine configurations
  - Define unified SearchEngineConfig interface
  - Support both JSONP and Fetch request methods
  - Implement response parsers for Google, Baidu, Bing, DuckDuckGo, and Bilibili
- Refactor src/utils/suggestions.ts to use centralized config
  - Simplify code from 126 lines to 81 lines
  - Support hybrid JSONP/Fetch mode (Bilibili uses Fetch via Vite proxy)
  - Remove duplicate URL construction and parsing logic
- Update path alias configuration
  - Change @/* from ./* to ./src/* in tsconfig.json
  - Update vite.config.ts alias to point to ./src
- Add Bilibili proxy configuration in vite.config.ts for development
- Remove Bilibili rewrites from vercel.json (use Vite proxy instead)
- Add @vercel/node to devDependencies
- Remove unused files: README.md, i18n/README.md, metadata.json, vite-env.d.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZyphrZero
2025-12-05 10:41:17 +08:00
parent aa46cd16b7
commit 2993f9b859
34 changed files with 1274 additions and 405 deletions

122
src/utils/storage.ts Normal file
View 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);
}
};

81
src/utils/suggestions.ts Normal file
View File

@@ -0,0 +1,81 @@
import {
getSearchEngineConfig,
buildJsonpUrl,
type SearchEngineType
} from '@/config/searchEngines';
let callbackCount = 0;
/**
* Fetch search suggestions (supports hybrid JSONP and Fetch mode)
*/
export const fetchSuggestions = (engine: string, query: string): Promise<string[]> => {
return new Promise((resolve) => {
if (!query || !query.trim()) {
resolve([]);
return;
}
const config = getSearchEngineConfig(engine as SearchEngineType);
if (!config) {
resolve([]);
return;
}
// Bilibili uses fetch (via Vite proxy)
if (engine === 'Bilibili') {
const url = `/bilibili?term=${encodeURIComponent(query)}`;
fetch(url)
.then(response => response.json())
.then(data => {
try {
const suggestions = config.parseResponse(data);
resolve(suggestions);
} catch (e) {
resolve([]);
}
})
.catch(() => {
resolve([]);
});
return;
}
// Other 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);
};
// 3 seconds timeout
timeoutId = setTimeout(() => {
cleanup();
resolve([]);
}, 3000);
// Set global callback function
(window as any)[callbackName] = (data: any) => {
cleanup();
try {
const suggestions = config.parseResponse(data);
resolve(suggestions);
} catch (e) {
resolve([]);
}
};
// Build URL and make request
script.src = buildJsonpUrl(config, query, callbackName);
script.onerror = () => {
cleanup();
resolve([]);
};
document.body.appendChild(script);
});
};