diff --git a/trailer.html b/trailer.html index bcff926..c49cfea 100644 --- a/trailer.html +++ b/trailer.html @@ -14,7 +14,6 @@ .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} @@ -28,7 +27,7 @@ .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 .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} @@ -36,7 +35,7 @@ .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 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} @@ -65,11 +64,21 @@ .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} + .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} + .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} @@ -92,14 +101,12 @@ navBar("trailer"); var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = []; - var timer = null; + var timer = null, EDITING = 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)); @@ -107,28 +114,42 @@ function busyRun() { return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || "")); } + // A shot is busy while it is being written, drawn or filmed. Any of those + // means the package on screen is about to change. + function busyShots() { + return (PKG.shots || []).some(function (s) { + return s.writing || s.drawing || s.filming; }); + } - async function load(repaint) { + 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; } - 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) || []; + await openTab(CUR); + tick(); + } - var busy = DRAFTS.some(busyDraft) || busyRun(); - if (busy && !timer) timer = setInterval(load, 5000); + function tick() { + var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots(); + if (busy && !timer) timer = setInterval(refresh, 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(); + // A refresh keeps the tab you are on and the box you are typing in. + 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(); + await openTab(CUR); + tick(); } function leadDraftId() { @@ -146,6 +167,7 @@ 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(" · "); @@ -159,30 +181,21 @@ : (s && !/^(drawn|failed)/.test(s)) ? '' + esc(s) + '' : d.score ? '' + d.score + '' : ''; - var isLead = d.writer === RUN.lead_writer; return ''; + + (d.writer === RUN.lead_writer ? ' ★' : '') + extra + ''; }).join(""); } - function paintWriting() { - document.getElementById("pane").innerHTML = - '

Working. Reading the whole book takes a few minutes.

'; - } - 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"); + CUR = id.getAttribute("data-tab"); EDITING = null; paintTabs(); openTab(CUR); }); - // ------------------------------------------------------------ one trailer - async function openTab(id) { var meta = DRAFTS.filter(function (d) { return d.id === id; })[0]; if (!meta) return; @@ -194,8 +207,10 @@ + esc(meta.id) + '">Try this writer again'; return; } - if (meta.state === "writing") { paintWriting(); return; } - pane.innerHTML = '

Loading…

'; + if (meta.state === "writing") { + pane.innerHTML = '

Working. Reading the whole book takes a few minutes.

'; + return; + } var r = await api("get_draft", { draft_id: id }); if (!r.ok) { pane.innerHTML = '

' + esc(r.error || "") + '

'; return; } PKG = r.draft.package || {}; @@ -204,95 +219,77 @@ } function barHtml(meta) { - var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length; - var imgs = cast * 3 + shots * 2; - var price = imgs * 0.068; + 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 = cast.length * 3 + shots.length * 2; 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(''); - } else { - out.push('★ head writer'); - } + if (!isLead) out.push(''); + else out.push('★ head writer'); - // 2. Drawing. - if (drawing) { - out.push(''); - } else { - out.push(''); - if (!drawn) out.push('' + imgs + ' pictures'); - else out.push('' + esc(s) - + (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + ''); - } + if (drawing) out.push(''); + else if (undrawn) out.push(''); + else out.push('storyboard drawn' + + (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + ''); - // 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") { + if (RUN.stage === "reviewing") out.push(''); - } else if (others) { + else if (others) out.push(''); - } - if (RUN.stage === "revising") { + + (REVIEWS.length ? "Review it again" : "Have the others review this") + ''); + if (RUN.stage === "revising") out.push(''); - } else if (REVIEWS.length || Object.keys(NOTES).length) { - out.push(''); - } - if (drawn) { + else if (REVIEWS.length || Object.keys(NOTES).length) + out.push(''); + + if (unfilmed && !undrawn) out.push(''); - } else { - out.push('Draw the storyboard before filming — ' - + 'every shot is filmed from its opening frame.'); - } + + '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots') + + ' · $' + (unfilmed * 0.75).toFixed(2) + ''); + if (filmed) + out.push(''); } 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 '
' + mine.map(function (r) { - var parts = ['

' + esc(r.label) - + '' + esc(r.score) + '/100

