diff --git a/src/routes/_authenticated/downloads/index.tsx b/src/routes/_authenticated/downloads/index.tsx
index a265ec13..14c1a537 100644
--- a/src/routes/_authenticated/downloads/index.tsx
+++ b/src/routes/_authenticated/downloads/index.tsx
@@ -1447,6 +1447,8 @@ function OverviewPanel({ task }: { task: DownloadTask }) {
return (
-
-
- {task.status.error?.message &&
{task.status.error?.message}
}
- {detail?.message &&
{detail.message}
}
-
+function TaskErrorNotice({ task }: { task: DownloadTask }) {
+ const { t } = useTranslation()
+ const detail = task.status.runtime
+ const messages = [task.status.error?.message, detail?.message].filter(Boolean) as string[]
+ if (messages.length === 0) return null
+
+ return (
+
+
+
+
{t('downloads.detail.errorMessage')}
+
+ {messages.map((message) => (
+
{message}
+ ))}
- )}
+
)
}
@@ -1615,8 +1628,8 @@ function PeersPanel({ task }: { task: DownloadTask }) {
maxWidth: 430,
render: (peer) => (
),
},
@@ -1754,7 +1767,7 @@ function ResizableDetailTable
({
}
return (
-
+
{columns.map((column) => (
@@ -1802,11 +1815,11 @@ function PeerRegionMark({ countryCode, regionCode }: { countryCode?: string; reg
return (
- {flag && {flag}}
- {label}
+ {flag && {flag}}
+ {label}
)
}
@@ -1834,27 +1847,80 @@ function countryCodeToFlag(countryCode: string) {
function LogPanel({ task }: { task: DownloadTask }) {
const { t } = useTranslation()
const detail = task.status.runtime
- const messages = [
- detail?.message && { label: t('downloads.detail.statusMessage'), value: detail.message },
- task.status.error?.message && { label: t('downloads.detail.errorMessage'), value: task.status.error?.message },
- { label: t('downloads.detail.createdAt'), value: formatDate(task.createdAt) },
- { label: t('downloads.detail.startedAt'), value: formatDate(task.status.startedAt) },
- { label: t('downloads.detail.finishedAt'), value: formatDate(task.status.finishedAt) },
- ].filter(Boolean) as Array<{ label: string; value: string }>
+ const events = [
+ {
+ id: 'created',
+ tone: 'neutral',
+ time: formatDate(task.createdAt),
+ title: t('downloads.detail.createdAt'),
+ detail: sourceUri(task),
+ },
+ task.status.startedAt && {
+ id: 'started',
+ tone: 'active',
+ time: formatDate(task.status.startedAt),
+ title: t('downloads.detail.startedAt'),
+ detail: [detail?.engine, formatPhase(detail?.phase, t)].filter(Boolean).join(' ยท '),
+ },
+ detail?.message && {
+ id: 'runtime-message',
+ tone: 'warning',
+ time: formatDate(detail.updatedAt),
+ title: t('downloads.detail.statusMessage'),
+ detail: detail.message,
+ },
+ task.status.error?.message && {
+ id: 'error',
+ tone: 'error',
+ time: formatDate(task.status.updatedAt),
+ title: t('downloads.detail.errorMessage'),
+ detail: task.status.error.message,
+ },
+ task.status.finishedAt && {
+ id: 'finished',
+ tone: task.status.state === 'completed' ? 'success' : 'neutral',
+ time: formatDate(task.status.finishedAt),
+ title: t('downloads.detail.finishedAt'),
+ detail: t(`downloads.status.${task.status.state}`),
+ },
+ ].filter(Boolean) as Array<{
+ id: string
+ tone: 'active' | 'error' | 'neutral' | 'success' | 'warning'
+ time: string
+ title: string
+ detail: string
+ }>
return (
-
- {messages.map((message) => (
-
-
{message.label}
-
{message.value || '-'}
+
+ {events.map((event, index) => (
+
+
+
+ {index < events.length - 1 && }
+
+
+
+ {event.title}
+ {event.time}
+
+ {event.detail &&
{event.detail}
}
+
))}
- {messages.length === 0 &&
}
+ {events.length === 0 &&
}
)
}
+function logEventDotClass(tone: 'active' | 'error' | 'neutral' | 'success' | 'warning') {
+ if (tone === 'active') return 'bg-sky-500'
+ if (tone === 'error') return 'bg-destructive'
+ if (tone === 'success') return 'bg-emerald-500'
+ if (tone === 'warning') return 'bg-amber-500'
+ return 'bg-muted-foreground/50'
+}
+
function SourceIcon({ type }: { type: DownloadTask['spec']['source']['type'] }) {
if (type === 'magnet') return
if (type === 'torrent_url') return