1594 lines
80 KiB
HTML
1594 lines
80 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Trailers — PayToLaugh</title>
|
||
<meta name="robots" content="noindex,nofollow">
|
||
<link rel="stylesheet" href="/assets/app.css">
|
||
<style>
|
||
.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:0 0 20px}
|
||
.tab{border:1px solid var(--line);border-bottom:0;background:none;color:var(--muted);
|
||
padding:10px 16px;border-radius:10px 10px 0 0;cursor:pointer;font:inherit;font-size:14px}
|
||
.tab.on{color:var(--ink);border-color:var(--line);background:var(--panel)}
|
||
.tab.lead{color:var(--accent)}
|
||
.scene{color:var(--accent2);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
|
||
.tab .sc{color:var(--accent2);margin-left:9px;font-weight:600;font-size:11px;
|
||
border:1px solid var(--accent2);border-radius:999px;padding:1px 7px;
|
||
font-variant-numeric:tabular-nums}
|
||
.tab .st{color:var(--muted);margin-left:7px;font-size:12px}
|
||
.head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px}
|
||
.head h2{margin:0;font-size:22px}
|
||
.tag{color:var(--muted);font-style:italic}
|
||
.facts{color:var(--muted);font-size:13px;margin:2px 0 18px}
|
||
.cast{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 22px}
|
||
.ch{border:1px solid var(--line);border-radius:10px;padding:10px;font-size:13px;width:250px}
|
||
.ch b{display:block}
|
||
.ch .vo{color:var(--muted);font-size:12px}
|
||
.ch .ap{color:var(--muted);font-size:12px;margin-top:4px}
|
||
.sheets{display:flex;gap:5px;margin-bottom:8px}
|
||
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
|
||
aspect-ratio:3/4;object-fit:cover;object-position:top}
|
||
img[data-zoom]{cursor:zoom-in}
|
||
.lookset{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px}
|
||
.lookset .lw{color:var(--muted);font-size:12px;margin-bottom:5px}
|
||
.lookset .lw b{display:inline;color:inherit}
|
||
.lookset.idle{opacity:.55}
|
||
.whoin{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0}
|
||
.whoin .p{display:flex;gap:8px;border:1px solid var(--line);border-radius:10px;
|
||
padding:8px;width:310px;font-size:12px;align-items:flex-start}
|
||
.whoin .p img{width:64px;aspect-ratio:3/4;object-fit:cover;object-position:top;
|
||
border-radius:6px;border:1px solid var(--line);flex:none}
|
||
.whoin .p .none{width:64px;aspect-ratio:3/4;border-radius:6px;flex:none;
|
||
border:1px dashed var(--line);display:flex;align-items:center;
|
||
justify-content:center;color:var(--muted);font-size:10px;text-align:center}
|
||
.whoin .p .t{flex:1;min-width:0}
|
||
.whoin .p .t b{display:block;font-size:13px}
|
||
.whoin .p .t .w{color:var(--muted);margin:2px 0 6px}
|
||
.whoin .p .t .st{color:#b4531f}
|
||
.whoin .p .t .new{color:#1f7a3a;font-weight:600}
|
||
.whoin .p input{width:100%;box-sizing:border-box;font:inherit;padding:5px 7px;
|
||
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
|
||
.whoin .p .go{margin-top:5px;font-size:12px;padding:3px 9px}
|
||
.guide{border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:0 0 18px}
|
||
.guide h4{margin:0 0 10px;font-size:15px}
|
||
.guide ol{list-style:none;margin:0;padding:0;display:grid;gap:7px}
|
||
.guide li{display:flex;gap:10px;align-items:baseline;font-size:13px;color:var(--muted)}
|
||
.guide li .n{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--line);
|
||
display:inline-flex;align-items:center;justify-content:center;font-size:11px}
|
||
.guide li.done .n{background:#1f7a3a;border-color:#1f7a3a;color:#fff}
|
||
.guide li.now{color:inherit;font-weight:600}
|
||
.guide li.now .n{background:#4B8BF5;border-color:#4B8BF5;color:#fff}
|
||
.guide li .d{font-weight:400;color:var(--muted);display:block;margin-top:2px}
|
||
.guide li .btn{margin-left:4px}
|
||
.fchk{font-size:12px;margin:0 0 10px;padding:7px 10px;border-radius:8px;border:1px solid var(--line)}
|
||
.fchk.ok{color:#1f7a3a;border-color:transparent;padding-left:0}
|
||
.fchk.bad{border-color:#b4531f}
|
||
.fchk .w{display:block;margin:3px 0}
|
||
.fchk .w.serious{color:#b4531f;font-weight:600}
|
||
.fchk .go{font-size:12px;padding:2px 9px;margin-left:6px}
|
||
.vars{border:1px solid var(--line);border-radius:10px;padding:10px;margin:0 0 10px}
|
||
.vars h5{margin:0 0 3px;font-size:13px}
|
||
.vars p{margin:0 0 8px;font-size:12px;color:var(--muted)}
|
||
.vars .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
|
||
.vars .v{font-size:12px}
|
||
.vars .v img{width:100%;border-radius:7px;border:2px solid var(--line);display:block}
|
||
.vars .v.good img{border-color:#1f7a3a}
|
||
.vars .v.bad img{border-color:#b4531f}
|
||
.vars .v .say{margin:4px 0;color:var(--muted);min-height:2.6em}
|
||
.vars .v.bad .say{color:#b4531f}
|
||
.vars .v .btn{font-size:12px;padding:3px 10px}
|
||
.fnote{margin:0 0 10px;font-size:12px}
|
||
.fnote label{display:block;color:var(--muted);margin-bottom:3px}
|
||
.fnote input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:5px 8px;
|
||
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
|
||
.fnote input.saved{border-color:#1f7a3a}
|
||
.ffault{display:flex;gap:8px;margin:0 0 10px}
|
||
.ffault input{flex:1;min-width:0;font:inherit;font-size:12px;padding:5px 8px;
|
||
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
|
||
.ffault .go{font-size:12px;padding:3px 10px;white-space:nowrap}
|
||
.whole{border:1px solid var(--line);border-radius:10px;padding:12px;margin:0 0 18px}
|
||
.whole h4{margin:0 0 4px;font-size:14px}
|
||
.whole p{margin:0 0 8px;color:var(--muted);font-size:13px}
|
||
.whole textarea{width:100%;box-sizing:border-box}
|
||
#zoom{position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:99;display:none;
|
||
align-items:center;justify-content:center;cursor:zoom-out;padding:18px}
|
||
#zoom.on{display:flex}
|
||
#zoom img{max-width:100%;max-height:100%;border-radius:6px}
|
||
#zoom .cap{position:fixed;left:0;right:0;bottom:10px;text-align:center;color:#ddd;font-size:13px}
|
||
.shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px}
|
||
.shot .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
|
||
.shot .body{flex:1;min-width:0}
|
||
.shot h3{margin:0 0 3px;font-size:15px}
|
||
.shot .where{color:var(--muted);font-size:12px;margin-bottom:8px}
|
||
.shot .act{font-size:14px;margin:0 0 8px}
|
||
.shot .cam,.shot .cont{color:var(--muted);font-size:12.5px;margin:0 0 5px}
|
||
.frames{display:flex;gap:10px;margin:10px 0}
|
||
.frames figure{margin:0;flex:1;min-width:0}
|
||
.frames img,.frames video{width:100%;border-radius:8px;border:1px solid var(--line);display:block}
|
||
.frames figcaption{color:var(--muted);font-size:11px;margin-top:3px}
|
||
.line{display:flex;gap:9px;align-items:flex-start;margin:7px 0}
|
||
.line .say{flex:1;min-width:0;font-size:14px}
|
||
.line .who{color:var(--accent);font-size:12px}
|
||
.line .dir{color:var(--muted);font-size:12px}
|
||
.play{border:1px solid var(--line);background:none;color:var(--ink);border-radius:999px;
|
||
width:30px;height:30px;flex:none;cursor:pointer;font:inherit;line-height:1}
|
||
.play:hover{border-color:var(--accent)}
|
||
.dur{color:var(--muted);font-size:12px;align-self:center}
|
||
.dur.over{color:#e0685f}
|
||
.note{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
|
||
background:transparent;color:var(--ink);border:1px dashed var(--line);
|
||
border-radius:8px;padding:8px 10px;resize:vertical}
|
||
.note:focus{border-style:solid;border-color:var(--accent);outline:none}
|
||
.note.saved{border-color:var(--accent2)}
|
||
.probs{border:1px solid #e0685f;border-radius:10px;padding:12px 14px;margin:0 0 18px;font-size:13px}
|
||
.probs b{color:#e0685f}
|
||
.probs ul{margin:6px 0 0;padding-left:18px}
|
||
.bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:0 0 20px}
|
||
.soon{color:var(--muted);font-size:12px}
|
||
.lip{color:var(--accent2);font-size:11px;border:1px solid var(--accent2);
|
||
border-radius:999px;padding:0 6px}
|
||
.vobadge{color:var(--muted);font-size:11px;border:1px solid var(--line);
|
||
border-radius:999px;padding:0 6px}
|
||
.spine{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 14px}
|
||
.spine h4{margin:0 0 8px;font-size:15px}
|
||
.spine .eng{font-size:14px;border-left:2px solid var(--accent);padding-left:11px;margin:0 0 12px}
|
||
.spine .kv{font-size:13px;margin:3px 0;color:var(--muted)}
|
||
.spine .kv b{color:var(--ink);font-weight:600}
|
||
.spine h5{margin:12px 0 4px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;
|
||
color:var(--muted)}
|
||
.spine ul{margin:0;padding-left:18px;font-size:13px}
|
||
.spine details{margin-top:10px}
|
||
.spine summary{cursor:pointer;color:var(--muted);font-size:13px}
|
||
.gaps{border:1px solid var(--accent);border-radius:10px;padding:14px 16px;margin:0 0 14px}
|
||
.gaps h4{margin:0 0 4px;font-size:15px;color:var(--accent)}
|
||
.gaps .verdict{font-size:14px;margin:0 0 10px}
|
||
.gaps ol{margin:0;padding-left:20px;font-size:13px}
|
||
.gaps li{margin-bottom:7px}
|
||
.gaps .fix{color:var(--muted);display:block}
|
||
.gaps .sh{color:var(--muted);font-variant-numeric:tabular-nums}
|
||
.rd{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
|
||
.rd h4{margin:0 0 6px;font-size:14px}
|
||
.rd h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
|
||
.rd blockquote{margin:0 0 8px;font-size:14px;border-left:2px solid var(--line);padding-left:10px}
|
||
.rd .kv{color:var(--muted);font-size:12.5px;margin:2px 0}
|
||
.rd ul{margin:6px 0 0;padding-left:18px;font-size:13px}
|
||
.rd .wrong{color:#e0685f}
|
||
.tools{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:10px}
|
||
.tools button{font-size:12px;padding:5px 10px}
|
||
.chk{font-size:12px;border-radius:999px;padding:1px 8px;border:1px solid var(--line);color:var(--muted)}
|
||
.chk.ok{color:var(--accent2);border-color:var(--accent2)}
|
||
.chk.bad{color:#e0685f;border-color:#e0685f}
|
||
.eyes{margin:8px 0 0;font-size:12.5px}
|
||
.eyes .f{margin:4px 0;padding-left:10px;border-left:2px solid var(--line)}
|
||
.eyes .f.serious{border-left-color:#e0685f}
|
||
.eyes .f b{color:var(--ink)}
|
||
.eyes .fix{color:var(--muted);display:block;margin-top:1px}
|
||
.eyes .sum{color:var(--muted);font-style:italic;margin-bottom:5px}
|
||
.ins{border-top:1px dashed var(--line);text-align:center;padding:6px 0}
|
||
.ins button{font-size:12px;padding:4px 12px;color:var(--muted)}
|
||
.rough{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
|
||
background:transparent;color:var(--ink);border:1px solid var(--accent);
|
||
border-radius:8px;padding:9px 11px;resize:vertical}
|
||
.busy{color:var(--accent);font-size:12px}
|
||
.timing{border:1px solid var(--line);border-radius:10px;padding:11px 14px;margin:0 0 16px;font-size:13px}
|
||
.timing.over{border-color:#e0685f}
|
||
.timing ul{margin:7px 0 0;padding-left:18px;color:var(--muted);font-size:12.5px}
|
||
.cuts{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 16px}
|
||
.cuts h4{margin:0 0 4px;font-size:15px}
|
||
.cuts .lede{color:var(--muted);font-size:13px;margin:0 0 12px}
|
||
.cut{border-top:1px solid var(--line);padding:11px 0}
|
||
.cut:first-of-type{border-top:0}
|
||
.cut b{font-size:14px}
|
||
.cut .len{color:var(--accent2);font-variant-numeric:tabular-nums;margin-left:8px;font-size:13px}
|
||
.cut .strat{font-size:13px;margin:3px 0}
|
||
.cut .why{color:var(--muted);font-size:12.5px;margin:3px 0}
|
||
.cut .sac{color:#e0a05f;font-size:12.5px;margin:3px 0}
|
||
.cut .ids{color:var(--muted);font-size:11.5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
||
margin:4px 0 0;word-break:break-word}
|
||
.cut .stat{color:var(--accent2);font-size:12.5px;margin:3px 0}
|
||
.playbox{margin:10px 0 0;background:#000;border-radius:10px;overflow:hidden}
|
||
.playbox video{width:100%;display:block;aspect-ratio:16/9;background:#000}
|
||
.playbox .bar2{display:flex;gap:10px;align-items:center;padding:7px 10px;font-size:12px;
|
||
color:var(--muted);background:var(--panel)}
|
||
.reel{border:1px solid var(--accent);border-radius:10px;padding:14px 16px;margin:0 0 18px}
|
||
.reel h4{margin:0 0 4px;font-size:15px;color:var(--accent)}
|
||
.reel .lede{color:var(--muted);font-size:13px;margin:0 0 12px}
|
||
.reelframes{display:flex;gap:12px}
|
||
.reelframes figure{margin:0;flex:1;min-width:0}
|
||
.reelframes img{width:100%;border-radius:10px;border:1px solid var(--line);display:block;
|
||
aspect-ratio:16/9;object-fit:cover;background:#000}
|
||
.reelframes img.on{border-color:var(--accent);box-shadow:0 0 0 2px rgba(255,170,80,.25)}
|
||
.reelframes figcaption{color:var(--muted);font-size:11px;margin-top:4px;text-align:center}
|
||
.reelnow{margin:12px 0 0;text-align:center}
|
||
.reelnow .t{font-size:15px;font-weight:600}
|
||
.reelnow .w{color:var(--muted);font-size:12.5px;margin-top:2px}
|
||
.reelnow .said{font-size:14px;margin-top:8px;max-width:46em;margin-left:auto;margin-right:auto}
|
||
.reelnow .said .who{color:var(--accent);font-size:12px;margin-right:6px}
|
||
.reelbar{display:flex;gap:8px;align-items:center;justify-content:center;
|
||
flex-wrap:wrap;margin-top:12px}
|
||
.reelbar .pos{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
|
||
.reelprog{height:4px;background:var(--line);border-radius:999px;margin-top:12px;overflow:hidden}
|
||
.reelprog i{display:block;height:100%;background:var(--accent);width:0}
|
||
.shot.lit{background:rgba(255,170,80,.06);border-radius:10px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header class="page">
|
||
<h1 id="title">Trailers</h1>
|
||
<p id="sub"></p>
|
||
</header>
|
||
<div class="msg" id="msg"></div>
|
||
<section><div class="card">
|
||
<div class="tabs" id="tabs"></div>
|
||
<div id="pane"><p class="empty">Loading…</p></div>
|
||
</div></section>
|
||
</div>
|
||
|
||
<script src="/assets/app.js"></script>
|
||
<script>
|
||
(function () {
|
||
if (!requireSession()) return;
|
||
navBar("trailer");
|
||
|
||
var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = [];
|
||
var timer = null, EDITING = null;
|
||
var PLAYING = null, STATS = {}, QUEUE = [], QAT = 0;
|
||
var REELWANT = false; // the author asked to play, not merely to prepare
|
||
var runId = new URLSearchParams(location.search).get("run") || "";
|
||
var audio = new Audio();
|
||
|
||
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
|
||
|
||
function busyDraft(d) {
|
||
var s = String(d.draw_state || "");
|
||
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
|
||
}
|
||
function busyRun() {
|
||
return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || ""));
|
||
}
|
||
// Work happens in the worker's memory and does not survive a deploy, so a
|
||
// flag saying "in progress" is only believed while it is recent.
|
||
function stillFilming(s) {
|
||
if (!s.filming) return false;
|
||
var t = Date.parse(s.filming_since || "");
|
||
if (!t) return false; // no stamp: from before this existed
|
||
return (Date.now() - t) < 25 * 60 * 1000;
|
||
}
|
||
|
||
var DRAFTNOW = null;
|
||
|
||
function busyShots() {
|
||
var ps = String(PKG.pics_state || "");
|
||
if (ps && ps !== "ready"
|
||
&& (Date.now() - (Date.parse(PKG.pics_started || "") || 0)) < 15 * 60 * 1000) return true;
|
||
if (String(PKG.look_state || "")
|
||
&& (Date.now() - (Date.parse(PKG.look_started || "") || 0)) < 15 * 60 * 1000) return true;
|
||
return (PKG.shots || []).some(function (s) {
|
||
return s.writing || s.drawing || s.fixing || s.varying || stillFilming(s); });
|
||
}
|
||
|
||
// Preparing the storyboard audio is work too. Without this the page stops
|
||
// polling and the progress sits at whatever it last happened to see.
|
||
function busyReel() {
|
||
var st = String(PKG.reel_state || "");
|
||
if (!st || st === "ready" || /^failed/.test(st)) return false;
|
||
var t = Date.parse(PKG.reel_started || "");
|
||
if (!t) return true;
|
||
return (Date.now() - t) < 15 * 60 * 1000; // older than that is gone
|
||
}
|
||
|
||
async function load() {
|
||
var r = await api("run_status", { run_id: runId });
|
||
if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; }
|
||
RUN = r.run; DRAFTS = r.drafts || [];
|
||
document.getElementById("title").textContent = RUN.book_title || "Trailers";
|
||
document.getElementById("sub").textContent = sub();
|
||
if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id;
|
||
paintTabs();
|
||
var rv = await api("list_reviews", { run_id: runId });
|
||
REVIEWS = (rv && rv.reviews) || [];
|
||
await openTab(CUR);
|
||
tick();
|
||
}
|
||
|
||
function tick() {
|
||
var es = String(PKG.eyes_state || "");
|
||
var busyEyes = es && es !== "ready" && !/^failed/.test(es)
|
||
&& (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000;
|
||
var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots() || busyReel()
|
||
|| busyEyes;
|
||
if (busy && !timer) timer = setInterval(refresh, 5000);
|
||
if (!busy && timer) { clearInterval(timer); timer = null; }
|
||
}
|
||
|
||
async function refresh() {
|
||
var r = await api("run_status", { run_id: runId });
|
||
if (!r.ok) return;
|
||
RUN = r.run; DRAFTS = r.drafts || [];
|
||
document.getElementById("sub").textContent = sub();
|
||
paintTabs();
|
||
var rv = await api("list_reviews", { run_id: runId });
|
||
REVIEWS = (rv && rv.reviews) || [];
|
||
await openTab(CUR);
|
||
tick();
|
||
}
|
||
|
||
function leadDraftId() {
|
||
var best = null;
|
||
DRAFTS.forEach(function (d) {
|
||
if (d.writer === RUN.lead_writer
|
||
&& (!best || Number(d.iteration) > Number(best.iteration))) best = d;
|
||
});
|
||
return best && best.id;
|
||
}
|
||
|
||
function sub() {
|
||
var bits = [];
|
||
if (RUN.stage) bits.push(RUN.stage);
|
||
bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s"));
|
||
var spent = DRAFTS.reduce(function (a, d) {
|
||
return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0);
|
||
(PKG.shots || []).forEach(function (s) { spent += Number(s.film_usd || 0); });
|
||
if (spent) bits.push("$" + spent.toFixed(2) + " so far");
|
||
if (RUN.created_at) bits.push(when(RUN.created_at));
|
||
return bits.join(" · ");
|
||
}
|
||
|
||
function paintTabs() {
|
||
document.getElementById("tabs").innerHTML = DRAFTS.map(function (d) {
|
||
var s = String(d.draw_state || "");
|
||
var extra = d.state === "writing" ? '<span class="st">writing…</span>'
|
||
: d.state === "failed" ? '<span class="st">failed</span>'
|
||
: (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>'
|
||
: d.score ? '<span class="sc" title="how well readers understood it">'
|
||
+ d.score + '</span>'
|
||
: '';
|
||
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
|
||
+ (d.writer === RUN.lead_writer ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
|
||
+ esc(d.label || d.writer)
|
||
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
|
||
+ (d.writer === RUN.lead_writer ? ' ★' : '') + extra + '</button>';
|
||
}).join("");
|
||
}
|
||
|
||
document.getElementById("tabs").addEventListener("click", function (e) {
|
||
var id = e.target.closest ? e.target.closest(".tab") : null;
|
||
if (!id) return;
|
||
CUR = id.getAttribute("data-tab"); EDITING = null;
|
||
paintTabs(); openTab(CUR);
|
||
});
|
||
|
||
async function openTab(id) {
|
||
var meta = DRAFTS.filter(function (d) { return d.id === id; })[0];
|
||
if (!meta) return;
|
||
var pane = document.getElementById("pane");
|
||
if (meta.state === "failed") {
|
||
pane.innerHTML = '<p class="empty"><b>' + esc(meta.label) + ' did not finish.</b><br>'
|
||
+ esc(meta.note || "No reason was recorded.") + '</p>'
|
||
+ '<div class="bar"><button type="button" class="btn" data-retry="'
|
||
+ esc(meta.id) + '">Try this writer again</button></div>';
|
||
return;
|
||
}
|
||
if (meta.state === "writing") {
|
||
pane.innerHTML = '<p class="empty">Working. It reads the whole book, works out what the '
|
||
+ 'story is, and then writes the trailer from that. Several minutes.</p>';
|
||
return;
|
||
}
|
||
var r = await api("get_draft", { draft_id: id });
|
||
if (!r.ok) { pane.innerHTML = '<p class="empty">' + esc(r.error || "") + '</p>'; return; }
|
||
PKG = r.draft.package || {};
|
||
NOTES = r.draft.notes || {};
|
||
STATS = {};
|
||
if ((PKG.cuts || []).some(function (c) { return c.project_id; })) {
|
||
var cs = await api("cut_stats", { draft_id: id });
|
||
((cs && cs.cuts) || []).forEach(function (x) { STATS[x.cut_id] = x; });
|
||
}
|
||
pane.innerHTML = paneHtml(meta, r.draft);
|
||
|
||
// "Prepare and play" promised both. Once the audio lands, play it.
|
||
if (REELWANT && String(PKG.reel_state || "") === "ready" && !REEL) {
|
||
REELWANT = false;
|
||
hideMsg("msg");
|
||
var rl = await api("reel", { draft_id: id });
|
||
if (rl.ok && (rl.shots || []).length) {
|
||
REEL = rl.shots; RAT = -1;
|
||
pane.innerHTML = paneHtml(meta, r.draft);
|
||
reelShow(0);
|
||
}
|
||
}
|
||
}
|
||
|
||
// The story the writer committed to before writing a shot. This is where a
|
||
// misreading of the book is visible and cheap to fix; by the time it is
|
||
// twenty-six shots and a storyboard, it is neither.
|
||
function spineHtml() {
|
||
var sp = PKG.spine;
|
||
if (!sp || typeof sp !== "object") return "";
|
||
var p = sp.protagonist || {};
|
||
function ul(items) {
|
||
return (!items || !items.length) ? ""
|
||
: '<ul>' + items.map(function (x) {
|
||
return '<li>' + esc(typeof x === "string" ? x : JSON.stringify(x)) + '</li>';
|
||
}).join("") + '</ul>';
|
||
}
|
||
var beats = (sp.beats || []).map(function (b) {
|
||
return '<li>' + esc(b.what || "")
|
||
+ (b.is_ending ? ' <b>(the ending)</b>' : '')
|
||
+ (b.why_it_matters ? ' <span class="kv">' + esc(b.why_it_matters) + '</span>' : '')
|
||
+ '</li>'; }).join("");
|
||
|
||
return '<div class="spine"><h4>The story, as the writer worked it out from the book</h4>'
|
||
+ (sp.the_engine ? '<p class="eng">' + esc(sp.the_engine) + '</p>' : '')
|
||
+ (p.who ? '<div class="kv"><b>who</b> ' + esc(p.who) + '</div>' : '')
|
||
+ (p.wants ? '<div class="kv"><b>wants</b> ' + esc(p.wants) + '</div>' : '')
|
||
+ (p.obstacle ? '<div class="kv"><b>in the way</b> ' + esc(p.obstacle) + '</div>' : '')
|
||
+ (p.cost ? '<div class="kv"><b>cost</b> ' + esc(p.cost) + '</div>' : '')
|
||
+ (sp.what_a_viewer_must_understand && sp.what_a_viewer_must_understand.length
|
||
? '<h5>A viewer must come away knowing</h5>' + ul(sp.what_a_viewer_must_understand) : '')
|
||
+ (sp.moments_to_dramatise && sp.moments_to_dramatise.length
|
||
? '<h5>Must be shown, not narrated</h5>' + ul(sp.moments_to_dramatise) : '')
|
||
+ (sp.premise ? '<details><summary>the premise in full</summary><p>'
|
||
+ esc(sp.premise) + '</p></details>' : '')
|
||
+ (beats ? '<details><summary>the beats of the book</summary><ol>' + beats + '</ol></details>' : '')
|
||
+ (sp.withhold ? '<details><summary>what the trailer is withholding</summary><p>'
|
||
+ esc(sp.withhold) + '</p></details>' : '')
|
||
+ '</div>';
|
||
}
|
||
|
||
function barHtml(meta) {
|
||
var shots = (PKG.shots || []), cast = (PKG.characters || []);
|
||
var undrawn = shots.filter(function (s) { return !s.opening_url; }).length;
|
||
var unfilmed = shots.filter(function (s) { return !s.clip_url; }).length;
|
||
var filmed = shots.length - unfilmed;
|
||
var imgs = boardImages(shots, cast);
|
||
var s = String(meta.draw_state || "");
|
||
var drawing = s && !/^(drawn|failed)/.test(s);
|
||
var isLead = meta.writer === RUN.lead_writer;
|
||
var out = [];
|
||
|
||
if (!isLead) out.push('<button type="button" class="btn primary" data-lead="'
|
||
+ esc(meta.id) + '">Make this the head writer</button>');
|
||
else out.push('<span class="soon">★ head writer</span>');
|
||
|
||
if (drawing) out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
|
||
else if (undrawn) out.push('<button type="button" class="btn" data-draw="' + esc(meta.id)
|
||
+ '">Draw ' + (undrawn === shots.length ? 'this storyboard'
|
||
: undrawn + ' undrawn shot' + (undrawn === 1 ? '' : 's'))
|
||
// The worker counts what it will really draw - looks, back views and
|
||
// frames - so the price here is the price. `imgs` knew only about three
|
||
// sheets a head and two frames a shot.
|
||
+ ' · $' + ((Number(PKG.pictures_to_draw) || imgs) * 0.068).toFixed(2) + '</button>');
|
||
else out.push('<span class="soon">storyboard drawn'
|
||
+ (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
|
||
|
||
if (isLead) {
|
||
var others = DRAFTS.filter(function (d) {
|
||
return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length;
|
||
if (RUN.stage === "reviewing")
|
||
out.push('<button type="button" class="btn" disabled>Readers are watching it…</button>');
|
||
else if (others)
|
||
out.push('<button type="button" class="btn" data-review="1" title="'
|
||
+ 'Each other model reads ONLY this script - no book - and says what it understood.">'
|
||
+ (REVIEWS.length ? "Test it again" : "Does it make sense? Ask " + others + " readers")
|
||
+ '</button>');
|
||
if ((PKG.shots || []).length >= 6 && !busyCuts())
|
||
out.push('<button type="button" class="btn" data-cuts="' + esc(meta.id)
|
||
+ '" title="Several shorter versions of what is already filmed. '
|
||
+ 'Costs nothing to propose or to watch.">'
|
||
+ ((PKG.cuts || []).length ? "Propose more cuts" : "Propose shorter cuts")
|
||
+ '</button>');
|
||
|
||
out.push('<button type="button" class="btn" data-measure="' + esc(meta.id)
|
||
+ '" title="Speaks every line in the cast voices and reports what the '
|
||
+ 'trailer really runs.">'
|
||
+ ((PKG.timing && PKG.timing.measured_seconds) ? "Time it again" : "Time it for real")
|
||
+ '</button>');
|
||
|
||
if (RUN.stage === "revising")
|
||
out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
|
||
else {
|
||
var over = (PKG.timing && Number(PKG.timing.over_by)) || 0;
|
||
// Always on offer. It used to appear only once a note existed, and
|
||
// a note typed a moment ago does not repaint the bar - so an author
|
||
// wrote their notes and found nothing to press.
|
||
if (true)
|
||
out.push('<button type="button" class="btn' + (over > 5 ? ' primary' : '')
|
||
+ '" data-revise="1">'
|
||
+ (over > 5 ? 'Cut ' + Math.round(over) + ' seconds and revise'
|
||
: 'Revise the whole script') + '</button>');
|
||
}
|
||
|
||
if (filmed) {
|
||
var es = String(PKG.eyes_state || "");
|
||
var looking = es && es !== "ready" && !/^failed/.test(es)
|
||
&& (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000;
|
||
if (looking)
|
||
out.push('<button type="button" class="btn" disabled>Looking at the '
|
||
+ 'footage… ' + esc(es) + '</button>');
|
||
else
|
||
out.push('<button type="button" class="btn" data-eyes="' + esc(meta.id)
|
||
+ '" title="Samples frames from every clip and asks what is wrong '
|
||
+ 'with the picture, then whether it matches the script.">'
|
||
+ (es === "ready" ? "Check the picture again" : "Check the picture")
|
||
+ ' · ' + "$" + (filmed * 0.02).toFixed(2)
|
||
+ '</button>');
|
||
}
|
||
|
||
if (unfilmed && !undrawn)
|
||
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
|
||
+ '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots')
|
||
+ ' · $' + (unfilmed * 0.75).toFixed(2) + '</button>');
|
||
if (filmed)
|
||
out.push('<button type="button" class="btn primary" data-assemble="' + esc(meta.id)
|
||
+ '">Make the trailer · ' + filmed + ' of ' + shots.length + ' shots</button>');
|
||
}
|
||
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(" |