From 9f80e84fa0c82cf8b7a683393051318b4923b372 Mon Sep 17 00:00:00 2001 From: Li Zhineng Date: Sat, 12 Jul 2025 16:48:26 +0800 Subject: communicate progress --- app.css | 40 ++++++++++++++++++++++ index.html | 1 + main.mjs | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/app.css b/app.css index d55b049..aa6c1ec 100644 --- a/app.css +++ b/app.css @@ -324,6 +324,46 @@ body { background-position: center; } +.progress { + margin-block-start: 2rem; + padding-inline: 3rem 1rem; +} + +.progress__item { + position: relative; + font-size: 0.875rem; + line-height: 2.25rem; + color: var(--color-zinc-700); + list-style: none; +} + +.progress__item::before { + position: absolute; + display: block; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none'%3E%3Crect width='16' height='16' y='.002' fill='%233F3F46' rx='8'/%3E%3C/svg%3E"); + width: 1rem; + height: 1rem; + left: -2rem; + top: 0.625rem; + content: ''; +} + +.progress__item[data-current] { + color: var(--color-zinc-300); +} + +.progress__item[data-current]::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none'%3E%3Crect width='16' height='16' y='.002' fill='%233F3F46' rx='8'/%3E%3Crect width='6.4' height='6.4' x='4.8' y='4.802' fill='%239F9FA9' rx='3.2'/%3E%3C/svg%3E"); +} + +.progress__item[data-complete] { + color: var(--color-zinc-300); +} + +.progress__item[data-complete]::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none'%3E%3Crect width='16' height='16' y='.002' fill='%2314532D' rx='8'/%3E%3Cpath fill='%2386EFAC' d='m6.873 9.231-1.485-1.48-.638.635 2.123 2.116 4.377-4.364-.638-.636-3.74 3.729Z'/%3E%3C/svg%3E"); +} + .retry-message { display: none; } diff --git a/index.html b/index.html index 86e3b8b..4371271 100644 --- a/index.html +++ b/index.html @@ -108,6 +108,7 @@ 👀 The list has been dismissed. Try again?

+
    diff --git a/main.mjs b/main.mjs index 5c74731..9d0d025 100644 --- a/main.mjs +++ b/main.mjs @@ -563,6 +563,9 @@ class CommunicationForm extends Form { /** @type {string} */ #descriptionText + /** @type {Progress} */ + #progress + #handshakeCommand #wifiCredentialsCommand #identityCommand @@ -590,6 +593,7 @@ class CommunicationForm extends Form { this.#messages = new IncomingMessageHandler() this.#messages.onMessage(this.handleMessage.bind(this)) this.#setupCounter() + this.#setupProgress() this.#handshakeCommand = new HandshakeCommand() this.#wifiCredentialsCommand = new ConfigureWifiCommand('', '') this.#identityCommand = new RequestIdentityCommand() @@ -611,25 +615,40 @@ class CommunicationForm extends Form { this.#description = this.form.querySelector('.form__description') this.#descriptionText = this.#description ? this.#description.textContent : '' this.#countdown = new Countdown(this.#description, 10) - this.#countdown.onComplete(this.transitToFailureResultForm.bind(this)) + this.#countdown.onComplete(() => { + this.#progress.clear() + this.disconnectIfNeeded() + this.transitToFailureResultForm() + }) this.#countdown.onStop(() => { this.#description.textContent = this.#descriptionText }) } + #setupProgress() { + const el = this.form.querySelector('[data-slot="progress"]') + this.#progress = new Progress(el, [ + { id: 'handshake', name: 'Say a hello to the machine' }, + { id: 'wifi', name: 'Send Wi-Fi credentials' }, + { id: 'identity', name: 'Receive the device\'s identity' } + ]) + } + async startPairing() { try { await this.connect() } catch { this.showRetryOption() - } finally { - this.disconnectIfNeeded() } } async connect() { await this.#handler.connect() + this.#countdown.start() + this.#progress.render() + + this.#progress.markAsCurrent('handshake') await this.#handler.dispatch(this.#handshakeCommand) } @@ -661,6 +680,8 @@ class CommunicationForm extends Form { * @param {CompleteMessage} message */ handleHandshakeMessage(message) { + this.#progress.markAsComplete('handshake') + this.#progress.markAsCurrent('wifi') this.#handler.dispatch(this.#wifiCredentialsCommand) } @@ -668,6 +689,8 @@ class CommunicationForm extends Form { * @param {CompleteMessage} message */ handleWifiCredentialsMessage(message) { + this.#progress.markAsComplete('wifi') + this.#progress.markAsCurrent('identity') this.#handler.dispatch(this.#identityCommand) } @@ -676,6 +699,9 @@ class CommunicationForm extends Form { */ handleIdentityMessage(message) { this.#countdown.stop() + this.#progress.markAsComplete('identity') + this.#progress.clear() + this.disconnectIfNeeded() this.transitToSuccessResultForm() } @@ -733,6 +759,85 @@ class FailureForm extends ProgressibleForm { } } +class Progress { + #el + #steps + + /** + * @param {HTMLElement|null} el - The HTML element to display the progress. + * @param {{ id: string, name: string }[]} steps - The progress steps. + */ + constructor(el, steps) { + this.#el = el + this.#steps = steps + if (this.#el === null) { + throw new Error('The HTML element could not be found to mount the progress.') + } + } + + render() { + if (! this.#el.classList.contains('progress')) { + this.#el.classList.add('progress') + } + + for (const step of this.#steps) { + const el = document.createElement('li') + el.innerText = step.name + el.classList.add('progress__item') + el.dataset.progress = step.id + this.#el.appendChild(el) + } + } + + clear() { + this.#el.innerHTML = '' + } + + /** + * @param {string} step - The step ID. + */ + markAsCurrent(step) { + this.markAsDefault(step) + + const el = this.#stepElement(step) + el.dataset.current = '' + } + + /** + * @param {string} step - The step ID. + */ + markAsComplete(step) { + this.markAsDefault(step) + + const el = this.#stepElement(step) + el.dataset.complete = '' + } + + /** + * @param {string} step - The step ID. + */ + markAsDefault(step) { + const el = this.#stepElement(step) + if ('current' in el.dataset) { + delete el.dataset.current + } + if ('complete' in el.dataset) { + delete el.dataset.complete + } + } + + /** + * @param {string} id - The step ID. + */ + #stepElement(id) { + const el = this.#el.querySelector(`[data-progress="${id}"]`) + if (! el) { + throw new Error(`Progress step "${step}" does not exist.`) + } + return el + } +} + class Application { static supportBluetoothApi() { return 'bluetooth' in navigator -- cgit v1.2.3