']; function list(items, cls, fmt) { - if (!items || !items.length) return ""; - return ''; + return (!items || !items.length) ? "" + : ''; } - parts.push(list(r.errors, "er", function (e) { - return '
  • ' + (e.shot ? 'shot ' + esc(e.shot) + ' ' : '') - + esc(e.problem || "") + (e.fix ? ' — ' + esc(e.fix) + '' : '') + '
  • '; })); - parts.push(list(r.suggestions, "", function (s) { - return '
  • ' + (s.shot ? 'shot ' + esc(s.shot) + ' ' : '') - + esc(s.change || "") + (s.why ? ' (' + esc(s.why) + ')' : '') + '
  • '; })); - parts.push(list(r.strengths, "kp", function (s) { - return '
  • ' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '
  • '; })); - return parts.join("") + '
    '; + return '

    ' + esc(r.label) + '' + esc(r.score) + '/100

    ' + + list(r.errors, "er", function (e) { + return '
  • ' + (e.shot ? 'shot ' + esc(e.shot) + ' ' : '') + + esc(e.problem || "") + (e.fix ? ' — ' + esc(e.fix) + '' : '') + '
  • '; }) + + list(r.suggestions, "", function (s) { + return '
  • ' + (s.shot ? 'shot ' + esc(s.shot) + ' ' : '') + + esc(s.change || "") + '
  • '; }) + + list(r.strengths, "kp", function (s) { + return '
  • ' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '
  • '; }) + + '
    '; }).join("") + '
    '; } function paneHtml(meta, draft) { - var p = PKG, shots = p.shots || [], cast = p.characters || []; - var voice = {}; + 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); @@ -303,43 +300,58 @@ + (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) : '') - + '

    '; + + ' · ' + cast.length + ' characters

    '; - var probs = ""; - if (draft.problems) { - probs = '
    The checker still has objections
    ' + : ""; var castHtml = '
    ' + cast.map(function (c) { - var sheets = ""; - if (c.head_url || c.front_url || c.side_url) { - sheets = '
    ' - + [c.head_url, c.front_url, c.side_url].map(function (u) { - return u ? '' : ''; }).join("") - + '
    '; - } + 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.age ? " · " + esc(c.age) : "") - + (c.voice_id ? " · voiced" : " · silent") + '' - + '
    ' + esc((c.appearance || "").slice(0, 150)) + '
    '; + + '' + 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) + '
    ' - + reviewsHtml(meta) + castHtml - + shots.map(function (s) { return shotHtml(s, voice); }).join(""); + + reviewsHtml(meta) + castHtml + body; + } + + function insertHtml(after) { + if (EDITING === "new:" + after) { + return '
    ' + + '' + + '
    ' + + '
    '; + } + return '
    '; } 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 || "") @@ -347,102 +359,168 @@ + '
    '; }).join(""); - var frames = ""; - if (s.opening_url || s.closing_url) { - frames = '
    ' + 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)) + '”' : '') + + ''; + } + + 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 + + ''; + } + return '
    ' + esc(s.id) + '
    ' + '

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

    ' + '
    ' + esc(s.setting || "") + ' · ' + Number(s.edit_seconds || 0).toFixed(1) + 's
    ' - + frames + + media + '

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

    ' + (s.camera ? '

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

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

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

    ' : '') + lines + + '
    ' + tools + '
    ' + '' + '
    '; } // ------------------------------------------------------------ actions + async function act(name, payload, msg) { + var r = await api(name, payload); + if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; } + hideMsg("msg"); + if (msg) showMsg("msg", msg(r), "ok"); + return r; + } + document.getElementById("pane").addEventListener("click", async function (e) { - var t = e.target.closest ? e.target : null; - if (!t) return; + var hit = function (attr) { var el = e.target.closest("[" + attr + "]"); return el; }; + var el, r; - 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; + 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; } - - 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; + 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; } - - 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; + 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; } - - 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; + 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; } - - 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); + 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; } - - 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; + 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-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 r = await api("shot_audio", { draft_id: CUR, shot_id: sid }); + var a = 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; } + if (!a.ok) { showMsg("msg", a.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" : ""); + 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 = r.url; audio.play(); + audio.pause(); audio.src = a.url; audio.play(); }); var noteTimers = {}; @@ -452,8 +530,6 @@ 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"); }