mirror of
https://github.com/handsomezhuzhu/AeroStart.git
synced 2026-02-20 20:10:15 +00:00
✨ 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:
38
src/App.tsx
38
src/App.tsx
@@ -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}>
|
||||||
@@ -195,10 +205,15 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay to ensure text readability */}
|
{/* Overlay to ensure text readability */}
|
||||||
<div className={`
|
<div
|
||||||
absolute inset-0 bg-black/40
|
className={`
|
||||||
|
absolute inset-0 transition-opacity duration-500
|
||||||
${settings.enableMaskBlur ? 'backdrop-blur-sm' : ''}
|
${settings.enableMaskBlur ? 'backdrop-blur-sm' : ''}
|
||||||
`} />
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: `rgba(0, 0, 0, ${settings.maskOpacity})`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Main Content Area (Search View)
|
Main Content Area (Search View)
|
||||||
@@ -262,17 +277,23 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Top Right Settings Button - Only visible in Dashboard */}
|
{/* Top Right Settings Button - Only visible in Dashboard */}
|
||||||
<div className="absolute top-6 right-6">
|
<div className="absolute top-6 right-6 z-50">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // Prevent clicking dashboard background
|
e.stopPropagation(); // Prevent clicking dashboard background
|
||||||
setIsSettingsOpen(true);
|
setIsSettingsMenuOpen(!isSettingsMenuOpen);
|
||||||
}}
|
}}
|
||||||
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"
|
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"
|
aria-label="Settings"
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-6 h-6 text-white/70 group-hover:text-white group-hover:rotate-90 transition-all duration-500" />
|
<SettingsIcon className={`w-5 h-5 text-white/70 group-hover:text-white transition-all duration-500 ${isSettingsMenuOpen ? 'rotate-90 text-white' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<SettingsMenu
|
||||||
|
isOpen={isSettingsMenuOpen}
|
||||||
|
onClose={() => setIsSettingsMenuOpen(false)}
|
||||||
|
onSelectSection={handleSettingsMenuSelect}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -283,6 +304,7 @@ const App: React.FC = () => {
|
|||||||
onClose={() => setIsSettingsOpen(false)}
|
onClose={() => setIsSettingsOpen(false)}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onUpdateSettings={setSettings}
|
onUpdateSettings={setSettings}
|
||||||
|
section={activeSettingsSection}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()) {
|
||||||
const results = await fetchSuggestions(selectedEngine.name, query);
|
fetchSuggestions(selectedEngine.name, query).then(results => {
|
||||||
setSuggestions(results.slice(0, 8));
|
// Only update if results are different to avoid unnecessary re-renders
|
||||||
|
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);
|
||||||
|
setSelectedIndex(-1);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setShowSuggestions(false);
|
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
|
||||||
|
key={animationKey}
|
||||||
|
className="
|
||||||
absolute top-full left-0 right-0 mt-2
|
absolute top-full left-0 right-0 mt-2
|
||||||
bg-black/80 backdrop-blur-3xl
|
bg-black/20 backdrop-blur-2xl
|
||||||
border border-white/10
|
border border-white/10
|
||||||
rounded-2xl shadow-[0_30px_60px_-12px_rgba(0,0,0,0.8)]
|
rounded-2xl shadow-[0_30px_60px_-12px_rgba(0,0,0,0.8)]
|
||||||
animate-in fade-in slide-in-from-top-2 duration-300
|
|
||||||
overflow-hidden z-40
|
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`}
|
||||||
|
|||||||
@@ -140,9 +140,9 @@ 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={() => {
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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">
|
||||||
|
|||||||
66
src/components/SettingsMenu.tsx
Normal file
66
src/components/SettingsMenu.tsx
Normal 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;
|
||||||
@@ -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' && (
|
||||||
|
|
||||||
{/* Search engine manager component */}
|
|
||||||
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
|
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const zh: Translation = {
|
|||||||
searchHistory: '搜索历史记录',
|
searchHistory: '搜索历史记录',
|
||||||
backgroundBlur: '背景模糊度',
|
backgroundBlur: '背景模糊度',
|
||||||
searchBoxOpacity: '搜索框不透明度',
|
searchBoxOpacity: '搜索框不透明度',
|
||||||
|
maskOpacity: '遮罩层不透明度',
|
||||||
|
|
||||||
// Wallpaper Settings
|
// Wallpaper Settings
|
||||||
wallpaperSettings: '壁纸设置',
|
wallpaperSettings: '壁纸设置',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user