Skip to content

Commit caedb65

Browse files
committed
MTP: explain why a phone's folders add up to less than the used space
On a phone reached over USB (MTP), the disk-space readout reports the whole userdata partition (47.67 GB used on one Redmi 9), but Cmdr can only browse the shared-storage subtree (~5.5 GB of folders). Apps and system data make up the rest and aren't reachable over MTP, so the numbers look impossible to reconcile and read as a Cmdr bug. Now a hover hint explains the gap. - `formatBarTooltip` takes an optional `mtpHint`, appended after the size/level sentences. Pure, TDD'd (`disk-space-utils.test.ts`). - The hint shows on BOTH surfaces that display the figure: the disk-usage bar tooltip and the visible free-space text in `SelectionInfo` (the number users actually read). Gated on `caps.kind === 'mtp'` per A6 (not a volume-id string), via a new `mtpSpaceHint` prop from `FilePane`. - New catalog key `fileExplorer.navigation.spaceMtpHint` with a translator `@key` description, translated into all nine shipped locales (de, es, fr, hu, nl, pt, sv, vi, zh) against their style guides; "Cmdr" and "USB" preserved. i18n coverage/parity/ICU green.
1 parent eada87e commit caedb65

17 files changed

Lines changed: 111 additions & 12 deletions

apps/desktop/src/lib/file-explorer/disk-space-utils.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,23 @@ describe('formatBarTooltip', () => {
172172
const customFormat = (bytes: number): string => `${String(Math.round(bytes / 1073741824))} GB`
173173
expect(formatBarTooltip(space, customFormat)).toBe('1 GB of 1 GB free (50%)')
174174
})
175+
176+
it('appends the extra hint after the sizes when space is OK', () => {
177+
const space = createSpace(1000, 400) // 60% used
178+
expect(formatBarTooltip(space, mockFormatSize, 'Phones hide app data.')).toBe(
179+
'400 B of 1000 B free (40%). Phones hide app data.',
180+
)
181+
})
182+
183+
it('appends the extra hint after a low-space warning', () => {
184+
const space = createSpace(1000, 100) // 90% used → yellow
185+
expect(formatBarTooltip(space, mockFormatSize, 'Phones hide app data.')).toBe(
186+
'100 B of 1000 B free (10%). This bar is yellow to indicate that the volume is somewhat low on space. Phones hide app data.',
187+
)
188+
})
189+
190+
it('omits the hint when none is provided', () => {
191+
const space = createSpace(1000, 400)
192+
expect(formatBarTooltip(space, mockFormatSize, undefined)).toBe('400 B of 1000 B free (40%)')
193+
})
175194
})

apps/desktop/src/lib/file-explorer/disk-space-utils.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,24 @@ export function formatDiskSpaceShort(space: VolumeSpaceInfo, formatSize: FormatS
3636
return `${freeText} free of ${totalText}`
3737
}
3838

39-
/** Formats the usage bar tooltip: sizes, percentage, and a contextual warning when space is low. */
40-
export function formatBarTooltip(space: VolumeSpaceInfo, formatSize: FormatSize): string {
39+
/**
40+
* Formats the usage bar tooltip: sizes, percentage, a contextual warning when
41+
* space is low, and an optional trailing hint. `mtpHint` carries the
42+
* phone-storage explanation (resolved from the message catalog by the caller)
43+
* for MTP volumes, where the browsable folders add up to less than the used
44+
* space because apps and system data aren't reachable over USB.
45+
*/
46+
export function formatBarTooltip(space: VolumeSpaceInfo, formatSize: FormatSize, mtpHint?: string): string {
4147
const freeText = formatSize(space.availableBytes)
4248
const totalText = formatSize(space.totalBytes)
4349
const usedPercent = getUsedPercent(space)
4450
const freePercent = 100 - usedPercent
4551
const level = getDiskUsageLevel(usedPercent)
52+
const sentences: string[] = []
53+
if (level.label === 'Critical') sentences.push('This bar is red to indicate that the volume is low on space.')
54+
else if (level.label === 'Warning')
55+
sentences.push('This bar is yellow to indicate that the volume is somewhat low on space.')
56+
if (mtpHint) sentences.push(mtpHint)
4657
const base = `${freeText} of ${totalText} free (${String(freePercent)}%)`
47-
if (level.label === 'Critical') return `${base}. This bar is red to indicate that the volume is low on space.`
48-
if (level.label === 'Warning')
49-
return `${base}. This bar is yellow to indicate that the volume is somewhat low on space.`
50-
return base
58+
return sentences.length > 0 ? `${base}. ${sentences.join(' ')}` : base
5159
}

apps/desktop/src/lib/file-explorer/pane/FilePane.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,14 @@
302302
*/
303303
const isSearchResultsView = $derived(caps.kind === 'search-results')
304304
305+
/**
306+
* The phone-storage caveat for the disk-space readout, only on MTP volumes
307+
* (keyed on `caps.kind`, not a volume-id string, per A6). Over USB a phone
308+
* exposes only its shared storage, so the browsable folders add up to far
309+
* less than the space reported as used; this explains the gap on hover.
310+
*/
311+
const mtpSpaceHint = $derived(caps.kind === 'mtp' ? tString('fileExplorer.navigation.spaceMtpHint') : undefined)
312+
305313
/**
306314
* Snapshot id encoded in `currentPath` for the search-results pane (`search-results://<id>`),
307315
* or `null` for any other pane / unparseable path. Drives the breadcrumb label, the
@@ -2929,12 +2937,13 @@
29292937
stats={listingStats}
29302938
selectedCount={selection.selectedIndices.size}
29312939
{volumeSpace}
2940+
{mtpSpaceHint}
29322941
/>
29332942
<!--suppress HtmlWrongAttributeValue -- We know this is not a valid ARIA role, it's fine -->
29342943
<div
29352944
class="disk-usage-bar-wrapper"
29362945
use:tooltip={volumeSpace
2937-
? { text: formatBarTooltip(volumeSpace, (b) => formatFileSizeWithFormat(b, getFileSizeFormat())) }
2946+
? { text: formatBarTooltip(volumeSpace, (b) => formatFileSizeWithFormat(b, getFileSizeFormat()), mtpSpaceHint) }
29382947
: ''}
29392948
>
29402949
<div

apps/desktop/src/lib/file-explorer/selection/CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ lives in `FilePane.svelte` as a `Set<number>`).
4141
briefly after a theme/accent change clears the cache) and swaps seamlessly to the live accent-tinted OS icon once
4242
`get_icons` populates the cache. The component subscribes to `$iconCacheVersion` to re-render then.
4343

44-
## Symlink and stale-size hints (`SelectionInfo`)
44+
## Status-bar hints (`SelectionInfo`)
4545

46+
- **Phone-storage hint (MTP)** tooltips the free-space readout on `caps.kind === 'mtp'` volumes (`mtpSpaceHint` from
47+
`FilePane`), explaining the folders-vs-used-space gap. See [DETAILS.md](DETAILS.md).
4648
- **Stale (hourglass) indicator** appears when directory sizes may be incomplete: in `selection-summary` mode while
4749
`isScanning()` and dirs are selected, and in `file-info` mode via the shared `getDirSizeDisplayState(...)` (the same
4850
decider FullList uses, so Brief's status bar matches Full's size column). File sizes come from metadata and are always

apps/desktop/src/lib/file-explorer/selection/DETAILS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ Other layout: filename truncation uses `useShortenMiddle` with `preferBreakAt: '
5454
`measureDateColumnWidth(formatDateTime)` to stay in sync with FullList; `formatDateTime` comes from
5555
`reactive-settings.svelte`.
5656

57+
## Phone-storage hint (MTP)
58+
59+
On a phone reached over USB (MTP), the disk-space readout reports the whole device userdata partition, but Cmdr can only
60+
browse the shared-storage subtree; apps and system data make up the rest and aren't reachable over MTP. So the visible
61+
folders add up to far less than the space reported as used, which reads as a Cmdr bug. A hover hint closes that gap.
62+
63+
- The copy lives in `fileExplorer.navigation.spaceMtpHint`. `FilePane` resolves it to the `mtpSpaceHint` prop only when
64+
`caps.kind === 'mtp'` (the A6-correct discriminant, not a volume-id string); it's `undefined` for every other kind.
65+
- Both surfaces that show the figure carry it: `SelectionInfo` tooltips the visible free-space text (the number users
66+
read), and `FilePane` passes the same string as the third arg of `formatBarTooltip` for the disk-usage bar, appended
67+
after the size/level sentences. One catalog key, so the two never drift.
68+
- It rides on `use:tooltip`, so it's hover-only: a touch/keyboard user reading the footer text never sees it. Making the
69+
footer text itself focusable is the honest a11y fix if that matters later.
70+
5771
## `FileIcon.svelte`
5872

5973
Props: `file: FileEntry`, `syncIcon?: string` (URL for sync overlay badge).

apps/desktop/src/lib/file-explorer/selection/SelectionInfo.svelte

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@
5757
selectedCount: number
5858
/** Disk space info for current volume (null when unavailable) */
5959
volumeSpace?: VolumeSpaceInfo | null
60+
/**
61+
* Phone-storage caveat for the disk-space readout, set only on MTP
62+
* volumes. When present, it tooltips the free/total text to explain why
63+
* the browsable folders add up to less than the used space.
64+
*/
65+
mtpSpaceHint?: string
6066
}
6167
62-
const { viewMode, entry, currentDirModifiedAt, stats, selectedCount, volumeSpace }: Props = $props()
68+
const { viewMode, entry, currentDirModifiedAt, stats, selectedCount, volumeSpace, mtpSpaceHint }: Props = $props()
6369
6470
// ========================================================================
6571
// Display mode determination
@@ -243,7 +249,7 @@
243249
{#if displayMode === 'empty'}
244250
<span class="summary-text">{tString('fileExplorer.selectionInfo.nothingHere')}</span>
245251
{#if volumeSpace}
246-
<span class="disk-space-text">{diskSpaceStatusText(volumeSpace)}</span>
252+
<span class="disk-space-text" use:tooltip={mtpSpaceHint ?? ''}>{diskSpaceStatusText(volumeSpace)}</span>
247253
{/if}
248254
{:else if displayMode === 'file-info' && entry}
249255
<!-- Brief mode without selection: show file info -->
@@ -297,13 +303,13 @@
297303
{#if datePlaceholder !== null}{datePlaceholder}{:else}<DateLabel modifiedAt={dateTimestamp} />{/if}
298304
</span>
299305
{#if volumeSpace}
300-
<span class="disk-space-text">{diskSpaceStatusText(volumeSpace)}</span>
306+
<span class="disk-space-text" use:tooltip={mtpSpaceHint ?? ''}>{diskSpaceStatusText(volumeSpace)}</span>
301307
{/if}
302308
{:else if displayMode === 'no-selection'}
303309
<!-- Full mode without selection: show totals -->
304310
<span class="summary-text">{noSelectionText}</span>
305311
{#if volumeSpace}
306-
<span class="disk-space-text">{diskSpaceStatusText(volumeSpace)}</span>
312+
<span class="disk-space-text" use:tooltip={mtpSpaceHint ?? ''}>{diskSpaceStatusText(volumeSpace)}</span>
307313
{/if}
308314
{:else if displayMode === 'selection-summary' && stats}
309315
<!-- Selection summary -->

apps/desktop/src/lib/intl/keys.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ export type MessageKey =
824824
| 'fileExplorer.navigation.reorderFavoritesFailed'
825825
| 'fileExplorer.navigation.searchResultsVolume'
826826
| 'fileExplorer.navigation.spaceFetchFailed'
827+
| 'fileExplorer.navigation.spaceMtpHint'
827828
| 'fileExplorer.navigation.spaceRetrying'
828829
| 'fileExplorer.navigation.spaceRetryingAuto'
829830
| 'fileExplorer.navigation.spaceRetryingText'

apps/desktop/src/lib/intl/messages/de/fileExplorer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,10 @@
11771177
"@fileExplorer.navigation.spaceUnavailableText": {
11781178
"sourceHash": "ca18449"
11791179
},
1180+
"fileExplorer.navigation.spaceMtpHint": "Auf einem Smartphone zählen hier auch Apps und Systemdateien mit. Über USB erreicht Cmdr nur deine freigegebenen Dateien, daher ergeben die Ordner hier zusammen weniger als der belegte Speicherplatz.",
1181+
"@fileExplorer.navigation.spaceMtpHint": {
1182+
"sourceHash": "43ec861"
1183+
},
11801184
"fileExplorer.navigation.volumesStillUnreachable": "Weiterhin nicht erreichbar. Versuche es später erneut",
11811185
"@fileExplorer.navigation.volumesStillUnreachable": {
11821186
"sourceHash": "e4661d2"

apps/desktop/src/lib/intl/messages/en/fileExplorer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,10 @@
17251725
"@fileExplorer.navigation.spaceUnavailableText": {
17261726
"description": "Short inline text shown in place of a volume''s disk-space figure when its free space couldn''t be fetched. Tight space; keep it short."
17271727
},
1728+
"fileExplorer.navigation.spaceMtpHint": "On a phone, this also counts apps and system files. Over USB, Cmdr can reach only your shared files, so the folders here add up to less than the used space.",
1729+
"@fileExplorer.navigation.spaceMtpHint": {
1730+
"description": "Hover tooltip on a connected phone''s disk-space readout (the free/total figure and its usage bar), shown only for phones reached over USB (MTP). Explains why the visible folders add up to far less than the space reported as used: a phone''s apps and system data count toward usage but can''t be browsed over USB. \"USB\" is the cable connection; \"Cmdr\" is the app name (do not translate)."
1731+
},
17281732

17291733
"fileExplorer.navigation.volumesStillUnreachable": "Still unreachable. Try again later",
17301734
"@fileExplorer.navigation.volumesStillUnreachable": {

apps/desktop/src/lib/intl/messages/es/fileExplorer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,10 @@
11711171
"@fileExplorer.navigation.spaceUnavailableText": {
11721172
"sourceHash": "ca18449"
11731173
},
1174+
"fileExplorer.navigation.spaceMtpHint": "En un teléfono, esto también incluye apps y archivos del sistema. Por USB, Cmdr solo puede acceder a tus archivos compartidos, así que las carpetas que ves aquí suman menos que el espacio usado.",
1175+
"@fileExplorer.navigation.spaceMtpHint": {
1176+
"sourceHash": "43ec861"
1177+
},
11741178
"fileExplorer.navigation.volumesStillUnreachable": "Sigue sin poder accederse. Inténtalo de nuevo más tarde",
11751179
"@fileExplorer.navigation.volumesStillUnreachable": {
11761180
"sourceHash": "e4661d2"

0 commit comments

Comments
 (0)