diff web/ascoltami/Light9AscoltamiUi.ts @ 2439:06da5db2fafe

rewrite ascoltami to use the graph for more playback data
author drewp@bigasterisk.com
date Thu, 30 May 2024 01:08:07 -0700
parents 4556eebe5d73
children 2ce77421c0b7
line wrap: on
line diff
--- a/web/ascoltami/Light9AscoltamiUi.ts	Wed May 29 17:28:25 2024 -0700
+++ b/web/ascoltami/Light9AscoltamiUi.ts	Thu May 30 01:08:07 2024 -0700
@@ -1,27 +1,19 @@
 import debug from "debug";
-import { css, html, LitElement } from "lit";
+import { css, html, LitElement, PropertyValues } from "lit";
 import { customElement, property } from "lit/decorators.js";
-import { classMap } from "lit/directives/class-map.js";
 import { NamedNode } from "n3";
-import Sylvester from "sylvester";
-import { Zoom } from "../light9-timeline-audio";
 import { PlainViewState } from "../Light9CursorCanvas";
 import { getTopGraph } from "../RdfdbSyncedGraph";
 import { SyncedGraph } from "../SyncedGraph";
-import { TimingUpdate } from "./main";
-import { showRoot } from "../show_specific";
-export { Light9TimelineAudio } from "../light9-timeline-audio";
-export { Light9CursorCanvas } from "../Light9CursorCanvas";
+import { PlayerState } from "./PlayerState";
 export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph";
 export { ResourceDisplay } from "../ResourceDisplay";
-const $V = Sylvester.Vector.create;
+export { Light9AscoltamiTimeline } from "./Light9AscoltamiTimeline";
+export { Light9SongListing } from "./Light9SongListing";
 
 debug.enable("*");
 const log = debug("asco");
 
-function byId(id: string): HTMLElement {
-  return document.getElementById(id)!;
-}
 async function postJson(url: string, jsBody: Object) {
   return fetch(url, {
     method: "POST",
@@ -29,157 +21,154 @@
     body: JSON.stringify(jsBody),
   });
 }
