diff --git a/trailer.html b/trailer.html index d5c52ee..cfc788d 100644 --- a/trailer.html +++ b/trailer.html @@ -97,6 +97,15 @@ #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} + .scenebar{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;border:0;border-radius:10px;padding:10px 14px;margin:14px 0 4px;cursor:pointer;font:inherit;font-size:14px;text-align:left;color:#fff} + .scenebar.ok{background:#1f7a3a} + .scenebar.todo{background:#a8322a} + .scenebar .n{font:12px var(--mono,monospace);opacity:.85;min-width:64px} + .scenebar .t{flex:1;font-weight:600} + .scenebar .st{font-size:12px;opacity:.9} + .scenebar .arrow{opacity:.8} + .sceneshare{display:flex;gap:8px;align-items:center;margin:0 0 8px;font-size:12px;color:var(--muted)} + .sharelink{font:12px var(--mono,monospace);word-break:break-all} .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} @@ -1025,7 +1034,7 @@ // A CHAPTER IS A HUNDRED SHOTS. Twelve to a page, with back and next, so // the page stays quick and the author is never scrolling through a wall. - var per = 12, pages = shots.length > 24 ? Math.ceil(shots.length / per) : 1; + var per = 12, pages = (shots.length > 24 && !hasScenes(shots)) ? Math.ceil(shots.length / per) : 1; if (PAGE_FOR !== draft.id) { PAGE_FOR = draft.id; PAGE = 0; } PAGE = Math.max(0, Math.min(PAGE, pages - 1)); var shown = pages > 1 ? shots.slice(PAGE * per, (PAGE + 1) * per) : shots; @@ -1038,9 +1047,13 @@ + ' of ' + pages + '' + '' : ""; - var body = pager + (PAGE === 0 ? insertHtml("start") : ""); - shown.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); }); - body += pager; + var body; + if (hasScenes(shots)) body = scenesHtml(shots, voice, draft); + else { + body = pager + (PAGE === 0 ? insertHtml("start") : ""); + shown.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); }); + body += pager; + } DRAFTNOW = draft; return head + partsHtml(draft) + contestHtml() + guideHtml(meta) + probs + '
' + barHtml(meta) + '
' @@ -1405,6 +1418,49 @@ return '
' + pic + '
' + body + '
'; } + // SCENES. Consecutive shots in one place are a scene. Every scene is a bar + // the author opens when they want it - green when every shot in it is + // filmed and current, red when it still needs work - so nobody scrolls + // through seventy cards to find one. + function hasScenes(shots) { return shots.length > 8 && shots.filter(function (x) { return x.scene; }).length > shots.length / 2; } + function sceneKey(s) { return String(s.place || s.scene || s.setting || "").toLowerCase().replace(/^(int|ext)\.?\s*\/?\s*(int|ext)?\.?\s*/, "").replace(/\s*[-–—]\s*(day|night|dawn|dusk|morning|evening|afternoon|late afternoon|continuous|the storm|the tornado).*$/, "").replace(/[^a-z0-9]+/g, " ").trim(); } + function sceneGroups(shots) { + var groups = [], cur = null; + shots.forEach(function (s) { + var k = sceneKey(s); + if (!cur || cur.key !== k) { cur = { key: k, name: s.scene || s.setting || k, shots: [] }; groups.push(cur); } + cur.shots.push(s); + }); + return groups; + } + function shotDone(s) { return !!s.clip_url && !s.clip_stale && !s.picture_stale; } + var OPEN = {}; + try { OPEN = JSON.parse(localStorage.getItem("ptl_open_scenes") || "{}"); } catch (e) { OPEN = {}; } + function rememberOpen() { try { localStorage.setItem("ptl_open_scenes", JSON.stringify(OPEN)); } catch (e) {} } + var SHARES = {}; + function scenesHtml(shots, voice, draft) { + var out = insertHtml("start"); + sceneGroups(shots).forEach(function (g, gi) { + var filmed = g.shots.filter(function (x) { return x.clip_url; }).length, done = g.shots.every(shotDone); + var busy = g.shots.some(function (x) { return x.writing || x.drawing || stillFilming(x); }); + var id = draft.id + "|" + gi + "|" + g.key, open = !!OPEN[id]; + var secs = g.shots.reduce(function (a, x) { return a + Number(x.edit_seconds || 0); }, 0); + out += ''; + if (open) { + var ids = g.shots.filter(function (x) { return x.clip_url; }).map(function (x) { return x.id; }); + out += '
' + (ids.length ? '' : 'Nothing filmed in this scene yet.') + + (SHARES["scene:" + ids.join(",")] ? ' ' + esc(SHARES["scene:" + ids.join(",")]) + '' : '') + '
'; + g.shots.forEach(function (x) { out += shotHtml(x, voice) + insertHtml(x.id); }); + } + }); + return out; + } + function shotHtml(s, voice) { if (s.writing) { return '
' + esc(s.id) + '
' @@ -1507,6 +1563,8 @@ + (s.opening_url ? 'Redraw the shot picture' : 'Draw the shot picture (actors in the background)') + ' · $0.14' + (s.opening_url ? '' : '') + + (s.clip_url ? '' + + (SHARES["shot:" + s.id] ? ' ' : '') : '') + (s.clip_url && s.take_url ? '' : '') + chkHtml + eyeHtml @@ -1589,6 +1647,23 @@ if (r) { showMsg("msg", "Shot " + ssid + " is now " + Number(r.seconds).toFixed(1) + " s." + (PKG.shots.some(function (x) { return x.id === ssid && x.clip_url; }) ? " The clip was filmed at the old length; make the shot again to refilm." : ""), "ok"); await refresh(); } return; } + if ((el = hit("data-scene"))) { var sk = el.getAttribute("data-scene"); OPEN[sk] = !OPEN[sk]; rememberOpen(); openTab(CUR); return; } + if ((el = hit("data-shareshot"))) { + var shid = el.getAttribute("data-shareshot"); + el.disabled = true; + r = await act("share_clips", { draft_id: CUR, shot_ids: [shid], label: "Shot " + shid }); + if (r) { SHARES["shot:" + shid] = r.url; showMsg("msg", "Shot " + shid + ": " + r.url, "ok"); openTab(CUR); } + else el.disabled = false; + return; + } + if ((el = hit("data-sharescene"))) { + var sids = el.getAttribute("data-sharescene").split(","); + el.disabled = true; + r = await act("share_clips", { draft_id: CUR, shot_ids: sids, label: el.getAttribute("data-scenelabel") }); + if (r) { SHARES["scene:" + sids.join(",")] = r.url; showMsg("msg", r.clips + " clips, played in order: " + r.url, "ok"); openTab(CUR); } + else el.disabled = false; + return; + } if ((el = hit("data-bgedit"))) { BGEDIT = el.getAttribute("data-bgedit"); openTab(CUR); return; } if ((el = hit("data-bgcancel"))) { BGEDIT = null; openTab(CUR); return; } if ((el = hit("data-bggen"))) {