view src/VideoPage.ts @ 42:1d2c65d260d1

factor out breadcrumbs
author drewp@bigasterisk.com
date Thu, 05 Dec 2024 21:34:00 -0800
parents 44bd161e4779
children df51269bcef4
line wrap: on
line source

import { LitElement, PropertyValues, TemplateResult, css, html, unsafeCSS } from "lit";
import { customElement, property } 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 { VideoSection } from "./VideoSection";
import { Routes, Router } from "@lit-labs/router";
export { VBreadcrumbs } from "./VBreadcrumbs";
interface VideoFile {
  webRelPath: string;
  webDataPath: string;
  label: string;
}

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

interface VideoListings {
  videos: VideoFile[];
  subdirs: Subdir[];
}

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

class route {
  path = "/video/*";
  videoListings: VideoListings | null = null;
  showVid: string | null = null;
  dirName?: string;

  link(wrp: string): string {
    return "/video/" + wrp;
  }

  async enter(params: { [key: string]: string | undefined }): Promise<boolean> {
    const webRelPath = "/" + params[0]!;
    this.dirName = webRelPath.replace(/.*\/([^/]+)/, "$1");
    const resp = await fetch("/video/api/videos?" + subdirQuery(webRelPath));
    if (resp.status == 404) {
      return false;
    }
    this.videoListings = await resp.json();

    if (webRelPath.endsWith("/")) {
      this.showVid = null;
    } else {
      this.showVid = webRelPath;
    }

    return true;
  }

  render(p: { [key: string]: string | undefined }) {
    return html`<video-page2
      .link=${this.link.bind(this)}
      .showVid=${this.showVid}
      .videoListings=${this.videoListings}
      .dirName=${this.dirName}
    ></video-page2>`;
  }
}

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

  render() {
    const requestedPath = "/" + this._router.params[0];

    return html`
      <header>
        <img src=${this._router.link("/video/logo1.png")} title="JelloBello" />
        <v-breadcrumbs .link=${this._router.link} dirPath=${requestedPath}></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() showVid?: string;
  @property() videoListings?: VideoListings;
  @property() link!: (s: string) => string;
  @property() dirName?: string;

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

  protected update(changedProperties: PropertyValues<this>): void {
    const resp = changedProperties.has("videoListings");
    if (resp) {
      // if (this.showVid) {
      //   this.openPlayer();
      // } else {
      //   this.closePlayer();
      // }
    }
    super.update(changedProperties);
  }

  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: 2px;
      }

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

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

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

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

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

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

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

  escapeALittle(fileUri: string): string {
    return fileUri.replace("#", encodeURIComponent("#"));
  }

  playVideo(ev: CustomEvent) {
    const player = this.shadowRoot!.querySelector("page-player")! as PagePlayer;

    player.manifest = this.escapeALittle(ev.detail.manifest);
    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";
  }
  closePlayer() {
    const player = this.shadowRoot!.querySelector("page-player")! as PagePlayer;
    player.size = "hidden";

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