diff --git a/trailer.html b/trailer.html index 558d271..a66df1d 100644 --- a/trailer.html +++ b/trailer.html @@ -1145,707 +1145,3 @@ - + (filmed * 0.02).toFixed(2) + ''); - } - - if (unfilmed && !undrawn) - out.push(''); - if (filmed) - out.push(''); - } - return out.join(""); - } - - // A cut may move shots as well as drop them, which makes it a different - // film rather than a shorter one. Worth saying out loud. - function reordered(ids) { - var a = (ids || []).map(String); - var b = a.slice().sort(); - return a.join("") !== b.join(""); - } - - // What the storyboard will actually cost to draw. Three sheets for every - // character who is seen or heard, one more per extra day they are dressed - // differently, and two frames per shot. - function boardImages(shots, cast) { - var seen = {}; - shots.forEach(function (s) { - (s.visible_characters || []).forEach(function (c) { seen[String(c)] = 1; }); - (s.audio || []).forEach(function (a) { - if (String(a.text || "").trim()) seen[String(a.character_id)] = 1; }); - }); - var present = cast.filter(function (c) { return seen[String(c.id)]; }); - - var days = {}; - shots.forEach(function (s) { - var d = String(s.day || "").trim(); - if (!d) return; - (s.visible_characters || []).forEach(function (c) { - (days[String(c)] = days[String(c)] || {})[d] = 1; }); - }); - var extra = 0; - present.forEach(function (c) { - var wd = c.wardrobe_by_day || {}; - var base = Object.keys(wd).sort()[0] || ""; - Object.keys(days[String(c.id)] || {}).forEach(function (d) { - if (d !== base && wd[d]) extra += 1; }); - }); - return present.length * 3 + extra + shots.length * 2; - } - - function mmss(n) { - return Math.floor(n / 60) + ":" + String(Math.round(n % 60)).padStart(2, "0"); - } - - // The header's runtime is the sum of what the writer typed. This is what the - // lines take when the cast actually says them, which is a different number - // and the only one worth cutting against. - function timingHtml() { - var t = PKG.timing; - if (!t || !t.measured_seconds) return ""; - var over = Number(t.over_by || 0); - var line = "Spoken by the cast this script runs " + mmss(t.measured_seconds) - + ", against a " + mmss(t.target_seconds) + " target"; - if (over > 5) line += " — " + Math.round(over) + " seconds over." - + " Revising the whole script will cut that much."; - else if (over < -15) line += " — " + Math.round(-over) + " seconds short."; - else line += " — that fits."; - var worst = (t.longest_lines || []).slice(0, 3).map(function (l) { - return "
  • " + Number(l.seconds || 0).toFixed(1) + "s · shot " - + esc(String(l.shot)) + " · “" - + esc(String(l.text || "").slice(0, 80)) + "”
  • "; }).join(""); - return '
    ' + line + "
    " - + (worst && over > 5 ? "" : "") + "
    "; - } - - // A job lives in the worker's memory and dies with it. Every deploy kills - // one, and the flag it set stays set, so anything older than this is treated - // as gone rather than leaving the page waiting on it for ever. - function busyCuts() { - if (String(PKG.cuts_state || "") !== "thinking") return false; - var t = Date.parse(PKG.cuts_started || ""); - if (!t) return false; - return (Date.now() - t) < 15 * 60 * 1000; - } - - // ---- the storyboard player ------------------------------------------- - // One call fetches the whole reel; after that nothing touches the network, - // which is what makes stop and jump-to instant. - var REEL = null, RAT = -1, RTIMER = null; - var reelAudio = new Audio(); - - function reelHtml() { - var st = String(PKG.reel_state || ""); - var shots = (PKG.shots || []); - if (!shots.length) return ""; - var running = st && st !== "ready" && !/^failed/.test(st); - - var out = '

    Watch the storyboard

    ' - + '

    The frames in order with the lines spoken over them — ' - + 'the trailer before it is filmed. Sequence problems are obvious here ' - + 'and nearly invisible in the script below.

    '; - - if (running) - out += '

    Speaking every shot… ' + esc(st) + '

    '; - else if (st === "ready") { - var spoken = shots.filter(function (s) { return s.reel_url; }).length; - var silent = shots.length - spoken; - out += '

    ' + spoken + ' shots have dialogue and are spoken' - + (silent ? '; ' + silent + ' are silent and hold on the frame' : '') - + '.

    '; - } - else if (/^failed/.test(st)) - out += '

    ' + esc(st) + '

    '; - else if (st) - out += '

    The last attempt stopped before it finished — ' - + 'the server restarted under it. Ask again.

    '; - - if (!REEL) { - out += '
    ' - + '
    '; - return out; - } - - var s = REEL[RAT] || REEL[0] || {}; - var lines = (s.lines || []).map(function (l) { - return '
    ' + esc(l.who || "") - + (l.lip_sync ? "" : " (v.o.)") + '' + esc(l.text || "") + '
    '; - }).join("") || '
    no dialogue in this shot
    '; - - out += '
    ' - + '
    opens on
    ' - + '
    ends on
    ' - + '
    ' + esc(s.id) + ' · ' - + esc(s.title || "") + '
    ' - + '
    ' + esc(s.setting || "") - + (s.scene ? ' · ' + esc(s.scene) : "") - + (s.day ? ' · ' + esc(s.day) : "") + '
    ' - + '
    ' + lines + '
    ' - + '
    ' - + '
    ' - + '' - + '' - + '' - + '' - + '' - + '' + (RAT + 1) + ' of ' + REEL.length + '' - + '
    '; - return out; - } - - function reelPaint() { - var host = document.getElementById("reelhost"); - if (host) host.innerHTML = reelHtml(); - } - - // The two frames split the shot's running time: the opening frame holds for - // the first half, the closing frame for the second. It is a storyboard, not - // an animation, but it reads as movement and it marks where the shot lands. - function reelShow(i) { - if (!REEL || i < 0 || i >= REEL.length) { reelStop(); return; } - RAT = i; - reelPaint(); - var s = REEL[i]; - var dur = Math.max(Number(s.seconds) || 0, Number(s.edit_seconds) || 0, 2.5); - if (s.audio_url) { reelAudio.src = s.audio_url; reelAudio.play().catch(function(){}); } - else reelAudio.pause(); - - var t0 = Date.now(), swapped = false; - clearInterval(RTIMER); - RTIMER = setInterval(function () { - var gone = (Date.now() - t0) / 1000; - var bar = document.getElementById("rprog"); - if (bar) bar.style.width = Math.min(100, 100 * gone / dur) + "%"; - if (!swapped && gone > dur / 2) { - swapped = true; - var a = document.getElementById("rfa"), b = document.getElementById("rfb"); - if (a && b && REEL[RAT].closing_url) { a.classList.remove("on"); b.classList.add("on"); } - } - if (gone >= dur) reelShow(RAT + 1); - }, 120); - } - - function reelStop() { - clearInterval(RTIMER); RTIMER = null; - reelAudio.pause(); - REEL = null; RAT = -1; - openTab(CUR); - } - - // A cut is an ordering, not a render. Playing one is free, so all of them - // can be looked at before anything is joined into a file. - function cutsHtml(meta) { - if (meta.writer !== RUN.lead_writer) return ""; - var shots = PKG.shots || []; - var filmed = shots.filter(function (s) { return s.clip_url; }).length; - var cuts = PKG.cuts || []; - var st = String(PKG.cuts_state || ""); - var out = '

    Shorter cuts of this same shoot

    ' - + '

    Nothing here is re-filmed. A cut is an order to play ' - + 'the shots in, so making another one costs nothing — which is why ' - + 'the question is not “what is the right length” but “which ' - + 'version do people watch to the end”.

    '; - - if (busyCuts()) out += '

    Working out what to drop…

    '; - else if (st === "thinking") out += '

    The last attempt stopped ' - + 'before it finished — the server restarted under it. Ask again.

    '; - else if (/^failed/.test(st)) out += '

    ' + esc(st) + '

    '; - - out += cuts.map(function (c) { - var stat = STATS[c.id]; - var play = (PLAYING === c.id) - ? '
    ' - + '
    loading…' - + '
    ' - : ''; - var tools = ''; - if (c.state === "made" && c.video_url) - tools += 'Published · in My videos'; - else if (c.state === "making") - tools += 'joining…'; - else if (filmed) - tools += ''; - - return '
    ' + esc(c.name || "") + '' - + '' + mmss(c.seconds || 0) + ' · ' - + (c.shot_ids || []).length + ' shots' - + (c.strategy ? '

    ' + esc(c.strategy) + '

    ' : '') - + (c.why ? '

    ' + esc(c.why) + '

    ' : '') - + (c.sacrifices ? '

    gives up: ' + esc(c.sacrifices) + '

    ' : '') - + (stat ? '

    ' + stat.views + ' viewer' - + (stat.views === 1 ? '' : 's') + ' · median ' + stat.median_percent - + '% watched · ' + stat.completion_rate + '% reached the end

    ' : '') - + '

    ' + esc((c.shot_ids || []).join(" ")) - + (reordered(c.shot_ids) ? ' · reordered' : '') + '

    ' - + '
    ' + tools + '
    ' + play + '
    '; - }).join(""); - - if (!cuts.length && st !== "thinking") - out += '

    No cuts proposed yet.

    '; - return out + '
    '; - } - - function reviewsHtml(meta) { - var mine = REVIEWS.filter(function (r) { - return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); }); - if (!mine.length) return ""; - var gaps = mine.filter(function (r) { return r.reviewer === "__gaps__"; })[0]; - var readers = mine.filter(function (r) { return r.reviewer !== "__gaps__"; }); - var out = ""; - - if (gaps) { - var v = gaps.strengths && gaps.strengths.verdict; - var list = gaps.suggestions || []; - out += '

    What this trailer does not tell a viewer

    ' - + (v ? '

    ' + esc(v) + '

    ' : '') - + (list.length ? '
      ' + list.map(function (g) { - return '
    1. ' + esc(g.what || "") - + 'shot ' + esc(g.shot || "?") + ' → ' - + esc(g.fix || "") + '
    2. '; }).join("") + '
    ' : '') - + '
    '; - } - - out += readers.map(function (r) { - var s = r.strengths || {}, e = r.errors || {}; - if (typeof s !== "object") s = {}; - if (typeof e !== "object") e = {}; - function ul(items, cls) { - return (!items || !items.length) ? "" - : ''; - } - return '

    ' + esc(r.label) + ' watched it without reading the book' - + (r.score ? '' - + r.score + '/100' : '') + '

    ' - + (s.summary ? '
    ' + esc(s.summary) + '
    ' : '') - + '
    main character: ' + esc(s.who || "not clear to them") + '
    ' - + '
    what they want: ' + esc(s.wants || "not clear to them") + '
    ' - + '
    what is in the way: ' + esc(s.obstacle || "not clear to them") + '
    ' - + (s.would_watch ? '
    would they watch the film: ' - + esc(s.would_watch) + '
    ' : '') - + (s.unanswered && s.unanswered.length - ? '
    still asking:
    ' + ul(s.unanswered, "") : '') - + (e.missed && e.missed.length ? '
    they missed:
    ' + ul(e.missed, "") : '') - + (e.got_wrong && e.got_wrong.length - ? '
    the trailer misled them:
    ' + ul(e.got_wrong, "wrong") : '') - + '
    '; - }).join(""); - - return '
    ' + out + '
    '; - } - - function paneHtml(meta, draft) { - var p = PKG, shots = (p.shots || []).slice().sort(function (a, b) { - return String(a.id) < String(b.id) ? -1 : 1; }); - var cast = p.characters || [], voice = {}; - cast.forEach(function (c) { voice[c.id] = c; }); - var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0); - - var head = '

    ' + esc(p.title || meta.label) + '

    ' - + (p.tagline ? '' + esc(p.tagline) + '' : '') + '
    ' - + (p.logline ? '

    ' + esc(p.logline) + '

    ' : '') - + '

    ' + esc(meta.label) - + (Number(meta.iteration) > 1 ? ' · revision ' + meta.iteration : '') - + ' · ' + shots.length + ' shots · ' - + Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0") - + ' · ' + cast.length + ' characters

    ' - + timingHtml(); - - var probs = draft.problems - ? '
    The checker has objections
    ' - : ""; - - var castHtml = '
    ' + cast.map(function (c) { - var sheets = (c.head_url || c.front_url || c.side_url) - ? '
    ' + [c.head_url, c.front_url, c.side_url].map(function (u) { - return u ? '' : ''; }).join("") + '
    ' : ''; - return '
    ' + sheets + '' + esc(c.name || c.id) + '' - + '' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent") - + '
    ' + esc((c.appearance || "").slice(0, 150)) + '
    '; - }).join("") + '
    '; - - var body = insertHtml("start"); - shots.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); }); - - return head + probs + '
    ' + barHtml(meta) + '
    ' - + '
    ' + reelHtml() + '
    ' - + spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + castHtml + body; - } - - function insertHtml(after) { - if (EDITING === "new:" + after) { - return '
    ' - + '' - + '
    ' - + '
    '; - } - return '
    '; - } - - // The faults themselves, with the prompt change that would prevent each - - // which is the half that makes the check actionable rather than a verdict. - function faultsHtml(s) { - var vc = s.vision_check; - if (!vc || !(vc.faults || []).length) return ""; - return '
    ' - + (vc.summary ? '
    ' + esc(vc.summary) + '
    ' : '') - + vc.faults.map(function (f) { - return '
    ' + esc(f.kind || "") + '' - + (f.at != null ? ' at ' + Number(f.at).toFixed(1) + 's' : '') - + ' · ' + esc(f.what || "") - + (f.prompt_fix ? 'fix: ' + esc(f.prompt_fix) - + '' : '') + '
    '; - }).join("") + '
    '; - } - - function shotHtml(s, voice) { - if (s.writing) { - return '
    ' + esc(s.id) + '
    ' - + '

    ' + esc(s.title || "New scene") + '

    ' - + '

    The writer is turning your note into a shot…

    ' - + '

    ' + esc(String(s.rough || "").slice(0, 300)) + '

    '; - } - - var lines = (s.audio || []).map(function (a) { - var who = voice[a.character_id] || {}; - return '
    ' - + '' - + '
    ' + esc(who.name || a.character_id) + ' ' - + (a.lip_sync ? 'on camera' : 'voice over') - + '
    ' + esc(a.text || "") - + (a.delivery ? '
    ' + esc(a.delivery) + '' : '') - + '
    '; - }).join(""); - - var media = ""; - if (s.clip_url) { - media = '
    the filmed shot' - + (s.clip_seconds ? ' · ' + Number(s.clip_seconds).toFixed(1) + 's' : '') - + '
    ' - + (s.opening_url ? '
    filmed from this frame
    ' : '') - + '
    '; - } else if (s.opening_url || s.closing_url) { - media = '
    ' - + (s.opening_url ? '
    opens on
    ' : '') - + (s.closing_url ? '
    ends on
    ' : '') - + '
    '; - } - - var chk = s.audio_check || {}; - var chkHtml = ""; - if (chk.verdict && chk.verdict !== "no dialogue") { - var good = chk.verdict === "ok" || chk.verdict === "drifted"; - chkHtml = 'audio ' + esc(chk.verdict) - + (chk.coverage != null ? ' ' + Math.round(chk.coverage * 100) + '%' : '') - + (chk.missing ? ' — lost “' + esc(String(chk.missing).slice(0, 40)) + '”' : '') - + ''; - } - - // What the eyes found, if they have looked. Kinds: render (the picture is - // broken), continuity (the frames disagree), script (wrong shot, well made). - var vc = s.vision_check || null; - var eyeHtml = ""; - if (vc && vc.verdict) { - var bad = vc.verdict === "broken"; - var faults = vc.faults || []; - eyeHtml = 'picture ' + esc(vc.verdict) - + (faults.length ? ' · ' + faults.length + ' fault' - + (faults.length === 1 ? '' : 's') : '') - + (vc.people_on_screen != null ? ' · ' + vc.people_on_screen - + ' on screen' : '') + ''; - } - - var tools; - if (s.drawing) tools = 'drawing…'; - else if (s.filming) tools = 'filming…'; - else if (EDITING === s.id) { - tools = '
    ' - + '
    ' - + '
    '; - } else { - tools = '' - + '' - + (s.opening_url ? '' : '') - + chkHtml + eyeHtml - + ''; - } - - return '
    ' + esc(s.id) + '
    ' - + '

    ' + esc(s.title || "") + '

    ' - + '
    ' + esc(s.setting || "") + ' · ' - + Number(s.edit_seconds || 0).toFixed(1) + 's' - + (s.scene ? ' · ' + esc(s.scene) + '' : '') - + (s.day ? ' · ' + esc(s.day) + '' : '') - + '
    ' - + media - + '

    ' + esc(s.action || "") + '

    ' - + (s.camera ? '

    camera: ' + esc(s.camera) + '

    ' : '') - + (s.continuity ? '

    continuity: ' + esc(s.continuity) + '

    ' : '') - + lines - + faultsHtml(s) - + '
    ' + tools + '
    ' - + '' - + '
    '; - } - - async function act(name, payload) { - var r = await api(name, payload); - if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; } - hideMsg("msg"); - return r; - } - - document.getElementById("pane").addEventListener("click", async function (e) { - var hit = function (attr) { return e.target.closest("[" + attr + "]"); }; - var el, r; - - if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; } - if ((el = hit("data-add"))) { EDITING = "new:" + el.getAttribute("data-add"); openTab(CUR); return; } - if ((el = hit("data-edit"))) { EDITING = el.getAttribute("data-edit"); openTab(CUR); return; } - - if ((el = hit("data-addgo"))) { - var rough = document.getElementById("newrough").value.trim(); - if (rough.length < 20) { showMsg("msg", "Say what happens in the scene."); return; } - el.disabled = true; - r = await act("add_shot", { draft_id: CUR, after: el.getAttribute("data-addgo"), rough: rough }); - EDITING = null; if (r) await refresh(); return; - } - if ((el = hit("data-rewrite"))) { - var rw = document.getElementById("rough").value.trim(); - if (rw.length < 20) { showMsg("msg", "Say what should change."); return; } - el.disabled = true; - r = await act("write_shot", { draft_id: CUR, shot_id: el.getAttribute("data-rewrite"), rough: rw }); - EDITING = null; if (r) await refresh(); return; - } - if ((el = hit("data-drawone"))) { - el.disabled = true; - r = await act("draw_one_shot", { draft_id: CUR, shot_id: el.getAttribute("data-drawone") }); - if (r) await refresh(); return; - } - if ((el = hit("data-filmone"))) { - el.disabled = true; - r = await act("film_one_shot_now", { draft_id: CUR, shot_id: el.getAttribute("data-filmone") }); - if (r) await refresh(); return; - } - if ((el = hit("data-delete"))) { - if (!confirm("Remove shot " + el.getAttribute("data-delete") + " from this trailer?")) return; - el.disabled = true; - r = await act("delete_shot", { draft_id: CUR, shot_id: el.getAttribute("data-delete") }); - if (r) await refresh(); return; - } - if ((el = hit("data-assemble"))) { - el.disabled = true; - r = await act("assemble_trailer", { draft_id: CUR }); - if (r) { showMsg("msg", "Joining " + r.shots + " shots. It appears in My videos.", "ok"); - setTimeout(function () { location.href = "/videos.html"; }, 2200); } - return; - } - if ((el = hit("data-lead"))) { - el.disabled = true; - r = await act("set_lead", { run_id: runId, draft_id: el.getAttribute("data-lead") }); - if (r) await refresh(); return; - } - if ((el = hit("data-draw"))) { - el.disabled = true; - r = await act("draw_storyboard", { draft_id: el.getAttribute("data-draw") }); - if (r) await refresh(); return; - } - if ((el = hit("data-reelgo"))) { - el.disabled = true; - var rl = await api("reel", { draft_id: CUR }); - if (!rl.ok) { showMsg("msg", rl.error || "Could not load the storyboard."); return; } - if (String(PKG.reel_state || "") !== "ready") { - el.textContent = "Speaking every shot…"; - REELWANT = true; - r = await act("prepare_reel", { draft_id: CUR }); - if (r) showMsg("msg", "Speaking " + r.shots + " shots. This is the same " - + "audio the film will use, so it is only paid for once. It starts " - + "playing on its own when it is ready.", "ok"); - else REELWANT = false; - await refresh(); - return; - } - hideMsg("msg"); - REEL = rl.shots || []; RAT = -1; - reelShow(0); - return; - } - if ((el = hit("data-reeltoggle"))) { - if (reelAudio.paused) { reelAudio.play().catch(function(){}); reelShow(RAT); } - else { reelAudio.pause(); clearInterval(RTIMER); RTIMER = null; reelPaint(); } - return; - } - if ((el = hit("data-reelstep"))) { - var d = Number(el.getAttribute("data-reelstep")) || 1; - reelShow(Math.max(0, Math.min((REEL || []).length - 1, RAT + d))); - return; - } - if ((el = hit("data-reelstop"))) { reelStop(); return; } - if ((el = hit("data-reeljump"))) { - var jid = el.getAttribute("data-reeljump"); - clearInterval(RTIMER); RTIMER = null; reelAudio.pause(); - var target = [...document.querySelectorAll(".shot")].find(function (x) { - var nu = x.querySelector(".num"); - return nu && nu.textContent.trim() === jid; }); - if (target) { - document.querySelectorAll(".shot.lit").forEach(function (x) { - x.classList.remove("lit"); }); - target.classList.add("lit"); - target.scrollIntoView({ behavior: "smooth", block: "center" }); - var ta = target.querySelector("textarea.note"); - if (ta) setTimeout(function () { ta.focus(); }, 500); - } - return; - } - if ((el = hit("data-cuts"))) { - el.disabled = true; el.textContent = "Working out what to drop…"; - r = await act("propose_cuts", { draft_id: el.getAttribute("data-cuts") }); - if (r) { showMsg("msg", "Asking for " + r.count + " different cuts.", "ok"); - setTimeout(refresh, 25000); } - return; - } - if ((el = hit("data-play"))) { - var cid = el.getAttribute("data-play"); - el.disabled = true; - var cc = await api("cut_clips", { draft_id: CUR, cut_id: cid }); - el.disabled = false; - if (!cc.ok) { showMsg("msg", cc.error || "That would not play."); return; } - QUEUE = (cc.shots || []).filter(function (s) { return s.clip_url; }); - if (!QUEUE.length) { - showMsg("msg", "None of the shots in this cut are filmed yet, so there " - + "is nothing to play. Film them first."); - return; - } - if (cc.filmed < cc.of) - showMsg("msg", "Playing the " + cc.filmed + " of " + cc.of - + " shots that are filmed. The rest are skipped."); - PLAYING = cid; QAT = 0; - await openTab(CUR); // the video element does not exist until this lands - playCut(); - return; - } - if ((el = hit("data-stop"))) { PLAYING = null; QUEUE = []; openTab(CUR); return; } - if ((el = hit("data-makecut"))) { - el.disabled = true; - r = await act("make_cut", { draft_id: CUR, cut_id: el.getAttribute("data-makecut") }); - if (r) { showMsg("msg", "Joining " + r.shots + " shots into this version. " - + "It appears in My videos with its own share page.", "ok"); - await refresh(); } - return; - } - if ((el = hit("data-measure"))) { - el.disabled = true; el.textContent = "Speaking every line…"; - r = await act("measure_script", { draft_id: el.getAttribute("data-measure") }); - if (r) { - showMsg("msg", "Speaking every line in the cast voices. A minute or two.", "ok"); - setTimeout(refresh, 20000); - } - return; - } - if ((el = hit("data-review"))) { - el.disabled = true; - r = await act("start_review", { run_id: runId }); if (r) await refresh(); return; - } - if ((el = hit("data-revise"))) { - el.disabled = true; - r = await act("start_revision", { run_id: runId }); if (r) await refresh(); return; - } - if ((el = hit("data-film"))) { - el.disabled = true; - r = await act("film_draft", { draft_id: el.getAttribute("data-film") }); - if (r) { showMsg("msg", "Filming " + r.shots + " shots, about $" + r.usd - + ". It appears in My videos as it goes.", "ok"); - setTimeout(function () { location.href = "/videos.html"; }, 2500); } - return; - } - if ((el = hit("data-retry"))) { - el.disabled = true; - r = await act("retry_writer", { draft_id: el.getAttribute("data-retry") }); - if (r) await refresh(); return; - } - - var btn = e.target.closest(".play"); - if (!btn) return; - var sid = btn.getAttribute("data-shot"); - btn.disabled = true; btn.innerHTML = "…"; - var a = await api("shot_audio", { draft_id: CUR, shot_id: sid }); - btn.disabled = false; btn.innerHTML = "▶"; - if (!a.ok) { showMsg("msg", a.error || "That would not play."); return; } - hideMsg("msg"); - if (!a.url) return; - document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (x) { - x.textContent = a.seconds.toFixed(1) + "s / " + Number(a.planned).toFixed(1) + "s"; - x.className = "dur" + (a.seconds > Number(a.planned) ? " over" : ""); - }); - audio.pause(); audio.src = a.url; audio.play(); - }); - - // Play the cut's clips one after another in a single element. This is the - // whole preview: no joining, no encoding, no upload, nothing to pay for. - function playCut() { - var v = document.getElementById("cutv"); - if (!v || !QUEUE.length) return; - var at = document.getElementById("cutat"); - function step() { - if (QAT >= QUEUE.length) { - if (at) at.textContent = "end of this cut"; - return; - } - var s = QUEUE[QAT]; - if (at) at.textContent = "shot " + s.id + " · " + (QAT + 1) - + " of " + QUEUE.length + (s.title ? " · " + s.title : ""); - v.src = s.clip_url; - v.play().catch(function () {}); - } - v.onended = function () { QAT += 1; step(); }; - v.onerror = function () { QAT += 1; step(); }; - step(); - } - - var noteTimers = {}; - document.getElementById("pane").addEventListener("input", function (e) { - var ta = e.target; - var sid = ta.getAttribute && ta.getAttribute("data-note"); - if (!sid) return; - ta.classList.remove("saved"); - clearTimeout(noteTimers[sid]); - noteTimers[sid] = setTimeout(async function () { - var r = await api("add_shot_note", { draft_id: CUR, shot_id: sid, note: ta.value }); - if (r.ok) { NOTES[sid] = ta.value; ta.classList.add("saved"); } - }, 700); - }); - - load(); -})(); - - -