Skip to content

Commit ff7a72f

Browse files
committed
Copy progress: format sub-1 files/s readouts instead of rounding to 0
Previously the dialog used `Math.round(filesPerSecond)`, so any rate below 0.5 displayed as "0 files/s" — misleading whenever an MTP→SMB copy was streaming a big file at ~0.3 actual files/s. New `formatFilesPerSecond(rate)`: - `< 3`: 1 decimal (`0.4 files/s`, `1.8 files/s`) - Rounds to exactly 1: `1 file/s` (singular) - `>= 3`: integer (`27 files/s`) - Returns `null` when the rate rounds to 0.0 so the readout hides instead of lying 15 unit tests cover all cases including the singular boundary (0.95/1.04 round to 1, 1.05 flips to plural) and the 0.05 floor.
1 parent 4737acb commit ff7a72f

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

apps/desktop/src/lib/file-operations/transfer/TransferProgressDialog.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
onScanPreviewError,
2121
onScanPreviewCancelled,
2222
formatDuration,
23+
formatFilesPerSecond,
2324
DEFAULT_VOLUME_ID,
2425
type WriteProgressEvent,
2526
type WriteCompleteEvent,
@@ -1027,8 +1028,11 @@
10271028
{#if bytesPerSecond !== null && bytesPerSecond > 0}
10281029
<span class="progress-speed"><Size bytes={bytesPerSecond} />/s</span>
10291030
{/if}
1030-
{#if filesPerSecond !== null && filesPerSecond > 0}
1031-
<span class="progress-speed">{formatNumber(Math.round(filesPerSecond))} files/s</span>
1031+
{#if filesPerSecond !== null}
1032+
{@const filesPerSecLabel = formatFilesPerSecond(filesPerSecond)}
1033+
{#if filesPerSecLabel !== null}
1034+
<span class="progress-speed">{filesPerSecLabel}</span>
1035+
{/if}
10321036
{/if}
10331037
</span>
10341038
{#if etaSecondsDisplay !== null}

apps/desktop/src/lib/tauri-commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export {
169169
onWriteConflict,
170170
formatBytes,
171171
formatDuration,
172+
formatFilesPerSecond,
172173
} from './write-operations'
173174
export type { Event, UnlistenFn } from './write-operations'
174175

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { formatFilesPerSecond } from './write-operations'
3+
4+
describe('formatFilesPerSecond', () => {
5+
describe('rates below 3 (1 decimal)', () => {
6+
it('formats sub-1 rates with 1 decimal', () => {
7+
expect(formatFilesPerSecond(0.4)).toBe('0.4 files/s')
8+
})
9+
10+
it('formats 1.x rates with 1 decimal (plural)', () => {
11+
expect(formatFilesPerSecond(1.8)).toBe('1.8 files/s')
12+
})
13+
14+
it('formats 2.x rates with 1 decimal', () => {
15+
expect(formatFilesPerSecond(2.5)).toBe('2.5 files/s')
16+
})
17+
18+
it('rounds to 1 decimal', () => {
19+
expect(formatFilesPerSecond(0.44)).toBe('0.4 files/s')
20+
expect(formatFilesPerSecond(0.45)).toBe('0.5 files/s')
21+
})
22+
})
23+
24+
describe('singular ("1 file/s")', () => {
25+
it('uses singular when rate is exactly 1', () => {
26+
expect(formatFilesPerSecond(1)).toBe('1 file/s')
27+
})
28+
29+
it('uses singular when rate rounds to 1.0 from below', () => {
30+
expect(formatFilesPerSecond(0.97)).toBe('1 file/s')
31+
})
32+
33+
it('uses singular when rate rounds to 1.0 from above', () => {
34+
expect(formatFilesPerSecond(1.04)).toBe('1 file/s')
35+
})
36+
37+
it('switches to plural at 1.05', () => {
38+
expect(formatFilesPerSecond(1.05)).toBe('1.1 files/s')
39+
})
40+
})
41+
42+
describe('rates at or above 3 (integer)', () => {
43+
it('rounds to integer at exactly 3', () => {
44+
expect(formatFilesPerSecond(3)).toBe('3 files/s')
45+
})
46+
47+
it('rounds 27.4 down to 27', () => {
48+
expect(formatFilesPerSecond(27.4)).toBe('27 files/s')
49+
})
50+
51+
it('rounds 27.5 up to 28', () => {
52+
expect(formatFilesPerSecond(27.5)).toBe('28 files/s')
53+
})
54+
55+
it('handles large rates', () => {
56+
expect(formatFilesPerSecond(1500)).toBe('1500 files/s')
57+
})
58+
})
59+
60+
describe('returns null when rate rounds to 0', () => {
61+
it('returns null for exactly 0', () => {
62+
expect(formatFilesPerSecond(0)).toBe(null)
63+
})
64+
65+
it('returns null for rates < 0.05 (round to 0.0)', () => {
66+
expect(formatFilesPerSecond(0.04)).toBe(null)
67+
expect(formatFilesPerSecond(0.0001)).toBe(null)
68+
})
69+
70+
it('returns "0.1 files/s" at the 0.05 boundary', () => {
71+
expect(formatFilesPerSecond(0.05)).toBe('0.1 files/s')
72+
})
73+
})
74+
})

apps/desktop/src/lib/tauri-commands/write-operations.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ export function formatBytes(bytes: number): string {
216216
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
217217
}
218218

219+
/**
220+
* Formats a files-per-second rate for the progress dialog.
221+
*
222+
* - `< 3`: 1 decimal (`"0.4 files/s"`, `"1.8 files/s"`). Small values aren't useful as integers.
223+
* - Rounds to exactly `1`: `"1 file/s"` (singular).
224+
* - `>= 3`: integer (`"27 files/s"`). Decimal precision adds nothing at high rates.
225+
*
226+
* Returns `null` for rates that round to `0.0` so the caller can hide the readout
227+
* entirely. The previous "0 files/s" display masked the real (sub-1) rates that
228+
* heterogeneous-size copies produce.
229+
*/
230+
export function formatFilesPerSecond(rate: number): string | null {
231+
if (rate < 3) {
232+
const oneDecimal = Math.round(rate * 10) / 10
233+
if (oneDecimal === 0) return null
234+
if (oneDecimal === 1) return '1 file/s'
235+
return `${oneDecimal.toFixed(1)} files/s`
236+
}
237+
return `${String(Math.round(rate))} files/s`
238+
}
239+
219240
/**
220241
* Formats seconds as human-readable duration (like "2m 30s").
221242
*/

0 commit comments

Comments
 (0)