From 4a47b58f6e841c81ee61f88fa56f9ff51d857b6d Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 6 Mar 2024 21:19:28 -0500 Subject: [PATCH 01/14] Create "Add to Scene" menu --- studio-frontend/src/app/app.element.scss | 6 + studio-frontend/src/app/app.element.ts | 70 +++++++- studio-ui/src/assets/icons/cylinder.svg | 3 + studio-ui/src/assets/icons/sphere.svg | 9 + studio-ui/src/assets/icons/torus.svg | 4 + studio-ui/src/index.ts | 2 + studio-ui/src/lib/button.element.ts | 11 +- studio-ui/src/lib/icon.element.ts | 19 +- studio-ui/src/lib/menu.element.scss | 35 ++++ studio-ui/src/lib/menu.element.ts | 166 ++++++++++++++++++ studio-ui/src/lib/overlay-provider.element.ts | 110 ++++++++++++ tsconfig.base.json | 4 +- 12 files changed, 433 insertions(+), 6 deletions(-) create mode 100644 studio-ui/src/assets/icons/cylinder.svg create mode 100644 studio-ui/src/assets/icons/sphere.svg create mode 100644 studio-ui/src/assets/icons/torus.svg create mode 100644 studio-ui/src/lib/menu.element.scss create mode 100644 studio-ui/src/lib/menu.element.ts create mode 100644 studio-ui/src/lib/overlay-provider.element.ts diff --git a/studio-frontend/src/app/app.element.scss b/studio-frontend/src/app/app.element.scss index 5d05551..5ad99a2 100644 --- a/studio-frontend/src/app/app.element.scss +++ b/studio-frontend/src/app/app.element.scss @@ -165,3 +165,9 @@ sts-button.active { } } } + +// TODO: Move this +.menu-button-icon { + font-size: 12px; + opacity: 0.75; +} diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 9212f25..bb2059b 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -5,7 +5,9 @@ import { type SceneStateEvent, SceneState, StudioMode, + BlockingCommand, } from "@storyteller/studio"; +import { ButtonElement } from "@storyteller/studio-ui/button"; import { SceneHierarchyElement, StudioURLParams, @@ -20,6 +22,8 @@ import { v4 as uuid } from "uuid"; import "@storyteller/studio-ui/button"; import "@storyteller/studio-ui/icon"; +import "@storyteller/studio-ui/menu"; +import "@storyteller/studio-ui/overlay-provider"; import "@storyteller/studio-ui/toolbar"; import "@storyteller/studio-ui/tree"; import "@storyteller/studio-web/canvas-provider"; @@ -48,6 +52,7 @@ export class AppElement extends LitElement { #enableAnimTimeline = false; // TODO + #addToSceneButtonRef = createRef(); #inspectorRef = createRef(); #hierarchyRef = createRef(); @@ -145,6 +150,7 @@ export class AppElement extends LitElement { } protected override render = () => html` + @@ -174,6 +180,16 @@ export class AppElement extends LitElement { > Save + + Add to Scene + + ` : nothing} ${this.#params.mode === StudioMode.Editor ? html` - + + + { + studio.dispatchBlockingCommand(BlockingCommand.Cube); + }} + > + Cube + + { + studio.dispatchBlockingCommand(BlockingCommand.Sphere); + }} + > + Sphere + + { + studio.dispatchBlockingCommand(BlockingCommand.Torus); + }} + > + Torus + + { + studio.dispatchBlockingCommand(BlockingCommand.Cylinder); + }} + > + Cylinder + +
+ + Import asset... + + + Media library... + +
@@ -276,5 +343,6 @@ export class AppElement extends LitElement { ` : nothing}
+
`; } diff --git a/studio-ui/src/assets/icons/cylinder.svg b/studio-ui/src/assets/icons/cylinder.svg new file mode 100644 index 0000000..c411bd6 --- /dev/null +++ b/studio-ui/src/assets/icons/cylinder.svg @@ -0,0 +1,3 @@ + + + diff --git a/studio-ui/src/assets/icons/sphere.svg b/studio-ui/src/assets/icons/sphere.svg new file mode 100644 index 0000000..7c948a1 --- /dev/null +++ b/studio-ui/src/assets/icons/sphere.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio-ui/src/assets/icons/torus.svg b/studio-ui/src/assets/icons/torus.svg new file mode 100644 index 0000000..7cd4b8b --- /dev/null +++ b/studio-ui/src/assets/icons/torus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/studio-ui/src/index.ts b/studio-ui/src/index.ts index fe3b7a5..3a2c7e2 100644 --- a/studio-ui/src/index.ts +++ b/studio-ui/src/index.ts @@ -1,5 +1,7 @@ export * from "./lib/button.element"; export * from "./lib/icon.element"; +export * from "./lib/menu.element"; +export * from "./lib/overlay-provider.element"; export * from "./lib/progress.element"; export * from "./lib/toolbar.element"; export * from "./lib/tree.element"; diff --git a/studio-ui/src/lib/button.element.ts b/studio-ui/src/lib/button.element.ts index 73d6f5b..81d47be 100644 --- a/studio-ui/src/lib/button.element.ts +++ b/studio-ui/src/lib/button.element.ts @@ -10,10 +10,12 @@ import "./icon.element"; export class ButtonElement extends LitElement { static override styles = unsafeCSS(styles); + @property({ reflect: true }) + override role = "button"; + @property() icon?: string; @property({ type: Boolean }) disabled = false; - override role = "button"; override tabIndex = 0; protected override willUpdate(changes: PropertyValues): void { @@ -36,7 +38,12 @@ export class ButtonElement extends LitElement { protected override render = () => html` ${this.icon - ? html`` + ? html` + ` : nothing} ` diff --git a/studio-ui/src/lib/icon.element.ts b/studio-ui/src/lib/icon.element.ts index 7e6b556..1f4f345 100644 --- a/studio-ui/src/lib/icon.element.ts +++ b/studio-ui/src/lib/icon.element.ts @@ -31,6 +31,14 @@ import fasCircleXmark from "@fortawesome/fontawesome-pro/svgs/solid/circle-xmark import fasMinus from "@fortawesome/fontawesome-pro/svgs/solid/minus.svg?raw"; import fasListTree from "@fortawesome/fontawesome-pro/svgs/solid/list-tree.svg?raw"; import fasBars from "@fortawesome/fontawesome-pro/svgs/solid/bars.svg?raw"; +import fasPlusLarge from "@fortawesome/fontawesome-pro/svgs/solid/plus-large.svg?raw"; +import farFileImport from "@fortawesome/fontawesome-pro/svgs/regular/file-import.svg?raw"; +import farPhotoFilmMusic from "@fortawesome/fontawesome-pro/svgs/regular/photo-film-music.svg?raw"; +import fasChevronDown from "@fortawesome/fontawesome-pro/svgs/solid/chevron-down.svg?raw"; + +import customCylinder from "../assets/icons/cylinder.svg?raw"; +import customSphere from "../assets/icons/sphere.svg?raw"; +import customTorus from "../assets/icons/torus.svg?raw"; import styles from "./icon.element.scss?inline"; @@ -61,13 +69,16 @@ function renderSvg(iconId: string) { "bone": () => farBone, "bwd-fast": () => fasBwdFast, "bwd-step": () => fasBwdStep, + "caret-right": () => fasCaretRight, "check": () => fasCheck, + "chevron-down": () => fasChevronDown, "circle-check": () => fasCircleCheck, "circle-question": () => fasCircleQuestion, "circle-xmark": () => fasCircleXmark, - "caret-right": () => fasCaretRight, "crosshairs": () => farCrosshairs, "cube": () => farCube, + "cylinder": () => customCylinder, + "file-import": () => farFileImport, "floppy-disk": () => fasFloppyDisk, "fwd-fast": () => fasFwdFast, "fwd-step": () => fasFwdStep, @@ -76,11 +87,15 @@ function renderSvg(iconId: string) { "list-tree": () => fasListTree, "minus": () => fasMinus, "globe": () => farGlobe, + "photo-film-music": () => farPhotoFilmMusic, "play": () => fasPlay, + "plus-large": () => fasPlusLarge, "question": () => fasQuestion, "rotate": () => farRotate, "scale": () => farScale, "skeleton": () => farSkeleton, + "sphere": () => customSphere, + "torus": () => customTorus, "up-down-left-right": () => farUpDownLeftRight, "video": () => farVideo, "xmark": () => fasXmark, @@ -89,7 +104,7 @@ function renderSvg(iconId: string) { if (!svgSrc) return nothing; - const [, attrsString, body] = svgSrc.match(/(.+?)<\/svg>/)!; + const [, attrsString, body] = svgSrc.match(/(.+?)<\/svg>/s)!; const attrs = Object.fromEntries( Array.from(attrsString.matchAll(/([-:_a-zA-Z0-9]+)(?:=['"]?([^'"]*)['"]?)/g)) .map(([, attr, value]) => [attr, value]) diff --git a/studio-ui/src/lib/menu.element.scss b/studio-ui/src/lib/menu.element.scss new file mode 100644 index 0000000..f1cb459 --- /dev/null +++ b/studio-ui/src/lib/menu.element.scss @@ -0,0 +1,35 @@ +:host { + display: flex; + flex-direction: column; + border-radius: 0.5rem; + padding: 0.5rem 0; + min-width: 220px; + + background: #0006; + backdrop-filter: blur(16px); + overflow: hidden; +} + +::slotted(sts-button) { + background: unset; + backdrop-filter: unset; + border-radius: unset; +} + +::slotted(sts-button:hover) { + background: #0004; + + // FIXME: https://github.com/w3c/csswg-drafts/issues/3896 + &::part(icon) { + opacity: 0.75; + } +} + +::slotted(hr) { + display: block; + width: 100%; + border: none; + height: 1px; + margin: 0.5rem 0; + background: #FFF1; +} diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts new file mode 100644 index 0000000..8b2547d --- /dev/null +++ b/studio-ui/src/lib/menu.element.ts @@ -0,0 +1,166 @@ +import { inject, on } from "@storyteller/framework"; +import { Fn } from "@storyteller/utility"; +import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { createRef, ref, Ref } from "lit/directives/ref.js"; +import { styleMap } from "lit/directives/style-map.js"; + +import panelStyles from "./menu.element.scss?inline"; + +import { + OVERLAY_PROVIDER, + type OverlayHandle, + type OverlayProvider, +} from "./overlay-provider.element"; + +@customElement("sts-menu") +export class MenuElement extends LitElement { + static override styles = css` + :host { + visibility: hidden; + } + `; + + @property({ attribute: false }) + trigger: Ref = createRef(); + + /** Close the menu when one of its menuitems is pressed */ + @property({ type: Boolean }) + autoClose = false; + + @inject(OVERLAY_PROVIDER) + overlay!: OverlayProvider; + + #menuId = crypto.randomUUID(); + #slotRef = createRef(); + #panelRef = createRef(); + #handle?: WeakRef; + #listener?: Fn; + + protected override update(changes: PropertyValues): void { + super.update(changes); + + if (this.#handle) + this.overlay.reRender(this.#handle); + } + + protected override updated(changes: PropertyValues): void { + if (changes.has("trigger") && this.trigger.value) { + const trigger = this.trigger.value; + + trigger.setAttribute("aria-owns", this.#menuId); + trigger.ariaHasPopup = "true"; + trigger.ariaExpanded = "false"; + + if (this.#listener) + trigger.removeEventListener("click", this.#listener); + + this.#listener = () => this.show(); + trigger.addEventListener("click", this.#listener); + } + + super.updated(changes); + } + + @on("window:click") + onWindowClick(event: PointerEvent): void { + if (!this.#handle || !this.#panelRef.value) + return; + + const path = event.composedPath(); + if ( + !path.includes(this.#panelRef.value) + && (!this.trigger.value || !path.includes(this.trigger.value)) + ) { + this.hide(); + } + } + + onMenuClick(event: PointerEvent): void { + event.stopPropagation(); + + if ( + event.target + && event.target instanceof HTMLElement + && event.target.role === "menuitem" + ) { + this.hide(); + } + } + + show(): void { + const trigger = this.trigger.value; + if (!trigger) return; + + trigger.classList.toggle("active", true); + trigger.ariaExpanded = "true"; + + const rect = trigger.getBoundingClientRect(); + + this.#handle = this.overlay.attach(this, html` + + ${ + // Move the nodes assigned to the 's slot into the + // 's slot + this.#slotRef.value?.assignedNodes() + } + + `); + } + + hide(): void { + const trigger = this.trigger.value; + if (trigger) { + trigger.classList.toggle("active", false); + trigger.ariaExpanded = "false"; + } + + if (this.#panelRef.value) { + // Move the nodes assigned to the 's slot back into the + // 's slot + for (let node of this.#panelRef.value.getSlot()!.assignedNodes()) + this.appendChild(node); + } + + if (this.#handle) + this.overlay.detach(this.#handle); + + this.#handle = undefined; + } + + protected override render = () => html` + + ` +} + +@customElement("sts-menu-panel") +export class MenuPanelElement extends LitElement { + static override styles = unsafeCSS(panelStyles); + + @property({ reflect: true }) + override role = "menu"; + + #slotRef = createRef(); + getSlot(): HTMLSlotElement | undefined { return this.#slotRef.value; } + + protected override render = () => html` + + ` +} + +declare global { + interface HTMLElementTagNameMap { + "sts-menu-panel": MenuPanelElement; + } +} diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts new file mode 100644 index 0000000..78d13e8 --- /dev/null +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -0,0 +1,110 @@ +import { UniqueToken, provide } from "@storyteller/framework"; +import { LitElement, TemplateResult, html, render } from "lit"; +import { customElement } from "lit/decorators.js"; +import { Ref, createRef, ref } from "lit/directives/ref.js"; +import { styleMap } from "lit/directives/style-map.js"; + +export const OVERLAY_PROVIDER = UniqueToken.create(); + +export interface OverlayHandle { + readonly id: number; +} + +class OverlayHandleInternal implements OverlayHandle { + #id: number; + get id() { return this.#id; } + + constructor (id: number) { + this.#id = id; + } + _updateId(id: number): void { + this.#id = id; + } +} + +export interface OverlayProvider { + attach(receiver: LitElement, template: TemplateResult): WeakRef; + detach(handle: WeakRef): void; + reRender(handle: WeakRef): void; +} + +@customElement("sts-overlay-provider") +@provide(OVERLAY_PROVIDER) +export class OverlayProviderElement + extends LitElement + implements OverlayProvider +{ + #handles: OverlayHandleInternal[] = []; + #containerRefs: Ref[] = []; + #containerTemplates: TemplateResult[] = []; + #templates: [LitElement, TemplateResult][] = []; + + attach(receiver: LitElement, template: TemplateResult): WeakRef { + const index = this.#templates.length; + const handle = new OverlayHandleInternal(index); + + this.#handles.push(handle); + this.#containerRefs.push(createRef()); + this.#containerTemplates.push(html` +
+ `); + this.#templates.push([receiver, template]); + + this.#updateLightDom(); + + return new WeakRef(handle); + } + + detach(handle: WeakRef): void { + const index = handle.deref()?.id; + if (index == null) return; + + this.#handles.splice(index, 1); + this.#containerRefs.splice(index, 1); + this.#containerTemplates.splice(index, 1); + this.#templates.splice(index, 1); + + for (let i = index; i < this.#handles.length; ++i) + this.#handles[i]._updateId(i); + + this.#updateLightDom(); + } + + reRender(handle: WeakRef): void { + const index = handle.deref()?.id; + if (index == null) return; + + const container = this.#containerRefs[index]?.value; + if (!container) return; + + const tuple = this.#templates[index]; + if (!tuple) return; + + const [receiver, template] = tuple; + render(template, container, { host: receiver }); + } + + #updateLightDom(): void { + render(this.#containerTemplates, this, { host: this }); + + for (let i = 0; i < this.#handles.length; ++i) { + const container = this.#containerRefs[i].value; + if (!container) throw new Error("Attempted to retrieve an invalid container ref!"); + + const [receiver, template] = this.#templates[i]; + render(template, container, { host: receiver }); + } + } + + protected override render = () => html` + + + `; +} diff --git a/tsconfig.base.json b/tsconfig.base.json index e4c0493..5b747bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,7 +10,7 @@ "importHelpers": true, "target": "es2015", "module": "esnext", - "lib": ["es2020", "dom"], + "lib": ["es2021", "dom"], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", @@ -20,6 +20,8 @@ "@storyteller/studio-ui": ["studio-ui/src/index.ts"], "@storyteller/studio-ui/button": ["studio-ui/src/lib/button.element.ts"], "@storyteller/studio-ui/icon": ["studio-ui/src/lib/icon.element.ts"], + "@storyteller/studio-ui/menu": ["studio-ui/src/lib/menu.element.ts"], + "@storyteller/studio-ui/overlay-provider": ["studio-ui/src/lib/overlay-provider.element.ts"], "@storyteller/studio-ui/progress": ["studio-ui/src/lib/progress.element.ts"], "@storyteller/studio-ui/toolbar": ["studio-ui/src/lib/toolbar.element.ts"], "@storyteller/studio-ui/tree": ["studio-ui/src/lib/tree.element.ts"], From 976e446f93b11976f205154b317b4e0978687fb8 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 6 Mar 2024 21:25:51 -0500 Subject: [PATCH 02/14] Formatting --- studio-ui/src/lib/button.element.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/studio-ui/src/lib/button.element.ts b/studio-ui/src/lib/button.element.ts index 81d47be..2a532e0 100644 --- a/studio-ui/src/lib/button.element.ts +++ b/studio-ui/src/lib/button.element.ts @@ -37,14 +37,13 @@ export class ButtonElement extends LitElement { } protected override render = () => html` - ${this.icon - ? html` - ` - : nothing} + ${this.icon ? html` + + ` : nothing} ` } From a2c4c9eaacc0e0c86ee73f03e1214d0247c693a2 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 6 Mar 2024 21:31:34 -0500 Subject: [PATCH 03/14] Respect the `autoClose` property --- studio-frontend/src/app/app.element.ts | 2 +- studio-ui/src/lib/menu.element.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index bb2059b..d7d9a0b 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -204,7 +204,7 @@ export class AppElement extends LitElement { ${this.#params.mode === StudioMode.Editor ? html` - + Date: Wed, 6 Mar 2024 21:37:40 -0500 Subject: [PATCH 04/14] Cleanup --- studio-ui/src/lib/overlay-provider.element.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts index 78d13e8..878610b 100644 --- a/studio-ui/src/lib/overlay-provider.element.ts +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -6,6 +6,15 @@ import { styleMap } from "lit/directives/style-map.js"; export const OVERLAY_PROVIDER = UniqueToken.create(); +// TODO: Document this +export interface OverlayProvider { + attach(host: LitElement, template: TemplateResult): WeakRef; + detach(handle: WeakRef): void; + reRender(handle: WeakRef): void; +} + + +// TODO: Document this export interface OverlayHandle { readonly id: number; } @@ -22,12 +31,6 @@ class OverlayHandleInternal implements OverlayHandle { } } -export interface OverlayProvider { - attach(receiver: LitElement, template: TemplateResult): WeakRef; - detach(handle: WeakRef): void; - reRender(handle: WeakRef): void; -} - @customElement("sts-overlay-provider") @provide(OVERLAY_PROVIDER) export class OverlayProviderElement @@ -39,7 +42,7 @@ export class OverlayProviderElement #containerTemplates: TemplateResult[] = []; #templates: [LitElement, TemplateResult][] = []; - attach(receiver: LitElement, template: TemplateResult): WeakRef { + attach(host: LitElement, template: TemplateResult): WeakRef { const index = this.#templates.length; const handle = new OverlayHandleInternal(index); @@ -55,7 +58,7 @@ export class OverlayProviderElement })} > `); - this.#templates.push([receiver, template]); + this.#templates.push([host, template]); this.#updateLightDom(); @@ -87,8 +90,8 @@ export class OverlayProviderElement const tuple = this.#templates[index]; if (!tuple) return; - const [receiver, template] = tuple; - render(template, container, { host: receiver }); + const [host, template] = tuple; + render(template, container, { host }); } #updateLightDom(): void { @@ -98,8 +101,8 @@ export class OverlayProviderElement const container = this.#containerRefs[i].value; if (!container) throw new Error("Attempted to retrieve an invalid container ref!"); - const [receiver, template] = this.#templates[i]; - render(template, container, { host: receiver }); + const [host, template] = this.#templates[i]; + render(template, container, { host }); } } From 890ac13c2d49870ebf3da6426a53cacffdccb4a5 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 6 Mar 2024 21:56:45 -0500 Subject: [PATCH 05/14] Fix build errors --- studio-frontend/tsconfig.json | 2 +- studio-ui/src/lib/menu.element.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/studio-frontend/tsconfig.json b/studio-frontend/tsconfig.json index 724ccfc..a8f3853 100644 --- a/studio-frontend/tsconfig.json +++ b/studio-frontend/tsconfig.json @@ -6,7 +6,7 @@ "experimentalDecorators": true, "useDefineForClassFields": false, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, "moduleResolution": "Bundler", diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 62abaeb..8784d19 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -3,7 +3,7 @@ import { Fn } from "@storyteller/utility"; import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { createRef, ref, Ref } from "lit/directives/ref.js"; +import { createRef, ref, type Ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; import panelStyles from "./menu.element.scss?inline"; From 0856caf0e0103d71186e35fd087c089571a67663 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 14:47:23 -0500 Subject: [PATCH 06/14] Clean up trigger event handling --- studio-ui/src/lib/menu.element.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 8784d19..2d0bd23 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -1,9 +1,8 @@ import { inject, on } from "@storyteller/framework"; -import { Fn } from "@storyteller/utility"; import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { createRef, ref, type Ref } from "lit/directives/ref.js"; +import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; import panelStyles from "./menu.element.scss?inline"; @@ -23,7 +22,7 @@ export class MenuElement extends LitElement { `; @property({ attribute: false }) - trigger: Ref = createRef(); + trigger = createRef(); /** Close the menu when one of its menuitems is pressed */ @property({ type: Boolean }) @@ -36,7 +35,6 @@ export class MenuElement extends LitElement { #slotRef = createRef(); #panelRef = createRef(); #handle?: WeakRef; - #listener?: Fn; protected override update(changes: PropertyValues): void { super.update(changes); @@ -46,23 +44,34 @@ export class MenuElement extends LitElement { } protected override updated(changes: PropertyValues): void { - if (changes.has("trigger") && this.trigger.value) { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + if (prevTrigger?.value) { + prevTrigger.value.removeEventListener("click", this.onTriggerClick); + } + const trigger = this.trigger.value; + if (!trigger) return; trigger.setAttribute("aria-owns", this.#menuId); trigger.ariaHasPopup = "true"; trigger.ariaExpanded = "false"; - if (this.#listener) - trigger.removeEventListener("click", this.#listener); - - this.#listener = () => this.show(); - trigger.addEventListener("click", this.#listener); + trigger.addEventListener("click", this.onTriggerClick); } super.updated(changes); } + override disconnectedCallback(): void { + this.trigger.value?.removeEventListener("click", this.onTriggerClick); + super.disconnectedCallback(); + } + + onTriggerClick = (): void => { + this.show(); + } + @on("window:click") onWindowClick(event: PointerEvent): void { if (!this.#handle || !this.#panelRef.value) @@ -145,6 +154,7 @@ export class MenuElement extends LitElement { ` } +// TODO: ARIA keyboard navigation: https://www.w3.org/WAI/ARIA/apg/patterns/menubar/ @customElement("sts-menu-panel") export class MenuPanelElement extends LitElement { static override styles = unsafeCSS(panelStyles); From 326424347f894350aae3abca4e1f1a2d8375571c Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 14:58:32 -0500 Subject: [PATCH 07/14] Move "Add to Scene" menu to `studio-web` --- studio-frontend/src/app/app.element.ts | 57 +--------------- studio-web/src/index.ts | 1 + .../src/lib/add-to-scene-menu.element.ts | 68 +++++++++++++++++++ tsconfig.base.json | 1 + 4 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 studio-web/src/lib/add-to-scene-menu.element.ts diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index d7d9a0b..922bda2 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -5,7 +5,6 @@ import { type SceneStateEvent, SceneState, StudioMode, - BlockingCommand, } from "@storyteller/studio"; import { ButtonElement } from "@storyteller/studio-ui/button"; import { @@ -22,10 +21,10 @@ import { v4 as uuid } from "uuid"; import "@storyteller/studio-ui/button"; import "@storyteller/studio-ui/icon"; -import "@storyteller/studio-ui/menu"; import "@storyteller/studio-ui/overlay-provider"; import "@storyteller/studio-ui/toolbar"; import "@storyteller/studio-ui/tree"; +import "@storyteller/studio-web/add-to-scene-menu"; import "@storyteller/studio-web/canvas-provider"; import "@storyteller/studio-web/entity-inspector"; import "@storyteller/studio-web/iframe-messaging-bridge"; @@ -203,59 +202,7 @@ export class AppElement extends LitElement { ${this.#params.mode === StudioMode.Editor ? html` - - - { - studio.dispatchBlockingCommand(BlockingCommand.Cube); - }} - > - Cube - - { - studio.dispatchBlockingCommand(BlockingCommand.Sphere); - }} - > - Sphere - - { - studio.dispatchBlockingCommand(BlockingCommand.Torus); - }} - > - Torus - - { - studio.dispatchBlockingCommand(BlockingCommand.Cylinder); - }} - > - Cylinder - -
- - Import asset... - - - Media library... - -
- + ${this.showInspector ? html` diff --git a/studio-web/src/index.ts b/studio-web/src/index.ts index f7b0abf..cb9fca3 100644 --- a/studio-web/src/index.ts +++ b/studio-web/src/index.ts @@ -1,3 +1,4 @@ +export * from "./lib/add-to-scene-menu.element"; export * from "./lib/canvas-provider.element"; export * from "./lib/iframe-messaging-bridge.element"; export * from "./lib/inspector/entity-inspector.element"; diff --git a/studio-web/src/lib/add-to-scene-menu.element.ts b/studio-web/src/lib/add-to-scene-menu.element.ts new file mode 100644 index 0000000..a8b0427 --- /dev/null +++ b/studio-web/src/lib/add-to-scene-menu.element.ts @@ -0,0 +1,68 @@ +import * as studio from "@storyteller/studio"; +import { BlockingCommand } from "@storyteller/studio"; +import { LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { createRef } from "lit/directives/ref.js" + +import "@storyteller/studio-ui/button"; +import "@storyteller/studio-ui/menu"; + +@customElement("sts-add-to-scene-menu") +export class AddToSceneMenuElement extends LitElement { + @property({ attribute: false }) + trigger = createRef(); + + protected override render = () => html` + + { + studio.dispatchBlockingCommand(BlockingCommand.Cube); + }} + > + Cube + + { + studio.dispatchBlockingCommand(BlockingCommand.Sphere); + }} + > + Sphere + + { + studio.dispatchBlockingCommand(BlockingCommand.Torus); + }} + > + Torus + + { + studio.dispatchBlockingCommand(BlockingCommand.Cylinder); + }} + > + Cylinder + +
+ + Import asset... + + + Media library... + +
+ `; +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 5b747bb..53ebcae 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,6 +27,7 @@ "@storyteller/studio-ui/tree": ["studio-ui/src/lib/tree.element.ts"], "@storyteller/studio-ui/forms/text-field": ["studio-ui/src/lib/forms/text-field.element.ts"], "@storyteller/studio-web": ["studio-web/src/index.ts"], + "@storyteller/studio-web/add-to-scene-menu": ["studio-web/src/lib/add-to-scene-menu.element.ts"], "@storyteller/studio-web/canvas-provider": ["studio-web/src/lib/canvas-provider.element.ts"], "@storyteller/studio-web/entity-inspector": ["studio-web/src/lib/inspector/entity-inspector.element.ts"], "@storyteller/studio-web/iframe-messaging-bridge": ["studio-web/src/lib/iframe-messaging-bridge.element.ts"], From 9b34a8232383160403344e314a487518f99f48a8 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 19:26:01 -0500 Subject: [PATCH 08/14] Simple API wrapper --- package-lock.json | 32 ++++++++------------ package.json | 4 +-- studio-frontend/src/app/app.element.ts | 42 +++++++++----------------- studio-web/src/index.ts | 1 + studio-web/src/lib/storyteller-api.ts | 29 ++++++++++++++++++ 5 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 studio-web/src/lib/storyteller-api.ts diff --git a/package-lock.json b/package-lock.json index 9d13de8..024ae5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "MIT", "dependencies": { "@fortawesome/fontawesome-pro": "^6.4.2", + "axios": "^1.6.7", "lit": "^2.8.0", "lodash": "^4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "tslib": "^2.3.0", - "uuid": "^9.0.1" + "tslib": "^2.3.0" }, "devDependencies": { "@nx/js": "16.9.1", @@ -4976,8 +4976,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -4992,12 +4991,11 @@ } }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "dev": true, + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -5606,7 +5604,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5950,7 +5947,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -6531,10 +6527,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "dev": true, + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -6591,7 +6586,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8041,7 +8035,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -8050,7 +8043,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8834,8 +8826,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", @@ -10202,6 +10193,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/package.json b/package.json index 65ddb8a..e2e5c35 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ }, "dependencies": { "@fortawesome/fontawesome-pro": "^6.4.2", + "axios": "^1.6.7", "lit": "^2.8.0", "lodash": "^4.17.21", "react": "18.2.0", "react-dom": "18.2.0", - "tslib": "^2.3.0", - "uuid": "^9.0.1" + "tslib": "^2.3.0" } } diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 922bda2..0c6627a 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -1,4 +1,4 @@ -import { drag, on } from "@storyteller/framework"; +import { drag, on, provide } from "@storyteller/framework"; import * as studio from "@storyteller/studio"; import { type SceneSavedEvent, @@ -9,6 +9,7 @@ import { import { ButtonElement } from "@storyteller/studio-ui/button"; import { SceneHierarchyElement, + StorytellerApi, StudioURLParams, } from "@storyteller/studio-web"; import { match } from "@storyteller/utility"; @@ -17,7 +18,6 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; -import { v4 as uuid } from "uuid"; import "@storyteller/studio-ui/button"; import "@storyteller/studio-ui/icon"; @@ -40,6 +40,10 @@ import "./blocking.element"; import "./app.element.scss"; @customElement("app-root") +@provide({ + token: StorytellerApi, + provide() { return this.api; } +}) export class AppElement extends LitElement { @state() showHelp = false; @state() showInspector = false; @@ -49,6 +53,7 @@ export class AppElement extends LitElement { @state() inspectorWidth = 320; @state() hierarchyHeight?: number; + private api = new StorytellerApi(); #enableAnimTimeline = false; // TODO #addToSceneButtonRef = createRef(); @@ -63,34 +68,15 @@ export class AppElement extends LitElement { @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { - const file = new File([scene], "scene.scn.ron", { type: "text/ron" }); - const formData = new FormData(); - - formData.append("uuid_idempotency_token", uuid()); - formData.append("file", file); - try { - const response = await fetch("https://api.fakeyou.com/v1/engine/create_scene", { - method: "POST", - credentials: "include", - headers: { "Accept": "text/plain" }, - body: formData, - }); + const file = new File([scene], "scene.scn.ron", { type: "text/ron" }); + const token = await this.api.createScene(file); - if (response.ok) { - const resultJson: { - success: boolean; - media_file_token: string; - } = await response.json(); - - if (resultJson.success) { - this.dispatchEvent(new CustomEvent("scene-uploaded", { - detail: resultJson.media_file_token, - bubbles: true, - composed: true, - })); - } - } + this.dispatchEvent(new CustomEvent("scene-uploaded", { + detail: token, + bubbles: true, + composed: true, + })); } catch (err) { console.error(err); diff --git a/studio-web/src/index.ts b/studio-web/src/index.ts index cb9fca3..99ace80 100644 --- a/studio-web/src/index.ts +++ b/studio-web/src/index.ts @@ -6,6 +6,7 @@ export * from "./lib/inspector/inspector.types"; export * from "./lib/loading.element"; export * from "./lib/scene-data-provider.element"; export * from "./lib/scene-hierarchy.element"; +export * from "./lib/storyteller-api"; export * from "./lib/studio.element"; export * from "./lib/transform-toolbar.element"; export * from "./lib/url-params"; diff --git a/studio-web/src/lib/storyteller-api.ts b/studio-web/src/lib/storyteller-api.ts new file mode 100644 index 0000000..714249d --- /dev/null +++ b/studio-web/src/lib/storyteller-api.ts @@ -0,0 +1,29 @@ +import axios from "axios"; + +export type MediaToken = string; + +export class StorytellerApi { + #api = axios.create({ + baseURL: "https://api.fakeyou.com", + withCredentials: true, + }); + + async createScene(file: File): Promise { + type Response = { + success: boolean; + media_file_token: MediaToken; + } + + const response = await this.#api.postForm("v1/engine/create_scene", { + uuid_idempotency_token: crypto.randomUUID(), + file, + }, { + headers: { Accept: "text/plain" }, + }); + + if (response.status === 200 && response.data.success) + return response.data.media_file_token; + + throw new Error(`API responded with status ${response.status}: "${response.statusText}"`); + } +} From 78c2d0b294acb9492b37fc2b69a95b1f21e3d93b Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 21:33:16 -0500 Subject: [PATCH 09/14] `session`, `media_files/list` endpoints --- studio-frontend/src/app/app.element.ts | 34 ++- studio-web/src/lib/storyteller-api.ts | 293 ++++++++++++++++++++++++- utility/src/index.ts | 1 + utility/src/lib/assert.ts | 8 + 4 files changed, 327 insertions(+), 9 deletions(-) create mode 100644 utility/src/lib/assert.ts diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 0c6627a..7f4a105 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -8,11 +8,12 @@ import { } from "@storyteller/studio"; import { ButtonElement } from "@storyteller/studio-ui/button"; import { + MediaFileType, SceneHierarchyElement, StorytellerApi, StudioURLParams, } from "@storyteller/studio-web"; -import { match } from "@storyteller/utility"; +import { assert, match } from "@storyteller/utility"; import { LitElement, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; @@ -66,6 +67,37 @@ export class AppElement extends LitElement { return this; } + // TODO + override async connectedCallback(): Promise { + try { + const session = await this.api.session(); + console.log("session:", session); + + if (!session.logged_in) + return; // TODO: Prompt for login? + + assert(session.user?.username != null); + + const mediaFilesResult = await this.api.listMediaFiles({ + username: session.user.username, + filterMediaType: [ + MediaFileType.Bvh, + MediaFileType.Fbx, + MediaFileType.Glb, + MediaFileType.Gltf, + MediaFileType.Mocap, + MediaFileType.ScnRon, + ], + }); + console.log(mediaFilesResult); + } + catch (err) { + console.error(err); + } + + super.connectedCallback(); + } + @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { try { diff --git a/studio-web/src/lib/storyteller-api.ts b/studio-web/src/lib/storyteller-api.ts index 714249d..5ec1cde 100644 --- a/studio-web/src/lib/storyteller-api.ts +++ b/studio-web/src/lib/storyteller-api.ts @@ -1,6 +1,4 @@ -import axios from "axios"; - -export type MediaToken = string; +import axios, { AxiosResponse } from "axios"; export class StorytellerApi { #api = axios.create({ @@ -8,10 +6,10 @@ export class StorytellerApi { withCredentials: true, }); - async createScene(file: File): Promise { + async createScene(file: File): Promise { type Response = { success: boolean; - media_file_token: MediaToken; + media_file_token: MediaFileToken; } const response = await this.#api.postForm("v1/engine/create_scene", { @@ -20,10 +18,289 @@ export class StorytellerApi { }, { headers: { Accept: "text/plain" }, }); + this.#validateResponse(response); - if (response.status === 200 && response.data.success) - return response.data.media_file_token; + return response.data.media_file_token; + } - throw new Error(`API responded with status ${response.status}: "${response.statusText}"`); + async session(): Promise { + const response = await this.#api.get("v1/session"); + this.#validateResponse(response); + + return response.data; + } + + async listMediaFiles(options: ListMediaFilesOptions = {}): Promise { + const params = {} as any; + + if (options.filterMediaType) + params.filter_media_type = options.filterMediaType.join(","); + if (options.sortAscending != null) + params.soer_ascending = options.sortAscending; + if (options.pageSize != null) + params.page_size = options.pageSize; + if (options.pageIndex != null) + params.page_index = options.pageIndex; + + const endpoint = options.username + ? `v1/media_files/list/user/${options.username}` + : "v1/media_files/list"; + + const response = await this.#api.get(endpoint, { params }); + this.#validateResponse(response); + + return response.data; + } + + #validateResponse(response: R): asserts response is OkResponse { + if (response.status !== 200) + throw new Error(`API responded with status ${response.status}: "${response.statusText}"`); + + if ( + response.data + && "success" in response.data + && response.data.success === false + ) { + throw new ApiError(response); + } } } + +export class ApiError extends Error { + detail: T; + + constructor (detail: T) { + super("API Error"); + this.detail = detail; + } +} + +type OkResponse = T & { status: 200 } + +export interface ListMediaFilesOptions { + username?: string; + filterMediaType?: MediaFileType[]; + pageSize?: number; + pageIndex?: number; + sortAscending?: boolean; +} + +export interface ListMediaFilesResult { + pagination: PaginationPage; + results: MediaFileForUserListItem[]; + success: boolean; +} + +/** + * Pagination by page id. + * + * This type of pagination is by "page id" and "page count". This should never + * be used to walk the entire public database as competitors and investors can + * use it to glean information about the scale of our service. This is best used + * for user profiles and scoped down results. + */ +export interface PaginationPage { + current: number; + total_page_count: number; +} + +export interface MediaFileListItem { + /** Date-time string */ + created_at: string; + creator_set_visibility: Visibility; + maybe_creator?: UserDetailsLight; + /** @deprecated */ + maybe_origin_model_token?: string; + maybe_origin_model_type?: MediaFileOriginModelType; + /** Text transcripts for TTS, etc. */ + maybe_text_transcript?: string; + media_type: MediaFileType; + origin: MediaFileOriginDetails; + origin_category: MediaFileOriginCategory; + origin_product_category: MediaFileOriginProductCategory; + /** URL to the media file. */ + public_bucket_path: string; + /** The primary key for Media Files */ + token: MediaFileToken; + /** Date-time string */ + updated_at: string; +} + +export type MediaFileForUserListItem = Omit; + +export enum Visibility { + Public = "public", + Hidden = "hidden", + Private = "private", +} + +/** + * Used in the `media_files` table in a `VARCHAR` field. + * + * DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. + */ +export enum MediaFileOriginModelType { + RvcV2 = "rvc_v2", + SadTalker = "sad_talker", + SoVitsSvc = "so_vits_svc", + Tacotron2 = "tacotron2", + VallEX = "vall_e_x", + Rerender = "rerender", + MocapNet = "mocap_net", + ComfyUi = "comfy_ui", + StyleTts2 = "styletts2", + StableDiffusion_1_5 = "stable_diffusion_1_5", +} + +/** + * Used in the `media_files` table in a `VARCHAR(16)` field. + * + * DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. + */ +export enum MediaFileType { + Audio = "audio", + Image = "image", + Video = "video", + Mocap = "mocap", + Bvh = "bvh", + Fbx = "fbx", + Glb = "glb", + Gltf = "gltf", + ScnRon = "scene_ron", +} + +/** Fields useful for enriching media file listings */ +export interface MediaFileOriginDetails { + maybe_model?: Model; + origin_category: MediaFileOriginCategory; + product_category: MediaFileOriginProductCategory; +} + +export interface Model { + model_type: MediaFileOriginModelType; + /** + * The model title (typically only populated for model_weights models, not + * legacy tables such as tts_models.) + */ + title?: string; + /** The primary key for the "model_weights" table. */ + token?: string; +} + +/** + * Used in the `media_files` table in a `VARCHAR` field. + * + * DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. + */ +export enum MediaFileOriginCategory { + Inference = "inference", + Processed = "processed", + Upload = "upload", + DeviceApi = "device_api", + StoryEngine = "story_engine", +} + +/** + * Used in the `media_files` table in `VARCHAR(16)` field + * `origin_product_category`. + * + * This value indicates what product originally created the media file. (Not the + * ML model or user upload process.) This will let us scope media files to the + * product that generated them and filter them out of unrelated products if + * necessary (eg. a user probably doesn't want "Voice Designer" dataset samples + * in a video generation flow.) + * + * DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. + */ +export enum MediaFileOriginProductCategory { + Unknown = "unknown", + FaceAnimator = "face_animator", + Tts = "tts", + VoiceConversion = "voice_conversion", + ZsVoice = "zs_voice", + VideoFilter = "video_filter", + Mocap = "mocap", + ImageGen = "image_gen", + Workflow = "workflow", +} + +export type MediaFileToken = string; + +export interface Session { + logged_in: boolean; + success: boolean; + user?: { + can_access_studio: boolean; + can_approve_w21_templates: boolean; + can_ban_users: boolean; + can_delete_other_users_tts_models: boolean; + can_delete_other_users_tts_results: boolean; + can_delete_other_users_w2l_results: boolean; + can_delete_other_users_w2l_templates: boolean; + can_delete_own_account: boolean; + can_delete_own_tts_models: boolean; + can_delete_own_tts_results: boolean; + can_delete_own_w2l_results: boolean; + can_delete_own_w2l_templates: boolean; + can_delete_users: boolean; + can_edit_other_users_profiles: boolean; + can_edit_other_users_tts_models: boolean; + can_edit_other_users_w2l_templates: boolean; + can_upload_tts_models: boolean; + can_upload_w2l_templates: boolean; + can_use_tts: boolean; + can_use_w2l: boolean; + core_info: UserDetailsLight; + display_name: string; + email_gravatar_hash: string; + fakeyou_plan: FakeYouPlan; + maybe_feature_flags: unknown; + storyteller_stream_plan: StorytellerStreamPlan; + user_token: string; + username: string; + }; +} + +/** + * Everything we need to refer to a user on the public web interface. + */ +export interface UserDetailsLight { + default_avatar: DefaultAvatarInfo; + /** + * As of 2023-08-23, this is the username with capitalization (In the future, + * a display name can be customized by the user.) + */ + display_name: string; + /** + * Email hash for Gravatar Always set for now since login is email/username + + * password. In the future this will need to become an optional OR be filled + * with a bogus hash. + */ + gravatar_hash: string; + /** The primary key for users. */ + user_token: string; + /** + * The unique username someone logs in with. As of 2023-08-23, this is always + * lowercase. + */ + username: string; +} + +export interface DefaultAvatarInfo { + color_index: number; + image_index: number; +} + +export enum FakeYouPlan { + Free = "free", + Basic = "basic", + Standard = "standard", + Pro = "pro", +} + +export enum StorytellerStreamPlan { + Free = "free", + Basic = "basic", + Standard = "standard", + Pro = "pro", +} diff --git a/utility/src/index.ts b/utility/src/index.ts index 1c552cf..e35da3a 100644 --- a/utility/src/index.ts +++ b/utility/src/index.ts @@ -1,3 +1,4 @@ +export * from "./lib/assert"; export * from "./lib/exists"; export * from "./lib/match"; export * from "./lib/types"; diff --git a/utility/src/lib/assert.ts b/utility/src/lib/assert.ts new file mode 100644 index 0000000..1da67e9 --- /dev/null +++ b/utility/src/lib/assert.ts @@ -0,0 +1,8 @@ +export function assert(expression: unknown, message?: string): asserts expression { + if (!expression) { + let err = "Assertion failed"; + if (message) err += `: ${message}`; + + throw new Error(err); + } +} From 096e143d0a7578ed7d7ab6d79b4de5544aad407e Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Thu, 7 Mar 2024 21:37:19 -0500 Subject: [PATCH 10/14] Move `super` call --- studio-frontend/src/app/app.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 7f4a105..915f8d6 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -69,6 +69,8 @@ export class AppElement extends LitElement { // TODO override async connectedCallback(): Promise { + super.connectedCallback(); + try { const session = await this.api.session(); console.log("session:", session); @@ -94,8 +96,6 @@ export class AppElement extends LitElement { catch (err) { console.error(err); } - - super.connectedCallback(); } @on("scene-saved") From e046e408b3a29a65608230adca5e123f201c6864 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 8 Mar 2024 17:37:07 -0500 Subject: [PATCH 11/14] More endpoints --- studio-web/src/lib/storyteller-api.ts | 123 +++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/studio-web/src/lib/storyteller-api.ts b/studio-web/src/lib/storyteller-api.ts index 5ec1cde..a56d804 100644 --- a/studio-web/src/lib/storyteller-api.ts +++ b/studio-web/src/lib/storyteller-api.ts @@ -1,3 +1,4 @@ +import { match } from "@storyteller/utility"; import axios, { AxiosResponse } from "axios"; export class StorytellerApi { @@ -7,17 +8,15 @@ export class StorytellerApi { }); async createScene(file: File): Promise { - type Response = { - success: boolean; - media_file_token: MediaFileToken; - } - - const response = await this.#api.postForm("v1/engine/create_scene", { - uuid_idempotency_token: crypto.randomUUID(), - file, - }, { - headers: { Accept: "text/plain" }, - }); + const response = await this.#api.postForm( + "v1/engine/create_scene", + { + uuid_idempotency_token: crypto.randomUUID(), + file, + }, { + headers: { Accept: "text/plain" }, + }, + ); this.#validateResponse(response); return response.data.media_file_token; @@ -52,6 +51,39 @@ export class StorytellerApi { return response.data; } + async uploadMediaFile(file: File, subtype: MediaFileSubtype): Promise { + const [, _basename, extension] = file.name.match(/(.+?)\.(.+)/) ?? []; + const mimeType = match (extension, { + "fbx": () => "application/fbx", + "gltf": () => "model/gltf+json", + "glb": () => "model/gltf-binary", + "scn.ron": () => "text/ron", + _: () => "application/octet-stream", + }); + + const response = await this.#api.postForm( + "v1/media_files/upload/engine_asset", + { + uuid_idempotency_token: crypto.randomUUID(), + file, + media_file_subtype: subtype, + }, + { + headers: { Accept: mimeType }, + }, + ); + this.#validateResponse(response); + + return response.data.media_file_token; + } + + async mediaFile(token: MediaFileToken): Promise { + const response = await this.#api.get(`v1/media_files/file/${token}`); + this.#validateResponse(response); + + return response.data.media_file; + } + #validateResponse(response: R): asserts response is OkResponse { if (response.status !== 200) throw new Error(`API responded with status ${response.status}: "${response.statusText}"`); @@ -91,6 +123,11 @@ export interface ListMediaFilesResult { success: boolean; } +export interface MediaUploadResponse { + success: boolean; + media_file_token: MediaFileToken; +} + /** * Pagination by page id. * @@ -104,6 +141,31 @@ export interface PaginationPage { total_page_count: number; } +export interface MediaFileResult { + success: boolean; + media_file: MediaFile; +} + +export interface MediaFile { + token: MediaFileToken; + media_type: MediaFileType; + maybe_media_subtype?: MediaFileSubtype; + maybe_batch_token?: unknown; + public_bucket_path: string; + maybe_model_weight_info?: unknown; + maybe_creator_user?: UserDetailsLight; + creator_set_visibility: Visibility; + maybe_text_transcript?: string; + maybe_prompt_token?: unknown; + maybe_original_filename?: string; + is_emulated_media_file: boolean; + stats: Stats; + /** Date-time string */ + created_at: string; + /** Date-time string */ + updated_at: string; +} + export interface MediaFileListItem { /** Date-time string */ created_at: string; @@ -115,6 +177,7 @@ export interface MediaFileListItem { /** Text transcripts for TTS, etc. */ maybe_text_transcript?: string; media_type: MediaFileType; + maybe_media_subtype?: MediaFileSubtype; origin: MediaFileOriginDetails; origin_category: MediaFileOriginCategory; origin_product_category: MediaFileOriginProductCategory; @@ -126,6 +189,11 @@ export interface MediaFileListItem { updated_at: string; } +export interface Stats { + positive_rating_count: number; + bookmark_count: number; +} + export type MediaFileForUserListItem = Omit; export enum Visibility { @@ -169,6 +237,39 @@ export enum MediaFileType { ScnRon = "scene_ron", } +export enum MediaFileSubtype { + /** + * Animation file from Mixamo. + * Primarily used for FBX and GLB. + */ + Mixamo = "mixamo", + /** + * Animation file from MocapNet. + * Primarily used for BVH. + */ + MocapNet = "mocap_net", + /** + * Generic animation case. + * Used for BVH files, but can also pertain to animation-only files of other + * types. + */ + AnimationOnly = "animation_only", + /** + * @deprecated Use `SceneImport` instead. + */ + Scene = "scene", + /** + * Generic 3D scene file. + * Can pertain to glTF, glB, FBX, etc. + */ + SceneImport = "scene_import", + /** + * Native Storyteller scene format. + * Typically stored in a `.scn.ron` file. + */ + StorytellerScene = "storyteller_scene", +} + /** Fields useful for enriching media file listings */ export interface MediaFileOriginDetails { maybe_model?: Model; From 0642c1c37017523a51da2b02491a27799970873a Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 8 Mar 2024 17:39:13 -0500 Subject: [PATCH 12/14] Fix overlay issues --- studio-ui/src/lib/menu.element.ts | 144 ++++++++---------- studio-ui/src/lib/overlay-provider.element.ts | 20 ++- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 2d0bd23..c9bab66 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -36,69 +36,6 @@ export class MenuElement extends LitElement { #panelRef = createRef(); #handle?: WeakRef; - protected override update(changes: PropertyValues): void { - super.update(changes); - - if (this.#handle) - this.overlay.reRender(this.#handle); - } - - protected override updated(changes: PropertyValues): void { - if (changes.has("trigger")) { - const prevTrigger = changes.get("trigger"); - if (prevTrigger?.value) { - prevTrigger.value.removeEventListener("click", this.onTriggerClick); - } - - const trigger = this.trigger.value; - if (!trigger) return; - - trigger.setAttribute("aria-owns", this.#menuId); - trigger.ariaHasPopup = "true"; - trigger.ariaExpanded = "false"; - - trigger.addEventListener("click", this.onTriggerClick); - } - - super.updated(changes); - } - - override disconnectedCallback(): void { - this.trigger.value?.removeEventListener("click", this.onTriggerClick); - super.disconnectedCallback(); - } - - onTriggerClick = (): void => { - this.show(); - } - - @on("window:click") - onWindowClick(event: PointerEvent): void { - if (!this.#handle || !this.#panelRef.value) - return; - - const path = event.composedPath(); - if ( - !path.includes(this.#panelRef.value) - && (!this.trigger.value || !path.includes(this.trigger.value)) - ) { - this.hide(); - } - } - - onMenuClick(event: PointerEvent): void { - event.stopPropagation(); - - if ( - this.autoClose - && event.target - && event.target instanceof HTMLElement - && event.target.role === "menuitem" - ) { - this.hide(); - } - } - show(): void { const trigger = this.trigger.value; if (!trigger) return; @@ -118,14 +55,13 @@ export class MenuElement extends LitElement { top: `${rect.bottom + 4}px`, left: `${rect.left}px`, })} - @click=${this.onMenuClick} - > - ${ - // Move the nodes assigned to the 's slot into the - // 's slot - this.#slotRef.value?.assignedNodes() - } - + ?autoClose=${this.autoClose} + @hide=${this.hide} + >${ + // Move the nodes assigned to the 's slot into the + // 's slot + this.#slotRef.value?.assignedNodes() + } `); } @@ -149,6 +85,43 @@ export class MenuElement extends LitElement { this.#handle = undefined; } + protected override update(changes: PropertyValues): void { + super.update(changes); + + if (this.#handle) + this.overlay.reRender(this.#handle); + } + + protected override updated(changes: PropertyValues): void { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + if (prevTrigger?.value) { + prevTrigger.value.removeEventListener("click", this.#onTriggerClick); + } + + const trigger = this.trigger.value; + if (!trigger) return; + + trigger.setAttribute("aria-owns", this.#menuId); + trigger.ariaHasPopup = "true"; + trigger.ariaExpanded = "false"; + + trigger.addEventListener("click", this.#onTriggerClick); + } + + super.updated(changes); + } + + override disconnectedCallback(): void { + this.trigger.value?.removeEventListener("click", this.#onTriggerClick); + super.disconnectedCallback(); + } + + #onTriggerClick = (event: Event): void => { + event.stopPropagation(); + this.show(); + } + protected override render = () => html` ` @@ -162,16 +135,33 @@ export class MenuPanelElement extends LitElement { @property({ reflect: true }) override role = "menu"; + @property({ type: Boolean }) + autoClose = false; + #slotRef = createRef(); getSlot(): HTMLSlotElement | undefined { return this.#slotRef.value; } + @on("click") + onClick(event: PointerEvent): void { + event.stopPropagation(); + + if ( + this.autoClose + && event.target + && event.target instanceof HTMLElement + && event.target.role === "menuitem" + ) { + this.dispatchEvent(new CustomEvent("hide")); + } + } + + @on("window:click") + onWindowClick(event: PointerEvent): void { + if (!event.composedPath().includes(this)) + this.dispatchEvent(new CustomEvent("hide")); + } + protected override render = () => html` ` } - -declare global { - interface HTMLElementTagNameMap { - "sts-menu-panel": MenuPanelElement; - } -} diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts index 878610b..774de97 100644 --- a/studio-ui/src/lib/overlay-provider.element.ts +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -2,17 +2,24 @@ import { UniqueToken, provide } from "@storyteller/framework"; import { LitElement, TemplateResult, html, render } from "lit"; import { customElement } from "lit/decorators.js"; import { Ref, createRef, ref } from "lit/directives/ref.js"; -import { styleMap } from "lit/directives/style-map.js"; +import { StyleInfo, styleMap } from "lit/directives/style-map.js"; export const OVERLAY_PROVIDER = UniqueToken.create(); // TODO: Document this export interface OverlayProvider { - attach(host: LitElement, template: TemplateResult): WeakRef; + attach( + host: LitElement, + template: TemplateResult, + options?: AttachOptions, + ): WeakRef; detach(handle: WeakRef): void; reRender(handle: WeakRef): void; } +export interface AttachOptions { + containerStyles?: Readonly; +} // TODO: Document this export interface OverlayHandle { @@ -42,10 +49,16 @@ export class OverlayProviderElement #containerTemplates: TemplateResult[] = []; #templates: [LitElement, TemplateResult][] = []; - attach(host: LitElement, template: TemplateResult): WeakRef { + attach( + host: LitElement, + template: TemplateResult, + options: AttachOptions = {}, + ): WeakRef { const index = this.#templates.length; const handle = new OverlayHandleInternal(index); + const containerStyles = options.containerStyles ?? {}; + this.#handles.push(handle); this.#containerRefs.push(createRef()); this.#containerTemplates.push(html` @@ -55,6 +68,7 @@ export class OverlayProviderElement style=${styleMap({ position: "fixed", inset: 0, + ...containerStyles, })} > `); From f347ac783f1608c95f64a1e283a8791839f3e88c Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 8 Mar 2024 17:40:11 -0500 Subject: [PATCH 13/14] WIP Media Library dialog --- studio-frontend/src/app/app.element.ts | 46 ++---- .../src/app/media-library.element.scss | 14 ++ .../src/app/media-library.element.ts | 131 ++++++++++++++++++ .../src/lib/add-to-scene-menu.element.ts | 3 + 4 files changed, 160 insertions(+), 34 deletions(-) create mode 100644 studio-frontend/src/app/media-library.element.scss create mode 100644 studio-frontend/src/app/media-library.element.ts diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 915f8d6..737d2eb 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -8,17 +8,17 @@ import { } from "@storyteller/studio"; import { ButtonElement } from "@storyteller/studio-ui/button"; import { - MediaFileType, SceneHierarchyElement, StorytellerApi, StudioURLParams, } from "@storyteller/studio-web"; -import { assert, match } from "@storyteller/utility"; +import { match } from "@storyteller/utility"; import { LitElement, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; +import { MediaLibraryElement } from "./media-library.element"; import "@storyteller/studio-ui/button"; import "@storyteller/studio-ui/icon"; @@ -37,6 +37,7 @@ import "./anim-editor.element"; import "./help.element"; import "./blocking.element"; +import "./media-library.element"; import "./app.element.scss"; @@ -58,6 +59,7 @@ export class AppElement extends LitElement { #enableAnimTimeline = false; // TODO #addToSceneButtonRef = createRef(); + #mediaLibraryRef = createRef(); #inspectorRef = createRef(); #hierarchyRef = createRef(); @@ -67,37 +69,6 @@ export class AppElement extends LitElement { return this; } - // TODO - override async connectedCallback(): Promise { - super.connectedCallback(); - - try { - const session = await this.api.session(); - console.log("session:", session); - - if (!session.logged_in) - return; // TODO: Prompt for login? - - assert(session.user?.username != null); - - const mediaFilesResult = await this.api.listMediaFiles({ - username: session.user.username, - filterMediaType: [ - MediaFileType.Bvh, - MediaFileType.Fbx, - MediaFileType.Glb, - MediaFileType.Gltf, - MediaFileType.Mocap, - MediaFileType.ScnRon, - ], - }); - console.log(mediaFilesResult); - } - catch (err) { - console.error(err); - } - } - @on("scene-saved") async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise { try { @@ -220,7 +191,14 @@ export class AppElement extends LitElement { ${this.#params.mode === StudioMode.Editor ? html` - + { + this.#mediaLibraryRef.value?.open(); + }} + > + + ${this.showInspector ? html` diff --git a/studio-frontend/src/app/media-library.element.scss b/studio-frontend/src/app/media-library.element.scss new file mode 100644 index 0000000..cd71d90 --- /dev/null +++ b/studio-frontend/src/app/media-library.element.scss @@ -0,0 +1,14 @@ +:host { + display: block; + min-width: 512px; + min-height: 256px; + max-width: 80%; + max-height: 80%; + padding: 1rem; + overflow: auto; + + background: #0006; + color: #FFFC; + backdrop-filter: blur(16px); + border-radius: 0.5rem; +} diff --git a/studio-frontend/src/app/media-library.element.ts b/studio-frontend/src/app/media-library.element.ts new file mode 100644 index 0000000..8fc5bea --- /dev/null +++ b/studio-frontend/src/app/media-library.element.ts @@ -0,0 +1,131 @@ +import { inject, on } from "@storyteller/framework"; +import { + OVERLAY_PROVIDER, + type OverlayHandle, + type OverlayProvider, +} from "@storyteller/studio-ui/overlay-provider"; +import { + type MediaFile, + MediaFileType, + StorytellerApi, +} from "@storyteller/studio-web"; +import { LitElement, unsafeCSS, type PropertyValues, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { createRef, ref } from "lit/directives/ref.js"; +import { until } from "lit/directives/until.js"; + +import dialogStyles from "./media-library.element.scss?inline"; + +@customElement("sts-media-library") +export class MediaLibraryElement extends LitElement { + @property({ attribute: false }) + trigger = createRef(); + + @inject(OVERLAY_PROVIDER) + overlay!: OverlayProvider; + + #overlayHandle?: WeakRef; + #dialogRef = createRef(); + + open(): void { + this.#overlayHandle = this.overlay.attach(this, html` + this.close()} + > + `, { + containerStyles: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + }); + } + + close(): void { + if (this.#overlayHandle) + this.overlay.detach(this.#overlayHandle); + } + + protected override updated(changes: PropertyValues): void { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + if (prevTrigger?.value) { + prevTrigger.value.removeEventListener("click", this.onTriggerClick); + } + + const trigger = this.trigger.value; + if (!trigger) return; + + trigger.addEventListener("click", this.onTriggerClick); + } + + super.updated(changes); + } + + override disconnectedCallback(): void { + this.trigger.value?.removeEventListener("click", this.onTriggerClick); + super.disconnectedCallback(); + } + + onTriggerClick = (): void => { + this.open(); + } +} + +@customElement("sts-media-library-dialog") +export class MediaLibraryDialogElement extends LitElement { + static override styles = unsafeCSS(dialogStyles); + + @property({ reflect: true }) + override role = "dialog"; + + @state() + mediaFiles?: Promise; + + @inject(StorytellerApi) + _api!: StorytellerApi; + + override async connectedCallback(): Promise { + this.mediaFiles = this._api + .session() + .then(session => { + if (!session.logged_in || !session.user?.username) + return Promise.reject(new Error("Not logged in")); + + return this._api.listMediaFiles({ + username: session.user.username, + filterMediaType: [ + MediaFileType.Bvh, + MediaFileType.Glb, + MediaFileType.Gltf, + MediaFileType.ScnRon, + ], + }); + }) + .then(result => Promise.all( + result.results.map(({ token }) => this._api.mediaFile(token)) + )) + .catch(err => { + console.error(err); + return []; + }); + + super.connectedCallback(); + } + + @on("window:click") + onWindowClick(event: PointerEvent): void { + if (!event.composedPath().includes(this)) + this.dispatchEvent(new CustomEvent("close")); + } + + protected override render = () => html` + ${until( + this.mediaFiles?.then(files => html` +
${JSON.stringify(files, null, "  ")}
+ `), + html`Loading...` + )} + `; +} diff --git a/studio-web/src/lib/add-to-scene-menu.element.ts b/studio-web/src/lib/add-to-scene-menu.element.ts index a8b0427..54684ad 100644 --- a/studio-web/src/lib/add-to-scene-menu.element.ts +++ b/studio-web/src/lib/add-to-scene-menu.element.ts @@ -60,6 +60,9 @@ export class AddToSceneMenuElement extends LitElement { { + this.dispatchEvent(new CustomEvent("open-media-library")); + }} > Media library... From f07ac0c0995c07100a99731bac40c111da6c90ab Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 8 Mar 2024 19:58:20 -0500 Subject: [PATCH 14/14] Replace calls to `crypto.randomUUID` --- studio-ui/src/lib/menu.element.ts | 3 ++- studio-web/src/lib/storyteller-api.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index c9bab66..db3c497 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { createRef, ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; +import { v4 as uuid } from "uuid"; import panelStyles from "./menu.element.scss?inline"; @@ -31,7 +32,7 @@ export class MenuElement extends LitElement { @inject(OVERLAY_PROVIDER) overlay!: OverlayProvider; - #menuId = crypto.randomUUID(); + #menuId = uuid(); #slotRef = createRef(); #panelRef = createRef(); #handle?: WeakRef; diff --git a/studio-web/src/lib/storyteller-api.ts b/studio-web/src/lib/storyteller-api.ts index a56d804..27e042d 100644 --- a/studio-web/src/lib/storyteller-api.ts +++ b/studio-web/src/lib/storyteller-api.ts @@ -1,5 +1,6 @@ import { match } from "@storyteller/utility"; import axios, { AxiosResponse } from "axios"; +import { v4 as uuid } from "uuid"; export class StorytellerApi { #api = axios.create({ @@ -11,7 +12,7 @@ export class StorytellerApi { const response = await this.#api.postForm( "v1/engine/create_scene", { - uuid_idempotency_token: crypto.randomUUID(), + uuid_idempotency_token: uuid(), file, }, { headers: { Accept: "text/plain" }, @@ -64,7 +65,7 @@ export class StorytellerApi { const response = await this.#api.postForm( "v1/media_files/upload/engine_asset", { - uuid_idempotency_token: crypto.randomUUID(), + uuid_idempotency_token: uuid(), file, media_file_subtype: subtype, },