A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: replace tab system webamp configurators

+516 -325
+128
docs/plans/per-source-listing-progress.md
··· 1 + # Per-source listing progress 2 + 3 + ## Context 4 + 5 + When tracks are processed, there are two phases: **listing** (discovering tracks from each source) and **metadata** (fetching tags/stats per track). Currently only the metadata phase reports progress. The listing phase can be slow (e.g. enumerating an S3 bucket) but appears as a black box — the UI has no visibility into it. 6 + 7 + The goal is to show per-source progress during listing, so the user sees something like "Listing sources (2/3)..." before the metadata phase kicks in. 8 + 9 + ## Architecture challenge 10 + 11 + The call chain crosses three worker boundaries: 12 + 13 + ``` 14 + process-tracks worker →(RPC)→ input configurator worker →(RPC)→ per-scheme workers 15 + ``` 16 + 17 + - `process-tracks/worker.js` calls `input.list(cachedTracks)` as one opaque RPC call 18 + - `configurator/input/worker.js` receives it, fans out to per-scheme workers via `Promise.all` 19 + - Each scheme worker (s3, https, opensubsonic) does the actual listing 20 + 21 + The `announce`/`listen` mechanism only communicates between a worker and **its owning element**. So announcements from the input configurator worker go to the `dc-input` element, not to the process-tracks element or worker. This means progress must be surfaced through the element layer. 22 + 23 + ## Plan 24 + 25 + ### 1. Add listing progress signal to input configurator worker 26 + 27 + **File:** `src/components/configurator/input/worker.js` 28 + 29 + - Import `signal`, `effect`, `announce` 30 + - Add module-level signal: `const $listingProgress = signal({ processed: 0, total: 0 })` 31 + - In the `list()` function, count groups and update `$listingProgress` as each source completes: 32 + 33 + ```js 34 + export async function list({ data, ports }) { 35 + const groups = await groupConsult({ data, ports }); 36 + const entries = Object.values(groups); 37 + 38 + $listingProgress.value = { processed: 0, total: entries.length }; 39 + let processed = 0; 40 + 41 + const promises = entries.map(async ({ available, scheme, tracks }) => { 42 + if (!available) { ... } 43 + const result = await input.list(tracks); 44 + processed++; 45 + $listingProgress.value = { processed, total: entries.length }; 46 + return result; 47 + }); 48 + 49 + const nested = await Promise.all(promises); 50 + return nested.flat(1); 51 + } 52 + ``` 53 + 54 + - In `ostiary()`, announce the signal and expose it via RPC: 55 + 56 + ```js 57 + ostiary((context) => { 58 + rpc(context, { ..., listingProgress: $listingProgress.get }); 59 + effect(() => announce("listingProgress", $listingProgress.value, context)); 60 + }); 61 + ``` 62 + 63 + ### 2. Expose listing progress from input configurator element 64 + 65 + **File:** `src/components/configurator/input/element.js` 66 + 67 + - Import `signal` from `@common/signal.js` and `listen` from `@common/worker.js` 68 + - Add a `#listingProgress` signal and a public `listingProgress` getter 69 + - Add `connectedCallback()` to set up `listen("listingProgress", ...)` on `this.workerLink()` 70 + 71 + ### 3. Proxy listing progress through the input orchestrator 72 + 73 + **File:** `src/components/orchestrator/input/element.js` 74 + 75 + - Add a `listingProgress` getter that delegates to `this.input.listingProgress()` 76 + 77 + ### 4. Add a phase to process-tracks progress 78 + 79 + **File:** `src/components/orchestrator/process-tracks/types.d.ts` 80 + 81 + - Extend the `Progress` type with a `phase` field: 82 + 83 + ```ts 84 + export type Progress = { 85 + phase: "listing" | "metadata"; 86 + processed: number; 87 + total: number; 88 + }; 89 + ``` 90 + 91 + **File:** `src/components/orchestrator/process-tracks/element.js` 92 + 93 + - The element already queries `input-selector` (the `do-input` element) 94 + - Add an `effect()` that watches `this.input.listingProgress()` while `isProcessing` is true, and maps it into `#progress` with `phase: "listing"` 95 + - When the worker's own metadata progress arrives (via the existing `listen("progress", ...)`), set it with `phase: "metadata"` 96 + 97 + **File:** `src/components/orchestrator/process-tracks/worker.js` 98 + 99 + - Update `$progress` initial value to include `phase: "metadata"` (the worker only knows about metadata) 100 + 101 + ### 5. Update the UI to show the phase 102 + 103 + **File:** `src/themes/webamp/configurators/input/element.js` 104 + 105 + - In `#renderProcessingProgress()`, read `phase` from the progress object 106 + - Show "Listing sources (2/3)..." during listing phase 107 + - Show "Gathering metadata (5/10)..." during metadata phase (current behavior) 108 + 109 + ## Files changed (6) 110 + 111 + | File | Change | Difficulty | 112 + |------|--------|-----------| 113 + | `src/components/configurator/input/worker.js` | Add `$listingProgress` signal, update `list()`, announce in `ostiary` | Low-medium | 114 + | `src/components/configurator/input/element.js` | Add signal, `connectedCallback`, `listen`, expose getter | Low | 115 + | `src/components/orchestrator/input/element.js` | Proxy `listingProgress` getter | Trivial | 116 + | `src/components/orchestrator/process-tracks/element.js` | Watch input's listing progress, merge into phased progress | Medium | 117 + | `src/components/orchestrator/process-tracks/worker.js` | Add `phase` to progress signal | Trivial | 118 + | `src/components/orchestrator/process-tracks/types.d.ts` | Add `phase` to `Progress` type | Trivial | 119 + | `src/themes/webamp/configurators/input/element.js` | Render phase-aware progress text | Low | 120 + 121 + **Overall difficulty: Medium.** Follows existing patterns (`announce`/`listen`, signals, proxied getters) throughout. The trickiest part is step 4 — having the process-tracks element watch the input element's listing progress and merge it with the worker's metadata progress into a single unified signal. 122 + 123 + ## Verification 124 + 125 + - Add an S3 source with enough files to make listing take a visible amount of time 126 + - Open the Overview tab in the input configurator 127 + - Observe "Listing sources (X/Y)..." during listing, transitioning to "Gathering metadata (X/Y)..." during metadata extraction 128 + - Confirm the progress bar advances during both phases
+223 -194
src/themes/webamp/configurators/input/element.js
··· 49 49 /** @type {import("@components/orchestrator/process-tracks/element.js").CLASS | undefined} */ (undefined), 50 50 ); 51 51 52 + $tab = signal("overview"); 53 + 52 54 // LIFECYCLE 53 55 54 56 /** ··· 246 248 [...(this.$output.value?.tracks.collection() ?? []), track], 247 249 ); 248 250 249 - /** @type {HTMLInputElement | null} */ 250 - const overviewTab = this.root().querySelector("#overview-tab"); 251 - if (overviewTab) overviewTab.checked = true; 251 + this.$tab.value = "overview"; 252 252 } 253 253 254 254 /** ··· 265 265 * @param {RenderArg} _ 266 266 */ 267 267 render({ html }) { 268 - const sources = this.$sourcesOrchestrator.value?.sources(); 269 - 270 268 return html` 271 269 <link rel="stylesheet" href="styles/vendor/98.css" /> 272 270 <link rel="stylesheet" href="themes/webamp/98-extra.css" /> ··· 300 298 padding: var(--radio-label-spacing); 301 299 } 302 300 303 - /* Copied styles from "li[aria-selected=true]" */ 304 - li:has(input:checked) { 301 + li[aria-selected="true"] { 305 302 padding-bottom: 2px; 306 303 margin-top: -2px; 307 304 background-color: var(--surface); 308 305 position: relative; 309 306 z-index: 8; 310 307 margin-left: -3px; 311 - } 312 - 313 - input { 314 - display: none 315 308 } 316 309 } 317 310 318 - .window-body { 319 - display: none 320 - } 321 - 322 - #tabbed:has(#overview-tab:checked) #overview-contents { display: block } 323 - #tabbed:has(#opensubsonic-tab:checked) #opensubsonic-contents { display: block } 324 - #tabbed:has(#s3-tab:checked) #s3-contents { display: block } 325 - #tabbed:has(#https-tab:checked) #https-contents { display: block } 326 - 327 311 /* LIST */ 328 312 329 313 table { ··· 349 333 350 334 <div id="tabbed"> 351 335 <menu role="tablist" class="multirows"> 352 - <li role="tab"> 353 - <label for="overview-tab"> 336 + <li role="tab" aria-selected="${this.$tab.value === "overview"}"> 337 + <label @click="${() => this.$tab.value = "overview"}"> 354 338 <span>Overview</span> 355 - <input name="input-tab" id="overview-tab" type="radio" checked="" /> 356 339 </label> 357 340 </li> 358 - <li role="tab"> 359 - <label for="https-tab"> 341 + <li role="tab" aria-selected="${this.$tab.value === "https"}"> 342 + <label @click="${() => this.$tab.value = "https"}"> 360 343 <span>HTTPS</span> 361 - <input name="input-tab" id="https-tab" type="radio" /> 362 344 </label> 363 345 </li> 364 - <li role="tab"> 365 - <label for="opensubsonic-tab"> 346 + <li role="tab" aria-selected="${this.$tab.value === "opensubsonic"}"> 347 + <label @click="${() => this.$tab.value = "opensubsonic"}"> 366 348 <span>OpenSubsonic</span> 367 - <input name="input-tab" id="opensubsonic-tab" type="radio" /> 368 349 </label> 369 350 </li> 370 - <li role="tab"> 371 - <label for="s3-tab"> 351 + <li role="tab" aria-selected="${this.$tab.value === "s3"}"> 352 + <label @click="${() => this.$tab.value = "s3"}"> 372 353 <span>S3</span> 373 - <input name="input-tab" id="s3-tab" type="radio" /> 374 354 </label> 375 355 </li> 376 356 </menu> 377 357 378 358 <div class="window" role="tabpanel"> 379 - <!-- Overview --> 380 - <div class="window-body" id="overview-contents"> 381 - <fieldset> 382 - <span class="with-icon with-icon--large"> 383 - <img 384 - src="images/icons/windows_98/cd_audio_cd_a-0.png" 385 - width="24" 386 - /> 387 - <span>Here you can configure where your audio comes from.<br />Add 388 - sources using the tabs above, then tracks will be processed 389 - automatically. 390 - </span> 391 - </span> 392 - </fieldset> 359 + ${this.#renderTab(html)} 360 + </div> 361 + </div> 362 + `; 363 + } 364 + 365 + /** 366 + * @param {RenderArg["html"]} html 367 + */ 368 + #renderTab(html) { 369 + switch (this.$tab.value) { 370 + case "overview": 371 + return this.#renderOverviewTab(html); 372 + case "https": 373 + return this.#renderHttpsTab(html); 374 + case "opensubsonic": 375 + return this.#renderOpenSubsonicTab(html); 376 + case "s3": 377 + return this.#renderS3Tab(html); 378 + default: 379 + return nothing; 380 + } 381 + } 382 + 383 + /** 384 + * @param {RenderArg["html"]} html 385 + */ 386 + #renderOverviewTab(html) { 387 + return html` 388 + <div class="window-body"> 389 + <fieldset> 390 + <span class="with-icon with-icon--large"> 391 + <img 392 + src="images/icons/windows_98/cd_audio_cd_a-0.png" 393 + width="24" 394 + /> 395 + <span>Here you can configure where your audio comes from.<br />Add sources 396 + using the tabs above, then tracks will be processed automatically. 397 + </span> 398 + </span> 399 + </fieldset> 400 + 401 + ${this.#renderProcessingProgress(html)} 402 + </div> 403 + `; 404 + } 405 + 406 + /** 407 + * @param {RenderArg["html"]} html 408 + */ 409 + #renderHttpsTab(html) { 410 + const sources = this.$sourcesOrchestrator.value?.sources(); 411 + 412 + return html` 413 + <div class="window-body"> 414 + <fieldset> 415 + ${this.#renderList( 416 + html, 417 + sources?.[HTTPS_SCHEME] ?? [], 418 + "Added URLs", 419 + )} 393 420 394 - ${this.#renderProcessingProgress(html)} 395 - </div> 421 + <p> 422 + <button disabled role="delete" @click="${this.#deleteSelected}"> 423 + Delete selected 424 + </button> 425 + </p> 426 + </fieldset> 396 427 397 - <!-- HTTPS --> 398 - <div class="window-body" id="https-contents"> 399 - <fieldset> 400 - ${this.renderList( 401 - html, 402 - sources?.[HTTPS_SCHEME] ?? [], 403 - "Added URLs", 404 - )} 428 + <form @submit="${this.#addHttpsUrl}"> 429 + <fieldset> 430 + <div class="field-row"> 431 + <label for="https-url">URL:</label> 432 + <input 433 + id="https-url" 434 + type="url" 435 + required 436 + placeholder="https://example.com/audio.mp3" 437 + /> 438 + </div> 439 + </fieldset> 405 440 406 - <p> 407 - <button disabled role="delete" @click="${this.#deleteSelected}"> 408 - Delete selected 409 - </button> 410 - </p> 411 - </fieldset> 441 + <p> 442 + <button type="submit" id="https-submit">Add URL</button> 443 + </p> 444 + </form> 445 + </div> 446 + `; 447 + } 412 448 413 - <form @submit="${this.#addHttpsUrl}"> 414 - <fieldset> 415 - <div class="field-row"> 416 - <label for="https-url">URL:</label> 417 - <input 418 - id="https-url" 419 - type="url" 420 - required 421 - placeholder="https://example.com/audio.mp3" 422 - /> 423 - </div> 424 - </fieldset> 449 + /** 450 + * @param {RenderArg["html"]} html 451 + */ 452 + #renderOpenSubsonicTab(html) { 453 + const sources = this.$sourcesOrchestrator.value?.sources(); 425 454 426 - <p> 427 - <button type="submit" id="https-submit">Add URL</button> 428 - </p> 429 - </form> 430 - </div> 455 + return html` 456 + <div class="window-body"> 457 + <fieldset> 458 + ${this.#renderList( 459 + html, 460 + sources?.[OPENSUBSONIC_SCHEME] ?? [], 461 + "Added servers", 462 + )} 431 463 432 - <!-- Opensubsonic --> 433 - <div class="window-body" id="opensubsonic-contents"> 434 - <fieldset> 435 - ${this.renderList( 436 - html, 437 - sources?.[OPENSUBSONIC_SCHEME] ?? [], 438 - "Added servers", 439 - )} 464 + <p> 465 + <button disabled role="delete" @click="${this.#deleteSelected}"> 466 + Delete selected 467 + </button> 468 + </p> 469 + </fieldset> 440 470 441 - <p> 442 - <button disabled role="delete" @click="${this.#deleteSelected}"> 443 - Delete selected 444 - </button> 445 - </p> 446 - </fieldset> 471 + <form @submit="${this.#addOpenSubsonicServer}"> 472 + <fieldset> 473 + <legend>Server details</legend> 447 474 448 - <form @submit="${this.#addOpenSubsonicServer}"> 449 - <fieldset> 450 - <legend>Server details</legend> 475 + <div class="field-row"> 476 + <label for="opensubsonic-host">Host domain:*</label> 477 + <input id="opensubsonic-host" type="text" required /> 478 + </div> 451 479 452 - <div class="field-row"> 453 - <label for="opensubsonic-host">Host domain:*</label> 454 - <input id="opensubsonic-host" type="text" required /> 455 - </div> 480 + <div class="field-row"> 481 + <label for="opensubsonic-tls">Use HTTPS/TLS:</label> 482 + <select id="opensubsonic-tls"> 483 + <option value="true" selected>Yes</option> 484 + <option value="false">No</option> 485 + </select> 486 + </div> 456 487 457 - <div class="field-row"> 458 - <label for="opensubsonic-tls">Use HTTPS/TLS:</label> 459 - <select id="opensubsonic-tls"> 460 - <option value="true" selected>Yes</option> 461 - <option value="false">No</option> 462 - </select> 463 - </div> 488 + <p> 489 + Either provide a username & password combination: 490 + </p> 464 491 465 - <p> 466 - Either provide a username & password combination: 467 - </p> 492 + <div class="field-row"> 493 + <label for="opensubsonic-username">Username:</label> 494 + <input id="opensubsonic-username" type="text" /> 495 + </div> 468 496 469 - <div class="field-row"> 470 - <label for="opensubsonic-username">Username:</label> 471 - <input id="opensubsonic-username" type="text" /> 472 - </div> 497 + <div class="field-row"> 498 + <label for="opensubsonic-password">Password:</label> 499 + <input id="opensubsonic-password" type="password" /> 500 + </div> 473 501 474 - <div class="field-row"> 475 - <label for="opensubsonic-password">Password:</label> 476 - <input id="opensubsonic-password" type="password" /> 477 - </div> 502 + <p> 503 + Or an API key: 504 + </p> 478 505 479 - <p> 480 - Or an API key: 481 - </p> 506 + <div class="field-row"> 507 + <label for="opensubsonic-apikey">API key:</label> 508 + <input id="opensubsonic-apikey" type="text" /> 509 + </div> 482 510 483 - <div class="field-row"> 484 - <label for="opensubsonic-apikey">API key:</label> 485 - <input id="opensubsonic-apikey" type="text" /> 486 - </div> 511 + <p> 512 + * are required fields. 513 + </p> 514 + </fieldset> 487 515 488 - <p> 489 - * are required fields. 490 - </p> 491 - </fieldset> 516 + <p> 517 + <button type="submit" id="opensubsonic-submit">Add server</button> 518 + </p> 519 + </form> 520 + </div> 521 + `; 522 + } 492 523 493 - <p> 494 - <button type="submit" id="opensubsonic-submit">Add server</button> 495 - </p> 496 - </form> 497 - </div> 524 + /** 525 + * @param {RenderArg["html"]} html 526 + */ 527 + #renderS3Tab(html) { 528 + const sources = this.$sourcesOrchestrator.value?.sources(); 498 529 499 - <!-- S3 --> 500 - <div class="window-body" id="s3-contents"> 501 - <fieldset> 502 - ${this.renderList( 503 - html, 504 - sources?.[S3_SCHEME] ?? [], 505 - "Added buckets", 506 - )} 530 + return html` 531 + <div class="window-body"> 532 + <fieldset> 533 + ${this.#renderList( 534 + html, 535 + sources?.[S3_SCHEME] ?? [], 536 + "Added buckets", 537 + )} 507 538 508 - <p> 509 - <button disabled role="delete" @click="${this.#deleteSelected}"> 510 - Delete selected 511 - </button> 512 - </p> 513 - </fieldset> 539 + <p> 540 + <button disabled role="delete" @click="${this.#deleteSelected}"> 541 + Delete selected 542 + </button> 543 + </p> 544 + </fieldset> 514 545 515 - <form @submit="${this.#addS3Bucket}"> 516 - <fieldset> 517 - <legend>Bucket details</legend> 546 + <form @submit="${this.#addS3Bucket}"> 547 + <fieldset> 548 + <legend>Bucket details</legend> 518 549 519 - <div class="field-row"> 520 - <label for="s3-access-key">Access Key:*</label> 521 - <input type="text" id="s3-access-key" required /> 522 - </div> 550 + <div class="field-row"> 551 + <label for="s3-access-key">Access Key:*</label> 552 + <input type="text" id="s3-access-key" required /> 553 + </div> 523 554 524 - <div class="field-row"> 525 - <label for="s3-secret-key">Secret Key:*</label> 526 - <input type="password" id="s3-secret-key" required /> 527 - </div> 555 + <div class="field-row"> 556 + <label for="s3-secret-key">Secret Key:*</label> 557 + <input type="password" id="s3-secret-key" required /> 558 + </div> 528 559 529 - <div class="field-row"> 530 - <label for="s3-bucket-name">Bucket Name:*</label> 531 - <input type="text" id="s3-bucket-name" required /> 532 - </div> 560 + <div class="field-row"> 561 + <label for="s3-bucket-name">Bucket Name:*</label> 562 + <input type="text" id="s3-bucket-name" required /> 563 + </div> 533 564 534 - <div class="field-row"> 535 - <label for="s3-host">Host:</label> 536 - <input 537 - type="text" 538 - id="s3-host" 539 - placeholder="s3.amazonaws.com" 540 - /> 541 - </div> 565 + <div class="field-row"> 566 + <label for="s3-host">Host:</label> 567 + <input 568 + type="text" 569 + id="s3-host" 570 + placeholder="s3.amazonaws.com" 571 + /> 572 + </div> 542 573 543 - <div class="field-row"> 544 - <label for="s3-region">Region:</label> 545 - <input 546 - type="text" 547 - id="s3-region" 548 - placeholder="us-east-1" 549 - /> 550 - </div> 574 + <div class="field-row"> 575 + <label for="s3-region">Region:</label> 576 + <input 577 + type="text" 578 + id="s3-region" 579 + placeholder="us-east-1" 580 + /> 581 + </div> 551 582 552 - <div class="field-row"> 553 - <label for="s3-path">Path:</label> 554 - <input type="text" id="s3-path" /> 555 - </div> 583 + <div class="field-row"> 584 + <label for="s3-path">Path:</label> 585 + <input type="text" id="s3-path" /> 586 + </div> 556 587 557 - <p> 558 - * are required fields. 559 - </p> 560 - </fieldset> 588 + <p> 589 + * are required fields. 590 + </p> 591 + </fieldset> 561 592 562 - <p> 563 - <button type="submit" id="s3-submit">Add bucket</button> 564 - </p> 565 - </form> 566 - </div> 567 - </div> 593 + <p> 594 + <button type="submit" id="s3-submit">Add bucket</button> 595 + </p> 596 + </form> 568 597 </div> 569 598 `; 570 599 } ··· 608 637 * @param {Array<{label: string, uri: string}>} list 609 638 * @param {string} title 610 639 */ 611 - renderList(html, list, title) { 640 + #renderList(html, list, title) { 612 641 return html` 613 642 <div class="sunken-panel"> 614 643 <table style="width: 100%;" @click="${this.#highlightTableEntry}">
+165 -131
src/themes/webamp/configurators/output/element.js
··· 26 26 /** @type {OutputOption<ATProtoOutputElement> | null} */ (null), 27 27 ); 28 28 29 + $tab = signal("overview"); 30 + 29 31 // LIFECYCLE 30 32 31 33 /** @override */ ··· 101 103 * @param {RenderArg} _ 102 104 */ 103 105 render({ html }) { 104 - const did = this.$atproto.value?.element.did() ?? null; 105 - const selectedOutput = 106 - this.$output.value && "selectedOutput" in this.$output.value 107 - ? this.$output.value.selectedOutput() 108 - : undefined; 109 - 110 106 return html` 111 107 <link rel="stylesheet" href="styles/vendor/98.css" /> 112 108 <link rel="stylesheet" href="themes/webamp/98-extra.css" /> ··· 141 137 padding: var(--radio-label-spacing); 142 138 } 143 139 144 - /* Copied styles from "li[aria-selected=true]" */ 145 - li:has(input:checked) { 140 + li[aria-selected="true"] { 146 141 padding-bottom: 2px; 147 142 margin-top: -2px; 148 143 background-color: var(--surface); ··· 150 145 z-index: 8; 151 146 margin-left: -3px; 152 147 } 153 - 154 - input { 155 - display: none 156 - } 157 - } 158 - 159 - .window-body { 160 - display: none 161 148 } 162 - 163 - #tabbed:has(#overview-tab:checked) #overview-contents { display: block } 164 - #tabbed:has(#atproto-tab:checked) #atproto-contents { display: block } 165 - #tabbed:has(#s3-tab:checked) #s3-contents { display: block } 166 149 </style> 167 150 168 151 <div id="tabbed"> 169 152 <menu role="tablist" class="multirows"> 170 - <li role="tab"> 171 - <label for="overview-tab"> 153 + <li role="tab" aria-selected="${this.$tab.value === "overview"}"> 154 + <label @click="${() => this.$tab.value = "overview"}"> 172 155 <span>Overview</span> 173 - <input name="output-tab" id="overview-tab" type="radio" checked="" /> 174 156 </label> 175 157 </li> 176 - <li role="tab"> 177 - <label for="atproto-tab"> 158 + <li role="tab" aria-selected="${this.$tab.value === "atproto"}"> 159 + <label @click="${() => this.$tab.value = "atproto"}"> 178 160 <span>AT Protocol</span> 179 - <input name="output-tab" id="atproto-tab" type="radio" /> 180 161 </label> 181 162 </li> 182 - <li role="tab"> 183 - <label for="s3-tab"> 163 + <li role="tab" aria-selected="${this.$tab.value === "s3"}"> 164 + <label @click="${() => this.$tab.value = "s3"}"> 184 165 <span>S3</span> 185 - <input name="output-tab" id="s3-tab" type="radio" /> 186 166 </label> 187 167 </li> 188 168 </menu> 189 169 190 170 <div class="window" role="tabpanel"> 191 - <!-- Overview --> 192 - <div class="window-body" id="overview-contents"> 193 - <fieldset> 194 - <span class="with-icon with-icon--large"> 195 - <img 196 - src="images/icons/windows_98/computer_user_pencil-0.png" 197 - width="24" 198 - /> 199 - <span>Here you can configure where to keep your user data.<br />Each 200 - storage method comes with its pros and cons.<br />By default your 201 - data is only kept locally here in the browser. 202 - </span> 203 - </span> 204 - </fieldset> 171 + ${this.#renderTab(html)} 172 + </div> 173 + </div> 174 + `; 175 + } 205 176 206 - <fieldset> 207 - <span class="with-icon with-icon--large"> 208 - <img 209 - src="images/icons/windows_98/msg_information-0.png" 210 - width="24" 211 - /> 212 - <span> 213 - Data does not transfer across storage methods!<br />You can however 214 - merge data between them though, if you wish to do so. 215 - </span> 216 - </span> 217 - </fieldset> 177 + /** 178 + * @param {RenderArg["html"]} html 179 + */ 180 + #renderTab(html) { 181 + switch (this.$tab.value) { 182 + case "overview": 183 + return this.#renderOverviewTab(html); 184 + case "atproto": 185 + return this.#renderAtprotoTab(html); 186 + case "s3": 187 + return this.#renderS3Tab(html); 188 + default: 189 + return nothing; 190 + } 191 + } 218 192 219 - <fieldset> 220 - <legend>Active storage method</legend> 221 - <div class="with-icon with-icon--large"> 222 - <img 223 - src="images/icons/windows_98/${selectedOutput 224 - ? `directory_channels-2.png` 225 - : `msg_warning-0.png`}" 226 - width="24" 227 - /> 228 - <div> 229 - ${this.$output.value && "selectedOutput" in this.$output.value 230 - ? selectedOutput 231 - ? html` 232 - <p> 233 - Selected output: 234 - <strong>${selectedOutput.label}</strong><br /> 235 - </p> 236 - <p> 237 - <button @click="${this 238 - .#handleDeactivate}">Deactivate</button> 239 - </p> 240 - ` 241 - : this.#defaultOutputMessage 242 - : this.#defaultOutputMessage} 243 - </div> 244 - </div> 245 - </fieldset> 193 + /** 194 + * @param {RenderArg["html"]} html 195 + */ 196 + #renderOverviewTab(html) { 197 + const selectedOutput = 198 + this.$output.value && "selectedOutput" in this.$output.value 199 + ? this.$output.value.selectedOutput() 200 + : undefined; 201 + 202 + return html` 203 + <div class="window-body"> 204 + <fieldset> 205 + <span class="with-icon with-icon--large"> 206 + <img 207 + src="images/icons/windows_98/computer_user_pencil-0.png" 208 + width="24" 209 + /> 210 + <span>Here you can configure where to keep your user data.<br />Each 211 + storage method comes with its pros and cons.<br />By default your data 212 + is only kept locally here in the browser. 213 + </span> 214 + </span> 215 + </fieldset> 216 + 217 + <fieldset> 218 + <span class="with-icon with-icon--large"> 219 + <img 220 + src="images/icons/windows_98/msg_information-0.png" 221 + width="24" 222 + /> 223 + <span> 224 + Data does not transfer across storage methods!<br />You can however 225 + merge data between them though, if you wish to do so. 226 + </span> 227 + </span> 228 + </fieldset> 229 + 230 + <fieldset> 231 + <legend>Active storage method</legend> 232 + <div class="with-icon with-icon--large"> 233 + <img 234 + src="images/icons/windows_98/${selectedOutput 235 + ? `directory_channels-2.png` 236 + : `msg_warning-0.png`}" 237 + width="24" 238 + /> 239 + <div> 240 + ${this.$output.value && 241 + "selectedOutput" in this.$output.value 242 + ? selectedOutput 243 + ? html` 244 + <p> 245 + Selected output: 246 + <strong>${selectedOutput.label}</strong><br /> 247 + </p> 248 + <p> 249 + <button @click="${this 250 + .#handleDeactivate}">Deactivate</button> 251 + </p> 252 + ` 253 + : this.#defaultOutputMessage 254 + : this.#defaultOutputMessage} 255 + </div> 246 256 </div> 257 + </fieldset> 258 + </div> 259 + `; 260 + } 247 261 248 - <!-- AT Protocol --> 249 - <div class="window-body" id="atproto-contents"> 250 - ${did 251 - ? html` 252 - <fieldset> 253 - <span class="with-icon with-icon--large"> 254 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 255 - <span>Signed in as <strong>${did}</strong></span> 256 - </span> 257 - </fieldset> 262 + /** 263 + * @param {RenderArg["html"]} html 264 + */ 265 + #renderAtprotoTab(html) { 266 + const did = this.$atproto.value?.element.did() ?? null; 267 + const selectedOutput = 268 + this.$output.value && "selectedOutput" in this.$output.value 269 + ? this.$output.value.selectedOutput() 270 + : undefined; 258 271 259 - <p class="button-row"> 260 - <button @click="${this 261 - .#handleAtprotoLogout}">Sign out</button> 262 - ${this.#renderAtprotoActivation(html, selectedOutput)} 263 - </p> 264 - ` 265 - : html` 266 - <fieldset> 267 - <span class="with-icon with-icon--large"> 268 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 269 - <span> 270 - Store your user data on the storage associated with your AT Protocol 271 - identity. 272 - </span> 273 - </span> 274 - </fieldset> 272 + const authenticated = () => { 273 + return html` 274 + <fieldset> 275 + <span class="with-icon with-icon--large"> 276 + <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 277 + <span>Signed in as <strong>${did}</strong></span> 278 + </span> 279 + </fieldset> 275 280 276 - <form @submit="${this.#handleAtprotoLogin}"> 277 - <fieldset> 278 - <div class="field-row"> 279 - <label for="atproto-handle">Your internet handle:</label> 280 - <input 281 - id="atproto-handle" 282 - type="text" 283 - required 284 - placeholder="you.bsky.social" 285 - /> 286 - </div> 287 - </fieldset> 281 + <p class="button-row"> 282 + <button @click="${this 283 + .#handleAtprotoLogout}">Sign out</button> 284 + ${this.#renderAtprotoActivation(html, selectedOutput)} 285 + </p> 286 + `; 287 + }; 288 288 289 - <p> 290 - <button type="submit" id="atproto-submit">Sign in</button> 291 - </p> 292 - </form> 293 - `} 294 - </div> 289 + const unauthenticated = () => { 290 + return html` 291 + <fieldset> 292 + <span class="with-icon with-icon--large"> 293 + <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 294 + <span> 295 + Store your user data on the storage associated with your AT Protocol 296 + identity. 297 + </span> 298 + </span> 299 + </fieldset> 295 300 296 - <!-- S3 --> 297 - <div class="window-body" id="s3-contents"> 298 - <p>TODO</p> 299 - </div> 300 - </div> 301 + <form @submit="${this.#handleAtprotoLogin}"> 302 + <fieldset> 303 + <div class="field-row"> 304 + <label for="atproto-handle">Your internet handle:</label> 305 + <input 306 + id="atproto-handle" 307 + type="text" 308 + required 309 + placeholder="you.bsky.social" 310 + /> 311 + </div> 312 + </fieldset> 313 + 314 + <p> 315 + <button type="submit" id="atproto-submit">Sign in</button> 316 + </p> 317 + </form> 318 + `; 319 + }; 320 + 321 + return html` 322 + <div class="window-body"> 323 + ${did ? authenticated() : unauthenticated()} 324 + </div> 325 + `; 326 + } 327 + 328 + /** 329 + * @param {RenderArg["html"]} html 330 + */ 331 + #renderS3Tab(html) { 332 + return html` 333 + <div class="window-body"> 334 + <p>TODO</p> 301 335 </div> 302 336 `; 303 337 }