import { CheckIcon } from '@radix-ui/react-icons'; import { GetBackups, GetLastLogs, RestoreBackup, SendCrashReport, } from '@wailsapp/go/main/App'; import type { main } from '@wailsapp/go/models'; import { EventsOff, EventsOn } from '@wailsapp/runtime'; import React, { Fragment, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { languages } from '~/locale/languages'; import { ProcessedLogEntry, processEntry } from '~/store/logging/reducer'; import DialogContent from '~/ui/components/DialogContent'; import { LogItem } from '~/ui/components/LogViewer'; import Scrollbar from '~/ui/components/utils/Scrollbar'; import { Button, Checkbox, CheckboxIndicator, Dialog, DialogActions, Field, FlexRow, InputBox, Label, MultiToggle, MultiToggleItem, PageContainer, PageHeader, SectionHeader, styled, Textarea, TextBlock, } from '~/ui/theme'; import AlertContent from './components/AlertContent'; import { Alert, AlertDescription, AlertTrigger } from './theme/alert'; const Container = styled('div', { position: 'relative', display: 'flex', flexDirection: 'row', overflow: 'hidden', height: '100vh', border: '2px solid $red10', }); const ErrorHeader = styled('h1', { color: '$red10', textTransform: 'capitalize', }); const ErrorDetails = styled('dl', { display: 'grid', gridTemplateColumns: '100px 1fr', margin: '0', }); const ErrorDetailKey = styled('dt', { fontWeight: 'bold', textTransform: 'capitalize', gridColumn: '1', }); const ErrorDetailValue = styled('dd', { padding: '0', margin: '0', marginBottom: '0.5rem', gridColumn: '2', }); const LogContainer = styled('div', { display: 'flex', flexDirection: 'column', gap: '3px', }); const Mono = styled('code', { background: '$gray5', padding: '3px 5px', borderRadius: '3px', whiteSpace: 'nowrap', }); const MiniHeader = styled(SectionHeader, { fontSize: '14pt', }); const LanguageSelector = styled('div', { top: '10px', right: '10px', display: 'flex', gap: '1rem', position: 'absolute', zIndex: '1', }); const LanguageItem = styled(MultiToggleItem, { fontSize: '8pt', padding: '5px 6px 4px', textTransform: 'uppercase', }); const BackupItem = styled('article', { backgroundColor: '$gray2', padding: '0.3rem 1rem 0.3rem 0.5rem', borderRadius: '0.25rem', borderBottom: '1px solid $gray5', transition: 'all 50ms', display: 'flex', '&:nth-child(odd)': { backgroundColor: '$gray3', }, gap: '0.5rem', }); const BackupDate = styled('div', { display: 'flex', alignItems: 'center', flex: '1', gap: '0.5rem', fontVariantNumeric: 'tabular-nums', }); const BackupSize = styled('div', { color: '$gray10', alignItems: 'center', display: 'flex', }); const BackupActions = styled('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.25rem', }); interface RecoveryDialogProps { open: boolean; onOpenChange: (state: boolean) => void; } // Returns a human-readable version of a byte size function hrsize(bytes: number): string { const units = ['B', 'KiB', 'MiB', 'GiB']; let fractBytes = bytes; while (fractBytes >= 1024) { fractBytes /= 1024; units.shift(); } return `${fractBytes.toFixed(2)} ${units[0]}`; } function RecoveryDialog({ open, onOpenChange }: RecoveryDialogProps) { const { t } = useTranslation(); const [backups, setBackups] = useState([]); const [restoreError, setRestoreError] = useState(null); const [restored, setRestored] = useState<'idle' | 'in-progress' | 'done'>( 'idle', ); useEffect(() => { void GetBackups().then((backupList) => { setBackups(backupList); }); }, []); const restore = async (filename: string) => { setRestored('in-progress'); try { await RestoreBackup(filename); setRestoreError(null); } catch (err) { setRestoreError(err as string); } setRestored('done'); }; if (restored === 'done' && restoreError == null) { return ( { if (onOpenChange) { onOpenChange(state); } setRestored('idle'); }} > { if (onOpenChange) { onOpenChange(false); } setRestored('idle'); }} /> ); } return ( <> { if (!val) { setRestoreError(null); } }} > { setRestoreError(null); }} /> { if (onOpenChange) { onOpenChange(state); } }} > {t('pages.crash.recovery.text-head')} {t('pages.crash.recovery.restore-head')} {t('pages.crash.recovery.restore-desc-1')} {backups .sort((a, b) => b.date - a.date) .map((backup) => { const date = new Date(backup.date); return ( {date.toLocaleDateString([], { year: 'numeric', month: 'short', day: '2-digit', })} {' - '} {date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })} {hrsize(backup.size)} { void restore(backup.filename); }} /> ); })} ); } interface ReportDialogProps { open: boolean; onOpenChange: (state: boolean) => void; errorData?: ProcessedLogEntry; } function ReportDialog({ open, onOpenChange, errorData }: ReportDialogProps) { const { t } = useTranslation(); const [errorDesc, setErrorDesc] = useState(''); const [contactEnabled, setContactEnabled] = useState(false); const [contactInfo, setContactInfo] = useState(''); const [submitted, setSubmitted] = useState(false); const [code, setCode] = useState(''); const [submissionError, setSubmissionError] = useState(null); const waiting = submitted && code.length < 1; if (code) { return ( { if (onOpenChange) { onOpenChange(state); } }} > { setSubmissionError(null); }} > ), }} /> ); } return ( <> { if (!val) { setSubmissionError(null); } }} > { setSubmissionError(null); }} /> { if (onOpenChange) { onOpenChange(state); } }} >
{ e.preventDefault(); console.log('test'); let desc = errorDesc; if (contactEnabled && contactInfo) { desc += `\n\nEmail contact: ${contactInfo}`; } SendCrashReport(JSON.stringify(errorData), desc) .then((submissionCode) => { setCode(submissionCode); }) .catch((err) => { setSubmissionError(err as string); }); setSubmitted(true); }} > {t('pages.crash.report.thanks-line')} {t('pages.crash.report.transparency-line')}
  • , }} />
  • {t('pages.crash.report.transparency-info')}
  • {t('pages.crash.report.transparency-user')}
setContactInfo(e.target.value)} />
); } export default function ErrorWindow(): JSX.Element { const [t, i18n] = useTranslation(); const [logs, setLogs] = useState([]); const [reportDialogOpen, setReportDialogOpen] = useState(false); const [recoveryDialogOpen, setRecoveryDialogOpen] = useState(false); useEffect(() => { void GetLastLogs().then((appLogs) => { setLogs(appLogs.map(processEntry).reverse()); }); EventsOn('log-event', (event: main.LogEntry) => { setLogs([processEntry(event), ...logs]); }); return () => { EventsOff('log-event'); }; }, []); const fatal = logs.find((log) => log.level === 'error'); return ( { void i18n.changeLanguage(newLang); }} > {languages.map((lang) => ( {lang.code} ))} {t('pages.crash.fatal-message')} {fatal ? ( <> {fatal.message} {Object.keys(fatal.data) .filter((key) => key.length > 1) .map((key) => ( {key} {fatal.data[key]} ))} ) : null} {t('pages.crash.action-header')} {t('pages.crash.action-submit-line')} {t('pages.crash.action-recover-line')} , }} /> {t('pages.crash.app-log-header')} {logs.map((log) => ( ))} ); }