|
1 | 1 | (() => { |
2 | 2 | const config = [ |
3 | 3 | { |
4 | | - title: 'Select Firmware', |
5 | | - description: 'Choose which firmware you\'ll be using with your device.', |
| 4 | + title: 'Select Satellite1 Firmware', |
| 5 | + description: 'Choose which firmware you\'ll be using with your Satellite1.', |
6 | 6 | choices: [ |
7 | 7 | { |
8 | 8 | id: "stable", |
|
37 | 37 | let stage = 0; |
38 | 38 | const selections = ['stable', 'default'] |
39 | 39 |
|
| 40 | + function isBetaSelected() { |
| 41 | + return selections[0] === 'beta'; |
| 42 | + } |
| 43 | + |
| 44 | + function activeStepIndexes() { |
| 45 | + return isBetaSelected() ? [0] : config.map((_, idx) => idx); |
| 46 | + } |
| 47 | + |
| 48 | + function isSummaryStage() { |
| 49 | + return !activeStepIndexes().includes(stage); |
| 50 | + } |
| 51 | + |
| 52 | + function getPreviousStage() { |
| 53 | + const steps = activeStepIndexes(); |
| 54 | + if (isSummaryStage()) return steps[steps.length - 1]; |
| 55 | + |
| 56 | + const idx = steps.indexOf(stage); |
| 57 | + return idx > 0 ? steps[idx - 1] : stage; |
| 58 | + } |
| 59 | + |
| 60 | + function getNextStage() { |
| 61 | + const steps = activeStepIndexes(); |
| 62 | + const idx = steps.indexOf(stage); |
| 63 | + return idx >= 0 && idx < steps.length - 1 ? steps[idx + 1] : config.length; |
| 64 | + } |
| 65 | + |
| 66 | + function selectedLabel(idx) { |
| 67 | + return idx === 1 ? 'Selected mmWave' : config[idx].title.replace(/^Select\b/, 'Selected'); |
| 68 | + } |
| 69 | + |
| 70 | + function selectedValue(selected, idx) { |
| 71 | + if (idx !== 0) return selected.title; |
| 72 | + |
| 73 | + const version = firmwareVersion(selected); |
| 74 | + return `${selected.title}${version ? ` (${version})` : ''}`; |
| 75 | + } |
| 76 | + |
| 77 | + function choiceTitle(choice, idx) { |
| 78 | + if (idx !== 0) return choice.title; |
| 79 | + |
| 80 | + const version = firmwareVersion(choice); |
| 81 | + return `${choice.title}${version ? ` (${version})` : ''}`; |
| 82 | + } |
| 83 | + |
| 84 | + function firmwareVersion(choice) { |
| 85 | + return window.fphFirmwareVersions?.[choice.id] || ''; |
| 86 | + } |
| 87 | + |
| 88 | + function firmwareReleaseUrl(choice) { |
| 89 | + const version = firmwareVersion(choice); |
| 90 | + return window.fphFirmwareReleaseUrls?.[choice.id] |
| 91 | + || (version ? `https://github.com/FutureProofHomes/Satellite1-ESPHome/releases/tag/${version}/` : ''); |
| 92 | + } |
| 93 | + |
| 94 | + function releaseLink(choice, className = 'firmware-release-link') { |
| 95 | + const releaseUrl = firmwareReleaseUrl(choice); |
| 96 | + if (!releaseUrl) return null; |
| 97 | + |
| 98 | + return el('a', { |
| 99 | + className, |
| 100 | + href: releaseUrl, |
| 101 | + textContent: 'Read release notes', |
| 102 | + attr: { |
| 103 | + target: '_blank', |
| 104 | + rel: 'noopener', |
| 105 | + }, |
| 106 | + }); |
| 107 | + } |
| 108 | + |
| 109 | + function summaryRow(label, value) { |
| 110 | + const row = el('p'); |
| 111 | + row.append( |
| 112 | + el('strong', { textContent: label }), |
| 113 | + document.createTextNode(`: ${value}`) |
| 114 | + ); |
| 115 | + return row; |
| 116 | + } |
| 117 | + |
| 118 | + function firmwareSummaryRow(selected) { |
| 119 | + const version = firmwareVersion(selected); |
| 120 | + const releaseUrl = firmwareReleaseUrl(selected); |
| 121 | + const row = el('p'); |
| 122 | + row.append( |
| 123 | + el('strong', { textContent: selectedLabel(0) }), |
| 124 | + document.createTextNode(': ') |
| 125 | + ); |
| 126 | + |
| 127 | + if (releaseUrl) { |
| 128 | + row.append(el('a', { |
| 129 | + href: releaseUrl, |
| 130 | + textContent: selectedValue(selected, 0), |
| 131 | + attr: { |
| 132 | + target: '_blank', |
| 133 | + rel: 'noopener', |
| 134 | + 'aria-label': `Read release notes for ${selected.title} ${version}`, |
| 135 | + }, |
| 136 | + })); |
| 137 | + } else { |
| 138 | + row.append(document.createTextNode(selectedValue(selected, 0))); |
| 139 | + } |
| 140 | + |
| 141 | + return row; |
| 142 | + } |
| 143 | + |
40 | 144 | function setAttr(el, obj) { |
41 | 145 | Object.entries(obj).forEach(([k, v]) => el[k] = v); |
42 | 146 | return el |
|
59 | 163 | attr: { 'aria-label': 'Progress' } |
60 | 164 | }) |
61 | 165 | const fragment = document.createDocumentFragment(); |
62 | | - config.forEach((_, idx) => { |
| 166 | + const steps = activeStepIndexes(); |
| 167 | + const currentPosition = steps.indexOf(stage); |
| 168 | + const completedPosition = isSummaryStage() ? steps.length : currentPosition; |
| 169 | + steps.forEach((_, idx) => { |
63 | 170 | const dot = el('div', { |
64 | 171 | className: 'dot', |
65 | | - attr: { 'aria-current': idx === stage ? 'step' : 'false' } |
| 172 | + attr: { 'aria-current': idx === currentPosition ? 'step' : 'false' } |
66 | 173 | }) |
67 | | - if (idx === stage) dot.classList.add('active'); |
68 | | - else if (idx < stage) dot.classList.add('done'); |
| 174 | + if (idx === currentPosition) dot.classList.add('active'); |
| 175 | + else if (idx < completedPosition) dot.classList.add('done'); |
69 | 176 | fragment.appendChild(dot); |
70 | 177 | }); |
71 | 178 | nav.appendChild(fragment); |
72 | 179 | return nav; |
73 | 180 | } |
74 | 181 |
|
75 | 182 | function getBackBtn() { |
| 183 | + const hasPrevious = isSummaryStage() || activeStepIndexes().indexOf(stage) > 0; |
76 | 184 | const back = el('button', { |
77 | 185 | className: 'md-button md-button--secondary', |
78 | | - textContent: 'Back', |
79 | | - disabled: stage <= 0, |
| 186 | + textContent: '← Back', |
| 187 | + disabled: !hasPrevious, |
80 | 188 | onclick: (e) => { |
81 | 189 | e.stopPropagation(); |
82 | | - stage -= 1; |
| 190 | + stage = getPreviousStage(); |
83 | 191 | render(); |
84 | 192 | } |
85 | 193 | }); |
86 | 194 |
|
87 | | - if (stage <= 0) { |
| 195 | + if (!hasPrevious) { |
88 | 196 | back.classList.add('hidden'); |
89 | 197 | } |
90 | 198 | return back |
|
168 | 276 | textContent: 'Next', |
169 | 277 | onclick: (e) => { |
170 | 278 | e.stopPropagation(); |
171 | | - stage += 1; |
| 279 | + stage = getNextStage(); |
172 | 280 | render(); |
173 | 281 | } |
174 | 282 | }) |
|
207 | 315 | const fragment = document.createDocumentFragment(); |
208 | 316 | config[stage].choices.forEach((choice, i) => { |
209 | 317 | const checked = selections[stage] === choice.id; |
210 | | - const btn = el('button', { |
211 | | - className: 'md-button md-button--primary choice-btn', |
212 | | - textContent: choice.title, |
| 318 | + const isFirmwareStep = stage === 0; |
| 319 | + const btn = el(isFirmwareStep ? 'div' : 'button', { |
| 320 | + className: 'choice-btn', |
213 | 321 | dataset: { id: choice.id }, |
214 | 322 | tabIndex: 0, |
215 | 323 | attr: { |
216 | 324 | role: 'radio', |
217 | 325 | 'aria-checked': checked |
218 | 326 | }, |
219 | | - onclick: render |
| 327 | + onclick: render, |
| 328 | + onkeydown: (e) => { |
| 329 | + if (!isFirmwareStep || !['Enter', ' '].includes(e.key)) return; |
| 330 | + e.preventDefault(); |
| 331 | + btn.click(); |
| 332 | + } |
220 | 333 | }) |
221 | 334 |
|
| 335 | + btn.append( |
| 336 | + el('span', { |
| 337 | + className: 'choice-check', |
| 338 | + textContent: '✓', |
| 339 | + attr: { 'aria-hidden': 'true' }, |
| 340 | + }), |
| 341 | + el('span', { |
| 342 | + className: 'choice-label', |
| 343 | + textContent: choiceTitle(choice, stage), |
| 344 | + }) |
| 345 | + ); |
| 346 | + |
222 | 347 | if (checked) btn.classList.add('selected'); |
223 | | - fragment.appendChild(btn); |
| 348 | + |
| 349 | + const link = isFirmwareStep ? releaseLink(choice) : null; |
| 350 | + if (link) { |
| 351 | + const choiceWrapper = el('div', { className: 'choice-option' }); |
| 352 | + choiceWrapper.append(btn, link); |
| 353 | + fragment.appendChild(choiceWrapper); |
| 354 | + } else { |
| 355 | + fragment.appendChild(btn); |
| 356 | + } |
224 | 357 | }); |
225 | 358 |
|
226 | 359 | choicesDiv.appendChild(fragment); |
|
232 | 365 | function summary() { |
233 | 366 | const section = el('section', { className: 'md-typeset' }); |
234 | 367 | const fragment = document.createDocumentFragment(); |
235 | | - config.forEach((step, idx) => { |
| 368 | + activeStepIndexes().forEach((idx) => { |
| 369 | + const step = config[idx]; |
236 | 370 | const selected = step.choices.find(i => i.id === selections[idx]); |
237 | 371 | if (!selected) return; |
238 | 372 | fragment.appendChild( |
239 | | - el('p', { textContent: `${step.title}: ${selected.title}` }) |
| 373 | + idx === 0 ? firmwareSummaryRow(selected) : summaryRow(selectedLabel(idx), selectedValue(selected, idx)) |
240 | 374 | ); |
241 | 375 | }); |
| 376 | + if (isBetaSelected()) { |
| 377 | + fragment.appendChild( |
| 378 | + summaryRow( |
| 379 | + 'Selected mmWave', |
| 380 | + 'Automatically supports both LD2410 and LD2450 if/when a sensor is attached.' |
| 381 | + ) |
| 382 | + ); |
| 383 | + } |
242 | 384 | const firmware = selections[0] === 'stable' ? '' : `-${selections[0]}`; |
243 | | - const mmwave = selections[1] === 'default' ? '' : `.${selections[1]}`; |
| 385 | + const mmwave = selections[0] === 'stable' && selections[1] !== 'default' ? `.${selections[1]}` : ''; |
244 | 386 | section.append( |
245 | 387 | el('h2', { textContent: 'Summary' }), |
246 | 388 | fragment, |
|
254 | 396 | function handleChoice(e) { |
255 | 397 | const btn = e.target.closest('.choice-btn'); |
256 | 398 | if (!btn) return; |
257 | | - const group = btn.parentElement; |
| 399 | + const group = btn.closest('.choices'); |
258 | 400 | if (group && group.dataset.step) { |
259 | | - selections[Number(group.dataset.step)] = btn.dataset.id; |
| 401 | + const step = Number(group.dataset.step); |
| 402 | + selections[step] = btn.dataset.id; |
| 403 | + if (step === 0 && isBetaSelected()) { |
| 404 | + selections[1] = 'default'; |
| 405 | + } |
260 | 406 | render(); |
261 | 407 | } |
262 | 408 | } |
263 | 409 |
|
264 | 410 | function render() { |
265 | 411 | const el = document.getElementById('firmware-selector'); |
266 | 412 | if (el) { |
267 | | - const renderStep = stage < config.length; |
| 413 | + const renderStep = !isSummaryStage(); |
268 | 414 | el.replaceChildren(); |
269 | 415 | el.addEventListener('click', handleChoice); |
270 | 416 | el.append(renderStep ? step() : summary()); |
|
0 commit comments