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:
@@ -39,7 +39,7 @@ const GlobalContextMenu: React.FC = () => {
|
||||
// This allows the event to bubble to React handlers (like SearchBox)
|
||||
// which will handle stopping propagation to the App background.
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
targetRef.current = inputTarget;
|
||||
|
||||
// Calculate position to prevent overflow
|
||||
@@ -95,7 +95,7 @@ const GlobalContextMenu: React.FC = () => {
|
||||
window.HTMLInputElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
|
||||
|
||||
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
@@ -179,7 +179,7 @@ const GlobalContextMenu: React.FC = () => {
|
||||
console.error('Failed to paste: ', err);
|
||||
showToast(t.cannotReadClipboard, 'error');
|
||||
}
|
||||
|
||||
|
||||
setVisible(false);
|
||||
element.focus();
|
||||
};
|
||||
@@ -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,30 +389,62 @@ 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)}
|
||||
onContextMenu={handleElementContextMenu}
|
||||
className={`
|
||||
group relative w-full px-3 py-1 rounded-md text-left flex items-center gap-3 transition-all duration-200 ease-out
|
||||
${index === selectedIndex
|
||||
? 'bg-white/10 text-white shadow-sm translate-x-1'
|
||||
${index === selectedIndex
|
||||
? '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`}
|
||||
|
||||
@@ -78,17 +78,17 @@ const SearchEngineManager: React.FC<SearchEngineManagerProps> = ({ settings, onU
|
||||
if (!newEngineName.trim() || !newEngineUrl.trim()) return;
|
||||
|
||||
const name = newEngineName.trim();
|
||||
|
||||
|
||||
// Check for duplicate name if we are adding new or renaming
|
||||
const isRenaming = editingOriginalName && editingOriginalName !== name;
|
||||
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'}
|
||||
`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user