+
 @customElement("light9-ascoltami-ui")
 export class Light9AscoltamiUi extends LitElement {
   graph!: SyncedGraph;
-  times!: { intro: number; post: number };
-  @property() nextText: string = "";
-  @property() isPlaying: boolean = false;
   @property() show: NamedNode | null = null;
   @property() song: NamedNode | null = null;
   @property() selectedSong: NamedNode | null = null;
-  @property() currentDuration: number = 0;
-  @property() zoom: Zoom;
-  @property() overviewZoom: Zoom;
   @property() viewState: PlainViewState | null = null;
+  @property() host: any;
+  @property() playerState: PlayerState = { duration: null, endOfSong: null, pausedSongTime: null, playing: null, song: null, wallStartTime: null };
+  @property() playerTime: number = 0;
   static styles = [
     css`
       :host {
         display: flex;
         flex-direction: column;
       }
-      .timeRow {
-        margin: 14px;
-        position: relative;
-      }
-      #overview {
-        height: 60px;
-      }
-      #zoomed {
-        margin-top: 40px;
-        height: 80px;
-      }
-      #cursor {
-        position: absolute;
-        left: 0;
-        top: 0;
-        width: 100%;
-        height: 100%;
-      }
-      #grow {
-        flex: 1 1 auto;
-        display: flex;
-      }
-      #grow > span {
-        display: flex;
-        position: relative;
-        width: 50%;
+
+      .keyCap {
+        color: #ccc;
+        background: #525252;
+        display: inline-block;
+        border: 1px outset #b3b3b3;
+        padding: 2px 3px;
+        margin: 3px 0;
+        margin-left: 0.4em;
+        font-size: 16px;
+        box-shadow: 0.9px 0.9px 0px 2px #565656;
+        border-radius: 2px;
       }
-      #playSelected {
-        height: 100px;
-      }
-      #songList {
-        overflow-y: scroll;
-        position: absolute;
-        left: 0;
-        top: 0;
-        right: 0;
-        bottom: 0;
+
+      button {
+        min-height: 48pt;
+        min-width: 65pt;
       }
-      #songList .row {
-        width: 60%;
-        min-height: 40px;
-        text-align: left;
-        position: relative;
-      }
-      #songList .row:nth-child(even) {
-        background: #333;
+
+      #mainRow {
+        display: flex;
+        flex-direction: row;
       }
-      #songList .row:nth-child(odd) {
-        background: #444;
+
+      light9-song-listing {
+        flex-grow: 1;
       }
-      #songList button {
-        min-height: 40px;
-        margin-bottom: 10px;
-      }
-      #songList .row.playing {
-        box-shadow: 0 0 30px red;
-        background-color: #de5050;
+
+      th {
+        text-align: right;
       }
     `,
   ];
-  render() {
-    return html`<rdfdb-synced-graph></rdfdb-synced-graph>
+
+  constructor() {
+    super();
+    getTopGraph().then((g) => {
+      this.graph = g;
+      this.graph.runHandler(this.updatePlayState.bind(this), "playstate-ui");
+    });
+    setInterval(this.updateT.bind(this), 100);
+  }
+
+  protected async firstUpdated(_changedProperties: PropertyValues<this>) {
+    this.bindKeys();
+    const config = await (await fetch("/service/ascoltami/config")).json();
+    document.title = document.title.replace("{{host}}", config.host);
+    this.host = config.host;
+  }
 
-      <link rel="stylesheet" href="../style.css" />
+  updatePlayState() {
+    const U = this.graph.U();
+    const asco = U(":ascoltami");
+    this.playerState = {
+      duration: this.graph.optionalFloatValue(asco, U(":duration")),
+      endOfSong: this.graph.optionalBooleanValue(asco, U(":endOfSong")),
+      pausedSongTime: this.graph.optionalFloatValue(asco, U(":pausedSongTime")),
+      wallStartTime: this.graph.optionalFloatValue(asco, U(":wallStartTime")),
+      playing: this.graph.optionalBooleanValue(asco, U(":playing")),
+      song: this.graph.optionalUriValue(asco, U(":song")),
+    };
+    this.updateT();
+  }
 
-      <!-- <h1>ascoltami <a href="metrics">[metrics]</a></h1> -->
+  updateT() {
+    if (this.playerState.wallStartTime !== null) {
+      this.playerTime = Date.now() / 1000 - this.playerState.wallStartTime;
+    } else if (this.playerState.pausedSongTime !== null) {
+      this.playerTime = this.playerState.pausedSongTime;
+    } else {
+      this.playerTime = 0;
+    }
+  }
+
+  render() {
+    return html`
+      <h1>ascoltami on ${this.host}</h1>
 
-      <div id="grow">
-        <span>
-          <div id="songList">
-            <table>
-              ${this.songList.map(
-                (song) => html`
-                  <tr
-                    class="row ${classMap({
-                      playing: !!(this.song && song.equals(this.song)),
-                    })}"
-                  >
-                    <td><resource-display .uri=${song} noclick></resource-display></td>
-                    <td>
-                      <button @click=${this.onSelectSong.bind(this, song)}>
-                        <span>Select</span>
-                      </button>
-                    </td>
-                  </tr>
-                `
-              )}
-            </table>
-          </div> </span
-        ><span>
-          <div id="right">
-            <div>
-              Selected:
-              <resource-display .uri=${this.selectedSong}></resource-display>
-            </div>
-            <div>
-              <button id="playSelected" ?disabled=${this.selectedSong === null} @click=${this.onPlaySelected}>Play selected from start</button>
-            </div>
+      <span><rdfdb-synced-graph></rdfdb-synced-graph></span>
+      <div id="mainRow">
+        <light9-song-listing
+          @selectsong=${(ev: any) => {
+            this.selectedSong = ev.detail.song;
+          }}
+          .selectedSong=${this.selectedSong}
+        ></light9-song-listing>
+        <div>
+          <light9-ascoltami-timeline .playerState=${this.playerState} .playerTime=${this.playerTime}></light9-ascoltami-timeline>
+          <div>
+            <button ?disabled=${this.selectedSong == null} @click=${this.onLoadSelected}>Change to and play selected <span class="keyCap">l</span></button>
+            <button ?disabled=${false} @click=${this.onCmdZero}>Seek to zero <span class="keyCap">z</span></button>
+            <button ?disabled=${this.playerState.playing || this.playerState.song == null} @click=${this.onCmdPlay}>Play <span class="keyCap">p</span></button>
+            <button ?disabled=${!this.playerState.playing} @click=${this.onCmdStop}>Stop <span class="keyCap">s</span></button>
+            <button ?disabled=${true} @click=${this.onCmdGo}>Go <span class="keyCap">g</span></button>
           </div>
-        </span>
-      </div>
-
-      <div class="timeRow">
-        <div id="timeSlider"></div>
-        <light9-timeline-audio id="overview" .show=${this.show} .song=${this.song} .zoom=${this.overviewZoom}> </light9-timeline-audio>
-        <light9-timeline-audio id="zoomed" .show=${this.show} .song=${this.song} .zoom=${this.zoom}></light9-timeline-audio>
-        <light9-cursor-canvas id="cursor" .viewState=${this.viewState}></light9-cursor-canvas>
+          ${this.renderPlayerStateTable()}
+        </div>
       </div>
-
-      <div class="commands">
-        <button id="cmd-stop" @click=${this.onCmdStop} class="playMode ${classMap({ active: !this.isPlaying })}">
-          <strong>Stop</strong>
-          <div class="key">s</div>
-        </button>
-        <button id="cmd-play" @click=${this.onCmdPlay} class="playMode ${classMap({ active: this.isPlaying })}">
-          <strong>Play</strong>
-          <div class="key">p</div>
-        </button>
-        <button id="cmd-intro" @click=${this.onCmdIntro}>
-          <strong>Skip intro</strong>
-          <div class="key">i</div>
-        </button>
-        <button id="cmd-post" @click=${this.onCmdPost}>
-          <strong>Skip to Post</strong>
-          <div class="key">t</div>
-        </button>
-        <button id="cmd-go" @click=${this.onCmdGo}>
-          <strong>Go</strong>
-          <div class="key">g</div>
-          <div id="next">${this.nextText}</div>
-        </button>
-      </div>`;
+    `;
+  }
+  renderPlayerStateTable() {
+    return html` <table>
+      <tr>
+        <th>duration</th>
+        <td>${this.playerState.duration}</td>
+      </tr>
+      <tr>
+        <th>endOfSong</th>
+        <td>${this.playerState.endOfSong}</td>
+      </tr>
+      <tr>
+        <th>pausedSongTime</th>
+        <td>${this.playerState.pausedSongTime?.toFixed(3)}</td>
+      </tr>
+      <tr>
+        <th>playing</th>
+        <td>${this.playerState.playing}</td>
+      </tr>
+      <tr>
+        <th>song</th>
+        <td>${this.playerState.song?.value}</td>
+      </tr>
+      <tr>
+        <th>wallStartTime</th>
+        <td>${this.playerState.wallStartTime}</td>
+      </tr>
+      <tr>
+        <th>t</th>
+        <td>${this.playerTime.toFixed(3)}</td>
+      </tr>
+    </table>`;
   }
 
   onSelectSong(song: NamedNode, ev: MouseEvent) {
@@ -189,122 +178,46 @@
       this.selectedSong = song;
     }
   }
