A dollar sign before a quote is a substitution token to patch_git_file, so a price label spliced the rest of the page in after itself. trailer.html carried 33 KB of raw JavaScript after its closing tag, which rendered as garbled text in the page body; script.html carried 65 KB, commented out. The copy above the first closing tag was verified complete: it parses, and it defines every function and calls every action the removed text did.
1147 lines
54 KiB
HTML
1147 lines
54 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:16/9;object-fit:cover}
|
||
.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;
|
||
}
|
||
|
||
function busyShots() {
|
||
return (PKG.shots || []).some(function (s) {
|
||
return s.writing || s.drawing || 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'))
|
||
+ ' · $' + (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;
|
||
if (REVIEWS.length || Object.keys(NOTES).length || over > 5)
|
||
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(" |