This commit is contained in:
parent
fa2be029a2
commit
724d70e58b
3339 changed files with 1075535 additions and 0 deletions
242
frontend/src/components/SettingsPage.tsx
Normal file
242
frontend/src/components/SettingsPage.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Settings, Shield, Image as ImageIcon, History, Trash2, Save, Loader2 } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface SettingsData {
|
||||
theme: string;
|
||||
thumbnail_images: string;
|
||||
thumbnail_videos: string;
|
||||
max_versions: string;
|
||||
trash_auto_purge_days: string;
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useState<SettingsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
setSettings({
|
||||
theme: data.theme || 'dark',
|
||||
thumbnail_images: data.thumbnail_images || 'true',
|
||||
thumbnail_videos: data.thumbnail_videos || 'true',
|
||||
max_versions: data.max_versions || '1',
|
||||
trash_auto_purge_days: data.trash_auto_purge_days || '14',
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!settings) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
await api.updateSettings(settings);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
|
||||
// Update local theme variable if changed
|
||||
if (settings.theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.style.setProperty('color-scheme', 'light');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.style.setProperty('color-scheme', 'dark');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to save settings');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'appearance',
|
||||
title: 'Appearance',
|
||||
icon: <Settings className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Theme</label>
|
||||
<select
|
||||
value={settings.theme}
|
||||
onChange={(e) => setSettings({ ...settings, theme: e.target.value })}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
|
||||
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
||||
>
|
||||
<option value="dark">Dark Mode</option>
|
||||
<option value="light">Light Mode</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'thumbnails',
|
||||
title: 'Media & Thumbnails',
|
||||
icon: <ImageIcon className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.thumbnail_images === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, thumbnail_images: e.target.checked ? 'true' : 'false' })}
|
||||
className="w-4 h-4 rounded text-accent focus:ring-accent"
|
||||
style={{ accentColor: 'var(--color-accent)' }}
|
||||
/>
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Generate thumbnails for images</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.thumbnail_videos === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, thumbnail_videos: e.target.checked ? 'true' : 'false' })}
|
||||
className="w-4 h-4 rounded text-accent focus:ring-accent"
|
||||
style={{ accentColor: 'var(--color-accent)' }}
|
||||
/>
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Generate thumbnails for videos (requires FFmpeg)</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'versioning',
|
||||
title: 'File Versioning',
|
||||
icon: <History className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Maximum Versions to Keep</label>
|
||||
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Older versions will be automatically deleted when a file is overwritten more times than this limit. Set to 0 to disable versioning.
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={settings.max_versions}
|
||||
onChange={(e) => setSettings({ ...settings, max_versions: e.target.value })}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
|
||||
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'trash',
|
||||
title: 'Trash Auto-Purge',
|
||||
icon: <Trash2 className="w-5 h-5" />,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Days to Keep Trashed Files</label>
|
||||
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Files in the trash will be permanently deleted after this many days. Set to 0 to disable auto-purge.
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="365"
|
||||
value={settings.trash_auto_purge_days}
|
||||
onChange={(e) => setSettings({ ...settings, trash_auto_purge_days: e.target.value })}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
|
||||
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto w-full">
|
||||
<div className="p-8 max-w-4xl mx-auto w-full">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Settings</h1>
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>Manage your Drive preferences and configurations.</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}>
|
||||
<Shield className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(34, 197, 94, 0.1)', color: 'var(--color-success)' }}>
|
||||
<Save className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">Settings saved successfully.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{sections.map((section, idx) => (
|
||||
<motion.div
|
||||
key={section.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
className="p-6 rounded-2xl"
|
||||
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6" style={{ color: 'var(--color-text-primary)' }}>
|
||||
<div className="p-2 rounded-lg" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
|
||||
{section.icon}
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold">{section.title}</h2>
|
||||
</div>
|
||||
{section.content}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-medium text-white transition-all hover:opacity-90 disabled:opacity-50"
|
||||
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in a new issue