feat(downloads): expose active Aria2 connections

This commit is contained in:
NimBold
2026-07-22 14:04:59 +03:30
parent 8e706b598c
commit d1452ce0c1
12 changed files with 249 additions and 20 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, size: string | null, size_is_final: boolean, downloaded_bytes?: number, total_bytes?: number, total_is_estimate?: boolean, };
export type DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, size: string | null, size_is_final: boolean, downloaded_bytes?: number, total_bytes?: number, total_is_estimate?: boolean, active_connections?: number, requested_connections?: number, };
+28 -1
View File
@@ -207,6 +207,32 @@ export const PropertiesModal = () => {
const identityLocked = getIdentityLocked(item.status);
const transferLocked = getTransferLocked(item.status);
const configuredConnections = resolveDownloadConnections(item.connections, perServerConnections);
const observedConnectionTotal = Math.max(
1,
liveProgress?.requested_connections ?? configuredConnections
);
const observedActiveConnections = liveProgress?.active_connections;
const connectionTelemetryActive = item.status === 'downloading' ||
item.status === 'processing' ||
item.status === 'retrying';
const connectionStatus = (() => {
if (item.isMedia) return t($ => $.properties.connectionsUnavailable);
if (!connectionTelemetryActive) return String(configuredConnections);
if (typeof observedActiveConnections === 'number') {
return t($ => $.properties.connectionCount, {
active: observedActiveConnections,
total: observedConnectionTotal,
});
}
if (item.status === 'downloading') {
return t($ => $.properties.connectionCountUnknown, { total: observedConnectionTotal });
}
return t($ => $.properties.connectionCount, {
active: 0,
total: observedConnectionTotal,
});
})();
const displayedFraction = item.status === 'completed'
? 1
: liveProgress?.fraction ?? item.fraction ?? 0;
@@ -298,7 +324,7 @@ export const PropertiesModal = () => {
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[40px] shrink-0">{t($ => $.properties.speed)}</span><span className="text-text-secondary truncate">{displayedSpeed}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[30px] shrink-0">{t($ => $.properties.eta)}</span><span className="text-text-secondary truncate">{displayedEta}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[90px] shrink-0">{t($ => $.properties.connections)}</span><span className="text-text-secondary truncate" title={item.connections !== undefined ? t($ => $.properties.savedTooltip) : t($ => $.properties.defaultTooltip)}>{resolveDownloadConnections(item.connections, perServerConnections)}{item.connections !== undefined ? t($ => $.properties.saved) : t($ => $.properties.defaultValue)}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[90px] shrink-0">{t($ => $.properties.connections)}</span><span className="text-text-secondary truncate" title={item.connections !== undefined ? t($ => $.properties.savedTooltip) : t($ => $.properties.defaultTooltip)}><bdi>{connectionStatus}</bdi>{item.connections !== undefined ? t($ => $.properties.saved) : t($ => $.properties.defaultValue)}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[60px] shrink-0">{t($ => $.properties.speedCap)}</span><span className="text-text-secondary truncate">{item.speedLimit || '-'}</span></div>
<div className="flex gap-1.5 min-w-0"><span className="text-text-muted font-medium w-[55px] shrink-0">{t($ => $.properties.category)}</span><span className="text-text-secondary truncate">{categoryLabel(item.category)}</span></div>
<div className="flex gap-1.5"><span className="text-text-muted font-medium w-[50px]">{t($ => $.properties.lastTry)}</span><span className="text-text-secondary truncate">{formatLastTry(item.lastTry)}</span></div>
@@ -352,6 +378,7 @@ export const PropertiesModal = () => {
<div className="flex items-center gap-2">
<input type="number" value={connections} min={1} max={16} onChange={e=>{ setConnections(Number(e.target.value)); setConnectionsDirty(true); }} disabled={transferLocked} className="w-16 bg-bg-input border border-border-modal rounded-lg px-2.5 py-1.5 text-xs text-text-primary focus:outline-none focus:border-accent disabled:opacity-50" />
<span className="text-xs text-text-muted">{t($ => $.properties.perFile)}</span>
<span className="text-xs text-text-secondary font-mono" aria-live="polite"><bdi>{connectionStatus}</bdi></span>
{!transferLocked && item.connections !== undefined && item.connections !== perServerConnections && (
<button
type="button"
+3
View File
@@ -212,6 +212,9 @@ const common = {
speed: 'Speed',
eta: 'ETA',
connections: 'Connections',
connectionCount: '{{active}}/{{total}}',
connectionCountUnknown: '—/{{total}}',
connectionsUnavailable: '—',
speedCap: 'Speed cap',
category: 'Category',
lastTry: 'Last try',
+3
View File
@@ -212,6 +212,9 @@ const fa = {
speed: 'سرعت',
eta: 'زمان باقیمانده',
connections: 'اتصالات',
connectionCount: '{{active}}/{{total}} فعال',
connectionCountUnknown: '—/{{total}} فعال',
connectionsUnavailable: '—',
speedCap: 'سقف سرعت',
category: 'دسته',
lastTry: 'آخرین تلاش',
+3
View File
@@ -212,6 +212,9 @@ const he = {
speed: 'מהירות',
eta: 'זמן נותר',
connections: 'חיבורים',
connectionCount: '{{active}}/{{total}} פעילות',
connectionCountUnknown: '—/{{total}} פעילות',
connectionsUnavailable: '—',
speedCap: 'הגבלת מהירות',
category: 'קטגוריה',
lastTry: 'ניסיון אחרון',
+3
View File
@@ -212,6 +212,9 @@ const ru = {
speed: 'Скорость',
eta: 'Осталось',
connections: 'Соединения',
connectionCount: '{{active}}/{{total}} активных',
connectionCountUnknown: '—/{{total}} активных',
connectionsUnavailable: '—',
speedCap: 'Ограничение скорости',
category: 'Категория',
lastTry: 'Последняя попытка',
+3
View File
@@ -212,6 +212,9 @@ const uk = {
speed: 'Швидкість',
eta: 'Залишилось',
connections: 'З\'єднання',
connectionCount: '{{active}}/{{total}} активних',
connectionCountUnknown: '—/{{total}} активних',
connectionsUnavailable: '—',
speedCap: 'Обмеження швидкості',
category: 'Категорія',
lastTry: 'Остання спроба',
+3
View File
@@ -212,6 +212,9 @@ const zhCN = {
speed: '速度',
eta: '剩余时间',
connections: '连接数',
connectionCount: '{{active}}/{{total}} 个连接',
connectionCountUnknown: '—/{{total}} 个连接',
connectionsUnavailable: '—',
speedCap: '速度上限',
category: '类别',
lastTry: '上次尝试',
+34
View File
@@ -90,6 +90,40 @@ describe('useDownloadProgressStore', () => {
release();
});
it('keeps Aria2 connection telemetry from the live progress event', async () => {
const handlers: Record<string, (event: any) => void> = {};
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {
handlers[event] = handler as (event: any) => void;
return Promise.resolve(vi.fn());
});
useDownloadStore.setState({
downloads: [{
id: 'connection-telemetry',
url: 'https://github.com/example/release.zip',
fileName: 'release.zip',
status: 'downloading',
category: 'Compressed',
dateAdded: ''
}]
});
const release = await initDownloadListener();
handlers['download-progress']({ payload: {
id: 'connection-telemetry',
fraction: 0.2,
speed: '3 MB/s',
eta: '10s',
size: '38 MB',
size_is_final: false,
active_connections: 16,
requested_connections: 16
} });
expect(useDownloadProgressStore.getState().progressMap['connection-telemetry'])
.toMatchObject({ active_connections: 16, requested_connections: 16 });
release();
});
it('clears progress when events arrive after a download row was removed', async () => {
const handlers: Record<string, (event: any) => void> = {};
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {