forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDownloadsLink.tsx
More file actions
117 lines (112 loc) · 5.5 KB
/
Copy pathDownloadsLink.tsx
File metadata and controls
117 lines (112 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// The way to the download window, at the foot of whatever screen it is on.
//
// It is the only route there since the Downloads tab went (chrome/tabs.ts),
// which is a reason to keep it findable and not a reason to give it the space
// the eye lands on first. One whole-corridor package is downloaded once and
// deleted maybe never, so a hiker opens this a handful of times ever - against
// a legend they open all day. It goes last, under a rule, where a footer link
// goes: present, not competing.
//
// It began directly under the background picker, on the grounds that the
// picker is the only other control in the app that mentions the downloaded
// map. The pairing was right and the position was not: the picker was the
// FIRST thing in the legend then, so a link beneath it sat near the top of the
// panel - prime space for a rare errand. Separating them cost the pairing, and
// for a while the legend answered "which background" at one end and "how do I
// get one" at the other. The picker came down to meet it instead: in the
// legend the two are one block at the foot (chrome/Legend.tsx), which is the
// arrangement this comment first described, at the end of the panel rather
// than the start of it.
//
// In Settings they are still apart - the picker belongs to "The map" group
// there, under a heading, and the screen scrolls - and that is fine, because a
// settings screen is read top to bottom by someone already looking for a
// setting.
//
// One component in two homes - the legend and Settings - rather than a copy in
// each, for the same reason BackgroundPicker is one component: two copies of a
// control drift, and then the app has two names for one thing.
//
// SINCE THE BAR, IT IS ALSO THE ONLY PLACE A DOWNLOAD IN PROGRESS IS VISIBLE
// FROM OUTSIDE ITS WINDOW. The transfer belongs to the shell, not to the
// window it was started from, so shutting that window left an app that looked
// idle while it spent someone's data - and the only way to check was to open
// the window again and hope. A rare errand does not earn a permanent readout;
// an errand that is HAPPENING RIGHT NOW does, for exactly as long as it is
// happening, and the link is already where somebody would go to look.
import { downloadPercent, type DownloadActivity } from '../lib/downloadActivity'
/** What the footer calls each wait. Two words at most, because this sits
* beside a label it must not outweigh - and the differences matter enough to
* say: one is spending signal, one is the phone reading its own disk (#197),
* and one is the trail data that has to arrive before either. The window is
* where each is explained in full. */
const ACTIVITY_WORD: Record<DownloadActivity['kind'], string> = {
preparing: 'Getting trail data',
downloading: 'Downloading',
checking: 'Checking',
}
export interface DownloadsLinkProps {
onOpen: () => void
/**
* Whether a finished corridor archive is on this phone, which is the whole
* difference between choosing a download and changing one.
*
* Two labels rather than one because "choose what to download" is wrong for
* someone who already has 314 MB of it, and "change your download" is a
* claim about a phone that may have nothing on it at all.
*/
hasDownload?: boolean
/**
* What is arriving right now, or null when nothing is
* (lib/downloadActivity.ts).
*
* Absent by default, and absent is silence: a footer that reserved room for
* a bar would spend that room on every screen for the sake of the few
* minutes a year one is moving. It appears when there is something to say
* and goes when there is not.
*/
downloadActivity?: DownloadActivity | null
}
export function DownloadsLink({
onOpen,
hasDownload = false,
downloadActivity = null,
}: DownloadsLinkProps) {
// Null while the trail data is still coming, and that is the honest answer
// rather than a placeholder 0%: those are four fetches of unannounced size,
// so there is no figure to round. The word alone carries it, and the bar
// arrives with the transfer it measures.
const percent =
downloadActivity === null || downloadActivity.kind === 'preparing'
? null
: downloadPercent(downloadActivity.doneBytes, downloadActivity.totalBytes)
return (
// The bar lives INSIDE the button rather than beside it, and that is a
// thumb decision before it is a markup one: the bar is what the eye lands
// on, so it is what a hiker taps, and a bar that is not part of the
// control is a tap that does nothing. It carries no role of its own for
// the same reason it cannot - a button's descendants are presentational to
// a screen reader, so a `role="progressbar"` in here would be silently
// dropped. The figure is said in the button's own text instead, which is
// what gets announced: "Choose what to download. Downloading 38%."
<button type="button" className="downloads-link" onClick={onOpen}>
<span className="downloads-link__line">
<span>
{hasDownload ? "Change what's downloaded" : 'Choose what to download'}
</span>
{downloadActivity !== null && (
<span className="downloads-link__status">
{ACTIVITY_WORD[downloadActivity.kind]}
{percent !== null && ` ${percent}%`}
{percent === null && '…'}
</span>
)}
</span>
{percent !== null && (
<span className="downloads-link__bar" aria-hidden="true">
<span className="downloads-link__bar-fill" style={{ width: `${percent}%` }} />
</span>
)}
</button>
)
}