160 lines
7.3 KiB
HTML
160 lines
7.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>My videos — PayToLaugh</title>
|
|
<meta name="description" content="Everything you have submitted, and every video that has come back.">
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
|
|
<header class="page">
|
|
<h1>My videos</h1>
|
|
<p>Everything you have submitted. Most recently worked on first.</p>
|
|
</header>
|
|
|
|
<section>
|
|
<div class="msg" id="msg"></div>
|
|
<div id="out"><p style="color:var(--muted)">Loading…</p></div>
|
|
</section>
|
|
|
|
<footer>PayToLaugh · your pet has opinions.</footer>
|
|
</div>
|
|
|
|
<script src="/assets/app.js"></script>
|
|
<script>
|
|
navBar("videos");
|
|
var email = requireSession();
|
|
|
|
var LABEL = {
|
|
submitted: ["submitted", "In the queue"],
|
|
scripting: ["submitted", "Writing the script"],
|
|
script_ready: ["ready", "Script ready to review"],
|
|
script_failed:["failed", "Script did not write"],
|
|
approved: ["ready", "Approved, waiting to film"],
|
|
rendering: ["submitted", "Filming"],
|
|
ready: ["ready", "Ready"],
|
|
failed: ["failed", "Did not work"]
|
|
};
|
|
|
|
// when() lives in app.js and includes the time. A local copy here used to
|
|
// shadow it and print the date alone, which makes several videos made on one
|
|
// day impossible to tell apart.
|
|
|
|
function card(p) {
|
|
var st = LABEL[p.status] || ["submitted", p.status || "In the queue"];
|
|
var media = p.video_url
|
|
? '<video controls playsinline preload="metadata" src="' + esc(p.video_url) + '"></video>'
|
|
: '<div class="ph">Not filmed yet</div>';
|
|
|
|
var cta = "";
|
|
if (p.status === "script_ready") {
|
|
cta = '<a class="btn" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
|
|
+ encodeURIComponent(p.id) + '">Review the script</a>';
|
|
} else if (p.status === "scripting") {
|
|
cta = '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
|
|
+ encodeURIComponent(p.id) + '">Watch it being written</a>';
|
|
} else if (p.status === "approved" || p.status === "script_failed") {
|
|
cta = '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
|
|
+ encodeURIComponent(p.id) + '">See the script</a>';
|
|
} else if (p.status === "draft") {
|
|
cta = '<a class="btn" style="display:inline-block;text-decoration:none;margin-top:10px" href="/create.html?id='
|
|
+ encodeURIComponent(p.id) + '">Finish setting it up</a>';
|
|
}
|
|
if (p.video_url) {
|
|
// A finished video was a dead end here: watch or download, and no way back
|
|
// into the shots, the cutaways or the cut without knowing the URL.
|
|
cta += '<button type="button" class="btn" style="margin-top:10px;margin-left:8px" '
|
|
+ 'data-share="' + encodeURIComponent(p.id) + '">Share</button>';
|
|
cta += '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px;margin-left:8px" href="/script.html?id='
|
|
+ encodeURIComponent(p.id) + '">Edit</a>';
|
|
cta += '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px;margin-left:8px" href="'
|
|
+ esc(dlUrl(p.video_url)) + '" download>Download</a>';
|
|
}
|
|
|
|
// `download` on a cross-origin link is ignored by the browser, and the
|
|
// storage URL is signed with an inline disposition, so a click just opened
|
|
// the video. Asking the worker for ?dl=1 makes it serve an attachment.
|
|
function dlUrl(u) {
|
|
u = String(u || "");
|
|
return /\/media\/[0-9a-fA-F-]{16,}$/.test(u) ? u + "?dl=1" : u;
|
|
}
|
|
|
|
// Through our own domain, so an open is a page load we can count. A link
|
|
// straight to the file is invisible to us. 33 characters against 74.
|
|
// A placeholder only; share_link returns the real address, which lives on
|
|
// its own hostname rather than on this site.
|
|
window.__shareUrl = window.__shareUrl || function (p) {
|
|
return (p.cut_url || p.video_url) ? "pending" : "";
|
|
};
|
|
|
|
return '<div class="item">'
|
|
+ '<div>' + media + '</div>'
|
|
+ '<div>'
|
|
+ '<span class="badge ' + st[0] + '">' + esc(st[1]) + '</span>'
|
|
+ '<h3>' + (p.kind === "trailer" || p.format === "trailer"
|
|
? esc(p.pet_name || "Book trailer") + ' — the trailer'
|
|
: esc(p.pet_name || "Your pet") + ' reacts') + '</h3>'
|
|
+ '<p class="meta">' + (p.source_url ? '<a href="' + esc(p.source_url) + '" rel="noopener noreferrer nofollow" target="_blank">' + esc(p.source_url) + '</a>' : "") + '</p>'
|
|
+ '<p class="meta">' + esc(p.owner_notes || "") + '</p>'
|
|
// Submitted and filmed are different dates and a re-film only moves the
|
|
// second one. Showing creation alone made a video filmed today read as
|
|
// days old and left it sitting mid-list, which looked exactly like a film
|
|
// that never ran.
|
|
+ '<p class="meta">' + ((p.segments || 4) * 8) + ' seconds · '
|
|
+ (p.price_usd != null ? "$" + Number(p.price_usd).toFixed(2) : "")
|
|
+ ' · submitted ' + esc(when(p.created_at))
|
|
+ (p.filmed_at ? ' · <b>filmed ' + esc(when(p.filmed_at)) + '</b>' : '')
|
|
+ '</p>'
|
|
+ cta
|
|
+ '</div></div>';
|
|
}
|
|
|
|
async function load() {
|
|
if (!email) return;
|
|
var r = await api("list_projects", { email: email });
|
|
var out = document.getElementById("out");
|
|
if (!r.ok) { out.innerHTML = ""; showMsg("msg", r.error || "Could not load your videos."); return; }
|
|
if (!r.projects || !r.projects.length) {
|
|
out.innerHTML = '<div class="empty"><p style="margin:0 0 16px">Nothing here yet.</p>'
|
|
+ '<a class="btn" style="display:inline-block;text-decoration:none" href="/create.html">Make your first video</a></div>';
|
|
return;
|
|
}
|
|
out.innerHTML = '<div class="list">' + r.projects.map(card).join("") + '</div>';
|
|
var byId = {};
|
|
r.projects.forEach(function (p) { byId[p.id] = p; });
|
|
[].forEach.call(out.querySelectorAll("[data-share]"), function (b) {
|
|
b.onclick = async function () {
|
|
var p = byId[decodeURIComponent(b.getAttribute("data-share"))];
|
|
if (!p) return;
|
|
var link = window.__shareUrl(p);
|
|
if (!link) { showMsg("msg", "That one has no video yet."); return; }
|
|
// Nothing to publish: the page is rendered per request. This only marks
|
|
// the video shared and returns its address.
|
|
b.disabled = true; b.textContent = "Preparing…";
|
|
var s = await api("share_link", { email: email, project_id: p.id });
|
|
b.disabled = false; b.textContent = "Share";
|
|
if (!s.ok) { showMsg("msg", s.error || "Could not prepare that link."); return; }
|
|
link = s.url || link;
|
|
var title = p.title || ((p.pet_name || "Your pet") + " has something to say");
|
|
// The native sheet is the shortest path into a text message on a phone.
|
|
if (navigator.share) {
|
|
try { await navigator.share({ title: title, text: title, url: link }); return; }
|
|
catch (e) { if (e && e.name === "AbortError") return; }
|
|
}
|
|
try {
|
|
await navigator.clipboard.writeText(link);
|
|
b.textContent = "Copied";
|
|
setTimeout(function () { b.textContent = "Share"; }, 1600);
|
|
} catch (e) { showMsg("msg", "Copy this: " + link); }
|
|
};
|
|
});
|
|
// Anything still being written will change shortly; refresh so the button appears.
|
|
if (r.projects.some(function (p) { return p.status === "scripting"; })) setTimeout(load, 6000);
|
|
}
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|