File size: 4,102 Bytes
070c931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { useEffect, useRef, useState } from "react";
import type { VideoScene } from "../../api/types";
import type { PanelProps } from "./EditorPanels";

const GROUP = 5;

const clock = (s: number) => {
  const t = Math.max(0, Math.round(s));
  return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, "0")}`;
};

/** Each scene's voiceover, grouped by five (one group open at a time), to play with a progress bar. */
export function Voiceovers(p: PanelProps & { scenes: VideoScene[] }) {
  const { scenes } = p;
  const [openGroup, setOpenGroup] = useState<number | null>(0);
  const [playing, setPlaying] = useState<{ id: number; at: number; length: number } | null>(null);
  const audio = useRef<HTMLAudioElement | null>(null);

  const ready = scenes.filter((s) => s.audio_url).length;
  const total = Math.round(scenes.reduce((n, s) => n + Number(s.duration_seconds ?? 0), 0));

  // Stop playback when leaving the tab.
  useEffect(() => () => audio.current?.pause(), []);

  function toggle(s: VideoScene) {
    const el = audio.current;
    if (!el || !s.audio_url) return;
    if (playing?.id === s.id) {
      el.pause();
      setPlaying(null);
      return;
    }
    el.src = s.audio_url;
    el.currentTime = 0;
    setPlaying({ id: s.id, at: 0, length: Number(s.duration_seconds ?? 0) });
    el.play().catch(() => setPlaying(null));
  }

  const groups: VideoScene[][] = [];
  for (let i = 0; i < scenes.length; i += GROUP) groups.push(scenes.slice(i, i + GROUP));

  return (
    <section className="card stack">
      <div className="vw-vo-head">
        <h3>Voiceovers</h3>
        <span className="muted small">{ready} / {scenes.length} scenes · {total}s total</span>
        <span className="vw-vo-legend muted small">
          <span><i className="vw-vo-dot ok" /> Ready</span>
          <span><i className="vw-vo-dot" /> Pending</span>
        </span>
      </div>
      <audio ref={audio} preload="none" hidden
             onTimeUpdate={(e) => {
               const el = e.currentTarget;
               setPlaying((pl) => pl && { ...pl, at: el.currentTime, length: el.duration || pl.length });
             }}
             onEnded={() => setPlaying(null)} />

      {groups.map((group, g) => {
        const first = g * GROUP + 1;
        const open = openGroup === g;
        return (
          <section key={g} className="vw-group">
            <button type="button" className="vw-group-head" aria-expanded={open} onClick={() => setOpenGroup(open ? null : g)}>
              <span>Scenes {first}–{first + group.length - 1}{!open && <span className="muted small"> · Expand to view scenes</span>}</span>
              <span className="muted">{open ? "︿" : "﹀"}</span>
            </button>
            {open && group.map((s) => {
              const on = playing?.id === s.id;
              const length = on ? playing.length : Number(s.duration_seconds ?? 0);
              const at = on ? playing.at : 0;
              return (
                <div key={s.id} className="vw-vo-row">
                  <span className="vw-num-badge">{s.order}</span>
                  <button type="button" className="vw-vo-play" disabled={!s.audio_url}
                          aria-label={on ? `Pause scene ${s.order}` : `Play scene ${s.order}`} onClick={() => toggle(s)}>
                    {on ? <span className="vw-vo-pause" /> : <span className="vw-vo-tri" />}
                  </button>
                  <div className="vw-vo-main">
                    <div className="vw-vo-line">
                      <strong>{s.title || `Scene ${s.order}`}</strong>
                      <span className="mono muted small">{clock(at)} / {clock(length)}</span>
                    </div>
                    <div className="vw-vo-bar"><span style={{ width: `${length ? Math.min(100, (at / length) * 100) : 0}%` }} /></div>
                  </div>
                  <i className={`vw-vo-dot${s.audio_url ? " ok" : ""}`} title={s.audio_url ? "Ready" : "Pending"} />
                </div>
              );
            })}
          </section>
        );
      })}
    </section>
  );
}