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: remove winamp config ui

+18 -1831
-12
src/_data/facets.json
··· 147 147 "category": "Browsing", 148 148 "featured": true, 149 149 "desc": "Collection browser and search." 150 - }, 151 - { 152 - "url": "themes/winamp/configurators/input/facet/index.html", 153 - "title": "Winamp / Input", 154 - "category": "Data", 155 - "desc": "Add your audio sources." 156 - }, 157 - { 158 - "url": "themes/winamp/configurators/output/facet/index.html", 159 - "title": "Winamp / Output", 160 - "category": "Data", 161 - "desc": "Manage your data storage." 162 150 } 163 151 ]
-882
src/themes/winamp/configurators/input/element.js
··· 1 - import * as TID from "@atcute/tid"; 2 - 3 - import { 4 - DiffuseElement, 5 - nothing, 6 - query, 7 - queryOptional, 8 - } from "~/common/element.js"; 9 - import { signal } from "~/common/signal.js"; 10 - 11 - import { buildURI as buildIcecastURI } from "~/components/input/icecast/common.js"; 12 - import { buildURI as buildOpenSubsonicURI } from "~/components/input/opensubsonic/common.js"; 13 - import { buildURI as buildS3URI } from "~/components/input/s3/common.js"; 14 - import { isSupported as supportsLocalFsAccess } from "~/components/input/local/common.js"; 15 - 16 - import { SCHEME as HTTPS_SCHEME } from "~/components/input/https/constants.js"; 17 - import { SCHEME as ICECAST_SCHEME } from "~/components/input/icecast/constants.js"; 18 - import { SCHEME as LOCAL_SCHEME } from "~/components/input/local/constants.js"; 19 - import { SCHEME as OPENSUBSONIC_SCHEME } from "~/components/input/opensubsonic/constants.js"; 20 - import { SCHEME as S3_SCHEME } from "~/components/input/s3/constants.js"; 21 - 22 - import { highlightTableEntry } from "../../common/ui.js"; 23 - 24 - /** 25 - * @import {RenderArg} from "~/common/element.d.ts" 26 - * @import {Track} from "~/definitions/types.d.ts" 27 - * @import {InputElement} from "~/components/input/types.d.ts" 28 - * @import {OutputElement} from "~/components/output/types.d.ts" 29 - * 30 - * @import {Server as OpenSubsonicServer} from "~/components/input/opensubsonic/types.d.ts" 31 - * @import {Bucket as S3Bucket} from "~/components/input/s3/types.d.ts" 32 - */ 33 - 34 - class InputConfig extends DiffuseElement { 35 - constructor() { 36 - super(); 37 - this.attachShadow({ mode: "open" }); 38 - } 39 - 40 - // SIGNALS 41 - 42 - $input = signal( 43 - /** @type {InputElement | undefined} */ (undefined), 44 - ); 45 - 46 - $output = signal( 47 - /** @type {OutputElement | undefined} */ (undefined), 48 - ); 49 - 50 - $sourcesOrchestrator = signal( 51 - /** @type {import("~/components/orchestrator/sources/element.js").CLASS | undefined} */ (undefined), 52 - ); 53 - 54 - $processTracksOrchestrator = signal( 55 - /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | undefined} */ (undefined), 56 - ); 57 - 58 - $tab = signal("overview"); 59 - 60 - // LIFECYCLE 61 - 62 - /** 63 - * @override 64 - */ 65 - async connectedCallback() { 66 - super.connectedCallback(); 67 - 68 - /** @type {InputElement} */ 69 - const input = query(this, "input-selector"); 70 - 71 - /** @type {OutputElement} */ 72 - const output = query(this, "output-selector"); 73 - 74 - /** @type {import("~/components/orchestrator/sources/element.js").CLASS} */ 75 - const sourcesOrchestrator = query(this, "sources-orchestrator-selector"); 76 - 77 - /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | null} */ 78 - const processTracksOrchestrator = queryOptional( 79 - this, 80 - "process-tracks-orchestrator-selector", 81 - ); 82 - 83 - await customElements.whenDefined(input.localName); 84 - await customElements.whenDefined(output.localName); 85 - await customElements.whenDefined(sourcesOrchestrator.localName); 86 - 87 - if (processTracksOrchestrator) { 88 - await customElements.whenDefined(processTracksOrchestrator.localName); 89 - } 90 - 91 - this.$input.value = input; 92 - this.$output.value = output; 93 - this.$sourcesOrchestrator.value = sourcesOrchestrator; 94 - 95 - if (processTracksOrchestrator) { 96 - this.$processTracksOrchestrator.value = processTracksOrchestrator; 97 - } 98 - 99 - // EFFECTS 100 - 101 - let skip = true; 102 - 103 - this.effect(() => { 104 - if (skip) { 105 - skip = false; 106 - return; 107 - } 108 - 109 - const _trigger = sourcesOrchestrator.sources(); 110 - if (output.tracks.collection().state !== "loaded") return; 111 - processTracksOrchestrator?.process(); 112 - }); 113 - } 114 - 115 - // EVENTS 116 - 117 - #processSources = () => { 118 - this.$processTracksOrchestrator.value?.process(); 119 - }; 120 - 121 - /** 122 - * @param {Event} event 123 - */ 124 - #addOpenSubsonicServer = async (event) => { 125 - event.preventDefault(); 126 - 127 - /** @type {HTMLButtonElement | null} */ 128 - const button = this.root().querySelector("#opensubsonic-submit"); 129 - if (button) button.disabled = true; 130 - 131 - const host = this.formElement("opensubsonic-host")?.value; 132 - const tls = this.formElement("opensubsonic-tls")?.value === "true"; 133 - const username = this.formElement("opensubsonic-username")?.value; 134 - const password = this.formElement("opensubsonic-password")?.value; 135 - const apiKey = this.formElement("opensubsonic-apikey")?.value; 136 - 137 - if (!host) { 138 - throw new Error("Missing required `host` input value"); 139 - } 140 - 141 - /** @type {OpenSubsonicServer} */ 142 - const server = { 143 - host, 144 - tls, 145 - username, 146 - password, 147 - apiKey, 148 - }; 149 - 150 - const uri = buildOpenSubsonicURI(server); 151 - await this.addSource(uri); 152 - 153 - if (button) button.disabled = false; 154 - }; 155 - 156 - /** 157 - * @param {Event} event 158 - */ 159 - #addS3Bucket = async (event) => { 160 - event.preventDefault(); 161 - 162 - /** @type {HTMLButtonElement | null} */ 163 - const button = this.root().querySelector("s3-submit"); 164 - if (button) button.disabled = true; 165 - 166 - const accessKey = this.formElement("s3-access-key")?.value; 167 - const bucketName = this.formElement("s3-bucket-name")?.value; 168 - const host = this.formElement("s3-host")?.value; 169 - const path = this.formElement("s3-path")?.value; 170 - const region = this.formElement("s3-region")?.value; 171 - const secretKey = this.formElement("s3-secret-key")?.value; 172 - 173 - if (!accessKey) { 174 - throw new Error("Missing required `accessKey` input value"); 175 - } 176 - 177 - if (!bucketName) { 178 - throw new Error("Missing required `bucketName` input value"); 179 - } 180 - 181 - if (!secretKey) { 182 - throw new Error("Missing required `secretKey` input value"); 183 - } 184 - 185 - /** @type {S3Bucket} */ 186 - const bucket = { 187 - accessKey, 188 - bucketName, 189 - host: host?.length ? host : "s3.amazonaws.com", 190 - path: path?.length ? path : "/", 191 - region: region?.length ? region : "us-east-1", 192 - secretKey, 193 - }; 194 - 195 - const uri = buildS3URI(bucket); 196 - await this.addSource(uri); 197 - 198 - if (button) button.disabled = false; 199 - }; 200 - 201 - #addLocalDirectory = async () => { 202 - const localInput = /** @type {any} */ (this.$input.value)?.input 203 - ?.inputs?.()[LOCAL_SCHEME]; 204 - if (!localInput) return; 205 - 206 - const uri = await localInput.addDirectory(); 207 - await this.addSource(uri); 208 - }; 209 - 210 - #addLocalFiles = async () => { 211 - const localInput = /** @type {any} */ (this.$input.value)?.input 212 - ?.inputs?.()[LOCAL_SCHEME]; 213 - if (!localInput) return; 214 - 215 - const uris = await localInput.addFiles(); 216 - for (const uri of uris) { 217 - await this.addSource(uri); 218 - } 219 - }; 220 - 221 - /** 222 - * @param {Event} event 223 - */ 224 - #addHttpsUrl = async (event) => { 225 - event.preventDefault(); 226 - 227 - /** @type {HTMLButtonElement | null} */ 228 - const button = this.root().querySelector("#https-submit"); 229 - if (button) button.disabled = true; 230 - 231 - const url = this.formElement("https-url")?.value; 232 - 233 - if (!url) { 234 - throw new Error("Missing required `url` input value"); 235 - } 236 - 237 - await this.addSource(url); 238 - 239 - if (button) button.disabled = false; 240 - }; 241 - 242 - /** 243 - * @param {Event} event 244 - */ 245 - #addIcecastUrl = async (event) => { 246 - event.preventDefault(); 247 - 248 - /** @type {HTMLButtonElement | null} */ 249 - const button = this.root().querySelector("#icecast-submit"); 250 - if (button) button.disabled = true; 251 - 252 - const url = this.formElement("icecast-url")?.value; 253 - 254 - if (!url) { 255 - throw new Error("Missing required `url` input value"); 256 - } 257 - 258 - await this.addSource(buildIcecastURI(url)); 259 - 260 - if (button) button.disabled = false; 261 - }; 262 - 263 - /** 264 - * @param {Event} event 265 - */ 266 - #deleteSelected = async (event) => { 267 - const button = /** @type {HTMLElement} */ (event.target); 268 - const fieldset = event.target ? button.closest("fieldset") : null; 269 - if (!fieldset) return; 270 - 271 - const selected = fieldset.querySelector( 272 - "table tr.highlighted", 273 - ); 274 - if (!selected) return; 275 - 276 - const uri = selected.getAttribute("data-uri"); 277 - if (!uri) throw new Error("Missing `uri` attribute"); 278 - 279 - const tracksCol = this.$output.value?.tracks.collection(); 280 - const detachedTracks = await this.$input.value?.detach({ 281 - fileUriOrScheme: uri, 282 - tracks: tracksCol?.state === "loaded" ? tracksCol.data : [], 283 - }); 284 - 285 - if (detachedTracks) this.$output.value?.tracks.save(detachedTracks); 286 - }; 287 - 288 - /** @param {MouseEvent} event */ 289 - #highlightTableEntry(event) { 290 - highlightTableEntry(event); 291 - 292 - const fieldset = event.target 293 - ? /** @type {HTMLElement} */ (event.target).closest("fieldset") 294 - : null; 295 - if (!fieldset) return; 296 - 297 - fieldset.querySelector('button[role="delete"]')?.removeAttribute( 298 - "disabled", 299 - ); 300 - } 301 - 302 - // 🛠️ 303 - 304 - /** 305 - * @param {string} uri 306 - */ 307 - async addSource(uri) { 308 - const now = new Date().toISOString(); 309 - 310 - /** @type {Track} */ 311 - const track = { 312 - $type: "sh.diffuse.output.track", 313 - id: TID.now(), 314 - createdAt: now, 315 - updatedAt: now, 316 - kind: "placeholder", 317 - uri, 318 - }; 319 - 320 - const existingTracksCol = this.$output.value?.tracks.collection(); 321 - const existingTracks = existingTracksCol?.state === "loaded" 322 - ? existingTracksCol.data 323 - : []; 324 - 325 - await this.$output.value?.tracks.save([...existingTracks, track]); 326 - 327 - this.$tab.value = "overview"; 328 - this.#processSources(); 329 - } 330 - 331 - /** 332 - * @param {string} id 333 - * @returns {HTMLInputElement | null} 334 - */ 335 - formElement(id) { 336 - return this.root().querySelector(`#${id}`); 337 - } 338 - 339 - // RENDER 340 - 341 - /** 342 - * @param {RenderArg} _ 343 - */ 344 - render({ html }) { 345 - return html` 346 - <link rel="stylesheet" href="vendor/98.css" /> 347 - <link rel="stylesheet" href="themes/winamp/98-extra.css" /> 348 - 349 - <style> 350 - @import "./themes/winamp/98-vars.css"; 351 - 352 - .button-row { 353 - display: inline-flex; 354 - gap: var(--grouped-button-spacing); 355 - } 356 - 357 - #tabbed { 358 - display: flex; 359 - flex-direction: column; 360 - height: 100%; 361 - } 362 - 363 - fieldset { 364 - margin-bottom: var(--element-spacing); 365 - } 366 - 367 - .window { 368 - flex: 1; 369 - overflow-y: auto; 370 - } 371 - 372 - /* TABS */ 373 - 374 - menu[role="tablist"] { 375 - padding-top: 2px; 376 - 377 - li > label { 378 - cursor: pointer; 379 - display: block; 380 - padding: var(--radio-label-spacing); 381 - } 382 - 383 - li[aria-selected="true"] { 384 - padding-bottom: 2px; 385 - margin-top: -2px; 386 - background-color: var(--surface); 387 - position: relative; 388 - z-index: 8; 389 - margin-left: -3px; 390 - } 391 - } 392 - 393 - /* LIST */ 394 - 395 - table { 396 - color: black; 397 - table-layout: fixed; 398 - } 399 - 400 - table td { 401 - overflow: hidden; 402 - text-overflow: ellipsis; 403 - } 404 - 405 - table tbody tr { 406 - cursor: pointer; 407 - } 408 - 409 - /* FORMS */ 410 - 411 - input, select, textarea { 412 - color: rgb(34, 34, 34); 413 - flex: 1; 414 - } 415 - </style> 416 - 417 - <div id="tabbed"> 418 - <menu role="tablist" class="multirows"> 419 - <li role="tab" aria-selected="${this.$tab.value === "overview"}"> 420 - <label @click="${() => this.$tab.value = "overview"}"> 421 - <span>Overview</span> 422 - </label> 423 - </li> 424 - <li role="tab" aria-selected="${this.$tab.value === "https"}"> 425 - <label @click="${() => this.$tab.value = "https"}"> 426 - <span>HTTPS</span> 427 - </label> 428 - </li> 429 - <li role="tab" aria-selected="${this.$tab.value === "icecast"}"> 430 - <label @click="${() => this.$tab.value = "icecast"}"> 431 - <span>Icecast</span> 432 - </label> 433 - </li> 434 - <li role="tab" aria-selected="${this.$tab.value === "local"}"> 435 - <label @click="${() => this.$tab.value = "local"}"> 436 - <span>Local</span> 437 - </label> 438 - </li> 439 - <li role="tab" aria-selected="${this.$tab.value === "opensubsonic"}"> 440 - <label @click="${() => this.$tab.value = "opensubsonic"}"> 441 - <span>OpenSubsonic</span> 442 - </label> 443 - </li> 444 - <li role="tab" aria-selected="${this.$tab.value === "s3"}"> 445 - <label @click="${() => this.$tab.value = "s3"}"> 446 - <span>S3</span> 447 - </label> 448 - </li> 449 - </menu> 450 - 451 - <div class="window" role="tabpanel"> 452 - ${this.#renderTab(html)} 453 - </div> 454 - </div> 455 - `; 456 - } 457 - 458 - /** 459 - * @param {RenderArg["html"]} html 460 - */ 461 - #renderTab(html) { 462 - switch (this.$tab.value) { 463 - case "overview": 464 - return this.#renderOverviewTab(html); 465 - case "https": 466 - return this.#renderHttpsTab(html); 467 - case "icecast": 468 - return this.#renderIcecastTab(html); 469 - case "local": 470 - return this.#renderLocalTab(html); 471 - case "opensubsonic": 472 - return this.#renderOpenSubsonicTab(html); 473 - case "s3": 474 - return this.#renderS3Tab(html); 475 - default: 476 - return nothing; 477 - } 478 - } 479 - 480 - /** 481 - * @param {RenderArg["html"]} html 482 - */ 483 - #renderOverviewTab(html) { 484 - return html` 485 - <div class="window-body"> 486 - <fieldset> 487 - <span class="with-icon with-icon--large"> 488 - <img 489 - src="images/icons/windows_98/cd_audio_cd_a-0.png" 490 - width="24" 491 - /> 492 - <span>Here you can configure where your audio comes from.<br />Add sources 493 - using the tabs above, then tracks will be processed automatically. 494 - </span> 495 - </span> 496 - </fieldset> 497 - 498 - ${this.#renderProcessingProgress(html)} 499 - </div> 500 - `; 501 - } 502 - 503 - /** 504 - * @param {RenderArg["html"]} html 505 - */ 506 - #renderHttpsTab(html) { 507 - const sources = this.$sourcesOrchestrator.value?.sources(); 508 - 509 - return html` 510 - <div class="window-body"> 511 - <fieldset> 512 - ${this.#renderList( 513 - html, 514 - sources?.[HTTPS_SCHEME] ?? [], 515 - "Added URLs", 516 - )} 517 - 518 - <p> 519 - <button disabled role="delete" @click="${this.#deleteSelected}"> 520 - Delete selected 521 - </button> 522 - </p> 523 - </fieldset> 524 - 525 - <form @submit="${this.#addHttpsUrl}"> 526 - <fieldset> 527 - <div class="field-row"> 528 - <label for="https-url">URL:</label> 529 - <input 530 - id="https-url" 531 - type="url" 532 - required 533 - placeholder="https://example.com/audio.mp3" 534 - /> 535 - </div> 536 - </fieldset> 537 - 538 - <p> 539 - <button type="submit" id="https-submit">Add URL</button> 540 - </p> 541 - </form> 542 - </div> 543 - `; 544 - } 545 - 546 - /** 547 - * @param {RenderArg["html"]} html 548 - */ 549 - #renderIcecastTab(html) { 550 - const sources = this.$sourcesOrchestrator.value?.sources(); 551 - 552 - return html` 553 - <div class="window-body"> 554 - <fieldset> 555 - ${this.#renderList( 556 - html, 557 - sources?.[ICECAST_SCHEME] ?? [], 558 - "Added streams", 559 - )} 560 - 561 - <p> 562 - <button disabled role="delete" @click="${this.#deleteSelected}"> 563 - Delete selected 564 - </button> 565 - </p> 566 - </fieldset> 567 - 568 - <form @submit="${this.#addIcecastUrl}"> 569 - <fieldset> 570 - <div class="field-row"> 571 - <label for="icecast-url">URL:</label> 572 - <input 573 - id="icecast-url" 574 - type="url" 575 - required 576 - placeholder="https://example.com/stream" 577 - /> 578 - </div> 579 - </fieldset> 580 - 581 - <p> 582 - <button type="submit" id="icecast-submit">Add stream</button> 583 - </p> 584 - </form> 585 - </div> 586 - `; 587 - } 588 - 589 - /** 590 - * @param {RenderArg["html"]} html 591 - */ 592 - #renderLocalTab(html) { 593 - const sources = this.$sourcesOrchestrator.value?.sources(); 594 - 595 - return html` 596 - <div class="window-body"> 597 - <fieldset> 598 - ${this.#renderList( 599 - html, 600 - sources?.[LOCAL_SCHEME] ?? [], 601 - "Added directories & files", 602 - )} 603 - 604 - <p> 605 - <button disabled role="delete" @click="${this.#deleteSelected}"> 606 - Delete selected 607 - </button> 608 - </p> 609 - </fieldset> 610 - 611 - <fieldset> 612 - ${supportsLocalFsAccess() 613 - ? html` 614 - <p class="button-row"> 615 - <button @click="${this.#addLocalDirectory}"> 616 - Add directory 617 - </button> 618 - <button @click="${this.#addLocalFiles}">Add files</button> 619 - </p> 620 - ` 621 - : html` 622 - <p class="with-icon with-icon--large"> 623 - <img 624 - src="images/icons/windows_98/msg_warning-0.png" 625 - width="24" 626 - /> 627 - Your browser does not support the File System Access API.<br /> 628 - Use a Chromium-based browser to add local files. 629 - <!-- TODO: Add an alternative facet where you can just drag & drop your local files. --> 630 - </p> 631 - `} 632 - </fieldset> 633 - </div> 634 - `; 635 - } 636 - 637 - /** 638 - * @param {RenderArg["html"]} html 639 - */ 640 - #renderOpenSubsonicTab(html) { 641 - const sources = this.$sourcesOrchestrator.value?.sources(); 642 - 643 - return html` 644 - <div class="window-body"> 645 - <fieldset> 646 - ${this.#renderList( 647 - html, 648 - sources?.[OPENSUBSONIC_SCHEME] ?? [], 649 - "Added servers", 650 - )} 651 - 652 - <p> 653 - <button disabled role="delete" @click="${this.#deleteSelected}"> 654 - Delete selected 655 - </button> 656 - </p> 657 - </fieldset> 658 - 659 - <form @submit="${this.#addOpenSubsonicServer}"> 660 - <fieldset> 661 - <legend>Server details</legend> 662 - 663 - <div class="field-row"> 664 - <label for="opensubsonic-host">Host domain:*</label> 665 - <input id="opensubsonic-host" type="text" required /> 666 - </div> 667 - 668 - <div class="field-row"> 669 - <label for="opensubsonic-tls">Use HTTPS/TLS:</label> 670 - <select id="opensubsonic-tls"> 671 - <option value="true" selected>Yes</option> 672 - <option value="false">No</option> 673 - </select> 674 - </div> 675 - 676 - <p> 677 - Either provide a username & password combination: 678 - </p> 679 - 680 - <div class="field-row"> 681 - <label for="opensubsonic-username">Username:</label> 682 - <input id="opensubsonic-username" type="text" /> 683 - </div> 684 - 685 - <div class="field-row"> 686 - <label for="opensubsonic-password">Password:</label> 687 - <input id="opensubsonic-password" type="password" /> 688 - </div> 689 - 690 - <p> 691 - Or an API key: 692 - </p> 693 - 694 - <div class="field-row"> 695 - <label for="opensubsonic-apikey">API key:</label> 696 - <input id="opensubsonic-apikey" type="text" /> 697 - </div> 698 - 699 - <p> 700 - * are required fields. 701 - </p> 702 - </fieldset> 703 - 704 - <p> 705 - <button type="submit" id="opensubsonic-submit">Add server</button> 706 - </p> 707 - </form> 708 - </div> 709 - `; 710 - } 711 - 712 - /** 713 - * @param {RenderArg["html"]} html 714 - */ 715 - #renderS3Tab(html) { 716 - const sources = this.$sourcesOrchestrator.value?.sources(); 717 - 718 - return html` 719 - <div class="window-body"> 720 - <fieldset> 721 - ${this.#renderList( 722 - html, 723 - sources?.[S3_SCHEME] ?? [], 724 - "Added buckets", 725 - )} 726 - 727 - <p> 728 - <button disabled role="delete" @click="${this.#deleteSelected}"> 729 - Delete selected 730 - </button> 731 - </p> 732 - </fieldset> 733 - 734 - <form @submit="${this.#addS3Bucket}"> 735 - <fieldset> 736 - <legend>Bucket details</legend> 737 - 738 - <div class="field-row"> 739 - <label for="s3-access-key">Access Key:*</label> 740 - <input type="text" id="s3-access-key" required /> 741 - </div> 742 - 743 - <div class="field-row"> 744 - <label for="s3-secret-key">Secret Key:*</label> 745 - <input type="password" id="s3-secret-key" required /> 746 - </div> 747 - 748 - <div class="field-row"> 749 - <label for="s3-bucket-name">Bucket Name:*</label> 750 - <input type="text" id="s3-bucket-name" required /> 751 - </div> 752 - 753 - <div class="field-row"> 754 - <label for="s3-host">Host:</label> 755 - <input 756 - type="text" 757 - id="s3-host" 758 - placeholder="s3.amazonaws.com" 759 - /> 760 - </div> 761 - 762 - <div class="field-row"> 763 - <label for="s3-region">Region:</label> 764 - <input 765 - type="text" 766 - id="s3-region" 767 - placeholder="us-east-1" 768 - /> 769 - </div> 770 - 771 - <div class="field-row"> 772 - <label for="s3-path">Path:</label> 773 - <input type="text" id="s3-path" /> 774 - </div> 775 - 776 - <p> 777 - * are required fields. 778 - </p> 779 - </fieldset> 780 - 781 - <p> 782 - <button type="submit" id="s3-submit">Add bucket</button> 783 - </p> 784 - </form> 785 - </div> 786 - `; 787 - } 788 - 789 - /** 790 - * @param {RenderArg["html"]} html 791 - */ 792 - #renderProcessingProgress(html) { 793 - const orchestrator = this.$processTracksOrchestrator.value; 794 - if (!orchestrator?.isProcessing()) { 795 - return html` 796 - <fieldset> 797 - <div class="with-icon with-icon--large"> 798 - <img 799 - src="images/icons/windows_98/gears-0.png" 800 - width="24" 801 - /> 802 - <div> 803 - <p> 804 - Go through all the inputs you've added and get the last audio files and 805 - their metadata. 806 - </p> 807 - <p> 808 - <button @click="${this 809 - .#processSources}">Process sources</button> 810 - </p> 811 - </div> 812 - </div> 813 - </fieldset> 814 - `; 815 - } 816 - 817 - const { processed, total } = orchestrator.progress(); 818 - const percentage = total > 0 ? Math.round((processed / total) * 100) : 0; 819 - 820 - return html` 821 - <fieldset> 822 - <legend>Processing tracks</legend> 823 - <div class="with-icon with-icon--large"> 824 - <img 825 - src="images/icons/windows_98/gears-0.png" 826 - width="24" 827 - /> 828 - <span> 829 - ${total === 0 830 - ? `Going through all the inputs and gathering the tracks ...` 831 - : `Making sure each track has metadata & statistics (${processed} / ${total}) ...`} 832 - </span> 833 - </div> 834 - <div 835 - class="progress-indicator" 836 - style="margin-top: var(--grouped-element-spacing);" 837 - > 838 - <div class="progress-indicator-bar" style="width: ${percentage}%"></div> 839 - </div> 840 - </fieldset> 841 - `; 842 - } 843 - 844 - /** 845 - * @param {RenderArg["html"]} html 846 - * @param {Array<{label: string, uri: string}>} list 847 - * @param {string} title 848 - */ 849 - #renderList(html, list, title) { 850 - return html` 851 - <div class="sunken-panel"> 852 - <table style="width: 100%;" @click="${this.#highlightTableEntry}"> 853 - <thead> 854 - <tr> 855 - <th>${title}</th> 856 - </tr> 857 - </thead> 858 - <tbody> 859 - ${list.map((item) => 860 - html` 861 - <tr data-uri="${item.uri}"> 862 - <td>${item.label}</td> 863 - </tr> 864 - ` 865 - )} 866 - </tbody> 867 - </table> 868 - </div> 869 - `; 870 - } 871 - } 872 - 873 - export default InputConfig; 874 - 875 - //////////////////////////////////////////// 876 - // REGISTER 877 - //////////////////////////////////////////// 878 - 879 - export const CLASS = InputConfig; 880 - export const NAME = "dtw-input-config"; 881 - 882 - customElements.define(NAME, CLASS);
-23
src/themes/winamp/configurators/input/facet/index.html
··· 1 - <div class="window"> 2 - <div class="title-bar"> 3 - <div class="title-bar-icon"> 4 - <img src="images/icons/windows_98/directory_explorer-4.png" height="14" /> 5 - </div> 6 - <div class="title-bar-text" draggable="false">Input configurator</div> 7 - <div class="title-bar-controls"> 8 - <button aria-label="Close" onclick="window.close()"></button> 9 - </div> 10 - </div> 11 - <div class="window-body"> 12 - <div id="placeholder"></div> 13 - </div> 14 - </div> 15 - 16 - <style> 17 - @import "./styles/variables.css"; 18 - @import "./themes/winamp/fonts.css"; 19 - @import "./themes/winamp/facet.css"; 20 - @import "./vendor/98.css"; 21 - </style> 22 - 23 - <script type="module" src="themes/winamp/configurators/input/facet/index.inline.js"></script>
-20
src/themes/winamp/configurators/input/facet/index.inline.js
··· 1 - import foundation from "~/common/foundation.js"; 2 - import InputConfigElement from "~/themes/winamp/configurators/input/element.js"; 3 - 4 - // Set doc title 5 - document.title = "Input | Winamp | Diffuse"; 6 - 7 - const [inp, out, pro, sou] = await Promise.all([ 8 - foundation.configurator.input(), 9 - foundation.orchestrator.output(), 10 - foundation.orchestrator.processTracks({ disableWhenReady: true }), 11 - foundation.orchestrator.sources(), 12 - ]); 13 - 14 - const el = new InputConfigElement(); 15 - el.setAttribute("input-selector", inp.selector); 16 - el.setAttribute("output-selector", out.selector); 17 - el.setAttribute("sources-orchestrator-selector", sou.selector); 18 - el.setAttribute("process-tracks-orchestrator-selector", pro.selector); 19 - 20 - document.querySelector("#placeholder")?.replaceWith(el);
-830
src/themes/winamp/configurators/output/element.js
··· 1 - import { DiffuseElement, nothing, query } from "~/common/element.js"; 2 - import { signal } from "~/common/signal.js"; 3 - 4 - import { NAME as ATPROTO_NAME } from "~/components/output/raw/atproto/element.js"; 5 - import { NAME as S3_NAME } from "~/components/output/bytes/s3/element.js"; 6 - 7 - import { NAME as PASSKEY_NAME } from "~/components/transformer/output/refiner/track-uri-passkey/element.js"; 8 - 9 - /** 10 - * @import {ATProtoOutputElement} from "~/components/output/raw/atproto/types.d.ts" 11 - * 12 - * @import {Bucket as S3Bucket} from "~/components/input/s3/types.d.ts" 13 - * @import {S3OutputElement} from "~/components/output/bytes/s3/types.d.ts" 14 - * 15 - * @import {OutputElement} from "~/components/output/types.d.ts" 16 - * @import {OutputConfiguratorElement} from "~/components/configurator/output/types.d.ts" 17 - * @import TrackUriPasskeyTransformer from "~/components/transformer/output/refiner/track-uri-passkey/element.js"; 18 - * 19 - * @import {RenderArg} from "~/common/element.d.ts" 20 - */ 21 - 22 - /** 23 - * @typedef {OutputElement<any>} VariousOutputElements 24 - */ 25 - 26 - class OutputConfig extends DiffuseElement { 27 - constructor() { 28 - super(); 29 - this.attachShadow({ mode: "open" }); 30 - } 31 - 32 - // SIGNALS 33 - 34 - $output = signal( 35 - /** @type {OutputElement | OutputConfiguratorElement<VariousOutputElements> | undefined} */ (undefined), 36 - ); 37 - 38 - $atproto = signal( 39 - /** @type {ATProtoOutputElement | null} */ (null), 40 - ); 41 - 42 - $atprotoPasskey = signal( 43 - /** @type {TrackUriPasskeyTransformer | null} */ (null), 44 - ); 45 - 46 - $s3 = signal( 47 - /** @type {S3OutputElement | null} */ (null), 48 - ); 49 - 50 - $atprotoError = signal(/** @type {string | null} */ (null)); 51 - $passkeyError = signal(/** @type {string | null} */ (null)); 52 - $passkeyWorking = signal(false); 53 - $tab = signal("overview"); 54 - 55 - // LIFECYCLE 56 - 57 - /** @override */ 58 - async connectedCallback() { 59 - super.connectedCallback(); 60 - 61 - /** @type {OutputElement | OutputConfiguratorElement<VariousOutputElements>} */ 62 - const output = query(this, "output-selector"); 63 - 64 - await customElements.whenDefined(output.localName); 65 - 66 - this.$output.value = output; 67 - 68 - // Try setting up specific outputs 69 - const atproto = output.root().querySelector(ATPROTO_NAME); 70 - 71 - if (atproto) { 72 - this.$atproto.value = /** @type {ATProtoOutputElement} */ (atproto); 73 - } 74 - 75 - const atprotoPasskey = output.root().querySelector( 76 - `${PASSKEY_NAME}[namespace="atproto"]`, 77 - ); 78 - 79 - if (atprotoPasskey) { 80 - await customElements.whenDefined(PASSKEY_NAME); 81 - this.$atprotoPasskey.value = 82 - /** @type {TrackUriPasskeyTransformer} */ (atprotoPasskey); 83 - } 84 - 85 - const s3 = output.root().querySelector(S3_NAME); 86 - 87 - if (s3) { 88 - this.$s3.value = /** @type {S3OutputElement} */ (s3); 89 - } 90 - } 91 - 92 - // EVENTS 93 - 94 - /** @param {Event} event */ 95 - #handleAtprotoLogin = async (event) => { 96 - event.preventDefault(); 97 - 98 - /** @type {HTMLInputElement | null} */ 99 - const input = this.root().querySelector("#atproto-handle"); 100 - const handle = input?.value?.trim(); 101 - if (!handle) return; 102 - 103 - const atproto = this.$atproto.value; 104 - if (!atproto) return; 105 - 106 - const output = this.$output.value; 107 - if (!output || !("select" in output)) return; 108 - 109 - /** @type {HTMLButtonElement | null} */ 110 - const button = this.root().querySelector("#atproto-submit"); 111 - if (button) { 112 - button.disabled = true; 113 - button.textContent = "Loading ..."; 114 - } 115 - 116 - const option = (await output.options()).find((o) => 117 - o.label === "AT Protocol" 118 - ); 119 - 120 - if (option) await output.select(option.id); 121 - this.$atprotoError.value = null; 122 - 123 - try { 124 - await atproto.login(handle); 125 - } catch (err) { 126 - console.error(err); 127 - 128 - this.$atprotoError.value = err instanceof Error 129 - ? err.message 130 - : "login failed"; 131 - 132 - if (button) { 133 - button.disabled = false; 134 - button.textContent = "Sign in"; 135 - } 136 - } 137 - }; 138 - 139 - #handleAtprotoLogout = async () => { 140 - const atproto = this.$atproto.value; 141 - if (!atproto) return; 142 - 143 - await atproto.logout(); 144 - }; 145 - 146 - #handlePasskeySetup = async () => { 147 - const passkey = this.$atprotoPasskey.value; 148 - if (!passkey) return; 149 - 150 - this.$passkeyError.value = null; 151 - this.$passkeyWorking.value = true; 152 - 153 - try { 154 - await passkey.setupPasskey(); 155 - } catch (err) { 156 - this.$passkeyError.value = err instanceof Error 157 - ? err.message 158 - : "Passkey setup failed"; 159 - } finally { 160 - this.$passkeyWorking.value = false; 161 - } 162 - }; 163 - 164 - #handlePasskeyAdopt = async () => { 165 - const passkey = this.$atprotoPasskey.value; 166 - if (!passkey) return; 167 - 168 - this.$passkeyError.value = null; 169 - this.$passkeyWorking.value = true; 170 - 171 - try { 172 - await passkey.adoptPasskey(); 173 - } catch (err) { 174 - this.$passkeyError.value = err instanceof Error 175 - ? err.message 176 - : "Passkey adoption failed"; 177 - } finally { 178 - this.$passkeyWorking.value = false; 179 - } 180 - }; 181 - 182 - #handlePasskeyRemove = async () => { 183 - const passkey = this.$atprotoPasskey.value; 184 - if (!passkey) return; 185 - 186 - this.$passkeyError.value = null; 187 - await passkey.removePasskey(); 188 - }; 189 - 190 - /** @param {Event} event */ 191 - #handleAtprotoActivate = async (event) => { 192 - event.preventDefault(); 193 - 194 - const output = this.$output.value; 195 - if (!output || !("select" in output)) return; 196 - 197 - const atproto = this.$atproto.value; 198 - if (!atproto) return; 199 - 200 - const option = (await output.options()).find((o) => 201 - o.label === "AT Protocol" 202 - ); 203 - 204 - if (option) await output.select(option.id); 205 - }; 206 - 207 - /** 208 - * @param {Event} event 209 - */ 210 - #handleS3SetBucket = async (event) => { 211 - event.preventDefault(); 212 - 213 - const s3 = this.$s3.value; 214 - if (!s3) return; 215 - 216 - const output = this.$output.value; 217 - if (!output || !("select" in output)) return; 218 - 219 - /** @type {HTMLButtonElement | null} */ 220 - const button = this.root().querySelector("#s3-submit"); 221 - if (button) button.disabled = true; 222 - 223 - const accessKey = 224 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 225 - "#s3-access-key", 226 - ))?.value; 227 - const bucketName = 228 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 229 - "#s3-bucket-name", 230 - ))?.value; 231 - const host = 232 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 233 - "#s3-host", 234 - ))?.value; 235 - const path = 236 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 237 - "#s3-path", 238 - ))?.value; 239 - const region = 240 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 241 - "#s3-region", 242 - ))?.value; 243 - const secretKey = 244 - /** @type {HTMLInputElement | null} */ (this.root().querySelector( 245 - "#s3-secret-key", 246 - ))?.value; 247 - 248 - if (!accessKey || !bucketName || !secretKey) return; 249 - 250 - /** @type {S3Bucket} */ 251 - const bucket = { 252 - accessKey, 253 - bucketName, 254 - host: host?.length ? host.replace(/^\w+:\/\//, "") : "s3.amazonaws.com", 255 - path: path?.length ? path : "/", 256 - region: region?.length ? region : "us-east-1", 257 - secretKey, 258 - }; 259 - 260 - await s3.setBucket(bucket); 261 - 262 - const option = (await output.options()).find((o) => 263 - o.label === "AT Protocol" 264 - ); 265 - 266 - if (option) await output.select(option.id); 267 - if (button) button.disabled = false; 268 - }; 269 - 270 - #handleS3Unset = async () => { 271 - const s3 = this.$s3.value; 272 - if (!s3) return; 273 - 274 - await s3.unsetBucket(); 275 - }; 276 - 277 - /** @param {Event} event */ 278 - #handleS3Activate = async (event) => { 279 - event.preventDefault(); 280 - 281 - const output = this.$output.value; 282 - if (!output || !("select" in output)) return; 283 - 284 - const s3 = this.$s3.value; 285 - if (!s3) return; 286 - 287 - const option = (await output.options()).find((o) => o.label === "S3"); 288 - if (option) await output.select(option.id); 289 - }; 290 - 291 - #handleDeactivate = async () => { 292 - const output = this.$output.value; 293 - if (!output || !("deselect" in output)) return; 294 - 295 - await output.deselect(); 296 - }; 297 - 298 - // RENDER 299 - 300 - /** 301 - * @param {RenderArg} _ 302 - */ 303 - render({ html }) { 304 - return html` 305 - <link rel="stylesheet" href="vendor/98.css" /> 306 - <link rel="stylesheet" href="themes/winamp/98-extra.css" /> 307 - 308 - <style> 309 - @import "./themes/winamp/98-vars.css"; 310 - 311 - input, select, textarea { 312 - color: rgb(34, 34, 34); 313 - } 314 - 315 - .button-row { 316 - display: inline-flex; 317 - gap: var(--grouped-button-spacing); 318 - } 319 - 320 - #tabbed { 321 - display: flex; 322 - flex-direction: column; 323 - height: 100%; 324 - } 325 - 326 - .window { 327 - flex: 1; 328 - overflow-y: auto; 329 - } 330 - 331 - /* TABS */ 332 - 333 - menu[role="tablist"] { 334 - padding-top: 2px; 335 - 336 - li > label { 337 - cursor: pointer; 338 - display: block; 339 - padding: var(--radio-label-spacing); 340 - } 341 - 342 - li[aria-selected="true"] { 343 - padding-bottom: 2px; 344 - margin-top: -2px; 345 - background-color: var(--surface); 346 - position: relative; 347 - z-index: 8; 348 - margin-left: -3px; 349 - } 350 - } 351 - </style> 352 - 353 - <div id="tabbed"> 354 - <menu role="tablist" class="multirows"> 355 - <li role="tab" aria-selected="${this.$tab.value === "overview"}"> 356 - <label @click="${() => this.$tab.value = "overview"}"> 357 - <span>Overview</span> 358 - </label> 359 - </li> 360 - <li role="tab" aria-selected="${this.$tab.value === "atproto"}"> 361 - <label @click="${() => this.$tab.value = "atproto"}"> 362 - <span>AT Protocol</span> 363 - </label> 364 - </li> 365 - <li role="tab" aria-selected="${this.$tab.value === "s3"}"> 366 - <label @click="${() => this.$tab.value = "s3"}"> 367 - <span>S3</span> 368 - </label> 369 - </li> 370 - </menu> 371 - 372 - <div class="window" role="tabpanel"> 373 - ${this.#renderTab(html)} 374 - </div> 375 - </div> 376 - `; 377 - } 378 - 379 - /** 380 - * @param {RenderArg["html"]} html 381 - */ 382 - #renderTab(html) { 383 - switch (this.$tab.value) { 384 - case "overview": 385 - return this.#renderOverviewTab(html); 386 - case "atproto": 387 - return this.#renderAtprotoTab(html); 388 - case "s3": 389 - return this.#renderS3Tab(html); 390 - default: 391 - return nothing; 392 - } 393 - } 394 - 395 - /** 396 - * @param {RenderArg["html"]} html 397 - */ 398 - #renderOverviewTab(html) { 399 - const selectedOutput = 400 - this.$output.value && "selected" in this.$output.value 401 - ? this.$output.value.selected() 402 - : undefined; 403 - 404 - return html` 405 - <div class="window-body"> 406 - <fieldset> 407 - <span class="with-icon with-icon--large"> 408 - <img 409 - src="images/icons/windows_98/computer_user_pencil-0.png" 410 - width="24" 411 - /> 412 - <span>Here you can configure where to keep your user data.<br />Each 413 - storage method comes with its pros and cons.<br />By default your data 414 - is only kept locally here in the browser. 415 - </span> 416 - </span> 417 - </fieldset> 418 - 419 - <fieldset> 420 - <span class="with-icon with-icon--large"> 421 - <img 422 - src="images/icons/windows_98/msg_information-0.png" 423 - width="24" 424 - /> 425 - <span> 426 - Data does not transfer across storage methods!<br />You can however 427 - merge data between them though, if you wish to do so. 428 - </span> 429 - </span> 430 - </fieldset> 431 - 432 - <fieldset> 433 - <legend>Active storage method</legend> 434 - <div class="with-icon with-icon--large"> 435 - <img 436 - src="images/icons/windows_98/${selectedOutput 437 - ? `directory_channels-2.png` 438 - : `msg_warning-0.png`}" 439 - width="24" 440 - /> 441 - <div> 442 - ${this.$output.value && 443 - "selected" in this.$output.value 444 - ? selectedOutput 445 - ? html` 446 - <p> 447 - Selected output: 448 - <strong>${selectedOutput.label}</strong><br /> 449 - </p> 450 - <p> 451 - <button @click="${this 452 - .#handleDeactivate}">Deactivate</button> 453 - </p> 454 - ` 455 - : this.#defaultOutputMessage 456 - : this.#defaultOutputMessage} 457 - </div> 458 - </div> 459 - </fieldset> 460 - </div> 461 - `; 462 - } 463 - 464 - /** 465 - * @param {RenderArg["html"]} html 466 - */ 467 - #renderAtprotoTab(html) { 468 - const did = this.$atproto.value?.did() ?? null; 469 - const selectedOutput = 470 - this.$output.value && "selected" in this.$output.value 471 - ? this.$output.value.selected() 472 - : undefined; 473 - 474 - const authenticated = () => { 475 - return html` 476 - <fieldset> 477 - <span class="with-icon with-icon--large"> 478 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 479 - <span>Signed in as <strong>${did}</strong></span> 480 - </span> 481 - </fieldset> 482 - 483 - ${this.#renderPasskeySection(html)} 484 - 485 - <p class="button-row"> 486 - <button @click="${this.#handleAtprotoLogout}">Sign out</button> 487 - ${this.#renderAtprotoActivation(html, selectedOutput)} 488 - </p> 489 - `; 490 - }; 491 - 492 - const unauthenticated = () => { 493 - return html` 494 - <fieldset> 495 - <span class="with-icon with-icon--large"> 496 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 497 - <span> 498 - Store your user data on the storage associated with your AT Protocol 499 - identity. 500 - </span> 501 - </span> 502 - </fieldset> 503 - 504 - <fieldset> 505 - <form @submit="${this.#handleAtprotoLogin}" class="field-row"> 506 - <label for="atproto-handle">Your internet handle:</label> 507 - <input 508 - id="atproto-handle" 509 - type="text" 510 - required 511 - placeholder="you.bsky.social" 512 - /> 513 - </form> 514 - </fieldset> 515 - 516 - ${this.$atprotoError.value 517 - ? html` 518 - <fieldset> 519 - <span class="with-icon with-icon--large"> 520 - <img src="images/icons/windows_98/msg_error-0.png" width="24" /> 521 - <span> 522 - Sign in failed, please check the provided handle and try again. 523 - </span> 524 - </span> 525 - </fieldset> 526 - ` 527 - : nothing} ${this.#renderPasskeySection(html)} 528 - 529 - <p> 530 - <button @click="${this 531 - .#handleAtprotoLogin}" id="atproto-submit">Sign in</button> 532 - ${this.#renderAtprotoActivation(html, selectedOutput)} 533 - </p> 534 - `; 535 - }; 536 - 537 - return html` 538 - <div class="window-body"> 539 - ${did ? authenticated() : unauthenticated()} 540 - </div> 541 - `; 542 - } 543 - 544 - /** 545 - * @param {RenderArg["html"]} html 546 - */ 547 - #renderS3Tab(html) { 548 - const s3 = this.$s3.value; 549 - const ready = s3?.ready() ?? false; 550 - const selectedOutput = 551 - this.$output.value && "selected" in this.$output.value 552 - ? this.$output.value.selected() 553 - : undefined; 554 - 555 - const configured = () => { 556 - const bucket = s3?.bucket(); 557 - 558 - return html` 559 - <fieldset> 560 - <div class="with-icon with-icon--large"> 561 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 562 - <div> 563 - Bucket configured: 564 - <ul 565 - style="margin-bottom: 0; padding-left: 0; list-style-position: inside;" 566 - > 567 - <li>Name: <strong>${bucket?.bucketName}</strong></li> 568 - <li>Host: ${bucket?.host}</li> 569 - <li>Access key: ${bucket?.accessKey}</li> 570 - </ul> 571 - </div> 572 - </div> 573 - </fieldset> 574 - 575 - <fieldset> 576 - <span class="with-icon with-icon--large"> 577 - <img 578 - src="images/icons/windows_98/msg_information-0.png" 579 - width="24" 580 - /> 581 - <span> 582 - Make sure the bucket has CORS configured properly. 583 - </span> 584 - </span> 585 - </fieldset> 586 - 587 - <p class="button-row"> 588 - <button id="s3-unset-bucket" @click="${this.#handleS3Unset}"> 589 - Remove bucket configuration 590 - </button> 591 - ${this.#renderS3Activation(html, selectedOutput)} 592 - </p> 593 - `; 594 - }; 595 - 596 - const unconfigured = () => { 597 - return html` 598 - <fieldset> 599 - <span class="with-icon with-icon--large"> 600 - <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 601 - <span> 602 - Store your user data on an S3-compatible storage service. 603 - </span> 604 - </span> 605 - </fieldset> 606 - 607 - <form @submit="${this.#handleS3SetBucket}"> 608 - <fieldset> 609 - <legend>Bucket details</legend> 610 - 611 - <div class="field-row"> 612 - <label for="s3-access-key">Access Key:*</label> 613 - <input type="text" id="s3-access-key" required /> 614 - </div> 615 - 616 - <div class="field-row"> 617 - <label for="s3-secret-key">Secret Key:*</label> 618 - <input type="password" id="s3-secret-key" required /> 619 - </div> 620 - 621 - <div class="field-row"> 622 - <label for="s3-bucket-name">Bucket Name:*</label> 623 - <input type="text" id="s3-bucket-name" required /> 624 - </div> 625 - 626 - <div class="field-row"> 627 - <label for="s3-host">Host:</label> 628 - <input 629 - type="text" 630 - id="s3-host" 631 - placeholder="s3.amazonaws.com" 632 - /> 633 - </div> 634 - 635 - <div class="field-row"> 636 - <label for="s3-region">Region:</label> 637 - <input 638 - type="text" 639 - id="s3-region" 640 - placeholder="us-east-1" 641 - /> 642 - </div> 643 - 644 - <div class="field-row"> 645 - <label for="s3-path">Path:</label> 646 - <input type="text" id="s3-path" /> 647 - </div> 648 - 649 - <p> 650 - * are required fields. 651 - </p> 652 - </fieldset> 653 - 654 - <p> 655 - <button type="submit" id="s3-submit">Set bucket</button> 656 - ${this.#renderS3Activation(html, selectedOutput)} 657 - </p> 658 - </form> 659 - `; 660 - }; 661 - 662 - return html` 663 - <div class="window-body"> 664 - ${ready ? configured() : unconfigured()} 665 - </div> 666 - `; 667 - } 668 - 669 - /** 670 - * @param {RenderArg["html"]} html 671 - */ 672 - #renderPasskeySection(html) { 673 - const passkey = this.$atprotoPasskey.value; 674 - if (!passkey) return nothing; 675 - 676 - const passkeyActive = passkey.passkeyActive() ?? false; 677 - const lockedTracksCount = passkey.lockedTracks().length ?? 0; 678 - 679 - return html` 680 - <fieldset> 681 - <legend>Passkey encryption (optional)</legend> 682 - 683 - <div class="with-icon with-icon--large"> 684 - <img src="images/icons/windows_98/keys-5.png" width="24" /> 685 - 686 - <div> 687 - ${passkeyActive 688 - ? html` 689 - <p class="with-icon with-icon--large"> 690 - <input type="checkbox" checked /> 691 - <label>Passkey active — Track URIs are encrypted</label> 692 - </p> 693 - 694 - ${this.$passkeyError.value 695 - ? html` 696 - <fieldset> 697 - <span class="with-icon with-icon--large"> 698 - <img src="images/icons/windows_98/msg_error-0.png" width="24" /> 699 - <span>${this.$passkeyError.value}</span> 700 - </span> 701 - </fieldset> 702 - ` 703 - : nothing} 704 - 705 - <p> 706 - <button @click="${this 707 - .#handlePasskeyRemove}">Remove passkey</button> 708 - </p> 709 - 710 - <p> 711 - Removing the passkey will expose all the sensitive<br /> 712 - information that was previously encrypted. 713 - </p> 714 - ` 715 - : html` 716 - <p> 717 - Track URIs can optionally be encrypted so that passwords and<br /> 718 - other sensitive authentication details are kept private. 719 - </p> 720 - <p> 721 - Note that, with this enabled, other people can NOT play audio listed on your 722 - account. 723 - </p> 724 - 725 - ${this.$passkeyError.value 726 - ? html` 727 - <fieldset> 728 - <span class="with-icon with-icon--large"> 729 - <img src="images/icons/windows_98/msg_error-0.png" width="24" /> 730 - <span>${this.$passkeyError.value}</span> 731 - </span> 732 - </fieldset> 733 - ` 734 - : nothing} 735 - 736 - <p class="button-row"> 737 - <button 738 - ?disabled="${this.$passkeyWorking.value}" 739 - @click="${this.#handlePasskeySetup}" 740 - > 741 - ${this.$passkeyWorking.value 742 - ? "Setting up ..." 743 - : "Set up passkey encryption"} 744 - </button> 745 - <button 746 - ?disabled="${this.$passkeyWorking.value}" 747 - @click="${this.#handlePasskeyAdopt}" 748 - > 749 - ${this.$passkeyWorking.value 750 - ? "Authenticating ..." 751 - : "Use existing passkey"} 752 - </button> 753 - </p> 754 - `} 755 - </div> 756 - </div> 757 - </fieldset> 758 - 759 - ${lockedTracksCount > 0 760 - ? html` 761 - <fieldset> 762 - <p class="with-icon with-icon--large"> 763 - <img 764 - src="images/icons/windows_98/msg_warning-0.png" 765 - width="24" 766 - /> 767 - ${lockedTracksCount} encrypted track(s) cannot be played until you unlock them with 768 - your passkey. If you're already using a passkey, remember that you have to 769 - use same passkey as the one you originally locked the tracks with. 770 - </p> 771 - </fieldset> 772 - ` 773 - : nothing} 774 - `; 775 - } 776 - 777 - /** 778 - * @param {RenderArg['html']} html 779 - * @param {VariousOutputElements | null | undefined} selectedOutput 780 - */ 781 - #renderAtprotoActivation(html, selectedOutput) { 782 - const output = this.$output.value; 783 - if (!output || !("select" in output)) return nothing; 784 - 785 - const isActive = selectedOutput?.label === "AT Protocol"; 786 - 787 - return isActive 788 - ? html` 789 - <button @click="${this.#handleDeactivate}">Deactivate</button> 790 - ` 791 - : html` 792 - <button @click="${this 793 - .#handleAtprotoActivate}">Activate this storage</button> 794 - `; 795 - } 796 - 797 - /** 798 - * @param {RenderArg['html']} html 799 - * @param {VariousOutputElements | null | undefined} selectedOutput 800 - */ 801 - #renderS3Activation(html, selectedOutput) { 802 - const output = this.$output.value; 803 - if (!output || !("select" in output)) return nothing; 804 - 805 - const isActive = selectedOutput?.label === "S3"; 806 - 807 - return isActive 808 - ? html` 809 - <button @click="${this.#handleDeactivate}">Deactivate</button> 810 - ` 811 - : html` 812 - <button @click="${this 813 - .#handleS3Activate}">Activate this storage</button> 814 - `; 815 - } 816 - 817 - #defaultOutputMessage = 818 - "Storing data locally in the browser without any backup or syncing enabled."; 819 - } 820 - 821 - export default OutputConfig; 822 - 823 - //////////////////////////////////////////// 824 - // REGISTER 825 - //////////////////////////////////////////// 826 - 827 - export const CLASS = OutputConfig; 828 - export const NAME = "dtw-output-config"; 829 - 830 - customElements.define(NAME, CLASS);
-23
src/themes/winamp/configurators/output/facet/index.html
··· 1 - <div class="window"> 2 - <div class="title-bar"> 3 - <div class="title-bar-icon"> 4 - <img src="images/icons/windows_98/directory_explorer-4.png" height="14" /> 5 - </div> 6 - <div class="title-bar-text" draggable="false">Output configurator</div> 7 - <div class="title-bar-controls"> 8 - <button aria-label="Close" onclick="window.close()"></button> 9 - </div> 10 - </div> 11 - <div class="window-body"> 12 - <div id="placeholder"></div> 13 - </div> 14 - </div> 15 - 16 - <style> 17 - @import "./styles/variables.css"; 18 - @import "./themes/winamp/fonts.css"; 19 - @import "./themes/winamp/facet.css"; 20 - @import "./vendor/98.css"; 21 - </style> 22 - 23 - <script type="module" src="themes/winamp/configurators/output/facet/index.inline.js"></script>
-12
src/themes/winamp/configurators/output/facet/index.inline.js
··· 1 - import foundation from "~/common/foundation.js"; 2 - import OutputConfigElement from "~/themes/winamp/configurators/output/element.js"; 3 - 4 - // Set doc title 5 - document.title = "Output | Winamp | Diffuse"; 6 - 7 - const out = await foundation.orchestrator.output(); 8 - 9 - const el = new OutputConfigElement(); 10 - el.setAttribute("output-selector", out.selector); 11 - 12 - document.querySelector("#placeholder")?.replaceWith(el);
+14 -27
src/themes/winamp/facet/index.html
··· 1 1 <link rel="stylesheet" href="themes/winamp/fonts.css" /> 2 2 <link rel="stylesheet" href="themes/winamp/index.css" /> 3 3 4 + <style> 5 + main { 6 + opacity: 0; 7 + pointer-events: none; 8 + 9 + &.has-loaded { 10 + opacity: 1; 11 + pointer-events: auto; 12 + } 13 + } 14 + </style> 15 + 4 16 <main> 5 17 <!-- 🪟 --> 6 18 <section class="windows"> ··· 60 72 </style> 61 73 62 74 <dtw-window-manager> 63 - <!-- INPUT --> 64 - <dtw-window id="input-window"> 65 - <span slot="title-icon" 66 - ><img src="images/icons/windows_98/cd_audio_cd_a-0.png" height="14" 67 - /></span> 68 - <span slot="title">Manage audio inputs</span> 69 - 70 - <dtw-input-config 71 - input-selector="#input" 72 - output-selector="#output" 73 - sources-orchestrator-selector="do-sources" 74 - process-tracks-orchestrator-selector="do-process-tracks" 75 - ></dtw-input-config> 76 - </dtw-window> 77 - 78 - <!-- OUTPUT --> 79 - <dtw-window id="output-window"> 80 - <span slot="title-icon" 81 - ><img src="images/icons/windows_98/computer_user_pencil-0.png" height="14" 82 - /></span> 83 - <span slot="title">Manage user data</span> 84 - 85 - <dtw-output-config output-selector="#output"></dtw-output-config> 86 - </dtw-window> 87 - 88 75 <!-- BROWSER --> 89 76 <dtw-window id="browser-window" window-style="height: 380px; width: 560px;"> 90 77 <span slot="title-icon" ··· 111 98 </a> 112 99 113 100 <!-- INPUT --> 114 - <a class="button desktop__item"> 101 + <a class="button desktop__item" href="l/?path=facets%2Fconnect%2Findex.html" target="_blank"> 115 102 <img src="images/icons/windows_98/cd_audio_cd_a-4.png" height="32" /> 116 103 <label for="input-window">Manage audio inputs</label> 117 104 </a> 118 105 119 106 <!-- OUTPUT --> 120 - <a class="button desktop__item"> 107 + <a class="button desktop__item" href="l/?path=facets%2Fconnect%2Findex.html" target="_blank"> 121 108 <img src="images/icons/windows_98/computer_user_pencil-0.png" height="32" /> 122 109 <label for="output-window">Manage user data</label> 123 110 </a>
+4 -2
src/themes/winamp/facet/index.inline.js
··· 7 7 // Set doc title 8 8 document.title = "Winamp | Diffuse"; 9 9 10 + // Doc loader 11 + const main = /** @type {HTMLElement} */ (document.querySelector("main")); 12 + main.classList.add("has-loaded"); 13 + 10 14 /** 11 15 * @import {OutputElement} from "~/components/output/types.d.ts" 12 16 * @import {Track} from "~/definitions/types.d.ts" ··· 21 25 await foundation.orchestrator.processTracks({ disableWhenReady: true }); 22 26 23 27 await import("~/themes/winamp/browser/element.js"); 24 - await import("~/themes/winamp/configurators/input/element.js"); 25 - await import("~/themes/winamp/configurators/output/element.js"); 26 28 await import("~/themes/winamp/window/element.js"); 27 29 28 30 /** @type {OutputElement | null} */