view src/VideoPage.ts @ 45:df51269bcef4

fix back/fwd nav and player loading
author drewp@bigasterisk.com
date Fri, 06 Dec 2024 01:02:33 -0800
parents 1d2c65d260d1
children 882d0bb0f801
line wrap: on
line source

import { Router } from "@lit-labs/router";
import { LitElement, PropertyValues, TemplateResult, css, html, unsafeCSS } from "lit";
import { customElement, property, queryAsync } from "lit/decorators.js";
import { PagePlayer, ShakaVideoElement } from "./PagePlayer";
import maincss from "./main.css?inline";
export { SlProgressBar } from "@shoelace-style/shoelace";
export { PagePlayer } from "./PagePlayer";
export { VBreadcrumbs } from "./VBreadcrumbs";
export { VideoSection } from "./VideoSection";

interface VideoFile {
  webRelPath: string;
  webDataPath: string;
  label: string;
}

interface Subdir {
  label: string;
  path: string;
}

interface PageData {
  webDirRelPath: string;
  dirLabel: string;
  videos: VideoFile[];
  subdirs: Subdir[];
  autoplay: VideoFile | null;
}

function subdirQuery(subdir: string): string {
  return "subdir=" + encodeURIComponent(subdir);
}

class route {
  path = "/video/*";
  pageData: PageData | null = null;

  async enter(params: { [key: string]: string | undefined }): Promise<boolean> {
    const webRelPath = "/" + params[0]!; // could be /a/dir/ or /a/video
    console.log("enter", webRelPath);
    const resp = await fetch("/video/api/videos?" + subdirQuery(webRelPath));
    if (resp.status == 404) {
      return false;
    }
    this.pageData = await resp.json();
    if (!this.pageData) return false;

    return true;
  }

  render(p: { [key: string]: string | undefined }) {
    console.log("render", this.pageData);
    return html`<video-page2 .pageData=${this.pageData}></video-page2>`;
  }
}

@customElement("video-page")
export class VideoPage extends LitElement {
  static styles = [unsafeCSS(maincss)];
  private _router = new Router(this, [new route()], {});

  constructor() {
    super();
    window.rr = this._router;
  }

  render() {
    const pd = (this._router.routes[0] as route).pageData;
    if (!pd) {
      console.log("no page data", this._router.routes);
      return html`loading...`;
      throw new Error("no page data");
    }
    console.log("yes page data", pd);
    return html`
      <header>
        <img src="/video/logo1.png" title="JelloBello" />
        <v-breadcrumbs dirPath=${pd.webDirRelPath}></v-breadcrumbs>
      </header>
      <main>${this._router.outlet()}</main>
      <footer>
        <bigast-loginbar></bigast-loginbar>
        <span><a href="ingest/">Add new videos...</a></span>
      </footer>
    `;
  }
}

@customElement("video-page2")
export class VideoPage2 extends LitElement {
  @property() pageData?: PageData;

  protected firstUpdated(_changedProperties: PropertyValues): void {
    document.addEventListener("keydown", (e) => {
      if (e.key == "Escape") {
        this.gotoDirListingPage();
      }
    });
  }

  async connectedCallback() {
    super.connectedCallback();
    if (this.pageData?.autoplay) {
      //this.playVideo({ detail: { manifest: this.pageData.autoplay.webDataPath } } as CustomEvent);
      console.log("we're a player page now", this.pageData.autoplay);
      this.startPlayer(this.pageData.autoplay);
      // this might not autoplay
    } else {
      this.closePlayer();
    }
  }
  protected update(changedProperties: PropertyValues): void {
    super.update(changedProperties);
    if (changedProperties.has("pageData")) {
      if (this.pageData?.autoplay) {
        this.startPlayer(this.pageData.autoplay);
      } else {
         this.closePlayer();
      }
    }
    console.log("updated", this.pageData?.autoplay);
  }

  static styles = [
    unsafeCSS(maincss),
    css`
      :host {
        display: block;
      }

      video-section {
        margin: 8px;
      }

      .listing a {
        font-size: 20px;
        text-transform: uppercase;
        text-underline-offset: 10px;
      }

      .listing a .subdir {
        transition: background 0.5s;
        transition-delay: 0.1s;
        background: #4ea1bd21;
      }

      .listing a:hover .subdir {
        transition: background 0.5s;
        background: #4ea1bd66;
      }

      .subdir {
        vertical-align: top;
        color: white;
        padding: 11px;
        display: inline-block;
        width: 300px;
        margin: 5px;
        border-radius: 5px;
      }

      .subdir:after {
        content: " › ";
        color: #979797;
      }

      #scrim {
        position: fixed;
        background: #000000b5;
        inset: 0;
        display: none;
      }
    `,
  ];

  thumbSrc(vf: VideoFile) {
    return "/video/api/thumbnail?webRelPath=" + encodeURIComponent(vf.webRelPath);
  }

  renderSubdir(subdir: Subdir): TemplateResult {
    return html`<a href="/video${subdir.path}"><div class="subdir">${subdir.label}</div></a>`;
  }

  renderVideoListing(video: VideoFile) {
    return html`<video-section
      @playVideo=${this.gotoVideoPlayerPage}
      webRelPath=${video.webRelPath}
      thumbRelPath=${this.thumbSrc(video)}
      title="${video.label}"
      manifest="/video/files${video.webDataPath}"
    ></video-section>`;
  }

  render() {
    if (this.pageData == null) {
      return html`<div>Loading...</div>`;
    }
    const listings = [
      html`${this.pageData.subdirs.map((s) => this.renderSubdir(s))}`, //
      html`<div>${this.pageData.videos.map((v) => this.renderVideoListing(v))}</div>`,
    ];
    const dirTitle = this.pageData.dirLabel ? html`<h2>${this.pageData.dirLabel}</h2>` : html``;
    return html`
      ${dirTitle}
      <div class="listing">${listings}</div>

      <div id="scrim" @click=${this.gotoDirListingPage}></div>
      <page-player manifest=""></page-player>
    `;
  }

  escapeALittle(fileUri: string): string {
    return fileUri.replace("#", encodeURIComponent("#"));
  }
  @queryAsync("page-player") pagePlayer: Promise<PagePlayer>;

  async gotoVideoPlayerPage(ev: CustomEvent) {
    const player = await this.pagePlayer;
    console.log("playing", player, ev.detail.manifest, ev.detail.webRelPath);
    this.goto("/video" + ev.detail.webRelPath);
  }
  async gotoDirListingPage() {
    this.goto("/video" + this.pageData?.webDirRelPath);
  }
  
  async startPlayer(p: VideoFile) {
    const player = await this.pagePlayer;
    player.manifest = this.escapeALittle("/video/files" + p.webDataPath);
    const sv = player.shadowRoot!.querySelector("shaka-video")! as ShakaVideoElement;

    sv.src = player.manifest;
    sv.autoplay = true;
    player.size = "big";
    this.shadowRoot!.querySelector("#scrim")!.style.display = "block";
  }
  goto(url: string) {
    window.rr.goto(url);
    window.history.pushState({}, "", url);
  }
  async closePlayer() {
    const player = await this.pagePlayer;
    if (player.size === "hidden") {
      console.log("wasn't playing", player.size);
      return;
    }
    // return;
    // this.goto("/video/" + this.pageData?.webDirRelPath);
    player.size = "hidden";

    this.shadowRoot!.querySelector("#scrim")!.style.display = "none";
  }
}