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
+53
View File
@@ -57,11 +57,26 @@ const IMPORT_ROUTE_PATHS = [IMPORT_ROUTE, '/tools/import', '/tools/import-export
const IMPORT_ROUTE_ALIASES: ReadonlySet<string> = new Set(IMPORT_ROUTE_PATHS.filter((path) => path !== IMPORT_ROUTE));
const SETTINGS_HOME_ROUTE = '/settings';
const SETTINGS_ACCOUNT_ROUTE = '/settings/account';
const THEME_STORAGE_KEY = 'nodewarden.theme.preference.v1';
const SIGNALR_RECORD_SEPARATOR = String.fromCharCode(0x1e);
const SIGNALR_UPDATE_TYPE_SYNC_VAULT = 5;
const SIGNALR_UPDATE_TYPE_LOG_OUT = 11;
const SIGNALR_UPDATE_TYPE_DEVICE_STATUS = 12;
type ThemePreference = 'system' | 'light' | 'dark';
function readThemePreference(): ThemePreference {
if (typeof window === 'undefined') return 'system';
const stored = String(window.localStorage.getItem(THEME_STORAGE_KEY) || '').trim();
if (stored === 'light' || stored === 'dark' || stored === 'system') return stored;
return 'system';
}
function resolveSystemTheme(): 'light' | 'dark' {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export default function App() {
const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []);
const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []);
@@ -100,6 +115,8 @@ export default function App() {
const [disableTotpOpen, setDisableTotpOpen] = useState(false);
const [disableTotpPassword, setDisableTotpPassword] = useState('');
const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' });
const [themePreference, setThemePreference] = useState<ThemePreference>(() => readThemePreference());
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => resolveSystemTheme());
const [confirm, setConfirm] = useState<AppConfirmState | null>(null);
const [mobileLayout, setMobileLayout] = useState(false);
@@ -175,6 +192,39 @@ export default function App() {
return () => media.removeListener(sync);
}, []);
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const sync = () => setSystemTheme(media.matches ? 'dark' : 'light');
sync();
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', sync);
return () => media.removeEventListener('change', sync);
}
media.addListener(sync);
return () => media.removeListener(sync);
}, []);
const resolvedTheme = themePreference === 'system' ? systemTheme : themePreference;
useEffect(() => {
if (typeof document === 'undefined') return;
document.documentElement.dataset.theme = resolvedTheme;
document.documentElement.style.colorScheme = resolvedTheme;
}, [resolvedTheme]);
useEffect(() => {
if (typeof window === 'undefined') return;
window.localStorage.setItem(THEME_STORAGE_KEY, themePreference);
}, [themePreference]);
function handleToggleTheme() {
setThemePreference((prev) => {
const current = prev === 'system' ? systemTheme : prev;
return current === 'dark' ? 'light' : 'dark';
});
}
function setSession(next: SessionState | null) {
sessionRef.current = next;
setSessionState(next);
@@ -1135,8 +1185,11 @@ export default function App() {
settingsAccountRoute={SETTINGS_ACCOUNT_ROUTE}
importRoute={IMPORT_ROUTE}
isImportRoute={isImportRoute}
darkMode={resolvedTheme === 'dark'}
themeToggleTitle={resolvedTheme === 'dark' ? t('txt_switch_to_light_mode') : t('txt_switch_to_dark_mode')}
onLock={handleLock}
onLogout={handleLogout}
onToggleTheme={handleToggleTheme}
mainRoutesProps={mainRoutesProps}
/>