mirror of
https://github.com/handsomezhuzhu/AeroStart.git
synced 2026-02-20 12:00: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:
216
src/App.tsx
216
src/App.tsx
@@ -3,10 +3,11 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import Clock from './components/Clock';
|
||||
import SearchBox from './components/SearchBox';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
import SettingsMenu from './components/SettingsMenu';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import GlobalContextMenu from './components/GlobalContextMenu';
|
||||
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 { loadSettings, saveSettings } from './utils/storage';
|
||||
import { I18nProvider } from './i18n';
|
||||
@@ -21,6 +22,7 @@ const DEFAULT_SETTINGS: UserSettings = {
|
||||
themeColor: THEMES[0].hex,
|
||||
searchOpacity: 0.8,
|
||||
enableMaskBlur: false,
|
||||
maskOpacity: 0.2,
|
||||
backgroundUrl: PRESET_WALLPAPERS[0].url,
|
||||
backgroundType: PRESET_WALLPAPERS[0].type,
|
||||
wallpaperFit: 'cover',
|
||||
@@ -35,6 +37,8 @@ type ViewMode = 'search' | 'dashboard';
|
||||
const App: React.FC = () => {
|
||||
// State for settings visibility
|
||||
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)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('search');
|
||||
@@ -157,6 +161,12 @@ const App: React.FC = () => {
|
||||
setSettings(prev => ({ ...prev, language: lang }));
|
||||
};
|
||||
|
||||
const handleSettingsMenuSelect = (section: SettingsSection) => {
|
||||
setActiveSettingsSection(section);
|
||||
setIsSettingsMenuOpen(false);
|
||||
setIsSettingsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<I18nProvider language={settings.language} onLanguageChange={handleLanguageChange}>
|
||||
@@ -168,123 +178,135 @@ const App: React.FC = () => {
|
||||
{/* Context Menu Global Listener */}
|
||||
<GlobalContextMenu />
|
||||
|
||||
{/* Background Layer */}
|
||||
<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'}`}
|
||||
style={{
|
||||
filter: `blur(${isSearchActive ? settings.backgroundBlur : 0}px)`,
|
||||
transform: isSearchActive ? 'scale(1.05)' : 'scale(1)',
|
||||
}}
|
||||
>
|
||||
{settings.backgroundType === 'video' ? (
|
||||
<video
|
||||
key={settings.backgroundUrl}
|
||||
className={`absolute inset-0 w-full h-full ${getVideoClass(settings.wallpaperFit)}`}
|
||||
src={settings.backgroundUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full"
|
||||
style={getBackgroundStyle(settings.wallpaperFit)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Background Layer */}
|
||||
<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'}`}
|
||||
style={{
|
||||
filter: `blur(${isSearchActive ? settings.backgroundBlur : 0}px)`,
|
||||
transform: isSearchActive ? 'scale(1.05)' : 'scale(1)',
|
||||
}}
|
||||
>
|
||||
{settings.backgroundType === 'video' ? (
|
||||
<video
|
||||
key={settings.backgroundUrl}
|
||||
className={`absolute inset-0 w-full h-full ${getVideoClass(settings.wallpaperFit)}`}
|
||||
src={settings.backgroundUrl}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full"
|
||||
style={getBackgroundStyle(settings.wallpaperFit)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Overlay to ensure text readability */}
|
||||
<div className={`
|
||||
absolute inset-0 bg-black/40
|
||||
${settings.enableMaskBlur ? 'backdrop-blur-sm' : ''}
|
||||
`} />
|
||||
{/* Overlay to ensure text readability */}
|
||||
<div
|
||||
className={`
|
||||
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)
|
||||
Pointer events are disabled when not visible to prevent interaction with hidden elements
|
||||
*/}
|
||||
<div
|
||||
className={`
|
||||
<div
|
||||
className={`
|
||||
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
|
||||
${viewMode === 'search' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'}
|
||||
`}
|
||||
>
|
||||
{/* Clock Component */}
|
||||
<ErrorBoundary>
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-150">
|
||||
<Clock
|
||||
showSeconds={settings.showSeconds}
|
||||
use24HourFormat={settings.use24HourFormat}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
>
|
||||
{/* Clock Component */}
|
||||
<ErrorBoundary>
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 delay-150">
|
||||
<Clock
|
||||
showSeconds={settings.showSeconds}
|
||||
use24HourFormat={settings.use24HourFormat}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Search Input Component */}
|
||||
<ErrorBoundary>
|
||||
<div className="w-full max-w-xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300">
|
||||
<SearchBox
|
||||
engines={settings.searchEngines}
|
||||
selectedEngineName={settings.selectedEngine}
|
||||
onSelectEngine={handleSelectEngine}
|
||||
themeColor={settings.themeColor}
|
||||
opacity={settings.searchOpacity}
|
||||
onInteractionChange={setIsSearchActive}
|
||||
enableHistory={settings.enableSearchHistory}
|
||||
history={settings.searchHistory}
|
||||
onUpdateHistory={handleUpdateHistory}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{/* Search Input Component */}
|
||||
<ErrorBoundary>
|
||||
<div className="w-full max-w-xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300">
|
||||
<SearchBox
|
||||
engines={settings.searchEngines}
|
||||
selectedEngineName={settings.selectedEngine}
|
||||
onSelectEngine={handleSelectEngine}
|
||||
themeColor={settings.themeColor}
|
||||
opacity={settings.searchOpacity}
|
||||
onInteractionChange={setIsSearchActive}
|
||||
enableHistory={settings.enableSearchHistory}
|
||||
history={settings.searchHistory}
|
||||
onUpdateHistory={handleUpdateHistory}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
{/*
|
||||
Dashboard Panel (Under Development)
|
||||
Visible only in dashboard mode
|
||||
*/}
|
||||
<div
|
||||
className={`
|
||||
<div
|
||||
className={`
|
||||
absolute inset-0 z-10 flex flex-col items-center justify-center
|
||||
transition-all duration-500 ease-in-out
|
||||
${viewMode === 'dashboard' ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-105 pointer-events-none'}
|
||||
`}
|
||||
>
|
||||
<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">
|
||||
DASHBOARD
|
||||
</h1>
|
||||
<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">
|
||||
Under Construction
|
||||
</span>
|
||||
>
|
||||
<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">
|
||||
DASHBOARD
|
||||
</h1>
|
||||
<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">
|
||||
Under Construction
|
||||
</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>
|
||||
|
||||
{/* Top Right Settings Button - Only visible in Dashboard */}
|
||||
<div className="absolute top-6 right-6">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent clicking dashboard background
|
||||
setIsSettingsOpen(true);
|
||||
}}
|
||||
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"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<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>
|
||||
{/* Settings Modal */}
|
||||
<ErrorBoundary>
|
||||
<SettingsModal
|
||||
isOpen={isSettingsOpen}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
settings={settings}
|
||||
onUpdateSettings={setSettings}
|
||||
section={activeSettingsSection}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -190,7 +190,7 @@ const GlobalContextMenu: React.FC = () => {
|
||||
<div
|
||||
ref={menuRef}
|
||||
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
|
||||
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
|
||||
@@ -200,39 +200,39 @@ const GlobalContextMenu: React.FC = () => {
|
||||
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
|
||||
onClick={handleCopy}
|
||||
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
|
||||
"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCut}
|
||||
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
|
||||
"
|
||||
>
|
||||
<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>
|
||||
</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
|
||||
onClick={handlePaste}
|
||||
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
|
||||
"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ const SearchBox: React.FC<SearchBoxProps> = ({
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [animationKey, setAnimationKey] = useState(0);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -77,21 +78,27 @@ const SearchBox: React.FC<SearchBoxProps> = ({
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Fetch suggestions with debounce
|
||||
// Fetch suggestions immediately without debounce
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
if (query.trim()) {
|
||||
const results = await fetchSuggestions(selectedEngine.name, query);
|
||||
setSuggestions(results.slice(0, 8));
|
||||
if (query.trim()) {
|
||||
fetchSuggestions(selectedEngine.name, query).then(results => {
|
||||
// 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);
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
setSelectedIndex(-1);
|
||||
});
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
setSelectedIndex(-1);
|
||||
}, 200);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [query, selectedEngine.name]);
|
||||
|
||||
const performSearch = useCallback((text: string) => {
|
||||
@@ -382,20 +389,48 @@ const SearchBox: React.FC<SearchBoxProps> = ({
|
||||
|
||||
{/* Suggestions / History Dropdown */}
|
||||
{((showSuggestions && suggestions.length > 0) || (showHistoryDropdown && history.length > 0)) && (
|
||||
<div className="
|
||||
absolute top-full left-0 right-0 mt-2
|
||||
bg-black/80 backdrop-blur-3xl
|
||||
border border-white/10
|
||||
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
|
||||
">
|
||||
<div
|
||||
key={animationKey}
|
||||
className="
|
||||
absolute top-full left-0 right-0 mt-2
|
||||
bg-black/20 backdrop-blur-2xl
|
||||
border border-white/10
|
||||
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 */}
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div className="flex flex-col gap-0.5 p-2">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
key={`${animationKey}-${index}`}
|
||||
// Prevent input blur when clicking
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
@@ -406,6 +441,10 @@ const SearchBox: React.FC<SearchBoxProps> = ({
|
||||
? 'bg-white/10 text-white shadow-sm translate-x-1'
|
||||
: '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
|
||||
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">
|
||||
{history.map((item, index) => (
|
||||
<button
|
||||
key={item + index}
|
||||
key={`history-${index}`}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => handleSuggestionClick(item)}
|
||||
onContextMenu={handleElementContextMenu}
|
||||
@@ -438,6 +477,10 @@ const SearchBox: React.FC<SearchBoxProps> = ({
|
||||
? 'bg-white/10 text-white shadow-sm translate-x-1'
|
||||
: '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
|
||||
className={`w-4 h-4 transition-all duration-300`}
|
||||
|
||||
@@ -84,11 +84,11 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
const isAdding = !editingOriginalName;
|
||||
|
||||
if (isAdding || isRenaming) {
|
||||
const exists = settings.searchEngines.some(e => e.name === name);
|
||||
if (exists) {
|
||||
showToast(t.duplicateEngineName, 'error');
|
||||
return;
|
||||
}
|
||||
const exists = settings.searchEngines.some(e => e.name === name);
|
||||
if (exists) {
|
||||
showToast(t.duplicateEngineName, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newEngine: SearchEngine = {
|
||||
@@ -101,21 +101,21 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
let updatedSelected = settings.selectedEngine;
|
||||
|
||||
if (editingOriginalName) {
|
||||
// Update existing
|
||||
const index = updatedEngines.findIndex(e => e.name === editingOriginalName);
|
||||
if (index !== -1) {
|
||||
updatedEngines[index] = newEngine;
|
||||
}
|
||||
// Update existing
|
||||
const index = updatedEngines.findIndex(e => e.name === editingOriginalName);
|
||||
if (index !== -1) {
|
||||
updatedEngines[index] = newEngine;
|
||||
}
|
||||
|
||||
// If the edited engine was the selected one, update the selection reference
|
||||
if (settings.selectedEngine === editingOriginalName) {
|
||||
updatedSelected = newEngine.name;
|
||||
}
|
||||
showToast(t.searchEngineUpdated, 'success');
|
||||
// If the edited engine was the selected one, update the selection reference
|
||||
if (settings.selectedEngine === editingOriginalName) {
|
||||
updatedSelected = newEngine.name;
|
||||
}
|
||||
showToast(t.searchEngineUpdated, 'success');
|
||||
} else {
|
||||
// Add new
|
||||
updatedEngines.push(newEngine);
|
||||
showToast(t.newSearchEngineAdded, 'success');
|
||||
// Add new
|
||||
updatedEngines.push(newEngine);
|
||||
showToast(t.newSearchEngineAdded, 'success');
|
||||
}
|
||||
|
||||
onUpdateSettings({
|
||||
@@ -140,17 +140,17 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-5">
|
||||
<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 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingOriginalName(null);
|
||||
setNewEngineName('');
|
||||
setNewEngineUrl('');
|
||||
setNewEngineIcon('');
|
||||
setIsAddingEngine(true);
|
||||
setEditingOriginalName(null);
|
||||
setNewEngineName('');
|
||||
setNewEngineUrl('');
|
||||
setNewEngineIcon('');
|
||||
setIsAddingEngine(true);
|
||||
}}
|
||||
className="p-1.5 rounded-full hover:bg-white/10 transition-colors"
|
||||
style={{ color: settings.themeColor }}
|
||||
@@ -171,7 +171,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
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' : ''}
|
||||
${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">
|
||||
<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>
|
||||
|
||||
@@ -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="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-semibold text-white/80">
|
||||
{editingOriginalName ? t.editSearchEngine : t.addCustomEngine}
|
||||
{editingOriginalName ? t.editSearchEngine : t.addCustomEngine}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -246,7 +246,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
value={newEngineName}
|
||||
onChange={(e) => setNewEngineName(e.target.value)}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -257,7 +257,7 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
value={newEngineUrl}
|
||||
onChange={(e) => setNewEngineUrl(e.target.value)}
|
||||
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 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 { XIcon } from './Icons';
|
||||
import { UserSettings } from '../types';
|
||||
import { UserSettings, SettingsSection } from '../types';
|
||||
import ThemeSettings from './ThemeSettings';
|
||||
import WallpaperManager from './WallpaperManager';
|
||||
import SearchEngineManager from './SearchEngineManager';
|
||||
@@ -12,9 +11,10 @@ interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
settings: UserSettings;
|
||||
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();
|
||||
|
||||
if (!isOpen) return null;
|
||||
@@ -25,6 +25,19 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
|
||||
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 (
|
||||
<div
|
||||
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 */}
|
||||
<div className="
|
||||
relative w-full max-w-md rounded-3xl
|
||||
relative w-full max-w-md rounded-2xl
|
||||
bg-[#1a1a1a]/90 backdrop-blur-xl
|
||||
border border-white/10
|
||||
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
|
||||
">
|
||||
{/* Header - fixed */}
|
||||
<div className="flex items-center justify-between px-6 pt-6 pb-4 shrink-0 z-10">
|
||||
<h2 className="text-2xl font-light tracking-wide">{t.settings}</h2>
|
||||
<div className="flex items-center justify-between px-5 pt-5 pb-3 shrink-0 z-10">
|
||||
<h2 className="text-lg font-medium tracking-wide text-white/90">{getTitle()}</h2>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Scrollable area */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6 custom-scrollbar">
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* 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 */}
|
||||
{section === 'general' && (
|
||||
<ThemeSettings settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
)}
|
||||
|
||||
{/* Wallpaper manager component */}
|
||||
{section === 'wallpaper' && (
|
||||
<WallpaperManager settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-[1px] bg-white/10 w-full" />
|
||||
|
||||
{/* Search engine manager component */}
|
||||
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
{section === 'search' && (
|
||||
<SearchEngineManager settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,10 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
|
||||
onUpdateSettings({ ...settings, searchOpacity: parseFloat(e.target.value) });
|
||||
};
|
||||
|
||||
const handleMaskOpacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onUpdateSettings({ ...settings, maskOpacity: parseFloat(e.target.value) });
|
||||
};
|
||||
|
||||
const handleThemeChange = (colorHex: string) => {
|
||||
onUpdateSettings({ ...settings, themeColor: colorHex });
|
||||
};
|
||||
@@ -46,15 +50,15 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-5">
|
||||
{/* Language Selection */}
|
||||
<div className="space-y-3">
|
||||
<span className="text-white/80 font-light block">{t.language}</span>
|
||||
<div className="space-y-4">
|
||||
<span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.language}</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
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'
|
||||
? 'bg-white text-black'
|
||||
: '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
|
||||
onClick={() => handleLanguageChange('zh')}
|
||||
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'
|
||||
? 'bg-white text-black'
|
||||
: '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>
|
||||
|
||||
{/* Theme color */}
|
||||
<div className="space-y-3">
|
||||
<span className="text-white/80 font-light block">{t.themeColor}</span>
|
||||
<div className="space-y-4">
|
||||
<span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.themeColor}</span>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{THEMES.map((theme) => (
|
||||
<button
|
||||
key={theme.hex}
|
||||
onClick={() => handleThemeChange(theme.hex)}
|
||||
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'}
|
||||
`}
|
||||
style={{ backgroundColor: theme.hex }}
|
||||
@@ -100,55 +104,55 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
|
||||
</div>
|
||||
|
||||
{/* Toggle settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<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
|
||||
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 }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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
|
||||
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 }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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
|
||||
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 }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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
|
||||
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 }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background blur slider */}
|
||||
<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>{settings.backgroundBlur}px</span>
|
||||
</div>
|
||||
@@ -165,7 +169,7 @@ const ThemeSettings: React.FC<ThemeSettingsProps> = ({ settings, onUpdateSetting
|
||||
|
||||
{/* Search box opacity slider */}
|
||||
<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>{Math.round(settings.searchOpacity * 100)}%</span>
|
||||
</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"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -180,8 +180,8 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
|
||||
}, [settings.customWallpapers]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<span className="text-white/80 font-light block">{t.wallpaperSettings}</span>
|
||||
<div className="space-y-5">
|
||||
<span className="text-xs font-semibold text-white/50 uppercase tracking-wider block">{t.wallpaperSettings}</span>
|
||||
|
||||
{/* Wallpaper grid */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
@@ -231,9 +231,9 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
<input
|
||||
@@ -249,21 +249,21 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
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()}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCustomUrlApply}
|
||||
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}
|
||||
</button>
|
||||
@@ -278,9 +278,9 @@ const WallpaperManager: React.FC<WallpaperManagerProps> = ({ settings, onUpdateS
|
||||
key={fit.value}
|
||||
onClick={() => onUpdateSettings({ ...settings, wallpaperFit: fit.value })}
|
||||
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')
|
||||
? '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'}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@ export const en: Translation = {
|
||||
searchHistory: 'Search History',
|
||||
backgroundBlur: 'Background Blur',
|
||||
searchBoxOpacity: 'Search Box Opacity',
|
||||
maskOpacity: 'Mask Opacity',
|
||||
|
||||
// Wallpaper Settings
|
||||
wallpaperSettings: 'Wallpaper Settings',
|
||||
|
||||
@@ -14,6 +14,7 @@ export const zh: Translation = {
|
||||
searchHistory: '搜索历史记录',
|
||||
backgroundBlur: '背景模糊度',
|
||||
searchBoxOpacity: '搜索框不透明度',
|
||||
maskOpacity: '遮罩层不透明度',
|
||||
|
||||
// Wallpaper Settings
|
||||
wallpaperSettings: '壁纸设置',
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Translation {
|
||||
searchHistory: string;
|
||||
backgroundBlur: string;
|
||||
searchBoxOpacity: string;
|
||||
maskOpacity: string;
|
||||
|
||||
// Wallpaper Settings
|
||||
wallpaperSettings: string;
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface UserSettings {
|
||||
themeColor: string;
|
||||
searchOpacity: number;
|
||||
enableMaskBlur: boolean;
|
||||
maskOpacity: number;
|
||||
backgroundUrl: string;
|
||||
backgroundType: BackgroundType;
|
||||
wallpaperFit: WallpaperFit;
|
||||
@@ -38,3 +39,5 @@ export interface UserSettings {
|
||||
searchHistory: string[];
|
||||
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)
|
||||
if (engine === 'Bilibili') {
|
||||
// In production (Vercel), use /api/bilibili; in development, use /bilibili (Vite proxy)
|
||||
const isDev = import.meta.env.DEV;
|
||||
const url = isDev
|
||||
? `/bilibili?term=${encodeURIComponent(query)}`
|
||||
: `/api/bilibili?term=${encodeURIComponent(query)}`;
|
||||
const url = `/api/bilibili?term=${encodeURIComponent(query)}`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -7,10 +7,10 @@ export default defineConfig({
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/bilibili': {
|
||||
'/api/bilibili': {
|
||||
target: 'https://s.search.bilibili.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/bilibili/, '/main/suggest'),
|
||||
rewrite: (path) => path.replace(/^\/api\/bilibili/, '/main/suggest'),
|
||||
secure: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user