/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import React, { useEffect, useState, useMemo } from 'react'; import { useActions, useSelector, useIsLoading } from '../../services/state/store'; import { Tabs, TabPane, TimePicker, Button, Checkbox, Input, Modal, AutoComplete, Select, Banner, } from '@douyinfe/semi-ui-19'; import { InputNumber } from '@douyinfe/semi-ui-19'; import { xhrPost, xhrGet } from '../../services/xhr'; import { Toast } from '@douyinfe/semi-ui-19'; import { SegmentPart } from '../../components/segment/SegmentPart'; import { downloadBackup as downloadBackupZip, precheckRestore as clientPrecheckRestore, restore as clientRestore, } from '../../services/backupRestoreClient'; import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder } from '@douyinfe/semi-icons'; import { debounce } from '../../utils'; import './GeneralSettings.less'; function formatFromTimestamp(ts) { const date = new Date(ts); return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`; } function formatFromTBackend(time) { if (time == null || time.length === 0) { return null; } const date = new Date(); const split = time.split(':'); date.setHours(split[0]); date.setMinutes(split[1]); return date.getTime(); } const GeneralSettings = function GeneralSettings() { const actions = useActions(); const [loading, setLoading] = React.useState(true); const settings = useSelector((state) => state.generalSettings.settings); const [interval, setInterval] = React.useState(''); const [port, setPort] = React.useState(''); const [workingHourFrom, setWorkingHourFrom] = React.useState(null); const [workingHourTo, setWorkingHourTo] = React.useState(null); const [demoMode, setDemoMode] = React.useState(null); const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null); const [sqlitePath, setSqlitePath] = React.useState(null); const fileInputRef = React.useRef(null); const [restoreModalVisible, setRestoreModalVisible] = React.useState(false); const [precheckInfo, setPrecheckInfo] = React.useState(null); const [restoreBusy, setRestoreBusy] = React.useState(false); const [selectedRestoreFile, setSelectedRestoreFile] = React.useState(null); // User settings state const homeAddress = useSelector((state) => state.userSettings.settings.home_address); const providerDetails = useSelector((state) => state.userSettings.settings.provider_details); const allProviders = useSelector((state) => state.provider); const [address, setAddress] = useState(homeAddress?.address || ''); const [coords, setCoords] = useState(homeAddress?.coords || null); const saving = useIsLoading(actions.userSettings.setHomeAddress); const [dataSource, setDataSource] = useState([]); React.useEffect(() => { async function init() { await actions.generalSettings.getGeneralSettings(); setLoading(false); } init(); }, []); React.useEffect(() => { async function init() { setInterval(settings?.interval); setPort(settings?.port); setWorkingHourFrom(settings?.workingHours?.from); setWorkingHourTo(settings?.workingHours?.to); setAnalyticsEnabled(settings?.analyticsEnabled || false); setDemoMode(settings?.demoMode || false); setSqlitePath(settings?.sqlitepath); } init(); }, [settings]); useEffect(() => { setAddress(homeAddress?.address || ''); setCoords(homeAddress?.coords || null); }, [homeAddress]); const nullOrEmpty = (val) => val == null || val.length === 0; const handleStore = async () => { if (nullOrEmpty(interval)) { Toast.error('Interval may not be empty.'); return; } if (nullOrEmpty(port)) { Toast.error('Port may not be empty.'); return; } if ( (!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) || (nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo)) ) { Toast.error('Working hours to and from must be set if either to or from has been set before.'); return; } if (nullOrEmpty(sqlitePath)) { Toast.error('SQLite db path cannot be empty.'); return; } try { await xhrPost('/api/admin/generalSettings', { interval, port, workingHours: { from: workingHourFrom, to: workingHourTo, }, demoMode, analyticsEnabled, sqlitepath: sqlitePath, }); } catch (exception) { console.error(exception); if (exception?.json?.message != null) { Toast.error(exception.json.message); } else { Toast.error('Error while trying to store settings.'); } return; } Toast.success('Settings stored successfully. We will reload your browser in 3 seconds.'); setTimeout(() => { location.reload(); }, 3000); }; const handleDownloadBackup = React.useCallback(async () => { try { await downloadBackupZip(); } catch (e) { console.error(e); Toast.error('Unexpected error while downloading backup.'); } }, []); const precheckRestore = React.useCallback(async (file) => { try { const data = await clientPrecheckRestore(file); setPrecheckInfo(data); setRestoreModalVisible(true); } catch (e) { console.error(e); Toast.error('Failed to analyze backup.'); } }, []); const performRestore = React.useCallback( async (force) => { try { setRestoreBusy(true); await clientRestore(selectedRestoreFile, force); Toast.success('Restore completed. Please restart the Fredy backend now!'); } catch (e) { console.error(e); Toast.error(e?.message || 'Unexpected error while restoring backup.'); } finally { setRestoreBusy(false); } }, [selectedRestoreFile], ); const handleSelectRestoreFile = React.useCallback( async (ev) => { const file = ev?.target?.files?.[0]; if (!file) return; setSelectedRestoreFile(file); await precheckRestore(file); ev.target.value = ''; }, [precheckRestore], ); const handleOpenFilePicker = React.useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); const handleSaveUserSettings = async () => { try { const responseJson = await actions.userSettings.setHomeAddress(address); setCoords(responseJson.coords); await actions.userSettings.getUserSettings(); Toast.success('Settings saved. Distance calculations are running in the background.'); } catch (error) { Toast.error(error.json?.error || 'Error while saving settings'); } }; const debouncedSearch = useMemo( () => debounce((value) => { xhrGet(`/api/user/settings/autocomplete?q=${encodeURIComponent(value)}`) .then((response) => { if (response.status === 200) { setDataSource(response.json); } }) .catch(() => {}); }, 300), [], ); const searchAddress = (value) => { if (!value) { setDataSource([]); return; } debouncedSearch(value); }; return (