Files @ 6697a68800d2
Branch filter:

Location: light9/web/ascoltami/Light9AscoltamiUi.ts - annotation

drewp@bigasterisk.com
junk merge just to avoid two heads
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
4556eebe5d73
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);
  }
}