-  async onPlaySelected(ev: Event) {
+
+  async onLoadSelected() {
     if (!this.selectedSong) {
       return;
     }
-    await fetch("../service/ascoltami/song", { method: "POST", body: this.selectedSong.value });
+    await fetch("/service/ascoltami/song", { method: "POST", body: this.selectedSong.value });
+    this.selectedSong = null;
   }
 
   onCmdStop(ev?: MouseEvent): void {
-    postJson("../service/ascoltami/time", { pause: true });
+    postJson("/service/ascoltami/time", { pause: true });
   }
+
   onCmdPlay(ev?: MouseEvent): void {
-    postJson("../service/ascoltami/time", { resume: true });
-  }
-  onCmdIntro(ev?: MouseEvent): void {
-    postJson("../service/ascoltami/time", { t: this.times.intro, resume: true });
+    postJson("/service/ascoltami/time", { resume: true });
   }
-  onCmdPost(ev?: MouseEvent): void {
-    postJson("../service/ascoltami/time", {
-      t: this.currentDuration - this.times.post,
-      resume: true,
-    });
+
+  onCmdGo(ev?: MouseEvent): void {
+    postJson("/service/ascoltami/go", {});
   }
-  onCmdGo(ev?: MouseEvent): void {
-    postJson("../service/ascoltami/go", {});
+
+  onCmdZero(ev?: MouseEvent): void {
+    postJson("/service/ascoltami/time", { t: 0 });
   }
 
   bindKeys() {
     document.addEventListener("keypress", (ev) => {
-      if (ev.which == 115) {
+      if (ev.key == "l") {
+        this.onLoadSelected();
+      } else if (ev.key == "z") {
+        this.onCmdZero();
+      } else if (ev.key == "p") {
+        this.onCmdPlay();
+      } else if (ev.key == "s") {
         this.onCmdStop();
-        return false;
-      }
-      if (ev.which == 112) {
-        this.onCmdPlay();
-        return false;
+      } else if (ev.key == "g") {
+        this.onCmdGo();
+      } else {
+        return true;
       }
-      if (ev.which == 105) {
-        this.onCmdIntro();
-        return false;
-      }
-      if (ev.which == 116) {
-        this.onCmdPost();
-        return false;
-      }
-
-      if (ev.key == "g") {
-        this.onCmdGo();
-        return false;
-      }
-      return true;
     });
   }
-
-  async musicSetup() {
-    // shoveled over from the vanillajs version
-    const config = await (await fetch("../service/ascoltami/config")).json();
-    this.show = new NamedNode(config.show);
-    this.times = config.times;
-    document.title = document.title.replace("{{host}}", config.host);
-    try {
-      const h1 = document.querySelector("h1")!;
-      h1.innerText = h1.innerText.replace("{{host}}", config.host);
-    } catch (e) {}
-
-    (window as any).finishOldStyleSetup(this.times, this.onOldStyleUpdate.bind(this));
-  }
-
-  onOldStyleUpdate(data: TimingUpdate) {
-    this.nextText = data.next;
-    this.isPlaying = data.playing;
-    this.currentDuration = data.duration;
-    this.song = new NamedNode(data.song);
-    this.overviewZoom = { duration: data.duration, t1: 0, t2: data.duration };
-    const t1 = data.t - 2,
-      t2 = data.t + 20;
-    this.zoom = { duration: data.duration, t1, t2 };
-    const timeRow = this.shadowRoot!.querySelector(".timeRow") as HTMLDivElement;
-    const w = timeRow.offsetWidth;
-    this.viewState = {
-      zoomSpec: { t1: () => t1, t2: () => t2 },
-      cursor: { t: () => data.t },
-      audioY: () => 0,
-      audioH: () => 60,
-      zoomedTimeY: () => 60,
-      zoomedTimeH: () => 40,
-      fullZoomX: (sec: number) => (sec / data.duration) * w,
-      zoomInX: (sec: number) => ((sec - t1) / (t2 - t1)) * w,
-      mouse: { pos: () => $V([0, 0]) },
-    };
-  }
-
-  @property() songList: NamedNode[] = [];
-  constructor() {
-    super();
-    this.bindKeys();
-    this.zoom = this.overviewZoom = { duration: null, t1: 0, t2: 1 };
-
-    getTopGraph().then((g) => {
-      this.graph = g;
-      this.musicSetup(); // async
-      this.graph.runHandler(this.graphChanged.bind(this), "loadsongs");
-    });
-  }
-  graphChanged() {
-    this.songList = [];
-    try {
-      const playList = this.graph.uriValue(
-        //
-        this.graph.Uri(showRoot),
-        this.graph.Uri(":playList")
-      );
-      log(playList);
-      this.songList = this.graph.items(playList) as NamedNode[];
-    } catch (e) {
-      log("no playlist yet");
-    }
-    log(this.songList.length);
-  }
 }