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.

wip: s3 output

+371 -119
+1 -1
deno.jsonc
··· 28 28 "@orama/orama": "npm:@orama/orama@^3.1.18", 29 29 "@phosphor-icons/web": "npm:@phosphor-icons/web@^2.1.2", 30 30 "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", 31 - "alien-signals": "npm:alien-signals@^3.0.0", 31 + "alien-signals": "npm:alien-signals@^3.1.2", 32 32 "bs58check": "npm:bs58check@^4.0.0", 33 33 "codemirror": "npm:codemirror@^6.0.2", 34 34 "fast-average-color": "npm:fast-average-color@^9.5.0",
+68 -45
src/components/configurator/output-fallback/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { batch, computed, signal } from "@common/signal.js"; 2 + import { computed, signal } from "@common/signal.js"; 3 3 4 4 /** 5 5 * @import {OutputManagerDeputy, OutputElement} from "@components/output/types.d.ts" 6 - */ 7 - 8 - /** 9 - * @typedef {OutputElement} Output 6 + * @import {OutputFallbackConfiguratorElement} from "./types.d.ts" 10 7 */ 11 8 12 9 //////////////////////////////////////////// ··· 19 16 * Checks child output elements in order and delegates 20 17 * to the first one whose `.ready()` signal returns `true`. 21 18 * 22 - * @implements {OutputManagerDeputy} 19 + * @template [Encoding=null] 20 + * @implements {OutputManagerDeputy<Encoding | undefined>} 21 + * @implements {OutputFallbackConfiguratorElement<Encoding>} 23 22 */ 24 23 class OutputFallbackConfigurator extends DiffuseElement { 25 24 static NAME = "diffuse/configurator/output-fallback"; ··· 27 26 constructor() { 28 27 super(); 29 28 30 - /** @type {OutputManagerDeputy} */ 29 + /** @type {OutputManagerDeputy<Encoding | undefined>} */ 31 30 const manager = { 32 31 facets: { 33 32 collection: computed(() => { 34 - return this.activeOutput()?.facets.collection() ?? []; 33 + return this.#activeOutput.value?.facets.collection(); 35 34 }), 36 35 reload: () => { 37 - const out = this.activeOutput(); 36 + const out = this.#activeOutput.value; 38 37 if (out) return out.facets.reload(); 39 38 return Promise.resolve(); 40 39 }, 41 40 save: async (newFacets) => { 42 - await Promise.all(this.#outputs.value.map((o) => o.facets.save(newFacets))); 41 + if (newFacets !== undefined) { 42 + await Promise.all( 43 + this.#outputs.map((o) => o.facets.save(newFacets)), 44 + ); 45 + } 43 46 }, 44 47 state: computed(() => { 45 - return this.activeOutput()?.facets.state() ?? "sleeping"; 48 + return this.#activeOutput.value?.facets.state() ?? "sleeping"; 46 49 }), 47 50 }, 48 51 playlists: { 49 52 collection: computed(() => { 50 - return this.activeOutput()?.playlists.collection() ?? []; 53 + return this.#activeOutput.value?.playlists.collection(); 51 54 }), 52 55 reload: () => { 53 - const out = this.activeOutput(); 56 + const out = this.#activeOutput.value; 54 57 if (out) return out.playlists.reload(); 55 58 return Promise.resolve(); 56 59 }, 57 60 save: async (newPlaylists) => { 58 - await Promise.all(this.#outputs.value.map((o) => o.playlists.save(newPlaylists))); 61 + if (newPlaylists !== undefined) { 62 + await Promise.all( 63 + this.#outputs.map((o) => o.playlists.save(newPlaylists)), 64 + ); 65 + } 59 66 }, 60 67 state: computed(() => { 61 - return this.activeOutput()?.playlists.state() ?? "sleeping"; 68 + return this.#activeOutput.value?.playlists.state() ?? "sleeping"; 62 69 }), 63 70 }, 64 71 themes: { 65 72 collection: computed(() => { 66 - return this.activeOutput()?.themes.collection() ?? []; 73 + return this.#activeOutput.value?.themes.collection(); 67 74 }), 68 75 reload: () => { 69 - const out = this.activeOutput(); 76 + const out = this.#activeOutput.value; 70 77 if (out) return out.themes.reload(); 71 78 return Promise.resolve(); 72 79 }, 73 80 save: async (newThemes) => { 74 - await Promise.all(this.#outputs.value.map((o) => o.themes.save(newThemes))); 81 + if (newThemes !== undefined) { 82 + await Promise.all( 83 + this.#outputs.map((o) => o.themes.save(newThemes)), 84 + ); 85 + } 75 86 }, 76 87 state: computed(() => { 77 - return this.activeOutput()?.themes.state() ?? "sleeping"; 88 + return this.#activeOutput.value?.themes.state() ?? "sleeping"; 78 89 }), 79 90 }, 80 91 tracks: { 81 92 collection: computed(() => { 82 - return this.activeOutput()?.tracks.collection() ?? []; 93 + return this.#activeOutput.value?.tracks.collection(); 83 94 }), 84 95 reload: () => { 85 - const out = this.activeOutput(); 96 + const out = this.#activeOutput.value; 86 97 if (out) return out.tracks.reload(); 87 98 return Promise.resolve(); 88 99 }, 89 100 save: async (newTracks) => { 90 - await Promise.all(this.#outputs.value.map((o) => o.tracks.save(newTracks))); 101 + if (newTracks !== undefined) { 102 + await Promise.all( 103 + this.#outputs.map((o) => o.tracks.save(newTracks)), 104 + ); 105 + } 91 106 }, 92 107 state: computed(() => { 93 - return this.activeOutput()?.tracks.state() ?? "sleeping"; 108 + return this.#activeOutput.value?.tracks.state() ?? "sleeping"; 94 109 }), 95 110 }, 96 111 97 112 // Other 98 - ready: this.ready, 113 + ready: computed(() => { 114 + if (this.#activeOutput.value) return true; 115 + return this.#setupFinished.value; 116 + }), 99 117 }; 100 118 101 119 this.facets = manager.facets; ··· 103 121 this.themes = manager.themes; 104 122 this.tracks = manager.tracks; 105 123 this.ready = manager.ready; 124 + 125 + this.effect(this.#setActiveOutput); 106 126 } 107 127 128 + #setActiveOutput = () => { 129 + const _trigger = this.#setupFinished.value; 130 + 131 + /** @type {OutputElement<Encoding> | null} */ 132 + let activeOutput = null; 133 + 134 + for (const output of this.#outputs) { 135 + if (output.ready()) activeOutput = output; 136 + } 137 + 138 + this.#activeOutput.value = activeOutput; 139 + }; 140 + 108 141 // SIGNALS 109 142 110 - #outputs = signal(/** @type {Output[]} */ ([])); 143 + #activeOutput = signal(/** @type {OutputElement<Encoding> | null} */ (null), { 144 + eager: true, 145 + }); 111 146 #setupFinished = signal(false); 112 147 113 148 // STATE 114 149 115 - /** 116 - * The first child output element whose `.ready()` returns `true`. 117 - */ 118 - activeOutput = computed(() => { 119 - const outputs = this.#outputs.value; 120 - for (const output of outputs) { 121 - if (output.ready()) return output; 122 - } 123 - return null; 124 - }); 150 + #outputs = /** @type {OutputElement<Encoding>[]} */ ([]); 125 151 126 - ready = computed(() => { 127 - if (this.activeOutput()) return true; 128 - return this.#setupFinished.value; 129 - }); 152 + activeOutput = this.#activeOutput.get; 130 153 131 154 // LIFECYCLE 132 155 ··· 138 161 139 162 const children = Array.from(this.root().children); 140 163 141 - /** @type {Output[]} */ 164 + /** @type {OutputElement<Encoding>[]} */ 142 165 const outputs = []; 143 166 144 167 for (const el of children) { 145 168 await customElements.whenDefined(el.localName); 146 169 147 170 if ("nameWithGroup" in el && "tracks" in el) { 148 - outputs.push(/** @type {Output} */ (/** @type {unknown} */ (el))); 171 + outputs.push( 172 + /** @type {OutputElement<Encoding>} */ (/** @type {unknown} */ (el)), 173 + ); 149 174 } 150 175 } 151 176 152 - batch(() => { 153 - this.#outputs.value = outputs; 154 - this.#setupFinished.value = true; 155 - }); 177 + this.#outputs = outputs; 178 + this.#setupFinished.value = true; 156 179 } 157 180 158 181 // MISC
+7 -2
src/components/configurator/output-fallback/types.d.ts
··· 1 - import type { OutputManagerDeputy } from "@components/output/types.d.ts"; 1 + import type { OutputElement } from "@components/output/types.d.ts"; 2 + import type { SignalReader } from "@common/signal.d.ts"; 2 3 3 - export type OutputFallbackConfiguratorElement = OutputManagerDeputy; 4 + export type OutputFallbackConfiguratorElement<Encoding = null> = 5 + & OutputElement<Encoding | undefined> 6 + & { 7 + activeOutput: SignalReader<OutputElement<Encoding> | null>; 8 + };
+9 -7
src/components/configurator/output/types.d.ts
··· 1 1 import type { OutputElement } from "@components/output/types.d.ts"; 2 2 import type { SignalReader } from "@common/signal.d.ts"; 3 3 4 - export type OutputConfiguratorElement = OutputElement & { 5 - deselect: () => Promise<void>; 6 - options: () => Promise<Array<OutputOption>>; 7 - select: (id: string) => Promise<void>; 8 - selectedOutput: SignalReader<OutputElement | null>; 9 - }; 4 + export type OutputConfiguratorElement<ElementType = OutputElement> = 5 + & OutputElement 6 + & { 7 + deselect: () => Promise<void>; 8 + options: () => Promise<Array<OutputOption<ElementType>>>; 9 + select: (id: string) => Promise<void>; 10 + selectedOutput: SignalReader<ElementType | null>; 11 + }; 10 12 11 13 export type OutputOption<ElementType = OutputElement> = { 12 14 id: string; 13 15 label: string; 14 16 element: ElementType; 15 - } 17 + };
+40 -27
src/components/orchestrator/output/element.js
··· 3 3 4 4 import "@components/configurator/output/element.js"; 5 5 import "@components/configurator/output-fallback/element.js"; 6 + import "@components/output/bytes/s3/element.js"; 6 7 import "@components/output/polymorphic/indexed-db/element.js"; 7 8 import "@components/output/raw/atproto/element.js"; 8 - // import "@components/output/bytes/automerge-repo-server/element.js"; 9 - // import "@components/transformer/output/bytes/automerge/element.js"; 9 + import "@components/transformer/output/bytes/automerge/element.js"; 10 10 import "@components/transformer/output/refiner/default/element.js"; 11 11 import "@components/transformer/output/string/json/element.js"; 12 12 ··· 44 44 */ 45 45 get outputConfigurator() { 46 46 /** @type {OutputConfiguratorElement | null} */ 47 - const outputConfigurator = this.root().querySelector("#do-output__dc-output"); 47 + const outputConfigurator = this.root().querySelector( 48 + "#do-output__dc-output", 49 + ); 48 50 49 - if (!outputConfigurator) throw new Error("Output orchestrator did not render yet."); 51 + if (!outputConfigurator) { 52 + throw new Error("Output orchestrator did not render yet."); 53 + } 50 54 return outputConfigurator; 51 55 } 52 56 ··· 75 79 // PROXY ADDITIONAL OUTPUT CONFIGURATOR ACTIONS 76 80 77 81 get deselect() { 78 - return this.outputConfigurator.deselect 82 + return this.outputConfigurator.deselect; 79 83 } 80 84 81 85 get options() { 82 - return this.outputConfigurator.options 86 + return this.outputConfigurator.options; 83 87 } 84 88 85 89 get select() { 86 - return this.outputConfigurator.select 90 + return this.outputConfigurator.select; 87 91 } 88 92 89 93 get selectedOutput() { 90 - return this.outputConfigurator.selectedOutput 94 + return this.outputConfigurator.selectedOutput; 91 95 } 92 96 93 97 // RENDER ··· 99 103 const group = this.group === DEFAULT_GROUP ? undefined : this.group; 100 104 101 105 return html` 102 - <!--<dob-automerge-repo-server 103 - id="do-output__dob-automerge-repo-server" 104 - namespace="automerge-repo-server" 105 - url="http://localhost:3030" 106 - ></dob-automerge-repo-server>--> 107 - 106 + <!-- DEFAULT --> 108 107 <dop-indexed-db 109 108 id="do-output__dop-indexed-db__json" 110 109 group="${ifDefined(group)}" 111 110 namespace="json" 112 111 ></dop-indexed-db> 113 112 114 - <dc-output id="do-output__dc-output" default="do-output__dtos-json"> 113 + <!-- S3 #2 --> 114 + <dc-output-fallback 115 + id="do-output__dob-s3-fallback" 116 + > 117 + <dob-s3 118 + id="do-output__dob-s3" 119 + group="${ifDefined(group)}" 120 + ></dob-s3> 121 + <dop-indexed-db 122 + id="do-output__dop-indexed-db__s3" 123 + group="${ifDefined(group)}" 124 + namespace="s3" 125 + ></dop-indexed-db> 126 + </dc-output-fallback> 127 + 128 + <!-- OUTPUT CONFIGURATOR --> 129 + <dc-output id="do-output__dc-output" default="do-output__dc-output__local"> 115 130 <dtos-json 116 - id="do-output__dtos-json" 117 - label="IndexedDB as a JSON string" 131 + id="do-output__dc-output__local" 118 132 output-selector="#do-output__dop-indexed-db__json" 133 + label="Local" 119 134 ></dtos-json> 120 135 136 + <!-- ATProto --> 121 137 <dc-output-fallback 122 - id="do-output__dor-atproto-fallback" 138 + id="do-output__dc-output__atproto" 123 139 label="AT Protocol" 124 140 > 125 141 <dor-atproto ··· 133 149 ></dop-indexed-db> 134 150 </dc-output-fallback> 135 151 136 - <!--<dor-automerge-repo 137 - id="do-output__dor-automerge-repo" 138 - namespace="automerge-repo" 139 - ></dor-automerge-repo>--> 140 - 141 - <!--<dtob-automerge 142 - id="do-output__dtob-automerge" 143 - output-selector="#do-output__dob-automerge-repo-server" 144 - ></dtob-automerge>--> 152 + <!-- S3 #1 --> 153 + <dtob-automerge 154 + id="do-output__dc-output__s3" 155 + output-selector="#do-output__dob-s3-fallback" 156 + label="S3" 157 + ></dtob-automerge> 145 158 </dc-output> 146 159 147 160 <!-- Entry -->
+1 -1
src/components/output/bytes/s3/constants.js
··· 1 - export const OBJECT_PREFIX = "diffuse/output/"; 1 + export const OBJECT_PREFIX = "diffuse/output/bytes/s3/";
+30 -19
src/components/output/bytes/s3/element.js
··· 1 + import * as IDB from "idb-keyval"; 1 2 import { BroadcastableDiffuseElement } from "@common/element.js"; 2 3 import { computed, signal } from "@common/signal.js"; 3 4 import { outputManager } from "../../common.js"; 5 + 6 + const STORAGE_PREFIX = "diffuse/output/bytes/s3"; 4 7 5 8 /** 6 9 * @import {ProxiedActions} from "@common/worker.d.ts" 7 10 * @import {OutputElement, OutputManager} from "../../types.d.ts" 8 11 * @import {Bucket} from "@components/input/s3/types.d.ts" 9 - * @import {S3OutputWorkerActions} from "./types.d.ts" 12 + * @import {S3OutputElement, S3OutputWorkerActions} from "./types.d.ts" 10 13 */ 11 14 12 15 //////////////////////////////////////////// ··· 15 18 16 19 /** 17 20 * @implements {OutputElement<Uint8Array | undefined>} 21 + * @implements {S3OutputElement} 18 22 */ 19 23 class S3Output extends BroadcastableDiffuseElement { 20 - static NAME = "diffuse/output/polymorphic/s3"; 21 - static WORKER_URL = "components/output/polymorphic/s3/worker.js"; 24 + static NAME = "diffuse/output/bytes/s3"; 25 + static WORKER_URL = "components/output/bytes/s3/worker.js"; 22 26 23 27 #manager; 24 28 ··· 62 66 // STATE 63 67 64 68 ready = computed(() => { 65 - return this.#bucketSignal.value !== undefined 69 + return this.#bucket.value !== undefined; 66 70 }); 67 71 68 72 // LIFECYCLE ··· 70 74 /** 71 75 * @override 72 76 */ 73 - connectedCallback() { 77 + async connectedCallback() { 74 78 // Broadcast if needed 75 79 if (this.hasAttribute("group")) { 76 80 const actions = this.broadcast(this.nameWithGroup, { ··· 81 85 this.#put = this.#putOutgoing(actions.put); 82 86 } 83 87 } 88 + 89 + // Restore bucket from IndexedDB 90 + /** @type {Bucket | undefined} */ 91 + const stored = await IDB.get(`${STORAGE_PREFIX}/bucket`); 92 + if (stored) this.#bucket.value = stored; 84 93 85 94 // Super 86 95 super.connectedCallback(); ··· 88 97 89 98 // BUCKET 90 99 91 - /** 92 - * @param {Bucket} bucket 93 - */ 94 - setBucket(bucket) { 95 - this.#bucketSignal.value = bucket; 96 - } 97 - 98 - #bucketSignal = signal(/** @type {Bucket | undefined} */ (undefined)); 100 + #bucket = signal(/** @type {Bucket | undefined} */ (undefined)); 99 101 100 102 /** @returns {Bucket} */ 101 - #bucket() { 102 - if (!this.#bucketSignal.value) { 103 + get bucket() { 104 + if (!this.#bucket.value) { 103 105 throw new Error("Bucket not set, call setBucket() first."); 104 106 } 105 - return this.#bucketSignal.value; 107 + 108 + return this.#bucket.value; 109 + } 110 + 111 + /** 112 + * @param {Bucket} bucket 113 + */ 114 + async setBucket(bucket) { 115 + this.#bucket.value = bucket; 116 + await IDB.set(`${STORAGE_PREFIX}/bucket`, bucket); 106 117 } 107 118 108 119 // GET & PUT 109 120 110 121 /** @param {string} name */ 111 122 #getProxy = (name) => 112 - this.proxy.get({ bucket: this.#bucket(), name: this.#cat(name) }); 123 + this.proxy.get({ bucket: this.bucket, name: this.#cat(name) }); 113 124 #get = this.#getProxy; 114 125 115 126 /** @param {string} name; @param {any} data */ 116 127 #putProxy = (name, data) => 117 - this.proxy.put({ bucket: this.#bucket(), data, name: this.#cat(name) }); 128 + this.proxy.put({ bucket: this.bucket, data, name: this.#cat(name) }); 118 129 #put = this.#putProxy; 119 130 120 131 /** ··· 161 172 //////////////////////////////////////////// 162 173 163 174 export const CLASS = S3Output; 164 - export const NAME = "dop-s3"; 175 + export const NAME = "dob-s3"; 165 176 166 177 customElements.define(NAME, S3Output);
+7
src/components/output/bytes/s3/types.d.ts
··· 1 + import type { OutputElement } from "../../types.d.ts"; 1 2 import type { Bucket } from "@components/input/s3/types.d.ts"; 3 + 4 + export type S3OutputElement = 5 + & OutputElement<Uint8Array | undefined> 6 + & { 7 + setBucket(bucket: Bucket): void; 8 + }; 2 9 3 10 export type S3OutputWorkerActions = { 4 11 get(args: { bucket: Bucket; name: string }): Promise<Uint8Array | undefined>;
+208 -17
src/themes/webamp/configurators/output/element.js
··· 2 2 import { signal } from "@common/signal.js"; 3 3 4 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"; 5 6 6 7 /** 7 8 * @import {ATProtoOutputElement} from "@components/output/raw/atproto/types.d.ts" 9 + * 10 + * @import {Bucket as S3Bucket} from "@components/input/s3/types.d.ts" 11 + * @import {S3OutputElement} from "@components/output/bytes/s3/types.d.ts" 12 + * 8 13 * @import {OutputElement} from "@components/output/types.d.ts" 9 14 * @import {OutputConfiguratorElement, OutputOption} from "@components/configurator/output/types.d.ts" 15 + * @import {OutputFallbackConfiguratorElement} from "@components/configurator/output-fallback/types.d.ts" 10 16 * @import {RenderArg} from "@common/element.d.ts" 11 17 */ 12 18 19 + /** 20 + * @typedef {OutputElement<any>} VariousOutputElements 21 + */ 22 + 13 23 class OutputConfig extends DiffuseElement { 14 24 constructor() { 15 25 super(); ··· 19 29 // SIGNALS 20 30 21 31 $output = signal( 22 - /** @type {OutputElement | OutputConfiguratorElement | undefined} */ (undefined), 32 + /** @type {OutputElement | OutputConfiguratorElement<VariousOutputElements> | undefined} */ (undefined), 23 33 ); 24 34 25 35 $atproto = signal( 26 - /** @type {OutputOption<ATProtoOutputElement> | null} */ (null), 36 + /** @type {ATProtoOutputElement | null} */ (null), 37 + ); 38 + 39 + $s3 = signal( 40 + /** @type {S3OutputElement | null} */ (null), 27 41 ); 28 42 29 43 $tab = signal("overview"); ··· 34 48 async connectedCallback() { 35 49 super.connectedCallback(); 36 50 37 - /** @type {OutputElement | OutputConfiguratorElement} */ 51 + /** @type {OutputElement | OutputConfiguratorElement<VariousOutputElements>} */ 38 52 const output = query(this, "output-selector"); 39 53 40 54 await customElements.whenDefined(output.localName); ··· 42 56 this.$output.value = output; 43 57 44 58 // Try setting up specific outputs 45 - if ("options" in output === false) return; 46 - const options = await output.options(); 47 - const atproto = options.find((o) => o.element.localName === ATPROTO_NAME); 59 + const atproto = output.root().querySelector(ATPROTO_NAME); 48 60 49 61 if (atproto) { 50 - this.$atproto.value = 51 - /** @type {OutputOption<ATProtoOutputElement>} */ (atproto); 62 + this.$atproto.value = /** @type {ATProtoOutputElement} */ (atproto); 63 + } 64 + 65 + const s3 = output.root().querySelector(S3_NAME); 66 + 67 + if (s3) { 68 + this.$s3.value = /** @type {S3OutputElement} */ (s3); 52 69 } 53 70 } 54 71 ··· 70 87 const button = this.root().querySelector("#atproto-submit"); 71 88 if (button) button.disabled = true; 72 89 73 - await atproto.element.login(handle); 90 + await atproto.login(handle); 74 91 }; 75 92 76 93 #handleAtprotoLogout = async () => { 77 94 const atproto = this.$atproto.value; 78 95 if (!atproto) return; 79 96 80 - await atproto.element.logout(); 97 + await atproto.logout(); 81 98 }; 82 99 83 100 #handleAtprotoActivate = async () => { ··· 87 104 const atproto = this.$atproto.value; 88 105 if (!atproto) return; 89 106 90 - await output.select(atproto.id); 107 + const option = (await output.options()).find((o) => 108 + o.label === "AT Protocol" 109 + ); 110 + if (option) await output.select(option.id); 111 + }; 112 + 113 + /** 114 + * @param {Event} event 115 + */ 116 + #handleS3SetBucket = (event) => { 117 + event.preventDefault(); 118 + 119 + const s3 = this.$s3.value; 120 + if (!s3) return; 121 + 122 + /** @type {HTMLButtonElement | null} */ 123 + const button = this.root().querySelector("#s3-submit"); 124 + if (button) button.disabled = true; 125 + 126 + const accessKey = 127 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 128 + "#s3-access-key", 129 + ))?.value; 130 + const bucketName = 131 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 132 + "#s3-bucket-name", 133 + ))?.value; 134 + const host = 135 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 136 + "#s3-host", 137 + ))?.value; 138 + const path = 139 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 140 + "#s3-path", 141 + ))?.value; 142 + const region = 143 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 144 + "#s3-region", 145 + ))?.value; 146 + const secretKey = 147 + /** @type {HTMLInputElement | null} */ (this.root().querySelector( 148 + "#s3-secret-key", 149 + ))?.value; 150 + 151 + if (!accessKey || !bucketName || !secretKey) return; 152 + 153 + /** @type {S3Bucket} */ 154 + const bucket = { 155 + accessKey, 156 + bucketName, 157 + host: host?.length ? host : "s3.amazonaws.com", 158 + path: path?.length ? path : "/", 159 + region: region?.length ? region : "us-east-1", 160 + secretKey, 161 + }; 162 + 163 + s3.setBucket(bucket); 164 + 165 + if (button) button.disabled = false; 166 + }; 167 + 168 + #handleS3Activate = async () => { 169 + const output = this.$output.value; 170 + if (!output || !("select" in output)) return; 171 + 172 + const s3 = this.$s3.value; 173 + if (!s3) return; 174 + 175 + const option = (await output.options()).find((o) => o.label === "S3"); 176 + if (option) await output.select(option.id); 91 177 }; 92 178 93 179 #handleDeactivate = async () => { ··· 263 349 * @param {RenderArg["html"]} html 264 350 */ 265 351 #renderAtprotoTab(html) { 266 - const did = this.$atproto.value?.element.did() ?? null; 352 + const did = this.$atproto.value?.did() ?? null; 267 353 const selectedOutput = 268 354 this.$output.value && "selectedOutput" in this.$output.value 269 355 ? this.$output.value.selectedOutput() ··· 329 415 * @param {RenderArg["html"]} html 330 416 */ 331 417 #renderS3Tab(html) { 418 + const s3 = this.$s3.value; 419 + const ready = s3?.ready() ?? false; 420 + const selectedOutput = 421 + this.$output.value && "selectedOutput" in this.$output.value 422 + ? this.$output.value.selectedOutput() 423 + : undefined; 424 + 425 + const configured = () => { 426 + return html` 427 + <fieldset> 428 + <span class="with-icon with-icon--large"> 429 + <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 430 + <span>S3 bucket configured.</span> 431 + </span> 432 + </fieldset> 433 + 434 + <p class="button-row"> 435 + ${this.#renderS3Activation(html, selectedOutput)} 436 + </p> 437 + `; 438 + }; 439 + 440 + const unconfigured = () => { 441 + return html` 442 + <fieldset> 443 + <span class="with-icon with-icon--large"> 444 + <img src="images/icons/windows_98/computer_user_pencil-0.png" width="24" /> 445 + <span> 446 + Store your user data on an S3-compatible storage service. 447 + </span> 448 + </span> 449 + </fieldset> 450 + 451 + <form @submit="${this.#handleS3SetBucket}"> 452 + <fieldset> 453 + <legend>Bucket details</legend> 454 + 455 + <div class="field-row"> 456 + <label for="s3-access-key">Access Key:*</label> 457 + <input type="text" id="s3-access-key" required /> 458 + </div> 459 + 460 + <div class="field-row"> 461 + <label for="s3-secret-key">Secret Key:*</label> 462 + <input type="password" id="s3-secret-key" required /> 463 + </div> 464 + 465 + <div class="field-row"> 466 + <label for="s3-bucket-name">Bucket Name:*</label> 467 + <input type="text" id="s3-bucket-name" required /> 468 + </div> 469 + 470 + <div class="field-row"> 471 + <label for="s3-host">Host:</label> 472 + <input 473 + type="text" 474 + id="s3-host" 475 + placeholder="s3.amazonaws.com" 476 + /> 477 + </div> 478 + 479 + <div class="field-row"> 480 + <label for="s3-region">Region:</label> 481 + <input 482 + type="text" 483 + id="s3-region" 484 + placeholder="us-east-1" 485 + /> 486 + </div> 487 + 488 + <div class="field-row"> 489 + <label for="s3-path">Path:</label> 490 + <input type="text" id="s3-path" /> 491 + </div> 492 + 493 + <p> 494 + * are required fields. 495 + </p> 496 + </fieldset> 497 + 498 + <p> 499 + <button type="submit" id="s3-submit">Set bucket</button> 500 + </p> 501 + </form> 502 + `; 503 + }; 504 + 332 505 return html` 333 506 <div class="window-body"> 334 - <p>TODO</p> 507 + ${ready ? configured() : unconfigured()} 335 508 </div> 336 509 `; 337 510 } 338 511 339 512 /** 340 513 * @param {RenderArg['html']} html 341 - * @param {OutputElement | null | undefined} selectedOutput 514 + * @param {VariousOutputElements | null | undefined} selectedOutput 342 515 */ 343 516 #renderAtprotoActivation(html, selectedOutput) { 344 517 const output = this.$output.value; 345 518 if (!output || !("select" in output)) return nothing; 346 519 347 - const atproto = this.$atproto.value; 348 - const isActive = selectedOutput && atproto && 349 - selectedOutput.selector === atproto.element.selector; 520 + const isActive = selectedOutput?.label === "AT Protocol"; 350 521 351 522 return isActive 352 523 ? html` ··· 355 526 : html` 356 527 <button @click="${this 357 528 .#handleAtprotoActivate}">Activate this storage</button> 529 + `; 530 + } 531 + 532 + /** 533 + * @param {RenderArg['html']} html 534 + * @param {VariousOutputElements | null | undefined} selectedOutput 535 + */ 536 + #renderS3Activation(html, selectedOutput) { 537 + const output = this.$output.value; 538 + if (!output || !("select" in output)) return nothing; 539 + 540 + const isActive = selectedOutput?.label === "S3"; 541 + 542 + return isActive 543 + ? html` 544 + <button @click="${this.#handleDeactivate}">Deactivate</button> 545 + ` 546 + : html` 547 + <button @click="${this 548 + .#handleS3Activate}">Activate this storage</button> 358 549 `; 359 550 } 360 551