Files @ 6697a68800d2
Branch filter:

Location: light9/web/ascoltami/Light9AscoltamiUi.ts

drewp@bigasterisk.com
junk merge just to avoid two heads
import debug from "debug";
import { css, html, LitElement } 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";
export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph";
export { ResourceDisplay } from "../ResourceDisplay";
const $V = Sylvester.Vector.create;

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",
    headers: { "Content-Type": "applcation/json" },
    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;
  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%;
      }
      #playSelected {
        height: 100px;
      }
      #songList {
        overflow-y: scroll;
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
      }
      #songList .row {
        width: 60%;
        min-height: 40px;
        text-align: left;
        position: relative;
      }
      #songList .row:nth-child(even) {
        background: #333;
      }
      #songList .row:nth-child(odd) {
        background: #444;
      }
      #songList button {
        min-height: 40px;
        margin-bottom: 10px;
      }
      #songList .row.playing {
        box-shadow: 0 0 30px red;
        background-color: #de5050;
      }
    `,
  ];
  render() {
    return html`<rdfdb-synced-graph></rdfdb-synced-graph>

      <link rel="stylesheet" href="../style.css" />

      <!-- <h1>ascoltami <a href="metrics">[metrics]</a></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>
          </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>
      </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>`;
  }

  onSelectSong(song: NamedNode, ev: MouseEvent) {
    if (this.selectedSong && song.equals(this.selectedSong)) {
      this.selectedSong = null;
    } else {
      this.selectedSong = song;
    }
  }
  async onPlaySelected(ev: Event) {
    if (!this.selectedSong) {
      return;
    }
    await fetch("../service/ascoltami/song", { method: "POST", body: this.selectedSong.value });
  }

  onCmdStop(ev?: MouseEvent): void {
    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 });
  }
  onCmdPost(ev?: MouseEvent): void {
    postJson("../service/ascoltami/time", {
      t: this.currentDuration - this.times.post,
      resume: true,
    });
  }
  onCmdGo(ev?: MouseEvent): void {
    postJson("../service/ascoltami/go", {});
  }

  bindKeys() {
    document.addEventListener("keypress", (ev) => {
      if (ev.which == 115) {
        this.onCmdStop();
        return false;
      }
      if (ev.which == 112) {
        this.onCmdPlay();
        return false;
      }
      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);
  }
}