feat: add backup start time configuration and theme switch functionality

- Introduced BACKUP_DEFAULT_START_TIME constant for backup scheduling.
- Updated BackupScheduleConfig interface to include startTime.
- Implemented normalizeStartTime function for validating and normalizing start time input.
- Enhanced backup settings parsing to accommodate start time.
- Added start time input field in BackupDestinationDetail component.
- Created ThemeSwitch component for toggling between light and dark themes.
- Integrated theme preference management in App component.
- Updated styles for dark mode support across the application.
- Added translations for theme toggle and backup start time labels.
This commit is contained in:
shuaiplus
2026-03-23 08:53:18 +08:00
parent 8b07cd4409
commit 7373eeb501
8 changed files with 722 additions and 9 deletions
@@ -1,6 +1,7 @@
import { ArrowUpDown, Cloud, Clock3, Folder as FolderIcon, KeyRound, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact';
import { Link } from 'wouter';
import AppMainRoutes from '@/components/AppMainRoutes';
import ThemeSwitch from '@/components/ThemeSwitch';
import type { AppMainRoutesProps } from '@/components/AppMainRoutes';
import { t } from '@/lib/i18n';
import type { Profile } from '@/lib/types';
@@ -15,8 +16,11 @@ interface AppAuthenticatedShellProps {
settingsAccountRoute: string;
importRoute: string;
isImportRoute: boolean;
darkMode: boolean;
themeToggleTitle: string;
onLock: () => void;
onLogout: () => void;
onToggleTheme: () => void;
mainRoutesProps: AppMainRoutesProps;
}
@@ -35,6 +39,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
<ShieldUser size={16} />
<span>{props.profile?.email}</span>
</div>
<ThemeSwitch checked={props.darkMode} title={props.themeToggleTitle} onToggle={props.onToggleTheme} />
<button type="button" className="btn btn-secondary small" onClick={props.onLock}>
<Lock size={14} className="btn-icon" /> {t('txt_lock')}
</button>
@@ -49,6 +54,9 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
<FolderIcon size={16} className="btn-icon" />
</button>
)}
<div className="mobile-theme-btn">
<ThemeSwitch checked={props.darkMode} title={props.themeToggleTitle} onToggle={props.onToggleTheme} />
</div>
<button type="button" className="btn btn-secondary small mobile-lock-btn" aria-label={t('txt_lock')} title={t('txt_lock')} onClick={props.onLock}>
<Lock size={14} className="btn-icon" />
</button>
+29
View File
@@ -0,0 +1,29 @@
interface ThemeSwitchProps {
checked: boolean;
title: string;
onToggle: () => void;
}
export default function ThemeSwitch(props: ThemeSwitchProps) {
return (
<div className="theme-switch-wrap" title={props.title}>
<label className="theme-switch" aria-label={props.title}>
<span className="sun" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g fill="#ffd43b">
<circle r={5} cy={12} cx={12} />
<path d="m21 13h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm-17 0h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm13.66-5.66a1 1 0 0 1 -.66-.29 1 1 0 0 1 0-1.41l.71-.71a1 1 0 1 1 1.41 1.41l-.71.71a1 1 0 0 1 -.75.29zm-12.02 12.02a1 1 0 0 1 -.71-.29 1 1 0 0 1 0-1.41l.71-.66a1 1 0 0 1 1.41 1.41l-.71.71a1 1 0 0 1 -.7.24zm6.36-14.36a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm0 17a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm-5.66-14.66a1 1 0 0 1 -.7-.29l-.71-.71a1 1 0 0 1 1.41-1.41l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.29zm12.02 12.02a1 1 0 0 1 -.7-.29l-.66-.71a1 1 0 0 1 1.36-1.36l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.24z" />
</g>
</svg>
</span>
<span className="moon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<path d="m223.5 32c-123.5 0-223.5 100.3-223.5 224s100 224 223.5 224c60.6 0 115.5-24.2 155.8-63.4 5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6-96.9 0-175.5-78.8-175.5-176 0-65.8 36-123.1 89.3-153.3 6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" />
</svg>
</span>
<input type="checkbox" className="theme-switch-input" checked={props.checked} onInput={props.onToggle} />
<span className="theme-switch-slider" />
</label>
</div>
);
}
@@ -256,6 +256,23 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
</div>
</div>
</label>
<label className="field">
<span>{t('txt_backup_start_time')}</span>
<input
className="input"
type="time"
step={300}
value={props.selectedDestination.schedule.startTime || '03:00'}
disabled={props.loadingSettings || props.disableWhileBusy}
onInput={(event) => props.onUpdateDestination((destination) => ({
...destination,
schedule: {
...destination.schedule,
startTime: (event.currentTarget as HTMLInputElement).value || '03:00',
},
}))}
/>
</label>
<label className="field">
<span>{t('txt_backup_timezone')}</span>
<select