feat(ui): add settings menu and mask opacity control

- Add SettingsMenu component for quick navigation between settings sections
- Add mask opacity control to adjust overlay transparency
- Optimize search suggestions with immediate response and smooth animations
- Refine component styles for more compact interface
- Fix Translation interface type definition for maskOpacity
- Unify Bilibili API path to simplify environment handling

🤖 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 14:45:36 +08:00
parent 7a1815069a
commit a6daa5d728
14 changed files with 387 additions and 225 deletions

View File

@@ -3,10 +3,11 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
import Clock from './components/Clock'; import Clock from './components/Clock';
import SearchBox from './components/SearchBox'; import SearchBox from './components/SearchBox';
import SettingsModal from './components/SettingsModal'; import SettingsModal from './components/SettingsModal';
import SettingsMenu from './components/SettingsMenu';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import GlobalContextMenu from './components/GlobalContextMenu'; import GlobalContextMenu from './components/GlobalContextMenu';
import { SettingsIcon } from './components/Icons'; import { SettingsIcon } from './components/Icons';
import { UserSettings, WallpaperFit } from './types'; import { UserSettings, WallpaperFit, SettingsSection } from './types';
import { PRESET_WALLPAPERS, SEARCH_ENGINES, THEMES } from './constants'; import { PRESET_WALLPAPERS, SEARCH_ENGINES, THEMES } from './constants';
import { loadSettings, saveSettings } from './utils/storage'; import { loadSettings, saveSettings } from './utils/storage';
import { I18nProvider } from './i18n'; import { I18nProvider } from './i18n';
@@ -21,6 +22,7 @@ const DEFAULT_SETTINGS: UserSettings = {
themeColor: THEMES[0].hex, themeColor: THEMES[0].hex,
searchOpacity: 0.8, searchOpacity: 0.8,
enableMaskBlur: false, enableMaskBlur: false,
maskOpacity: 0.2,
backgroundUrl: PRESET_WALLPAPERS[0].url, backgroundUrl: PRESET_WALLPAPERS[0].url,
backgroundType: PRESET_WALLPAPERS[0].type, backgroundType: PRESET_WALLPAPERS[0].type,
wallpaperFit: 'cover', wallpaperFit: 'cover',
@@ -35,6 +37,8 @@ type ViewMode = 'search' | 'dashboard';
const App: React.FC = () => { const App: React.FC = () => {
// State for settings visibility // State for settings visibility
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isSettingsMenuOpen, setIsSettingsMenuOpen] = useState(false);
const [activeSettingsSection, setActiveSettingsSection] = useState<SettingsSection>('general');
// State for view mode (Search Panel vs Dashboard Panel) // State for view mode (Search Panel vs Dashboard Panel)
const [viewMode, setViewMode] = useState<ViewMode>('search'); const [viewMode, setViewMode] = useState<ViewMode>('search');
@@ -157,6 +161,12 @@ const App: React.FC = () => {
setSettings(prev => ({ ...prev, language: lang })); setSettings(prev => ({ ...prev, language: lang }));
}; };
const handleSettingsMenuSelect = (section: SettingsSection) => {
setActiveSettingsSection(section);
setIsSettingsMenuOpen(false);
setIsSettingsOpen(true);
};
return ( return (
<ErrorBoundary> <ErrorBoundary>
<I18nProvider language={settings.language} onLanguageChange={handleLanguageChange}> <I18nProvider language={settings.language} onLanguageChange={handleLanguageChange}>
@@ -168,123 +178,135 @@ const App: React.FC = () => {
{/* Context Menu Global Listener */} {/* Context Menu Global Listener */}
<GlobalContextMenu /> <GlobalContextMenu />
{/* Background Layer */} {/* Background Layer */}
<div <div
className={`absolute inset-0 transition-all duration-700 ease-[cubic-bezier(0.25,0.4,0.25,1)] overflow-hidden ${bgLoaded ? 'opacity-100' : 'opacity-0'}`} className={`absolute inset-0 transition-all duration-700 ease-[cubic-bezier(0.25,0.4,0.25,1)] overflow-hidden ${bgLoaded ? 'opacity-100' : 'opacity-0'}`}
style={{ style={{
filter: `blur(${isSearchActive ? settings.backgroundBlur : 0}px)`, filter: `blur(${isSearchActive ? settings.backgroundBlur : 0}px)`,
transform: isSearchActive ? 'scale(1.05)' : 'scale(1)', transform: isSearchActive ? 'scale(1.05)' : 'scale(1)',
}} }}
> >
{settings.backgroundType === 'video' ? ( {settings.backgroundType === 'video' ? (
<video <video
key={settings.backgroundUrl} key={settings.backgroundUrl}
className={`absolute inset-0 w-full h-full ${getVideoClass(settings.wallpaperFit)}`} className={`absolute inset-0 w-full h-full ${getVideoClass(settings.wallpaperFit)}`}
src={settings.backgroundUrl} src={settings.backgroundUrl}
autoPlay autoPlay
loop loop
muted muted
playsInline playsInline
/> />
) : ( ) : (
<div <div
className="absolute inset-0 w-full h-full" className="absolute inset-0 w-full h-full"
style={getBackgroundStyle(settings.wallpaperFit)} style={getBackgroundStyle(settings.wallpaperFit)}
/> />
)} )}
</div> </div>
{/* Overlay to ensure text readability */} {/* Overlay to ensure text readability */}
<div className={` <div
absolute inset-0 bg-black/40 className={`
${settings.enableMaskBlur ? 'backdrop-blur-sm' : ''} absolute inset-0 transition-opacity duration-500
`} /> ${settings.enableMaskBlur ? 'backdrop-blur-sm' : ''}
`}
style={{
backgroundColor: `rgba(0, 0, 0, ${settings.maskOpacity})`
}}
/>
{/* {/*
Main Content Area (Search View) Main Content Area (Search View)
Pointer events are disabled when not visible to prevent interaction with hidden elements Pointer events are disabled when not visible to prevent interaction with hidden elements
*/} */}
<div <div
className={` className={`
absolute inset-0 z-10 flex flex-col items-center pt-[18vh] w-full px-4 space-y-8 absolute inset-0 z-10 flex flex-col items-center pt-[18vh] w-full px-4 space-y-8
transition-all duration-500 ease-in-out transition-all duration-500 ease-in-out
${viewMode === 'search' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'} ${viewMode === 'search' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'}
`} `}
> >
{/* Clock Component */} {/* Clock Component */}
<ErrorBoundary> <ErrorBoundary>
<div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-150"> <div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-150">
<Clock <Clock
showSeconds={settings.showSeconds} showSeconds={settings.showSeconds}
use24HourFormat={settings.use24HourFormat} use24HourFormat={settings.use24HourFormat}
/> />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
{/* Search Input Component */} {/* Search Input Component */}
<ErrorBoundary> <ErrorBoundary>
<div className="w-full max-w-xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300"> <div className="w-full max-w-xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300">
<SearchBox <SearchBox
engines={settings.searchEngines} engines={settings.searchEngines}
selectedEngineName={settings.selectedEngine} selectedEngineName={settings.selectedEngine}
onSelectEngine={handleSelectEngine} onSelectEngine={handleSelectEngine}
themeColor={settings.themeColor} themeColor={settings.themeColor}
opacity={settings.searchOpacity} opacity={settings.searchOpacity}
onInteractionChange={setIsSearchActive} onInteractionChange={setIsSearchActive}
enableHistory={settings.enableSearchHistory} enableHistory={settings.enableSearchHistory}
history={settings.searchHistory} history={settings.searchHistory}
onUpdateHistory={handleUpdateHistory} onUpdateHistory={handleUpdateHistory}
/> />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
{/* {/*
Dashboard Panel (Under Development) Dashboard Panel (Under Development)
Visible only in dashboard mode Visible only in dashboard mode
*/} */}
<div <div
className={` className={`
absolute inset-0 z-10 flex flex-col items-center justify-center absolute inset-0 z-10 flex flex-col items-center justify-center
transition-all duration-500 ease-in-out transition-all duration-500 ease-in-out
${viewMode === 'dashboard' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-105 pointer-events-none'} ${viewMode === 'dashboard' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-105 pointer-events-none'}
`} `}
> >
<div className="relative group cursor-default"> <div className="relative group cursor-default">
<h1 className="text-4xl md:text-5xl font-extralight tracking-[0.2em] text-white/30 group-hover:text-white/50 transition-colors duration-500 select-none"> <h1 className="text-4xl md:text-5xl font-extralight tracking-[0.2em] text-white/30 group-hover:text-white/50 transition-colors duration-500 select-none">
DASHBOARD DASHBOARD
</h1> </h1>
<div className="absolute -bottom-4 left-0 w-full flex justify-center"> <div className="absolute -bottom-4 left-0 w-full flex justify-center">
<span className="text-xs font-mono text-white/20 tracking-widest uppercase bg-white/5 px-2 py-0.5 rounded"> <span className="text-xs font-mono text-white/20 tracking-widest uppercase bg-white/5 px-2 py-0.5 rounded">
Under Construction Under Construction
</span> </span>
</div>
</div>
{/* Top Right Settings Button - Only visible in Dashboard */}
<div className="absolute top-6 right-6 z-50">
<button
onClick={(e) => {
e.stopPropagation(); // Prevent clicking dashboard background
setIsSettingsMenuOpen(!isSettingsMenuOpen);
}}
className={`group p-2 rounded-full bg-black/30 hover:bg-white/20 backdrop-blur-md border border-white/10 hover:border-white/30 transition-all duration-300 shadow-lg ${isSettingsMenuOpen ? 'bg-white/20 border-white/30' : ''}`}
aria-label="Settings"
>
<SettingsIcon className={`w-5 h-5 text-white/70 group-hover:text-white transition-all duration-500 ${isSettingsMenuOpen ? 'rotate-90 text-white' : ''}`} />
</button>
<SettingsMenu
isOpen={isSettingsMenuOpen}
onClose={() => setIsSettingsMenuOpen(false)}
onSelectSection={handleSettingsMenuSelect}
/>
</div> </div>
</div> </div>
{/* Top Right Settings Button - Only visible in Dashboard */} {/* Settings Modal */}
<div className="absolute top-6 right-6"> <ErrorBoundary>
<button <SettingsModal
onClick={(e) => { isOpen={isSettingsOpen}
e.stopPropagation(); // Prevent clicking dashboard background onClose={() => setIsSettingsOpen(false)}
setIsSettingsOpen(true); settings={settings}
}} onUpdateSettings={setSettings}
className="group p-3 rounded-full bg-black/20 hover:bg-white/20 backdrop-blur-md border border-white/5 hover:border-white/30 transition-all duration-300 shadow-lg" section={activeSettingsSection}
aria-label="Settings" />
> </ErrorBoundary>
<SettingsIcon className="w-6 h-6 text-white/70 group-hover:text-white group-hover:rotate-90 transition-all duration-500" />
</button>
</div>
</div>
{/* Settings Modal */}
<ErrorBoundary>
<SettingsModal
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
settings={settings}
onUpdateSettings={setSettings}
/>
</ErrorBoundary>
</div> </div>
</I18nProvider> </I18nProvider>
</ErrorBoundary> </ErrorBoundary>

View File

@@ -190,7 +190,7 @@ const GlobalContextMenu: React.FC = () => {
<div <div
ref={menuRef} ref={menuRef}
className=" className="
fixed z-[9999] min-w-[140px] py-1.5 fixed z-[9999] min-w-[120px] py-1
rounded-xl bg-[#1a1a1a]/95 backdrop-blur-xl rounded-xl bg-[#1a1a1a]/95 backdrop-blur-xl
border border-white/10 shadow-[0_10px_40px_rgba(0,0,0,0.6)] border border-white/10 shadow-[0_10px_40px_rgba(0,0,0,0.6)]
animate-in fade-in zoom-in-95 duration-200 origin-top-left animate-in fade-in zoom-in-95 duration-200 origin-top-left
@@ -200,39 +200,39 @@ const GlobalContextMenu: React.FC = () => {
left: position.x, left: position.x,
}} }}
> >
<div className="flex flex-col gap-0.5 px-1.5"> <div className="flex flex-col gap-0.5 px-1">
<button <button
onClick={handleCopy} onClick={handleCopy}
className=" className="
flex items-center gap-3 px-3 py-2 text-sm text-white/90 rounded-lg flex items-center gap-2.5 px-2.5 py-2 text-xs text-white/90 rounded-lg
hover:bg-white/10 transition-colors w-full text-left group hover:bg-white/10 transition-colors w-full text-left group
" "
> >
<CopyIcon className="w-4 h-4 text-white/60 group-hover:text-white" /> <CopyIcon className="w-3.5 h-3.5 text-white/60 group-hover:text-white" />
<span>{t.copy}</span> <span>{t.copy}</span>
</button> </button>
<button <button
onClick={handleCut} onClick={handleCut}
className=" className="
flex items-center gap-3 px-3 py-2 text-sm text-white/90 rounded-lg flex items-center gap-2.5 px-2.5 py-2 text-xs text-white/90 rounded-lg
hover:bg-white/10 transition-colors w-full text-left group hover:bg-white/10 transition-colors w-full text-left group
" "
> >
<ScissorsIcon className="w-4 h-4 text-white/60 group-hover:text-white" /> <ScissorsIcon className="w-3.5 h-3.5 text-white/60 group-hover:text-white" />
<span>{t.cut}</span> <span>{t.cut}</span>
</button> </button>
<div className="h-[1px] bg-white/10 my-1 mx-1" /> <div className="h-[1px] bg-white/10 my-0.5 mx-1" />
<button <button
onClick={handlePaste} onClick={handlePaste}
className=" className="
flex items-center gap-3 px-3 py-2 text-sm text-white/90 rounded-lg flex items-center gap-2.5 px-2.5 py-2 text-xs text-white/90 rounded-lg
hover:bg-white/10 transition-colors w-full text-left group hover:bg-white/10 transition-colors w-full text-left group
" "
> >
<ClipboardIcon className="w-4 h-4 text-white/60 group-hover:text-white" /> <ClipboardIcon className="w-3.5 h-3.5 text-white/60 group-hover:text-white" />
<span>{t.paste}</span> <span>{t.paste}</span>
</button> </button>
</div> </div>

View File

@@ -35,6 +35,7 @@ const SearchBox: React.FC<SearchBoxProps> = ({
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [animationKey, setAnimationKey] = useState(0);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
@@ -77,21 +78,27 @@ const SearchBox: React.FC<SearchBoxProps> = ({
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }, []);
// Fetch suggestions with debounce // Fetch suggestions immediately without debounce
useEffect(() => { useEffect(() => {
const timer = setTimeout(async () => { if (query.trim()) {
if (query.trim()) { fetchSuggestions(selectedEngine.name, query).then(results => {
const results = await fetchSuggestions(selectedEngine.name, query); // Only update if results are different to avoid unnecessary re-renders
setSuggestions(results.slice(0, 8)); setSuggestions(prev => {
const newSuggestions = results.slice(0, 8);
// Trigger animation only when content actually changes
if (JSON.stringify(prev) !== JSON.stringify(newSuggestions)) {
setAnimationKey(k => k + 1);
}
return newSuggestions;
});
setShowSuggestions(true); setShowSuggestions(true);
} else { setSelectedIndex(-1);
setSuggestions([]); });
setShowSuggestions(false); } else {
} setSuggestions([]);
setShowSuggestions(false);
setSelectedIndex(-1); setSelectedIndex(-1);
}, 200); }
return () => clearTimeout(timer);
}, [query, selectedEngine.name]); }, [query, selectedEngine.name]);
const performSearch = useCallback((text: string) => { const performSearch = useCallback((text: string) => {
@@ -382,20 +389,48 @@ const SearchBox: React.FC<SearchBoxProps> = ({
{/* Suggestions / History Dropdown */} {/* Suggestions / History Dropdown */}
{((showSuggestions && suggestions.length > 0) || (showHistoryDropdown && history.length > 0)) && ( {((showSuggestions && suggestions.length > 0) || (showHistoryDropdown && history.length > 0)) && (
<div className=" <div
absolute top-full left-0 right-0 mt-2 key={animationKey}
bg-black/80 backdrop-blur-3xl className="
border border-white/10 absolute top-full left-0 right-0 mt-2
rounded-2xl shadow-[0_30px_60px_-12px_rgba(0,0,0,0.8)] bg-black/20 backdrop-blur-2xl
animate-in fade-in slide-in-from-top-2 duration-300 border border-white/10
overflow-hidden z-40 rounded-2xl shadow-[0_30px_60px_-12px_rgba(0,0,0,0.8)]
"> overflow-hidden z-40
origin-top
"
style={{
animation: 'dropdownSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
}}
>
<style>{`
@keyframes dropdownSlideIn {
from {
opacity: 0;
transform: translateY(-8px) scaleY(0.95);
}
to {
opacity: 1;
transform: translateY(0) scaleY(1);
}
}
@keyframes itemFadeIn {
from {
opacity: 0;
transform: translateX(-4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`}</style>
{/* Suggestions List */} {/* Suggestions List */}
{showSuggestions && suggestions.length > 0 && ( {showSuggestions && suggestions.length > 0 && (
<div className="flex flex-col gap-0.5 p-2"> <div className="flex flex-col gap-0.5 p-2">
{suggestions.map((suggestion, index) => ( {suggestions.map((suggestion, index) => (
<button <button
key={index} key={`${animationKey}-${index}`}
// Prevent input blur when clicking // Prevent input blur when clicking
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => handleSuggestionClick(suggestion)} onClick={() => handleSuggestionClick(suggestion)}
@@ -406,6 +441,10 @@ const SearchBox: React.FC<SearchBoxProps> = ({
? 'bg-white/10 text-white shadow-sm translate-x-1' ? 'bg-white/10 text-white shadow-sm translate-x-1'
: 'text-white/70 hover:bg-white/5 hover:text-white'} : 'text-white/70 hover:bg-white/5 hover:text-white'}
`} `}
style={{
animation: `itemFadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards ${index * 0.03}s`,
opacity: 0
}}
> >
<SearchIcon <SearchIcon
className={`w-4 h-4 transition-all duration-300`} className={`w-4 h-4 transition-all duration-300`}
@@ -428,7 +467,7 @@ const SearchBox: React.FC<SearchBoxProps> = ({
<div className="flex flex-col gap-0.5 px-2"> <div className="flex flex-col gap-0.5 px-2">
{history.map((item, index) => ( {history.map((item, index) => (
<button <button
key={item + index} key={`history-${index}`}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => handleSuggestionClick(item)} onClick={() => handleSuggestionClick(item)}
onContextMenu={handleElementContextMenu} onContextMenu={handleElementContextMenu}
@@ -438,6 +477,10 @@ const SearchBox: React.FC<SearchBoxProps> = ({
? 'bg-white/10 text-white shadow-sm translate-x-1' ? 'bg-white/10 text-white shadow-sm translate-x-1'
: 'text-white/60 hover:bg-white/5 hover:text-white'} : 'text-white/60 hover:bg-white/5 hover:text-white'}
`} `}
style={{
animation: `itemFadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards ${index * 0.03}s`,
opacity: 0
}}
> >
<HistoryIcon <HistoryIcon
className={`w-4 h-4 transition-all duration-300`} className={`w-4 h-4 transition-all duration-300`}

View File

@@ -84,11 +84,11 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
const isAdding = !editingOriginalName; const isAdding = !editingOriginalName;
if (isAdding || isRenaming) { if (isAdding || isRenaming) {
const exists = settings.searchEngines.some(e => e.name === name); const exists = settings.searchEngines.some(e => e.name === name);
if (exists) { if (exists) {
showToast(t.duplicateEngineName, 'error'); showToast(t.duplicateEngineName, 'error');
return; return;
} }
} }
const newEngine: SearchEngine = { const newEngine: SearchEngine = {
@@ -101,21 +101,21 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
let updatedSelected = settings.selectedEngine; let updatedSelected = settings.selectedEngine;
if (editingOriginalName) { if (editingOriginalName) {
// Update existing // Update existing
const index = updatedEngines.findIndex(e => e.name === editingOriginalName); const index = updatedEngines.findIndex(e => e.name === editingOriginalName);
if (index !== -1) { if (index !== -1) {
updatedEngines[index] = newEngine; updatedEngines[index] = newEngine;
} }
// If the edited engine was the selected one, update the selection reference // If the edited engine was the selected one, update the selection reference
if (settings.selectedEngine === editingOriginalName) { if (settings.selectedEngine === editingOriginalName) {
updatedSelected = newEngine.name; updatedSelected = newEngine.name;
} }
showToast(t.searchEngineUpdated, 'success'); showToast(t.searchEngineUpdated, 'success');
} else { } else {
// Add new // Add new
updatedEngines.push(newEngine); updatedEngines.push(newEngine);
showToast(t.newSearchEngineAdded, 'success'); showToast(t.newSearchEngineAdded, 'success');
} }
onUpdateSettings({ onUpdateSettings({
@@ -140,17 +140,17 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
}, []); }, []);
return ( return (
<div className="space-y-4"> <div className="space-y-5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white/40 uppercase tracking-wider">{t.searchEngines}</h3> <span className="text-xs font-semibold text-white/50 uppercase tracking-wider">{t.searchEngines}</span>
{!isAddingEngine && ( {!isAddingEngine && (
<button <button
onClick={() => { onClick={() => {
setEditingOriginalName(null); setEditingOriginalName(null);
setNewEngineName(''); setNewEngineName('');
setNewEngineUrl(''); setNewEngineUrl('');
setNewEngineIcon(''); setNewEngineIcon('');
setIsAddingEngine(true); setIsAddingEngine(true);
}} }}
className="p-1.5 rounded-full hover:bg-white/10 transition-colors" className="p-1.5 rounded-full hover:bg-white/10 transition-colors"
style={{ color: settings.themeColor }} style={{ color: settings.themeColor }}
@@ -171,7 +171,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
onDragOver={(e) => handleDragOver(e, index)} onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
className={` className={`
flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5 group transition-all duration-200 flex items-center justify-between p-2.5 rounded-lg bg-white/5 border border-white/5 group transition-all duration-200
${draggedIndex === index ? 'opacity-40 border-dashed border-white/30' : ''} ${draggedIndex === index ? 'opacity-40 border-dashed border-white/30' : ''}
${editingOriginalName === engine.name ? 'ring-1 ring-white/30 bg-white/10' : ''} ${editingOriginalName === engine.name ? 'ring-1 ring-white/30 bg-white/10' : ''}
`} `}
@@ -184,7 +184,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
<div className="flex flex-col overflow-hidden"> <div className="flex flex-col overflow-hidden">
<span className="text-sm font-medium text-white/90 truncate">{engine.name}</span> <span className="text-sm font-medium text-white/90 truncate">{engine.name}</span>
<span className="text-xs text-white/40 truncate font-mono">{engine.urlPattern}</span> <span className="text-[10px] text-white/40 truncate font-mono">{engine.urlPattern}</span>
</div> </div>
</div> </div>
@@ -236,7 +236,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
<div className="mt-4 p-4 rounded-xl bg-white/5 border border-white/10 space-y-3 animate-in fade-in slide-in-from-top-2"> <div className="mt-4 p-4 rounded-xl bg-white/5 border border-white/10 space-y-3 animate-in fade-in slide-in-from-top-2">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-white/80"> <span className="text-sm font-semibold text-white/80">
{editingOriginalName ? t.editSearchEngine : t.addCustomEngine} {editingOriginalName ? t.editSearchEngine : t.addCustomEngine}
</span> </span>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -246,7 +246,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
value={newEngineName} value={newEngineName}
onChange={(e) => setNewEngineName(e.target.value)} onChange={(e) => setNewEngineName(e.target.value)}
placeholder="e.g.: Google" placeholder="e.g.: Google"
className="w-full bg-black/20 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none transition-colors" className="w-full bg-black/20 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-white/30 focus:outline-none transition-colors"
autoFocus autoFocus
/> />
</div> </div>
@@ -257,7 +257,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
value={newEngineUrl} value={newEngineUrl}
onChange={(e) => setNewEngineUrl(e.target.value)} onChange={(e) => setNewEngineUrl(e.target.value)}
placeholder="https://example.com/search?q=" placeholder="https://example.com/search?q="
className="w-full bg-black/20 border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none transition-colors font-mono" className="w-full bg-black/20 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-white/30 focus:outline-none transition-colors font-mono"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">

View File

@@ -0,0 +1,66 @@
import React, { useRef, useEffect } from 'react';
import { SettingsIcon, ImageIcon, SearchIcon } from './Icons';
import { SettingsSection } from '../types';
import { useTranslation } from '../i18n';
interface SettingsMenuProps {
isOpen: boolean;
onClose: () => void;
onSelectSection: (section: SettingsSection) => void;
}
const SettingsMenu: React.FC<SettingsMenuProps> = ({ isOpen, onClose, onSelectSection }) => {
const { t } = useTranslation();
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
onClose();
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const menuItems: { id: SettingsSection; label: string; icon: React.ReactNode }[] = [
{ id: 'general', label: t.appearance, icon: <SettingsIcon className="w-3.5 h-3.5" /> },
{ id: 'wallpaper', label: t.wallpaperSettings, icon: <ImageIcon className="w-3.5 h-3.5" /> },
{ id: 'search', label: t.searchEngines, icon: <SearchIcon className="w-3.5 h-3.5" /> },
];
return (
<div
ref={menuRef}
className="absolute top-full right-0 mt-2 w-40 rounded-xl bg-[#1a1a1a]/90 backdrop-blur-xl border border-white/10 shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200 origin-top-right z-50"
>
<div className="p-1">
{menuItems.map((item) => (
<button
key={item.id}
onClick={(e) => {
e.stopPropagation();
onSelectSection(item.id);
}}
className="w-full flex items-center gap-2.5 px-2.5 py-2 text-xs text-white/80 hover:text-white hover:bg-white/10 rounded-lg transition-colors group"
>
<span className="text-white/60 group-hover:text-white transition-colors">
{item.icon}
</span>
{item.label}
</button>
))}
</div>
</div>
);
};
export default SettingsMenu;

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { XIcon } from './Icons'; import { XIcon } from './Icons';
import { UserSettings } from '../types'; import { UserSettings, SettingsSection } from '../types';
import ThemeSettings from './ThemeSettings'; import ThemeSettings from './ThemeSettings';
import WallpaperManager from './WallpaperManager'; import WallpaperManager from './WallpaperManager';
import SearchEngineManager from './SearchEngineManager'; import SearchEngineManager from './SearchEngineManager';
@@ -12,9 +11,10 @@ interface SettingsModalProps {
onClose: () => void; onClose: () => void;
settings: UserSettings; settings: UserSettings;
onUpdateSettings: (newSettings: UserSettings) => void; onUpdateSettings: (newSettings: UserSettings) => void;
section: SettingsSection;
} }
const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings, onUpdateSettings }) => { const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings, onUpdateSettings, section }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (!isOpen) return null; if (!isOpen) return null;
@@ -25,6 +25,19 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
e.stopPropagation(); e.stopPropagation();
}; };
const getTitle = () => {
switch (section) {
case 'general':
return t.appearance;
case 'wallpaper':
return t.wallpaperSettings;
case 'search':
return t.searchEngines;
default:
return t.settings;
}
};
return ( return (
<div <div
className="fixed inset-0 z-50 flex items-center justify-center p-4" className="fixed inset-0 z-50 flex items-center justify-center p-4"
@@ -38,7 +51,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
{/* Modal content */} {/* Modal content */}
<div className=" <div className="
relative w-full max-w-md rounded-3xl relative w-full max-w-md rounded-2xl
bg-[#1a1a1a]/90 backdrop-blur-xl bg-[#1a1a1a]/90 backdrop-blur-xl
border border-white/10 border border-white/10
shadow-[0_20px_50px_rgba(0,0,0,0.5)] shadow-[0_20px_50px_rgba(0,0,0,0.5)]
@@ -46,35 +59,30 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
max-h-[85vh] flex flex-col overflow-hidden max-h-[85vh] flex flex-col overflow-hidden
"> ">
{/* Header - fixed */} {/* Header - fixed */}
<div className="flex items-center justify-between px-6 pt-6 pb-4 shrink-0 z-10"> <div className="flex items-center justify-between px-5 pt-5 pb-3 shrink-0 z-10">
<h2 className="text-2xl font-light tracking-wide">{t.settings}</h2> <h2 className="text-lg font-medium tracking-wide text-white/90">{getTitle()}</h2>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 rounded-full hover:bg-white/10 transition-colors text-white/70 hover:text-white" className="p-1.5 rounded-full hover:bg-white/10 transition-colors text-white/60 hover:text-white"
> >
<XIcon className="w-6 h-6" /> <XIcon className="w-5 h-5" />
</button> </button>
</div> </div>
{/* Scrollable area */} {/* Scrollable area */}
<div className="flex-1 overflow-y-auto px-6 pb-6 custom-scrollbar"> <div className="flex-1 overflow-y-auto px-6 pb-6 custom-scrollbar">
<div className="space-y-8"> <div className="space-y-8">
{section === 'general' && (
{/* Appearance settings section */}
<div className="space-y-6">
<h3 className="text-sm font-semibold text-white/40 uppercase tracking-wider">{t.appearance}</h3>
{/* Theme settings component */}
<ThemeSettings settings={settings} onUpdateSettings={onUpdateSettings} /> <ThemeSettings settings={settings} onUpdateSettings={onUpdateSettings} />
)}
{/* Wallpaper manager component */} {section === 'wallpaper' && (
<WallpaperManager settings={settings} onUpdateSettings={onUpdateSettings} /> <WallpaperManager settings={settings} onUpdateSettings={onUpdateSettings} />
</div> )}
<div className="h-[1px] bg-white/10 w-full" /> {section === 'search' && (
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
{/* Search engine manager component */} )}
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -37,6 +37,10 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
onUpdateSettings({ ...settings, searchOpacity: parseFloat(e.target.value) }); onUpdateSettings({ ...settings, searchOpacity: parseFloat(e.target.value) });
}; };
const handleMaskOpacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onUpdateSettings({ ...settings, maskOpacity: parseFloat(e.target.value) });
};
const handleThemeChange = (colorHex: string) => { const handleThemeChange = (colorHex: string) => {
onUpdateSettings({ ...settings, themeColor: colorHex }); onUpdateSettings({ ...settings, themeColor: colorHex });
}; };
@@ -46,15 +50,15 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-5">
{/* Language Selection */} {/* Language Selection */}
<div className="space-y-3"> <div className="space-y-4">
<span className="text-white/80 font-light block">{t.language}</span> <span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.language}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => handleLanguageChange('en')} onClick={() => handleLanguageChange('en')}
className={` className={`
flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 flex-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200
${settings.language === 'en' ${settings.language === 'en'
? 'bg-white text-black' ? 'bg-white text-black'
: 'bg-white/5 text-white/60 hover:bg-white/10 hover:text-white'} : 'bg-white/5 text-white/60 hover:bg-white/10 hover:text-white'}
@@ -65,7 +69,7 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
<button <button
onClick={() => handleLanguageChange('zh')} onClick={() => handleLanguageChange('zh')}
className={` className={`
flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 flex-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200
${settings.language === 'zh' ${settings.language === 'zh'
? 'bg-white text-black' ? 'bg-white text-black'
: 'bg-white/5 text-white/60 hover:bg-white/10 hover:text-white'} : 'bg-white/5 text-white/60 hover:bg-white/10 hover:text-white'}
@@ -77,15 +81,15 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
</div> </div>
{/* Theme color */} {/* Theme color */}
<div className="space-y-3"> <div className="space-y-4">
<span className="text-white/80 font-light block">{t.themeColor}</span> <span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.themeColor}</span>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{THEMES.map((theme) => ( {THEMES.map((theme) => (
<button <button
key={theme.hex} key={theme.hex}
onClick={() => handleThemeChange(theme.hex)} onClick={() => handleThemeChange(theme.hex)}
className={` className={`
w-8 h-8 rounded-full flex items-center justify-center transition-all duration-300 w-6 h-6 rounded-full flex items-center justify-center transition-all duration-300
${settings.themeColor === theme.hex ? 'ring-2 ring-white scale-110' : 'hover:scale-110 opacity-80 hover:opacity-100'} ${settings.themeColor === theme.hex ? 'ring-2 ring-white scale-110' : 'hover:scale-110 opacity-80 hover:opacity-100'}
`} `}
style={{ backgroundColor: theme.hex }} style={{ backgroundColor: theme.hex }}
@@ -100,55 +104,55 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
</div> </div>
{/* Toggle settings */} {/* Toggle settings */}
<div className="space-y-4"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-white/80 font-light">{t.showSeconds}</span> <span className="text-sm text-white/70">{t.showSeconds}</span>
<button <button
onClick={toggleSeconds} onClick={toggleSeconds}
className="w-12 h-6 rounded-full transition-colors duration-300 relative bg-white/10" className="w-10 h-5 rounded-full transition-colors duration-300 relative bg-white/10"
style={{ backgroundColor: settings.showSeconds ? settings.themeColor : undefined }} style={{ backgroundColor: settings.showSeconds ? settings.themeColor : undefined }}
> >
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.showSeconds ? 'left-7' : 'left-1'}`} /> <div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.showSeconds ? 'left-6' : 'left-1'}`} />
</button> </button>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-white/80 font-light">{t.use24HourFormat}</span> <span className="text-sm text-white/70">{t.use24HourFormat}</span>
<button <button
onClick={toggle24Hour} onClick={toggle24Hour}
className="w-12 h-6 rounded-full transition-colors duration-300 relative bg-white/10" className="w-10 h-5 rounded-full transition-colors duration-300 relative bg-white/10"
style={{ backgroundColor: settings.use24HourFormat ? settings.themeColor : undefined }} style={{ backgroundColor: settings.use24HourFormat ? settings.themeColor : undefined }}
> >
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.use24HourFormat ? 'left-7' : 'left-1'}`} /> <div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.use24HourFormat ? 'left-6' : 'left-1'}`} />
</button> </button>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-white/80 font-light">{t.maskBlurEffect}</span> <span className="text-sm text-white/70">{t.maskBlurEffect}</span>
<button <button
onClick={toggleMaskBlur} onClick={toggleMaskBlur}
className="w-12 h-6 rounded-full transition-colors duration-300 relative bg-white/10" className="w-10 h-5 rounded-full transition-colors duration-300 relative bg-white/10"
style={{ backgroundColor: settings.enableMaskBlur ? settings.themeColor : undefined }} style={{ backgroundColor: settings.enableMaskBlur ? settings.themeColor : undefined }}
> >
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.enableMaskBlur ? 'left-7' : 'left-1'}`} /> <div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.enableMaskBlur ? 'left-6' : 'left-1'}`} />
</button> </button>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-white/80 font-light">{t.searchHistory}</span> <span className="text-sm text-white/70">{t.searchHistory}</span>
<button <button
onClick={toggleSearchHistory} onClick={toggleSearchHistory}
className="w-12 h-6 rounded-full transition-colors duration-300 relative bg-white/10" className="w-10 h-5 rounded-full transition-colors duration-300 relative bg-white/10"
style={{ backgroundColor: settings.enableSearchHistory ? settings.themeColor : undefined }} style={{ backgroundColor: settings.enableSearchHistory ? settings.themeColor : undefined }}
> >
<div className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.enableSearchHistory ? 'left-7' : 'left-1'}`} /> <div className={`absolute top-1 w-3 h-3 rounded-full bg-white transition-transform duration-300 shadow-md ${settings.enableSearchHistory ? 'left-6' : 'left-1'}`} />
</button> </button>
</div> </div>
</div> </div>
{/* Background blur slider */} {/* Background blur slider */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between text-sm text-white/60 font-light"> <div className="flex justify-between text-xs text-white/50 font-medium uppercase tracking-wider">
<span>{t.backgroundBlur}</span> <span>{t.backgroundBlur}</span>
<span>{settings.backgroundBlur}px</span> <span>{settings.backgroundBlur}px</span>
</div> </div>
@@ -165,7 +169,7 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
{/* Search box opacity slider */} {/* Search box opacity slider */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between text-sm text-white/60 font-light"> <div className="flex justify-between text-xs text-white/50 font-medium uppercase tracking-wider">
<span>{t.searchBoxOpacity}</span> <span>{t.searchBoxOpacity}</span>
<span>{Math.round(settings.searchOpacity * 100)}%</span> <span>{Math.round(settings.searchOpacity * 100)}%</span>
</div> </div>
@@ -179,6 +183,23 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
className="w-full h-1 bg-white/20 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-lg" className="w-full h-1 bg-white/20 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-lg"
/> />
</div> </div>
{/* Mask opacity slider */}
<div className="space-y-3">
<div className="flex justify-between text-xs text-white/50 font-medium uppercase tracking-wider">
<span>{t.maskOpacity}</span>
<span>{Math.round(settings.maskOpacity * 100)}%</span>
</div>
<input
type="range"
min="0"
max="1"
step="0.05"
value={settings.maskOpacity}
onChange={handleMaskOpacityChange}
className="w-full h-1 bg-white/20 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:shadow-lg"
/>
</div>
</div> </div>
); );
}; };

View File

@@ -180,8 +180,8 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
}, [settings.customWallpapers]); }, [settings.customWallpapers]);
return ( return (
<div className="space-y-3"> <div className="space-y-5">
<span className="text-white/80 font-light block">{t.wallpaperSettings}</span> <span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.wallpaperSettings}</span>
{/* Wallpaper grid */} {/* Wallpaper grid */}
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
@@ -231,9 +231,9 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-colors text-sm text-white/80 hover:text-white" className="flex-1 flex items-center justify-center gap-2 px-3 py-1.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-colors text-xs text-white/80 hover:text-white"
> >
<UploadIcon className="w-4 h-4" /> <UploadIcon className="w-3.5 h-3.5" />
<span>{t.uploadImageVideo}</span> <span>{t.uploadImageVideo}</span>
</button> </button>
<input <input
@@ -249,21 +249,21 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
<div className="flex gap-2"> <div className="flex gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40"> <div className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40">
<ImageIcon className="w-4 h-4" /> <ImageIcon className="w-3.5 h-3.5" />
</div> </div>
<input <input
type="text" type="text"
value={customUrl} value={customUrl}
onChange={(e) => setCustomUrl(e.target.value)} onChange={(e) => setCustomUrl(e.target.value)}
placeholder={t.enterImageVideoUrl} placeholder={t.enterImageVideoUrl}
className="w-full bg-black/20 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none transition-colors" className="w-full bg-black/20 border border-white/10 rounded-lg pl-8 pr-3 py-1.5 text-xs text-white focus:border-white/30 focus:outline-none transition-colors"
onKeyDown={(e) => e.key === 'Enter' && handleCustomUrlApply()} onKeyDown={(e) => e.key === 'Enter' && handleCustomUrlApply()}
/> />
</div> </div>
<button <button
onClick={handleCustomUrlApply} onClick={handleCustomUrlApply}
disabled={!customUrl.trim()} disabled={!customUrl.trim()}
className="px-3 py-2 bg-white text-black text-xs font-bold rounded-xl hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="px-3 py-1.5 bg-white text-black text-xs font-bold rounded-lg hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
{t.apply} {t.apply}
</button> </button>
@@ -278,9 +278,9 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
key={fit.value} key={fit.value}
onClick={() => onUpdateSettings({ ...settings, wallpaperFit: fit.value })} onClick={() => onUpdateSettings({ ...settings, wallpaperFit: fit.value })}
className={` className={`
flex-1 px-3 py-1.5 text-xs rounded-lg whitespace-nowrap transition-colors border flex-1 px-2.5 py-1 text-[10px] uppercase tracking-wide rounded-md whitespace-nowrap transition-colors border
${settings.wallpaperFit === fit.value || (!settings.wallpaperFit && fit.value === 'cover') ${settings.wallpaperFit === fit.value || (!settings.wallpaperFit && fit.value === 'cover')
? 'bg-white text-black font-medium border-white' ? 'bg-white text-black font-bold border-white'
: 'bg-transparent text-white/60 border-transparent hover:text-white hover:bg-white/10'} : 'bg-transparent text-white/60 border-transparent hover:text-white hover:bg-white/10'}
`} `}
> >

View File

@@ -14,6 +14,7 @@ export const en: Translation = {
searchHistory: 'Search History', searchHistory: 'Search History',
backgroundBlur: 'Background Blur', backgroundBlur: 'Background Blur',
searchBoxOpacity: 'Search Box Opacity', searchBoxOpacity: 'Search Box Opacity',
maskOpacity: 'Mask Opacity',
// Wallpaper Settings // Wallpaper Settings
wallpaperSettings: 'Wallpaper Settings', wallpaperSettings: 'Wallpaper Settings',

View File

@@ -14,6 +14,7 @@ export const zh: Translation = {
searchHistory: '搜索历史记录', searchHistory: '搜索历史记录',
backgroundBlur: '背景模糊度', backgroundBlur: '背景模糊度',
searchBoxOpacity: '搜索框不透明度', searchBoxOpacity: '搜索框不透明度',
maskOpacity: '遮罩层不透明度',
// Wallpaper Settings // Wallpaper Settings
wallpaperSettings: '壁纸设置', wallpaperSettings: '壁纸设置',

View File

@@ -14,6 +14,7 @@ export interface Translation {
searchHistory: string; searchHistory: string;
backgroundBlur: string; backgroundBlur: string;
searchBoxOpacity: string; searchBoxOpacity: string;
maskOpacity: string;
// Wallpaper Settings // Wallpaper Settings
wallpaperSettings: string; wallpaperSettings: string;

View File

@@ -30,6 +30,7 @@ export interface UserSettings {
themeColor: string; themeColor: string;
searchOpacity: number; searchOpacity: number;
enableMaskBlur: boolean; enableMaskBlur: boolean;
maskOpacity: number;
backgroundUrl: string; backgroundUrl: string;
backgroundType: BackgroundType; backgroundType: BackgroundType;
wallpaperFit: WallpaperFit; wallpaperFit: WallpaperFit;
@@ -38,3 +39,5 @@ export interface UserSettings {
searchHistory: string[]; searchHistory: string[];
language: Language; language: Language;
} }
export type SettingsSection = 'general' | 'wallpaper' | 'search';

View File

@@ -24,11 +24,7 @@ export const fetchSuggestions = (engine: string, query: string): Promise<string[
// Bilibili uses fetch (via Vite proxy in dev, Vercel Function in production) // Bilibili uses fetch (via Vite proxy in dev, Vercel Function in production)
if (engine === 'Bilibili') { if (engine === 'Bilibili') {
// In production (Vercel), use /api/bilibili; in development, use /bilibili (Vite proxy) const url = `/api/bilibili?term=${encodeURIComponent(query)}`;
const isDev = import.meta.env.DEV;
const url = isDev
? `/bilibili?term=${encodeURIComponent(query)}`
: `/api/bilibili?term=${encodeURIComponent(query)}`;
fetch(url) fetch(url)
.then(response => response.json()) .then(response => response.json())

View File

@@ -7,10 +7,10 @@ export default defineConfig({
port: 3000, port: 3000,
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/bilibili': { '/api/bilibili': {
target: 'https://s.search.bilibili.com', target: 'https://s.search.bilibili.com',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/bilibili/, '/main/suggest'), rewrite: (path) => path.replace(/^\/api\/bilibili/, '/main/suggest'),
secure: false, secure: false,
} }
} }