summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLi Zhineng <[email protected]>2025-07-12 16:48:26 +0800
committerLi Zhineng <[email protected]>2025-07-12 16:48:26 +0800
commit9f80e84fa0c82cf8b7a683393051318b4923b372 (patch)
tree84e5e379ec1cd819f617e70c44792a503a411080
parent3f07ce0826968c1461e7bf6c3e8072bc18862416 (diff)
downloadsetup-9f80e84fa0c82cf8b7a683393051318b4923b372.tar.gz
setup-9f80e84fa0c82cf8b7a683393051318b4923b372.zip
communicate progress
-rw-r--r--app.css40
-rw-r--r--index.html1
-rw-r--r--main.mjs111
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.
<a href="javascript:void(0)" class="form__link retry-link">Try again?</a>
</p>
+ <ol data-slot="progress"></ol>
</form>
<form id="form-result-success" class="form">
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