The four scripts had no next step. Everything after "read them" lived in the API and nothing on the page reached it. The choice of head writer is the author's, and it is a judgement about concept rather than a score, so it is a button on each tab rather than a leaderboard. Naming the lead first also changes what the reviews are for: three models arguing about how to fix THIS script is a more useful argument than three models arguing about whose is better. Reviews are split the way they come back - errors are things that are wrong and are worth fixing, suggestions are matters of judgement, and "keeps" is what a revision must not quietly remove. The author's shot notes outrank all of it and the revise button says so. Film is priced and gated on having a storyboard, because every shot needs an opening frame to film from.
467 lines
21 KiB
HTML
467 lines
21 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)}
|
|
.tab .sc{color:var(--accent2);margin-left:7px;font-weight:600}
|
|
.tab .st{color:var(--muted);margin-left:7px;font-size:12px}
|
|
.tab .crown{margin-left:6px}
|
|
.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:34px;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{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}
|
|
.revs{margin:0 0 22px}
|
|
.rev{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
|
|
.rev h4{margin:0 0 6px;font-size:14px}
|
|
.rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
|
|
.rev .v{color:var(--muted);font-size:13px;margin:0 0 8px}
|
|
.rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px}
|
|
.rev .er{color:#e0685f}
|
|
.rev .kp{color:var(--accent2)}
|
|
.rev .sh{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
</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;
|
|
var runId = new URLSearchParams(location.search).get("run") || "";
|
|
var audio = new Audio();
|
|
|
|
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
|
|
|
|
// A draft is busy while it is being written or drawn; a run is busy while it
|
|
// is reviewing or revising. Both are minutes long and report on the row.
|
|
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 || ""));
|
|
}
|
|
|
|
async function load(repaint) {
|
|
var r = await api("run_status", { run_id: runId });
|
|
if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; }
|
|
var wasBusy = (DRAFTS.length && DRAFTS.some(busyDraft)) || busyRun();
|
|
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) || [];
|
|
|
|
var busy = DRAFTS.some(busyDraft) || busyRun();
|
|
if (busy && !timer) timer = setInterval(load, 5000);
|
|
if (!busy && timer) { clearInterval(timer); timer = null; }
|
|
|
|
var me = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
|
if (me && me.state === "writing") { paintWriting(); return; }
|
|
if (repaint !== false && (wasBusy || !document.querySelector(".shot"))) openTab(CUR);
|
|
else paintBar();
|
|
}
|
|
|
|
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);
|
|
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">' + d.score + '</span>'
|
|
: '';
|
|
var isLead = d.writer === RUN.lead_writer;
|
|
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
|
|
+ (isLead ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
|
|
+ esc(d.label || d.writer)
|
|
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
|
|
+ (isLead ? '<span class="crown" title="head writer">★</span>' : '')
|
|
+ extra + '</button>';
|
|
}).join("");
|
|
}
|
|
|
|
function paintWriting() {
|
|
document.getElementById("pane").innerHTML =
|
|
'<p class="empty">Working. Reading the whole book takes a few minutes.</p>';
|
|
}
|
|
|
|
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");
|
|
paintTabs(); openTab(CUR);
|
|
});
|
|
|
|
// ------------------------------------------------------------ one trailer
|
|
|
|
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") { paintWriting(); return; }
|
|
pane.innerHTML = '<p class="empty">Loading…</p>';
|
|
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 || {};
|
|
pane.innerHTML = paneHtml(meta, r.draft);
|
|
}
|
|
|
|
function barHtml(meta) {
|
|
var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length;
|
|
var imgs = cast * 3 + shots * 2;
|
|
var price = imgs * 0.068;
|
|
var s = String(meta.draw_state || "");
|
|
var drawing = s && !/^(drawn|failed)/.test(s);
|
|
var drawn = /^drawn/.test(s);
|
|
var isLead = meta.writer === RUN.lead_writer;
|
|
var out = [];
|
|
|
|
// 1. Whose concept wins. The author's call, not a score's.
|
|
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>');
|
|
}
|
|
|
|
// 2. Drawing.
|
|
if (drawing) {
|
|
out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
|
|
} else {
|
|
out.push('<button type="button" class="btn" data-draw="' + esc(meta.id) + '">'
|
|
+ (drawn ? "Draw it again" : "Draw this storyboard") + ' · $'
|
|
+ price.toFixed(2) + '</button>');
|
|
if (!drawn) out.push('<span class="soon">' + imgs + ' pictures</span>');
|
|
else out.push('<span class="soon">' + esc(s)
|
|
+ (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
|
|
}
|
|
|
|
// 3. Review, revise, film - only meaningful on the head writer's tab.
|
|
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>The others are reading it…</button>');
|
|
} else if (others) {
|
|
out.push('<button type="button" class="btn" data-review="1">'
|
|
+ (REVIEWS.length ? "Review it again" : "Have the others review this")
|
|
+ ' · ' + others + ' readers</button>');
|
|
}
|
|
if (RUN.stage === "revising") {
|
|
out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
|
|
} else if (REVIEWS.length || Object.keys(NOTES).length) {
|
|
out.push('<button type="button" class="btn primary" data-revise="1">'
|
|
+ 'Revise with the notes</button>');
|
|
}
|
|
if (drawn) {
|
|
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
|
|
+ '">Film this trailer</button>');
|
|
} else {
|
|
out.push('<span class="soon">Draw the storyboard before filming — '
|
|
+ 'every shot is filmed from its opening frame.</span>');
|
|
}
|
|
}
|
|
return out.join("");
|
|
}
|
|
|
|
function paintBar() {
|
|
var el = document.querySelector(".bar");
|
|
var meta = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
|
if (el && meta) el.innerHTML = barHtml(meta);
|
|
}
|
|
|
|
function reviewsHtml(meta) {
|
|
var mine = REVIEWS.filter(function (r) {
|
|
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
|
|
if (!mine.length) return "";
|
|
return '<div class="revs">' + mine.map(function (r) {
|
|
var parts = ['<div class="rev"><h4>' + esc(r.label)
|
|
+ '<span class="n">' + esc(r.score) + '/100</span></h4>'];
|
|
function list(items, cls, fmt) {
|
|
if (!items || !items.length) return "";
|
|
return '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>';
|
|
}
|
|
parts.push(list(r.errors, "er", function (e) {
|
|
return '<li>' + (e.shot ? '<span class="sh">shot ' + esc(e.shot) + '</span> ' : '')
|
|
+ esc(e.problem || "") + (e.fix ? ' — <i>' + esc(e.fix) + '</i>' : '') + '</li>'; }));
|
|
parts.push(list(r.suggestions, "", function (s) {
|
|
return '<li>' + (s.shot ? '<span class="sh">shot ' + esc(s.shot) + '</span> ' : '')
|
|
+ esc(s.change || "") + (s.why ? ' <span class="sh">(' + esc(s.why) + ')</span>' : '') + '</li>'; }));
|
|
parts.push(list(r.strengths, "kp", function (s) {
|
|
return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; }));
|
|
return parts.join("") + '</div>';
|
|
}).join("") + '</div>';
|
|
}
|
|
|
|
function paneHtml(meta, draft) {
|
|
var p = PKG, shots = p.shots || [], cast = p.characters || [];
|
|
var 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 = '<div class="head"><h2>' + esc(p.title || meta.label) + '</h2>'
|
|
+ (p.tagline ? '<span class="tag">' + esc(p.tagline) + '</span>' : '') + '</div>'
|
|
+ (p.logline ? '<p>' + esc(p.logline) + '</p>' : '')
|
|
+ '<p class="facts">' + 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'
|
|
+ (meta.write_usd ? ' · $' + Number(meta.write_usd).toFixed(2) + ' to write' : '')
|
|
+ (meta.note ? ' · ' + esc(meta.note) : '')
|
|
+ '</p>';
|
|
|
|
var probs = "";
|
|
if (draft.problems) {
|
|
probs = '<div class="probs"><b>The checker still has objections</b><ul>'
|
|
+ draft.problems.split("\n").filter(Boolean).map(function (x) {
|
|
return '<li>' + esc(x) + '</li>'; }).join("")
|
|
+ '</ul></div>';
|
|
}
|
|
|
|
var castHtml = '<div class="cast">' + cast.map(function (c) {
|
|
var sheets = "";
|
|
if (c.head_url || c.front_url || c.side_url) {
|
|
sheets = '<div class="sheets">'
|
|
+ [c.head_url, c.front_url, c.side_url].map(function (u) {
|
|
return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("")
|
|
+ '</div>';
|
|
}
|
|
return '<div class="ch">' + sheets + '<b>' + esc(c.name || c.id) + '</b>'
|
|
+ '<span class="vo">' + esc(c.role || "") + (c.age ? " · " + esc(c.age) : "")
|
|
+ (c.voice_id ? " · voiced" : " · silent") + '</span>'
|
|
+ '<div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
|
|
}).join("") + '</div>';
|
|
|
|
return head + probs + '<div class="bar">' + barHtml(meta) + '</div>'
|
|
+ reviewsHtml(meta) + castHtml
|
|
+ shots.map(function (s) { return shotHtml(s, voice); }).join("");
|
|
}
|
|
|
|
function shotHtml(s, voice) {
|
|
var lines = (s.audio || []).map(function (a) {
|
|
var who = voice[a.character_id] || {};
|
|
return '<div class="line">'
|
|
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '" title="Hear this shot">▶</button>'
|
|
+ '<div class="say"><span class="who">' + esc(who.name || a.character_id) + '</span> '
|
|
+ (a.lip_sync ? '<span class="lip">on camera</span>' : '<span class="vobadge">voice over</span>')
|
|
+ '<br>' + esc(a.text || "")
|
|
+ (a.delivery ? '<br><span class="dir">' + esc(a.delivery) + '</span>' : '')
|
|
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
|
|
}).join("");
|
|
|
|
var frames = "";
|
|
if (s.opening_url || s.closing_url) {
|
|
frames = '<div class="frames">'
|
|
+ (s.opening_url ? '<figure><img src="' + esc(s.opening_url) + '" alt=""><figcaption>opens on</figcaption></figure>' : '')
|
|
+ (s.closing_url ? '<figure><img src="' + esc(s.closing_url) + '" alt=""><figcaption>ends on</figcaption></figure>' : '')
|
|
+ '</div>';
|
|
}
|
|
|
|
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
|
|
+ '<h3>' + esc(s.title || "") + '</h3>'
|
|
+ '<div class="where">' + esc(s.setting || "") + ' · '
|
|
+ Number(s.edit_seconds || 0).toFixed(1) + 's</div>'
|
|
+ frames
|
|
+ '<p class="act">' + esc(s.action || "") + '</p>'
|
|
+ (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '')
|
|
+ (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '')
|
|
+ lines
|
|
+ '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" '
|
|
+ 'placeholder="Your note on this shot. It outranks everything the other writers said.">'
|
|
+ esc(NOTES[s.id] || "") + '</textarea>'
|
|
+ '</div></div>';
|
|
}
|
|
|
|
// ------------------------------------------------------------ actions
|
|
|
|
document.getElementById("pane").addEventListener("click", async function (e) {
|
|
var t = e.target.closest ? e.target : null;
|
|
if (!t) return;
|
|
|
|
var lead = e.target.closest("[data-lead]");
|
|
if (lead) {
|
|
lead.disabled = true;
|
|
var a = await api("set_lead", { run_id: runId, draft_id: lead.getAttribute("data-lead") });
|
|
if (!a.ok) { lead.disabled = false; showMsg("msg", a.error || ""); return; }
|
|
hideMsg("msg"); await load(); openTab(CUR); return;
|
|
}
|
|
|
|
var draw = e.target.closest("[data-draw]");
|
|
if (draw) {
|
|
draw.disabled = true;
|
|
var d = await api("draw_storyboard", { draft_id: draw.getAttribute("data-draw") });
|
|
if (!d.ok) { draw.disabled = false; showMsg("msg", d.error || ""); return; }
|
|
hideMsg("msg"); load(); return;
|
|
}
|
|
|
|
var rev = e.target.closest("[data-review]");
|
|
if (rev) {
|
|
rev.disabled = true;
|
|
var v = await api("start_review", { run_id: runId });
|
|
if (!v.ok) { rev.disabled = false; showMsg("msg", v.error || ""); return; }
|
|
hideMsg("msg"); load(); return;
|
|
}
|
|
|
|
var rvs = e.target.closest("[data-revise]");
|
|
if (rvs) {
|
|
rvs.disabled = true;
|
|
var w = await api("start_revision", { run_id: runId });
|
|
if (!w.ok) { rvs.disabled = false; showMsg("msg", w.error || ""); return; }
|
|
hideMsg("msg"); load(); return;
|
|
}
|
|
|
|
var film = e.target.closest("[data-film]");
|
|
if (film) {
|
|
film.disabled = true;
|
|
var f = await api("film_draft", { draft_id: film.getAttribute("data-film") });
|
|
if (!f.ok) { film.disabled = false; showMsg("msg", f.error || ""); return; }
|
|
showMsg("msg", "Filming " + f.shots + " shots, about $" + f.usd
|
|
+ ". It appears in My videos as it goes.", "ok");
|
|
setTimeout(function () { location.href = "/videos.html"; }, 2500);
|
|
return;
|
|
}
|
|
|
|
var retry = e.target.closest("[data-retry]");
|
|
if (retry) {
|
|
retry.disabled = true;
|
|
var y = await api("retry_writer", { draft_id: retry.getAttribute("data-retry") });
|
|
if (!y.ok) { retry.disabled = false; showMsg("msg", y.error || ""); return; }
|
|
hideMsg("msg"); load(); return;
|
|
}
|
|
|
|
var btn = e.target.closest(".play");
|
|
if (!btn) return;
|
|
var sid = btn.getAttribute("data-shot");
|
|
btn.disabled = true; btn.innerHTML = "…";
|
|
var r = await api("shot_audio", { draft_id: CUR, shot_id: sid });
|
|
btn.disabled = false; btn.innerHTML = "▶";
|
|
if (!r.ok) { showMsg("msg", r.error || "That would not play."); return; }
|
|
hideMsg("msg");
|
|
if (!r.url) return;
|
|
// Spoken length against the length the writer planned for. A line that
|
|
// overruns its shot is the single most common fault in these scripts.
|
|
document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (el) {
|
|
el.textContent = r.seconds.toFixed(1) + "s / " + Number(r.planned).toFixed(1) + "s";
|
|
el.className = "dur" + (r.seconds > Number(r.planned) ? " over" : "");
|
|
});
|
|
audio.pause(); audio.src = r.url; audio.play();
|
|
});
|
|
|
|
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]);
|
|
// Saved as you stop typing. A note the author has to remember to save is
|
|
// a note that gets lost between tabs.
|
|
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();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|