view src/VideoPage.ts @ 39:b5b29f6ef5cb

cleanup and refactor
author drewp@bigasterisk.com
date Thu, 05 Dec 2024 17:01:53 -0800
parents 0aea9e55899b
children 44bd161e4779
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";

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];
    const segs = ["."].concat(requestedPath || "".split("/"));
    const crumbs = [];
    // todo: goal is to have '🏠 TOP' and every level not including current dir
    for (let i = 0; i < segs.length; i++) {
      const seg = segs[i];
      const href = "../".repeat(segs.length - i - 1);
      const label = i == 0 ? html`<sl-icon name="house"></sl-icon>` : seg;
      console.log(href, label);
      crumbs.push(html`<sl-breadcrumb-item><a href=${href}>${label}</a></sl-breadcrumb-item>`);
    }
    return html`
      <header>
        <img src="${this._router.link("/video/logo1.png")}" title="JelloBello" />
        <sl-breadcrumb>${crumbs}</sl-breadcrumb>
      </header>
      <main>${this._router.outlet()}</main>
      <footer>foot</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;
      }
      .listing a {
        font-size: 20px;
        text-transform: uppercase;
        text-underline-offset: 10px;
      }

      .subdir {
        vertical-align: top;
        color: white;
        padding: 11px;
        display: inline-block;
        width: 300px;
        background: #4ea1bd21;
        margin: 5px;
        border-bottom-right-radius: 29px;
      }
      #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`<div class="subdir"><a href="${this.link(subdir.path) + "/"}">${subdir.label}</a></div>`;
  }

  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))}`,
    ];
    return html`
      <h2>${this.dirName}</h2>
      <div class="listing">${listings}</div>

      <p><a href="ingest/">Add new videos...</a></p>

      <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";
  }
}