@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vidfarm-media/SKILL.md +43 -6
- package/SKILL.director.md +37 -23
- package/SKILL.platform.md +6 -6
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1832 -213
- package/dist/src/cli.js +238 -17
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +116 -73
- package/dist/src/devcli/composition-edit.js +262 -0
- package/dist/src/devcli/timeline-edit.js +283 -0
- package/dist/src/editor-chat.js +29 -6
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +744 -0
- package/dist/src/frontend/template-editor-chat.js +22 -19
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +25 -1
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +503 -187
- package/dist/src/reskin/chat-page.js +739 -299
- package/dist/src/reskin/discover-page.js +1450 -279
- package/dist/src/reskin/document.js +1392 -16
- package/dist/src/reskin/help-page.js +139 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +547 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +1188 -317
- package/dist/src/reskin/login-page.js +124 -87
- package/dist/src/reskin/pricing-page.js +197 -166
- package/dist/src/reskin/settings-page.js +566 -187
- package/dist/src/reskin/theme.js +434 -17
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +79 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +14 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +114 -0
- package/dist/src/services/storage.js +24 -1
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,217 +1,307 @@
|
|
|
1
|
-
// /reskin/library — the director's own studio,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// /reskin/library/approved → your
|
|
5
|
-
// /reskin/library/clips →
|
|
1
|
+
// /reskin/library — the director's own studio, now MIGRATED to real production
|
|
2
|
+
// data (mirrors src/reskin/settings-page.ts). This is ONE page with two
|
|
3
|
+
// server-routed tabs at distinct URLs:
|
|
4
|
+
// /reskin/library/approved → your REAL published cuts (ready + scheduled)
|
|
5
|
+
// /reskin/library/clips → your REAL mined clip library (client-fetched)
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// the real
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// The renderer accepts the SAME input object the live `/library` route already
|
|
8
|
+
// builds — `Parameters<typeof renderLibraryPage>[0]` from ../account-pages.js —
|
|
9
|
+
// so wiring the route is just "fetch real data + call this". Every action points
|
|
10
|
+
// at a real endpoint:
|
|
11
|
+
// • Open → /editor/:templateId (the real editor, account-scoped)
|
|
12
|
+
// • View post → post.share_url (the real share/preview page)
|
|
13
|
+
// • Download → the primary video asset's real MP4 url
|
|
14
|
+
// • Schedule → /reskin/calendar (the real scheduling surface)
|
|
15
|
+
// • Clips tab → GET /clips/feed, POST /clips/search, POST /clips/scan (real)
|
|
16
|
+
//
|
|
17
|
+
// Follow-up: the live library ships an editorChat / chat-dock runtime
|
|
18
|
+
// (buildLibraryEditorChatBoot → editorChat). That is intentionally OMITTED here
|
|
19
|
+
// for now — this pass focuses on real projects + real clip search. `editorChat`
|
|
20
|
+
// is accepted in the input but not yet rendered.
|
|
21
|
+
//
|
|
22
|
+
// Design stays farmville (shared rk-* primitives + a page-scoped rk-library-* /
|
|
23
|
+
// rk-clips-* CSS block); only the data + actions became real.
|
|
12
24
|
import { reskinDocument, rkEscape } from "./document.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const GRAD = {
|
|
26
|
-
honey: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.6),transparent 55%),linear-gradient(158deg,#ffe9cc,#ffc738 96%)",
|
|
27
|
-
coral: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.5),transparent 55%),linear-gradient(158deg,#ffe1c2,#f97316 98%)",
|
|
28
|
-
sky: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.58),transparent 55%),linear-gradient(158deg,#e2effd,#3f9fe0 98%)",
|
|
29
|
-
violet: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.5),transparent 55%),linear-gradient(158deg,#ece4fd,#9f5bde 98%)",
|
|
30
|
-
green: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.5),transparent 55%),linear-gradient(158deg,#dcfce7,#22c55e 98%)",
|
|
31
|
-
ink: "radial-gradient(120% 78% at 22% 4%,rgba(255,255,255,.18),transparent 55%),linear-gradient(158deg,#3f3f46,#171717 98%)"
|
|
25
|
+
import { jobRunsPanel, JOBRUNS_PAGE_CSS, JOBRUNS_SCRIPT } from "./job-runs-page.js";
|
|
26
|
+
// A safe default so the current route (`renderReskinLibrary("approved")`, no
|
|
27
|
+
// input) keeps working until app.ts is updated to pass real data (mirror the
|
|
28
|
+
// live `/library` route). Once wired, the parent passes the real object.
|
|
29
|
+
const EMPTY_LIBRARY_INPUT = {
|
|
30
|
+
userId: "",
|
|
31
|
+
currentAccountId: "",
|
|
32
|
+
name: null,
|
|
33
|
+
email: "",
|
|
34
|
+
posts: [],
|
|
35
|
+
schedule: { channels: [], connectHref: "/settings?tab=channels&connect=flockposter" },
|
|
36
|
+
editorChat: null
|
|
32
37
|
};
|
|
38
|
+
const PLAY = `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13l11-6.5-11-6.5Z"/></svg>`;
|
|
39
|
+
const SEARCH_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
|
|
40
|
+
// Compact per-card action icons + viewer-modal chrome (ported from the /discover
|
|
41
|
+
// reskin TikTok viewer so the library modal reads identical).
|
|
42
|
+
const ICON_EDIT = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>`;
|
|
43
|
+
const ICON_SHARE = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>`;
|
|
44
|
+
const ICON_DL = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg>`;
|
|
45
|
+
const CROSS_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>`;
|
|
46
|
+
const SOUND_ON_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M18.5 5.5a9 9 0 0 1 0 13"/></svg>`;
|
|
47
|
+
const SOUND_OFF_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="m23 9-6 6"/><path d="m17 9 6 6"/></svg>`;
|
|
48
|
+
const CHEV_UP_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 15 6-6 6 6"/></svg>`;
|
|
49
|
+
const CHEV_DOWN_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`;
|
|
50
|
+
const ICON_MORE = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.9"/><circle cx="12" cy="12" r="1.9"/><circle cx="12" cy="19" r="1.9"/></svg>`;
|
|
51
|
+
const ICON_COPY = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>`;
|
|
52
|
+
const ICON_TRASH = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg>`;
|
|
53
|
+
const ICON_FOLDER = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z"/></svg>`;
|
|
54
|
+
const ICON_CHEVLEFT = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>`;
|
|
55
|
+
const ICON_LOGS = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01M3 12h.01M3 18h.01"/></svg>`;
|
|
56
|
+
// Logs is no longer a primary tab — it's a right-floated ghost button that rides
|
|
57
|
+
// the topbar (to the right of the tab's quick-search). Shared by the Approved,
|
|
58
|
+
// Raws and Files topbars so the row reads identically everywhere.
|
|
59
|
+
const LOGS_BTN = `<a class="rk-btn rk-btn-ghost rk-btn-sm rk-library-logs-btn" href="/library/logs">${ICON_LOGS}<span>Logs</span></a>`;
|
|
60
|
+
/* ──────────────────────────── Approved (projects) tab ─────────────────────── */
|
|
33
61
|
const STATUS = {
|
|
34
|
-
draft: { short: "Draft", pill: "rk-pill" },
|
|
35
|
-
rendering: { short: "Rendering", pill: "rk-pill-sky" },
|
|
36
62
|
ready: { short: "Ready", pill: "rk-pill-green" },
|
|
37
|
-
scheduled: { short: "Scheduled", pill: "rk-pill-violet" }
|
|
38
|
-
posted: { short: "Posted", pill: "rk-pill-gold" }
|
|
63
|
+
scheduled: { short: "Scheduled", pill: "rk-pill-violet" }
|
|
39
64
|
};
|
|
40
|
-
|
|
41
|
-
["", "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const PLAY = `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13l11-6.5-11-6.5Z"/></svg>`;
|
|
49
|
-
function count(status) {
|
|
50
|
-
return status === "" ? PROJECTS.length : PROJECTS.filter((p) => p.status === status).length;
|
|
65
|
+
function statusInfo(status) {
|
|
66
|
+
return STATUS[status] || { short: status ? status.charAt(0).toUpperCase() + status.slice(1) : "Ready", pill: "rk-pill" };
|
|
67
|
+
}
|
|
68
|
+
function fmtDate(iso) {
|
|
69
|
+
const t = Date.parse(iso);
|
|
70
|
+
if (Number.isNaN(t))
|
|
71
|
+
return iso || "";
|
|
72
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
51
73
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
function isVideoAsset(a) {
|
|
75
|
+
return a.kind === "video" || String(a.content_type || "").startsWith("video");
|
|
76
|
+
}
|
|
77
|
+
function isImageAsset(a) {
|
|
78
|
+
return a.kind === "image" || String(a.content_type || "").startsWith("image");
|
|
79
|
+
}
|
|
80
|
+
// primaryAsset mirrors the live library: role="primary" → first video → media[0].
|
|
81
|
+
function primaryAsset(post) {
|
|
82
|
+
return post.media.find((a) => a.role === "primary")
|
|
83
|
+
|| post.media.find((a) => isVideoAsset(a))
|
|
84
|
+
|| post.media[0]
|
|
85
|
+
|| null;
|
|
86
|
+
}
|
|
87
|
+
function downloadAsset(post) {
|
|
88
|
+
const v = post.media.find((a) => isVideoAsset(a));
|
|
89
|
+
return v || primaryAsset(post);
|
|
90
|
+
}
|
|
91
|
+
// "View post" now always opens the account-scoped public post page in a new
|
|
92
|
+
// tab: /u/:accountId/approved/posts/:postId (falls back to the un-scoped path or
|
|
93
|
+
// the stored share_url when the account id is unknown).
|
|
94
|
+
function shareHref(post, accountId) {
|
|
95
|
+
if (accountId) {
|
|
96
|
+
return `/u/${encodeURIComponent(accountId)}/approved/posts/${encodeURIComponent(post.post_id)}`;
|
|
64
97
|
}
|
|
98
|
+
return post.share_url && post.share_url.length
|
|
99
|
+
? post.share_url
|
|
100
|
+
: `/approved/posts/${encodeURIComponent(post.post_id)}`;
|
|
65
101
|
}
|
|
66
|
-
function
|
|
67
|
-
if (
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
function editorHref(post, accountId) {
|
|
103
|
+
if (!post.template_id)
|
|
104
|
+
return null;
|
|
105
|
+
const q = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
|
|
106
|
+
return `/editor/${encodeURIComponent(post.template_id)}${q}`;
|
|
107
|
+
}
|
|
108
|
+
function posterMedia(post) {
|
|
109
|
+
const asset = post.media.find((a) => isVideoAsset(a)) || post.media.find((a) => isImageAsset(a)) || primaryAsset(post);
|
|
110
|
+
if (asset && isVideoAsset(asset)) {
|
|
111
|
+
return `<video class="rk-library-video" muted loop playsinline preload="metadata" src="${rkEscape(asset.url)}#t=0.1"></video>`;
|
|
71
112
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
if (asset && isImageAsset(asset)) {
|
|
114
|
+
return `<img class="rk-library-video" loading="lazy" src="${rkEscape(asset.url)}" alt="${rkEscape(post.title || "Published render")}" />`;
|
|
115
|
+
}
|
|
116
|
+
return `<span class="rk-library-noprev">No preview</span>`;
|
|
75
117
|
}
|
|
76
|
-
function projectCard(
|
|
77
|
-
const s =
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
?
|
|
118
|
+
function projectCard(post, accountId, index) {
|
|
119
|
+
const s = statusInfo(post.status);
|
|
120
|
+
const title = post.title || "Published render";
|
|
121
|
+
const subLine = post.tracer
|
|
122
|
+
? post.tracer
|
|
123
|
+
: post.template_id
|
|
124
|
+
? `Composition ${post.template_id}`
|
|
125
|
+
: "Ready-to-post cut";
|
|
126
|
+
const searchHay = [title, post.caption, post.tracer, post.template_id].filter(Boolean).join(" ").toLowerCase();
|
|
127
|
+
const editHref = editorHref(post, accountId);
|
|
128
|
+
// Compact one-row action bar: "Share Post" is the labelled primary (opens the
|
|
129
|
+
// approved-post preview page in a new tab); "Edit" is the labelled secondary
|
|
130
|
+
// (opens the composition in the editor in a new tab). A right-floated "⋯"
|
|
131
|
+
// overflow menu carries Copy ID + Delete (with a popconfirm).
|
|
132
|
+
const actions = [
|
|
133
|
+
`<a class="rk-btn rk-btn-gold rk-library-act" href="${rkEscape(shareHref(post, accountId))}" target="_blank" rel="noreferrer">Share Post</a>`,
|
|
134
|
+
editHref ? `<a class="rk-btn rk-btn-ghost rk-library-act" href="${rkEscape(editHref)}" target="_blank" rel="noreferrer" title="Open in editor">Edit</a>` : ""
|
|
135
|
+
].filter(Boolean).join("");
|
|
136
|
+
// Delete targets the real account-scoped DELETE route; only wired when we know
|
|
137
|
+
// the owning account (always true for the authed library).
|
|
138
|
+
const delUrl = accountId
|
|
139
|
+
? `/u/${encodeURIComponent(accountId)}/approved/posts/${encodeURIComponent(post.post_id)}`
|
|
81
140
|
: "";
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
const menu = `<div class="rk-library-menu" data-rk-menu>
|
|
142
|
+
<button type="button" class="rk-library-kebab" data-rk-menu-btn aria-label="More actions" aria-haspopup="true" aria-expanded="false">${ICON_MORE}</button>
|
|
143
|
+
<div class="rk-library-menu-pop" data-rk-menu-pop hidden role="menu">
|
|
144
|
+
<button type="button" class="rk-library-menu-item" data-rk-copy-id="${rkEscape(post.post_id)}" role="menuitem">${ICON_COPY}<span>Copy ID</span></button>
|
|
145
|
+
<button type="button" class="rk-library-menu-item is-danger" data-rk-del="${rkEscape(delUrl)}" role="menuitem">${ICON_TRASH}<span>Delete</span></button>
|
|
146
|
+
<div class="rk-library-menu-confirm" data-rk-confirm hidden>
|
|
147
|
+
<p class="rk-library-menu-confirm-q">Delete this post?</p>
|
|
148
|
+
<div class="rk-library-menu-confirm-row">
|
|
149
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-confirm-no>Cancel</button>
|
|
150
|
+
<button type="button" class="rk-btn rk-btn-danger rk-btn-sm" data-rk-confirm-yes>Delete</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>`;
|
|
155
|
+
return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}" data-post-id="${rkEscape(post.post_id)}"
|
|
156
|
+
<button type="button" class="rk-library-poster" data-rk-view="${index}" aria-label="Play ${rkEscape(title)}">
|
|
157
|
+
${posterMedia(post)}
|
|
158
|
+
<span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
|
|
159
|
+
<span class="rk-library-dur">9:16</span>
|
|
87
160
|
<span class="rk-library-play">${PLAY}</span>
|
|
88
|
-
|
|
89
|
-
</a>
|
|
161
|
+
</button>
|
|
90
162
|
<div class="rk-library-meta">
|
|
91
|
-
<h3 class="rk-library-title">${rkEscape(
|
|
92
|
-
|
|
93
|
-
<div class="rk-
|
|
94
|
-
<span class="rk-library-time">${rkEscape(p.when)}</span>
|
|
95
|
-
${cardAction(p)}
|
|
96
|
-
</div>
|
|
163
|
+
<h3 class="rk-library-title">${rkEscape(title)}</h3>
|
|
164
|
+
<div class="rk-library-origin"><span class="rk-library-time">${rkEscape(fmtDate(post.created_at))}</span><span class="rk-library-origin-sep">·</span><span class="rk-library-origin-txt">${rkEscape(subLine)}</span></div>
|
|
165
|
+
<div class="rk-library-actions">${actions}${menu}</div>
|
|
97
166
|
</div>
|
|
98
167
|
</article>`;
|
|
99
168
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
169
|
+
function approvedPanel(input, tabs) {
|
|
170
|
+
const posts = input.posts;
|
|
171
|
+
const accountId = input.currentAccountId;
|
|
172
|
+
// The quickfilter searchbar mirrors the Clips tab's pill search — always
|
|
173
|
+
// rendered so the topbar reads the same on both tabs (disabled when empty).
|
|
174
|
+
const qCount = posts.length;
|
|
175
|
+
const searchBar = `<label class="rk-clips-search rk-library-qsearch">
|
|
176
|
+
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
177
|
+
<input class="rk-input rk-clips-search-input" data-rk-search type="search"
|
|
178
|
+
placeholder="Search ${qCount} approved post${qCount === 1 ? "" : "s"}…" autocomplete="off" aria-label="Search approved posts"${posts.length ? "" : " disabled"} />
|
|
179
|
+
</label>`;
|
|
180
|
+
if (posts.length === 0) {
|
|
181
|
+
return `
|
|
182
|
+
<div class="rk-library-topbar">
|
|
183
|
+
${tabs}
|
|
184
|
+
${searchBar}
|
|
185
|
+
${LOGS_BTN}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="rk-library-empty is-visible">
|
|
189
|
+
<h3>No published cuts yet</h3>
|
|
190
|
+
<p class="rk-muted">Approve a finished render and it lands here — ready to schedule, share, or download. Start from a template or a prompt.</p>
|
|
191
|
+
<a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
|
|
107
192
|
</div>`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
193
|
+
}
|
|
194
|
+
const cards = posts.map((p, i) => projectCard(p, accountId, i)).join("");
|
|
195
|
+
// Data the client viewer needs to build the vertical scroll deck (order MUST
|
|
196
|
+
// match the cards' data-rk-view index). Videos play in the modal; image-only
|
|
197
|
+
// posts show their poster still.
|
|
198
|
+
const viewerData = posts.map((p) => {
|
|
199
|
+
const asset = primaryAsset(p);
|
|
200
|
+
const isVideo = Boolean(asset && isVideoAsset(asset));
|
|
201
|
+
const poster = p.media.find((a) => isImageAsset(a));
|
|
202
|
+
return {
|
|
203
|
+
title: p.title || "Published render",
|
|
204
|
+
sub: p.tracer || (p.template_id ? `Composition ${p.template_id}` : "Ready-to-post cut"),
|
|
205
|
+
url: asset ? asset.url : "",
|
|
206
|
+
is_video: isVideo,
|
|
207
|
+
poster: poster ? poster.url : "",
|
|
208
|
+
post_href: shareHref(p, accountId),
|
|
209
|
+
editor_href: editorHref(p, accountId)
|
|
210
|
+
};
|
|
211
|
+
});
|
|
119
212
|
return `
|
|
120
|
-
<div class="rk-library-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<input class="rk-input rk-library-search" data-rk-search type="search" placeholder="Search projects…" autocomplete="off" aria-label="Search projects">
|
|
213
|
+
<div class="rk-library-topbar">
|
|
214
|
+
${tabs}
|
|
215
|
+
${searchBar}
|
|
216
|
+
${LOGS_BTN}
|
|
125
217
|
</div>
|
|
126
218
|
|
|
127
219
|
<div class="rk-library-grid" data-rk-grid>
|
|
128
|
-
${newTile}
|
|
129
220
|
${cards}
|
|
130
221
|
</div>
|
|
131
222
|
|
|
132
223
|
<div class="rk-library-empty" data-rk-empty>
|
|
133
224
|
<h3>Nothing here yet</h3>
|
|
134
|
-
<p class="rk-muted">No
|
|
135
|
-
<a class="rk-btn rk-btn-gold" href="/
|
|
136
|
-
</div
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
filters: [
|
|
141
|
-
{ slug: "all", label: "All clips" },
|
|
142
|
-
{ slug: "reactions", label: "Reactions" },
|
|
143
|
-
{ slug: "hooks", label: "Hooks & hot takes" },
|
|
144
|
-
{ slug: "money", label: "Money shots" },
|
|
145
|
-
{ slug: "talking", label: "Talking head" },
|
|
146
|
-
{ slug: "broll", label: "B-roll" },
|
|
147
|
-
{ slug: "establishing", label: "Establishing" }
|
|
148
|
-
],
|
|
149
|
-
clips: [
|
|
150
|
-
{ title: "Guest freezes mid-sentence after the host's cold-read stat", durationSec: 18, aspect: "9:16", score: 94, source: "The Founder Hour — Ep. 212", tags: ["reaction", "close-up", "high energy"], glyph: "😮", tone: "gold", cat: "reactions" },
|
|
151
|
-
{ title: "“That number can't be real” — investor's slow double-take", durationSec: 19, aspect: "9:16", score: 93, source: "Contrarian Capital — Ep. 11", tags: ["reaction", "surprise", "close-up"], glyph: "😳", tone: "coral", cat: "reactions" },
|
|
152
|
-
{ title: "The “nobody talks about this” hook that opened the episode", durationSec: 22, aspect: "9:16", score: 96, source: "Contrarian Capital — Ep. 09", tags: ["hook", "hot take", "talking"], glyph: "🎤", tone: "ink", cat: "hooks" },
|
|
153
|
-
{ title: "Hands counting a stack of hundreds on the desk", durationSec: 15, aspect: "9:16", score: 88, source: "Cashflow Diaries — Ep. 17", tags: ["money", "hands", "macro"], glyph: "💵", tone: "green", cat: "money" },
|
|
154
|
-
{ title: "Founder tears up recalling the very first sale", durationSec: 31, aspect: "9:16", score: 90, source: "The Founder Hour — Ep. 205", tags: ["emotion", "close-up", "calm"], glyph: "🥹", tone: "violet", cat: "talking" },
|
|
155
|
-
{ title: "Slow push-in on the neon “SOLD OUT” window sign", durationSec: 12, aspect: "16:9", score: 78, source: "Retail Nerds — Ep. 40", tags: ["b-roll", "establishing", "calm"], glyph: "🪧", tone: "sky", cat: "broll" },
|
|
156
|
-
{ title: "City skyline timelapse at golden hour", durationSec: 14, aspect: "16:9", score: 74, source: "Built Different — Ep. 51", tags: ["establishing", "b-roll", "wide"], glyph: "🌇", tone: "coral", cat: "establishing" },
|
|
157
|
-
{ title: "Whiteboard reveal: the four-step funnel, drawn live", durationSec: 27, aspect: "16:9", score: 82, source: "Growth Loops — Ep. 63", tags: ["explainer", "talking", "medium"], glyph: "📊", tone: "sky", cat: "talking" },
|
|
158
|
-
{ title: "Barista latte-art pour in extreme slow-mo", durationSec: 16, aspect: "9:16", score: 80, source: "Small Shop Big Dreams — Ep. 30", tags: ["b-roll", "macro", "calm"], glyph: "☕", tone: "gold", cat: "broll" },
|
|
159
|
-
{ title: "Coins dropping into a glass jar, tight macro", durationSec: 13, aspect: "9:16", score: 85, source: "Cashflow Diaries — Ep. 21", tags: ["money", "macro", "sfx"], glyph: "🪙", tone: "green", cat: "money" },
|
|
160
|
-
{ title: "He didn't realize the camera was still rolling", durationSec: 24, aspect: "9:16", score: 91, source: "Lightbulb Pod — Ep. 88", tags: ["reaction", "candid", "high energy"], glyph: "😅", tone: "violet", cat: "reactions" },
|
|
161
|
-
{ title: "The mic-drop closing line, right before the outro", durationSec: 21, aspect: "16:9", score: 89, source: "Lightbulb Pod — Ep. 90", tags: ["hook", "hot take", "talking"], glyph: "🎙", tone: "ink", cat: "hooks" }
|
|
162
|
-
]
|
|
163
|
-
};
|
|
164
|
-
const SEARCH_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
|
|
165
|
-
function fmtClock(sec) {
|
|
166
|
-
const m = Math.floor(sec / 60);
|
|
167
|
-
const s = Math.floor(sec % 60);
|
|
168
|
-
return `${m}:${s < 10 ? "0" : ""}${s}`;
|
|
225
|
+
<p class="rk-muted">No cuts match your search. Try a different term or spin up a fresh render.</p>
|
|
226
|
+
<a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<script type="application/json" data-rk-library-data>${JSON.stringify(viewerData).replace(/</g, "\\u003c")}</script>
|
|
230
|
+
${LIBRARY_VIEWER_HTML}`;
|
|
169
231
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
<div class="rk-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
<button type="button" class="rk-
|
|
232
|
+
// Fullscreen TikTok-style viewer shell — ported from the /discover reskin so the
|
|
233
|
+
// library modal reads identically. The client fills [data-viewer-track] on open.
|
|
234
|
+
const LIBRARY_VIEWER_HTML = `
|
|
235
|
+
<div class="rk-library-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Published cut viewer">
|
|
236
|
+
<div class="rk-library-viewer-scrim" data-viewer-close></div>
|
|
237
|
+
<div class="rk-library-viewer-track" data-viewer-track></div>
|
|
238
|
+
<div class="rk-library-viewer-ui">
|
|
239
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-mute aria-label="Toggle sound"><span class="rk-lvm-on">${SOUND_ON_ICON}</span><span class="rk-lvm-off">${SOUND_OFF_ICON}</span></button>
|
|
240
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
178
241
|
</div>
|
|
179
|
-
<div class="rk-
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
<div class="rk-clips-foot">
|
|
183
|
-
<span class="rk-clips-src" title="${rkEscape(clip.source)}">${rkEscape(clip.source)}</span>
|
|
184
|
-
<a class="rk-clips-dl" href="#">Download</a>
|
|
185
|
-
</div>
|
|
242
|
+
<div class="rk-library-viewer-nav">
|
|
243
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-prev aria-label="Previous cut">${CHEV_UP_ICON}</button>
|
|
244
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-next aria-label="Next cut">${CHEV_DOWN_ICON}</button>
|
|
186
245
|
</div>
|
|
187
|
-
</
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
246
|
+
</div>`;
|
|
247
|
+
/* ──────────────────────────────── Clips tab ───────────────────────────────── */
|
|
248
|
+
// The clip library is NOT part of the library route's input — the client fetches
|
|
249
|
+
// the REAL endpoints on load: GET /clips/feed (library), POST /clips/search
|
|
250
|
+
// (hybrid semantic search), POST /clips/scan + GET /clips/scan/:id (mine a
|
|
251
|
+
// source), download via /clips/:clipId/download.
|
|
252
|
+
function clipsPanel(tabs) {
|
|
253
|
+
// Raws render as a file-directory explorer: raws are foldered by their source
|
|
254
|
+
// video (source_video_id / source_filename). The root shows folders; opening a
|
|
255
|
+
// folder shows its raws as big scannable thumbnails; typing a query flips to a
|
|
256
|
+
// flat semantic-search results view. The clip count lives in the search
|
|
257
|
+
// placeholder (kept in sync by the client) — no separate summary/count row.
|
|
193
258
|
return `
|
|
194
|
-
<div class="rk-library-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
<div class="rk-clips-
|
|
207
|
-
<div class="rk-clips-status" id="rkClipsStatus">Showing all ${s.clips.length} clips from ${s.stats.sources} sources</div>
|
|
259
|
+
<div class="rk-library-topbar">
|
|
260
|
+
${tabs}
|
|
261
|
+
<form class="rk-clips-search rk-library-qsearch" id="rkClipsSearch">
|
|
262
|
+
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
263
|
+
<input class="rk-input rk-clips-search-input" id="rkClipsQuery" type="search"
|
|
264
|
+
placeholder="Search raw clips…" autocomplete="off" aria-label="Search raw clips" />
|
|
265
|
+
</form>
|
|
266
|
+
${LOGS_BTN}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div class="rk-raws-bar">
|
|
270
|
+
<nav class="rk-raws-crumbs" id="rkRawsCrumbs" aria-label="Breadcrumb"></nav>
|
|
271
|
+
<div class="rk-clips-status" id="rkClipsStatus"></div>
|
|
208
272
|
</div>
|
|
209
273
|
|
|
210
|
-
<div class="rk-
|
|
274
|
+
<div class="rk-raws-explorer" id="rkClipsGrid"></div>
|
|
211
275
|
|
|
212
276
|
<div class="rk-clips-empty" id="rkClipsEmpty" hidden>
|
|
213
|
-
<h3>No
|
|
214
|
-
<p class="rk-muted">
|
|
277
|
+
<h3>No raws yet</h3>
|
|
278
|
+
<p class="rk-muted">Click <b>Import source</b> to mine a long video into tagged, searchable short clips.</p>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="rk-raws-hover" id="rkRawsHover" hidden aria-hidden="true">
|
|
282
|
+
<video class="rk-raws-hover-vid" muted loop playsinline preload="none"></video>
|
|
283
|
+
<div class="rk-raws-hover-cap" id="rkRawsHoverCap"></div>
|
|
284
|
+
</div>
|
|
285
|
+
${LIBRARY_VIEWER_HTML}`;
|
|
286
|
+
}
|
|
287
|
+
/* ──────────────────────────────── Files tab ───────────────────────────────── */
|
|
288
|
+
// The Files tab is the full-page mount of the reusable file-directory explorer
|
|
289
|
+
// (src/frontend/file-directory.ts). It opens at the virtual root so the three
|
|
290
|
+
// backends read as folders — Files / Temp / Raws — with the component's own
|
|
291
|
+
// unified search (semantic vector + substring + absolute-path, mode=auto),
|
|
292
|
+
// breadcrumb navigation, multi-select, and drag-drop upload. The bundle
|
|
293
|
+
// (/assets/file-directory-app.js) already ships on every full-chrome reskin
|
|
294
|
+
// page and auto-mounts any `[data-rk-directory]`, so no page script is needed;
|
|
295
|
+
// preview routes through the chrome's `window.__rkDirectoryHost.onPreview`.
|
|
296
|
+
function filesPanel(tabs) {
|
|
297
|
+
return `
|
|
298
|
+
<div class="rk-library-topbar">
|
|
299
|
+
${tabs}
|
|
300
|
+
${LOGS_BTN}
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div class="rk-files-explorer">
|
|
304
|
+
<div data-rk-directory data-mode="standalone" data-show-upload="true" data-show-multiselect="true" data-primary-click="preview"></div>
|
|
215
305
|
</div>`;
|
|
216
306
|
}
|
|
217
307
|
const CLIPS_DIALOG = `
|
|
@@ -219,19 +309,27 @@ const CLIPS_DIALOG = `
|
|
|
219
309
|
<form class="rk-clips-dialog-form" id="rkClipsScanForm" method="dialog">
|
|
220
310
|
<div class="rk-card-head">
|
|
221
311
|
<span class="rk-eyebrow">Import source</span>
|
|
222
|
-
<h2>
|
|
223
|
-
<p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll
|
|
312
|
+
<h2>Import a video</h2>
|
|
313
|
+
<p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll add it to your raws library.</p>
|
|
224
314
|
</div>
|
|
225
315
|
<div class="rk-field">
|
|
226
316
|
<label class="rk-label" for="rkScanUrl">Video URL</label>
|
|
227
317
|
<input class="rk-input" id="rkScanUrl" name="url" type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" />
|
|
228
318
|
</div>
|
|
229
319
|
<div class="rk-field">
|
|
230
|
-
<label class="rk-label" for="
|
|
231
|
-
<
|
|
232
|
-
<span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
|
|
320
|
+
<label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
|
|
321
|
+
<input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
|
|
233
322
|
</div>
|
|
234
|
-
<
|
|
323
|
+
<label class="rk-clips-toggle" for="rkScanClip">
|
|
324
|
+
<input type="checkbox" id="rkScanClip" name="clip" />
|
|
325
|
+
<span><b>Clip raws</b> — mine this video into matching moments with AI, instead of importing the whole thing.</span>
|
|
326
|
+
</label>
|
|
327
|
+
<div class="rk-clips-advanced" id="rkClipsAdvanced" hidden>
|
|
328
|
+
<div class="rk-field">
|
|
329
|
+
<label class="rk-label" for="rkScanPrompt">What to clip for</label>
|
|
330
|
+
<textarea class="rk-textarea" id="rkScanPrompt" name="prompt" placeholder="Clip scenes of people reacting, no text on screen, vertical, between 5–10 secs long"></textarea>
|
|
331
|
+
<span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
|
|
332
|
+
</div>
|
|
235
333
|
<div class="rk-field">
|
|
236
334
|
<label class="rk-label" for="rkScanProvider">AI provider</label>
|
|
237
335
|
<select class="rk-select" id="rkScanProvider" name="provider">
|
|
@@ -241,15 +339,11 @@ const CLIPS_DIALOG = `
|
|
|
241
339
|
<option value="openrouter">OpenRouter</option>
|
|
242
340
|
</select>
|
|
243
341
|
</div>
|
|
244
|
-
<div class="rk-field">
|
|
245
|
-
<label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
|
|
246
|
-
<input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
|
|
247
|
-
</div>
|
|
248
342
|
</div>
|
|
249
|
-
<p class="rk-hint rk-clips-dialog-note">
|
|
343
|
+
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">We'll pull the whole video into your raws library.</p>
|
|
250
344
|
<div class="rk-clips-dialog-actions">
|
|
251
345
|
<button type="button" class="rk-btn rk-btn-ghost" id="rkClipsScanCancel">Cancel</button>
|
|
252
|
-
<button type="submit" class="rk-btn rk-btn-gold">
|
|
346
|
+
<button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Import video <span class="rk-arrow">→</span></button>
|
|
253
347
|
</div>
|
|
254
348
|
</form>
|
|
255
349
|
</dialog>`;
|
|
@@ -257,93 +351,653 @@ const CLIPS_DIALOG = `
|
|
|
257
351
|
const APPROVED_SCRIPT = `
|
|
258
352
|
(function(){
|
|
259
353
|
var root=document;
|
|
260
|
-
var grid=root.querySelector('[data-rk-grid]');
|
|
261
354
|
var empty=root.querySelector('[data-rk-empty]');
|
|
262
|
-
var chips=Array.prototype.slice.call(root.querySelectorAll('[data-rk-filter]'));
|
|
263
|
-
var newTile=root.querySelector('[data-rk-newtile]');
|
|
264
355
|
var search=root.querySelector('[data-rk-search]');
|
|
356
|
+
var grid=root.querySelector('[data-rk-grid]');
|
|
265
357
|
var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
|
|
266
|
-
var
|
|
358
|
+
var total=cards.length;
|
|
359
|
+
function syncPlaceholder(){
|
|
360
|
+
if(!search)return;
|
|
361
|
+
search.setAttribute('placeholder','Search '+total+' approved post'+(total===1?'':'s')+String.fromCharCode(8230));
|
|
362
|
+
}
|
|
267
363
|
function apply(){
|
|
268
364
|
var q=(search&&search.value||'').trim().toLowerCase();
|
|
269
365
|
var shown=0;
|
|
270
366
|
cards.forEach(function(card){
|
|
271
|
-
|
|
272
|
-
var
|
|
273
|
-
var visible=okStatus&&okSearch;
|
|
367
|
+
if(card.getAttribute('data-removed'))return;
|
|
368
|
+
var visible=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
|
|
274
369
|
card.hidden=!visible;
|
|
275
370
|
if(visible)shown++;
|
|
276
371
|
});
|
|
277
|
-
if(newTile)newTile.hidden=!(activeFilter===''&&!q);
|
|
278
372
|
if(empty)empty.classList.toggle('is-visible',shown===0);
|
|
279
373
|
}
|
|
280
|
-
chips.forEach(function(btn){
|
|
281
|
-
btn.addEventListener('click',function(){
|
|
282
|
-
activeFilter=btn.getAttribute('data-rk-filter')||'';
|
|
283
|
-
chips.forEach(function(b){b.classList.toggle('is-active',b===btn)});
|
|
284
|
-
apply();
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
374
|
if(search)search.addEventListener('input',apply);
|
|
375
|
+
// hover-to-play the real MP4 previews on the grid cards
|
|
376
|
+
root.querySelectorAll('video.rk-library-video').forEach(function(v){
|
|
377
|
+
var card=v.closest('.rk-library-card');
|
|
378
|
+
if(!card)return;
|
|
379
|
+
card.addEventListener('mouseenter',function(){ if(v.play){var p=v.play(); if(p&&p.catch)p.catch(function(){});} });
|
|
380
|
+
card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
|
|
381
|
+
});
|
|
288
382
|
apply();
|
|
383
|
+
|
|
384
|
+
// ── per-card "⋯" overflow menu: Copy ID + Delete (popconfirm) ────────────
|
|
385
|
+
function closeAllMenus(except){
|
|
386
|
+
root.querySelectorAll('[data-rk-menu].is-open').forEach(function(m){
|
|
387
|
+
if(m===except)return;
|
|
388
|
+
m.classList.remove('is-open');
|
|
389
|
+
var pop=m.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=true;
|
|
390
|
+
var cf=m.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
|
|
391
|
+
var btn=m.querySelector('[data-rk-menu-btn]'); if(btn)btn.setAttribute('aria-expanded','false');
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function flashKebab(menu,text){
|
|
395
|
+
var btn=menu&&menu.querySelector('[data-rk-menu-btn]'); if(!btn)return;
|
|
396
|
+
btn.classList.add('is-ok'); var prev=btn.getAttribute('title')||'';
|
|
397
|
+
btn.setAttribute('title',text||'Done');
|
|
398
|
+
setTimeout(function(){ btn.classList.remove('is-ok'); btn.setAttribute('title',prev); },1100);
|
|
399
|
+
}
|
|
400
|
+
document.addEventListener('click',function(e){
|
|
401
|
+
var t=e.target;
|
|
402
|
+
var btn=t&&t.closest?t.closest('[data-rk-menu-btn]'):null;
|
|
403
|
+
if(btn){
|
|
404
|
+
e.preventDefault(); e.stopPropagation();
|
|
405
|
+
var menu=btn.closest('[data-rk-menu]');
|
|
406
|
+
var open=menu.classList.contains('is-open');
|
|
407
|
+
closeAllMenus(open?null:menu);
|
|
408
|
+
if(open){ menu.classList.remove('is-open'); btn.setAttribute('aria-expanded','false'); return; }
|
|
409
|
+
menu.classList.add('is-open'); btn.setAttribute('aria-expanded','true');
|
|
410
|
+
var pop=menu.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=false;
|
|
411
|
+
var cf=menu.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Copy ID
|
|
415
|
+
var copy=t&&t.closest?t.closest('[data-rk-copy-id]'):null;
|
|
416
|
+
if(copy){
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
var id=copy.getAttribute('data-rk-copy-id')||'';
|
|
419
|
+
var menuC=copy.closest('[data-rk-menu]');
|
|
420
|
+
var done=function(){ flashKebab(menuC,'Copied ID'); closeAllMenus(null); };
|
|
421
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(id).then(done,done); }
|
|
422
|
+
else { try{ var ta=document.createElement('textarea'); ta.value=id; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }catch(err){} done(); }
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Delete → reveal popconfirm
|
|
426
|
+
var del=t&&t.closest?t.closest('[data-rk-del]'):null;
|
|
427
|
+
if(del){
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
var m2=del.closest('[data-rk-menu]');
|
|
430
|
+
var cf2=m2.querySelector('[data-rk-confirm]'); if(cf2)cf2.hidden=false;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Cancel popconfirm
|
|
434
|
+
var no=t&&t.closest?t.closest('[data-rk-confirm-no]'):null;
|
|
435
|
+
if(no){ e.preventDefault(); var mN=no.closest('[data-rk-menu]'); closeAllMenus(null); void mN; return; }
|
|
436
|
+
// Confirm delete → real DELETE
|
|
437
|
+
var yes=t&&t.closest?t.closest('[data-rk-confirm-yes]'):null;
|
|
438
|
+
if(yes){
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
var m3=yes.closest('[data-rk-menu]');
|
|
441
|
+
var card=m3.closest('.rk-library-card');
|
|
442
|
+
var delBtn=m3.querySelector('[data-rk-del]');
|
|
443
|
+
var url=delBtn&&delBtn.getAttribute('data-rk-del')||'';
|
|
444
|
+
if(!url){ window.alert('Cannot delete this post.'); return; }
|
|
445
|
+
yes.disabled=true; yes.classList.add('is-loading');
|
|
446
|
+
fetch(url,{method:'DELETE',credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
447
|
+
.then(function(r){ if(!r.ok)throw new Error('delete failed'); return r.json().catch(function(){return {};}); })
|
|
448
|
+
.then(function(){
|
|
449
|
+
if(card){ card.setAttribute('data-removed','1'); card.hidden=true; card.parentNode&&card.parentNode.removeChild(card); }
|
|
450
|
+
total=Math.max(0,total-1); syncPlaceholder(); apply();
|
|
451
|
+
})
|
|
452
|
+
.catch(function(){ yes.disabled=false; yes.classList.remove('is-loading'); window.alert('Could not delete that post. Please try again.'); });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// click elsewhere closes any open menu
|
|
456
|
+
if(!(t&&t.closest&&t.closest('[data-rk-menu]')))closeAllMenus(null);
|
|
457
|
+
});
|
|
458
|
+
document.addEventListener('keydown',function(e){ if(e.key==='Escape')closeAllMenus(null); });
|
|
459
|
+
|
|
460
|
+
// ── TikTok-style viewer modal (mirrors the /discover reskin viewer) ──────
|
|
461
|
+
var DATA=[]; try{ var el=root.querySelector('[data-rk-library-data]'); if(el) DATA=JSON.parse(el.textContent||'[]'); }catch(e){ DATA=[]; }
|
|
462
|
+
function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
|
|
463
|
+
function attr(t){ return esc(t).replace(/"/g,'"'); }
|
|
464
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
465
|
+
var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
|
|
466
|
+
if(viewer&&vTrack){
|
|
467
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
|
|
468
|
+
function slideHtml(t,i){
|
|
469
|
+
var media=t.is_video
|
|
470
|
+
? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.url)+'"'+(t.poster?' poster="'+attr(t.poster)+'"':'')+' loop playsinline preload="none"></video>'
|
|
471
|
+
: '<img class="rk-library-viewer-media" src="'+attr(t.url||t.poster)+'" alt="'+attr(t.title||'Published cut')+'">';
|
|
472
|
+
var cta=t.editor_href
|
|
473
|
+
? '<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+attr(t.editor_href)+'">Open in editor</a>'
|
|
474
|
+
: '';
|
|
475
|
+
var view=t.post_href
|
|
476
|
+
? '<a class="rk-btn rk-btn-ghost-light rk-btn-sm rk-library-viewer-cta" href="'+attr(t.post_href)+'" target="_blank" rel="noreferrer">View post</a>'
|
|
477
|
+
: '';
|
|
478
|
+
return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
|
|
479
|
+
+'<figure class="rk-library-viewer-stage">'
|
|
480
|
+
+ media
|
|
481
|
+
+'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
|
|
482
|
+
+'<div class="rk-library-viewer-shade"></div>'
|
|
483
|
+
+'<div class="rk-library-viewer-info">'
|
|
484
|
+
+'<div class="rk-library-viewer-title">'+esc(t.title)+'</div>'
|
|
485
|
+
+(t.sub?'<div class="rk-library-viewer-desc">'+esc(t.sub)+'</div>':'')
|
|
486
|
+
+'<div class="rk-library-viewer-actions">'+cta+view+'</div>'
|
|
487
|
+
+'</div>'
|
|
488
|
+
+'</figure>'
|
|
489
|
+
+'</section>';
|
|
490
|
+
}
|
|
491
|
+
function hydrate(slide){ var v=slide.querySelector('video[data-vsrc]'); if(!v||v.getAttribute('src'))return; v.preload='auto'; v.setAttribute('src',v.getAttribute('data-vsrc')); }
|
|
492
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
493
|
+
function setActive(idx){
|
|
494
|
+
vActive=idx;
|
|
495
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
496
|
+
if(Math.abs(i-idx)<=1) hydrate(s);
|
|
497
|
+
s.classList.remove('is-paused');
|
|
498
|
+
var v=s.querySelector('video'); if(!v)return;
|
|
499
|
+
if(i===idx){ v.muted=!vSound; var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){ v.muted=true; vSound=false; updateMuteUi(); var p2=v.play(); if(p2&&p2.catch)p2.catch(function(){}); }); }
|
|
500
|
+
else { v.pause(); }
|
|
501
|
+
});
|
|
502
|
+
updateMuteUi();
|
|
503
|
+
}
|
|
504
|
+
function step(d){ if(!vOpen)return; var n=Math.max(0,Math.min(vItems.length-1,vActive+d)); if(n===vActive)return; vTrack.scrollTo({top:n*vTrack.clientHeight,behavior:'smooth'}); }
|
|
505
|
+
function open(clickedIdx){
|
|
506
|
+
vItems=[]; var start=0;
|
|
507
|
+
DATA.forEach(function(t,i){ if(!t.url&&!t.poster)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
|
|
508
|
+
if(!vItems.length)return;
|
|
509
|
+
vTrack.innerHTML=vItems.map(slideHtml).join('');
|
|
510
|
+
vOpen=true; vSound=true; viewer.hidden=false;
|
|
511
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
512
|
+
if(vIO)vIO.disconnect();
|
|
513
|
+
vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting)return; var i=Number(en.target.getAttribute('data-vslide')); if(i!==vActive)setActive(i); }); },{root:vTrack,threshold:0.6}):null;
|
|
514
|
+
if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
515
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
516
|
+
setActive(start);
|
|
517
|
+
}
|
|
518
|
+
function close(){
|
|
519
|
+
if(!vOpen)return; vOpen=false; vActive=-1;
|
|
520
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
521
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
522
|
+
vTrack.innerHTML=''; viewer.hidden=true;
|
|
523
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
524
|
+
}
|
|
525
|
+
// open from a card poster
|
|
526
|
+
document.addEventListener('click',function(e){
|
|
527
|
+
var b=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
|
|
528
|
+
if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-view')));
|
|
529
|
+
});
|
|
530
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
531
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
532
|
+
if(muteBtn)muteBtn.addEventListener('click',function(){ vSound=!vSound; updateMuteUi(); var s=vTrack.children[vActive]; var v=s&&s.querySelector('video'); if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch)pr.catch(function(){}); s.classList.remove('is-paused'); } } });
|
|
533
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
|
|
534
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
|
|
535
|
+
vTrack.addEventListener('click',function(e){
|
|
536
|
+
var t=e.target;
|
|
537
|
+
if(t&&t.closest&&t.closest('a'))return;
|
|
538
|
+
var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
|
|
539
|
+
if(media&&media.tagName==='VIDEO'){ var slide=media.closest('.rk-library-viewer-slide'); if(media.paused){ var pr=media.play(); if(pr&&pr.catch)pr.catch(function(){}); if(slide)slide.classList.remove('is-paused'); } else { media.pause(); if(slide)slide.classList.add('is-paused'); } return; }
|
|
540
|
+
if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
|
|
541
|
+
});
|
|
542
|
+
document.addEventListener('keydown',function(e){ if(!vOpen)return; if(e.key==='Escape'){ e.preventDefault(); close(); } else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); } else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); } });
|
|
543
|
+
window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
|
|
544
|
+
}
|
|
289
545
|
})();`;
|
|
290
546
|
const CLIPS_SCRIPT = `
|
|
291
547
|
(function(){
|
|
292
548
|
var root=document;
|
|
293
|
-
var chips=root.getElementById('rkClipsChips');
|
|
294
549
|
var grid=root.getElementById('rkClipsGrid');
|
|
295
550
|
var statusEl=root.getElementById('rkClipsStatus');
|
|
551
|
+
var crumbsEl=root.getElementById('rkRawsCrumbs');
|
|
296
552
|
var empty=root.getElementById('rkClipsEmpty');
|
|
297
|
-
var
|
|
298
|
-
var
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
553
|
+
var form=root.getElementById('rkClipsSearch');
|
|
554
|
+
var query=root.getElementById('rkClipsQuery');
|
|
555
|
+
var hoverEl=root.getElementById('rkRawsHover');
|
|
556
|
+
var hoverVid=hoverEl&&hoverEl.querySelector('.rk-raws-hover-vid');
|
|
557
|
+
var hoverCap=root.getElementById('rkRawsHoverCap');
|
|
558
|
+
var canHover=!(window.matchMedia&&window.matchMedia('(hover:none)').matches);
|
|
559
|
+
|
|
560
|
+
// ── file-directory state ────────────────────────────────────────────────
|
|
561
|
+
var ALL=[]; // every raw in the library
|
|
562
|
+
var FOLDERS=[]; // [{id,name,clips:[...]}] grouped by source video
|
|
563
|
+
var VIEW=[]; // clips currently on screen (drives viewer + hover, by index)
|
|
564
|
+
var MODE='root'; // 'root' (folders) | 'folder' | 'search'
|
|
565
|
+
var CUR_FOLDER=''; // active folder id when MODE==='folder'
|
|
566
|
+
var LAST_Q=''; // active query when MODE==='search'
|
|
567
|
+
var PENDING=[]; // optimistic import cards: {id,url,tracer,clip,label,error}
|
|
568
|
+
|
|
569
|
+
function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
|
|
570
|
+
function attr(t){ return esc(t).replace(/"/g,'"'); }
|
|
571
|
+
function cssUrl(u){ return "'"+String(u||'').replace(/['")\\\\]/g,'').replace(/\\s/g,'%20')+"'"; }
|
|
572
|
+
function fmtClock(sec){ sec=Number(sec)||0; var m=Math.floor(sec/60), s=Math.floor(sec%60); return m+':'+(s<10?'0':'')+s; }
|
|
573
|
+
function topTags(tags){
|
|
574
|
+
if(!tags||typeof tags!=='object') return [];
|
|
575
|
+
var out=[];
|
|
576
|
+
// content_type leads — it's the shot-kind filter the chips key off.
|
|
577
|
+
(tags.content_type||[]).slice(0,2).forEach(function(v){ out.push(v.replace(/_/g,' ')); });
|
|
578
|
+
['subject','action','emotion'].forEach(function(k){ (tags[k]||[]).slice(0,1).forEach(function(v){ out.push(v); }); });
|
|
579
|
+
if(tags.energy) out.push('energy:'+tags.energy);
|
|
580
|
+
return out.slice(0,5);
|
|
302
581
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
582
|
+
function setGridEmpty(show){ if(empty)empty.hidden=!show; if(grid)grid.style.display=show?'none':''; }
|
|
583
|
+
function syncPlaceholder(){
|
|
584
|
+
if(!query)return;
|
|
585
|
+
var n=ALL.length;
|
|
586
|
+
query.setAttribute('placeholder','Search '+n+' raw clip'+(n===1?'':'s')+String.fromCharCode(8230));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Group raws into folders by their source video (source_video_id, falling back
|
|
590
|
+
// to source_filename). First-seen order is preserved.
|
|
591
|
+
function buildFolders(){
|
|
592
|
+
var map={}; FOLDERS=[];
|
|
593
|
+
ALL.forEach(function(c){
|
|
594
|
+
var id=String(c.source_video_id||c.source_filename||'ungrouped');
|
|
595
|
+
var f=map[id];
|
|
596
|
+
if(!f){ f={id:id,name:c.source_filename||'Untitled source',clips:[]}; map[id]=f; FOLDERS.push(f); }
|
|
597
|
+
f.clips.push(c);
|
|
309
598
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Optimistic "importing…" placeholder card (spinner, or an error state with a
|
|
602
|
+
// Dismiss button). Kept in PENDING and re-prepended on every root repaint so a
|
|
603
|
+
// feed reload never wipes a still-running import.
|
|
604
|
+
function importCardHtml(p){
|
|
605
|
+
var label=p.error?'Import failed':(p.label||'Importing…');
|
|
606
|
+
var sub=p.error?p.error:(p.url||'');
|
|
607
|
+
var badge=p.error
|
|
608
|
+
? '<button type="button" class="rk-clips-dl" data-import-dismiss="'+attr(p.id)+'">Dismiss</button>'
|
|
609
|
+
: '<span class="rk-clips-dl rk-clips-importing-tag">'+(p.clip?'Mining':'Importing')+'</span>';
|
|
610
|
+
return '<article class="rk-clips-card rk-clips-importing'+(p.error?' is-error':'')+'" data-import-id="'+attr(p.id)+'">'
|
|
611
|
+
+ '<div class="rk-clips-thumb">'
|
|
612
|
+
+ (p.error?'<span class="rk-clips-importing-x">'+${JSON.stringify(CROSS_ICON)}+'</span>':'<span class="rk-clips-importing-spin" aria-hidden="true"></span>')
|
|
613
|
+
+ '</div>'
|
|
614
|
+
+ '<div class="rk-clips-body">'
|
|
615
|
+
+ '<h3 class="rk-clips-title">'+esc(label)+'</h3>'
|
|
616
|
+
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(sub)+'">'+esc(sub)+'</span>'+badge+'</div>'
|
|
617
|
+
+ '</div></article>';
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Folder tile — a 2×2 thumbnail mosaic + name + clip count.
|
|
621
|
+
function folderCardHtml(folder){
|
|
622
|
+
var thumbs=folder.clips.filter(function(c){ return c.thumbnail_url; }).slice(0,4);
|
|
623
|
+
var mosaic;
|
|
624
|
+
if(thumbs.length){
|
|
625
|
+
mosaic='<span class="rk-raws-folder-mosaic'+(thumbs.length===1?' is-single':'')+'">'
|
|
626
|
+
+ thumbs.map(function(c){ return '<span class="rk-raws-folder-cell" style="background-image:url('+cssUrl(c.thumbnail_url)+')"></span>'; }).join('')
|
|
627
|
+
+ '</span>';
|
|
628
|
+
} else {
|
|
629
|
+
mosaic='<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
|
|
630
|
+
}
|
|
631
|
+
var n=folder.clips.length;
|
|
632
|
+
return '<button type="button" class="rk-raws-folder rk-card-hover" data-folder-id="'+attr(folder.id)+'">'
|
|
633
|
+
+ mosaic
|
|
634
|
+
+ '<span class="rk-raws-folder-meta">'
|
|
635
|
+
+ '<span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
|
|
636
|
+
+ '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(folder.name)+'">'+esc(folder.name)+'</span>'
|
|
637
|
+
+ '<span class="rk-raws-folder-count">'+n+' clip'+(n===1?'':'s')+'</span></span>'
|
|
638
|
+
+ '</span></button>';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function card(clip,i){
|
|
642
|
+
var range=fmtClock(clip.start_time_sec)+'–'+fmtClock(clip.end_time_sec);
|
|
643
|
+
var dur=(clip.duration_sec!=null)?(Number(clip.duration_sec).toFixed(1)+'s'):'';
|
|
644
|
+
var media=clip.thumbnail_url
|
|
645
|
+
? '<img class="rk-clips-thumb-img" loading="lazy" src="'+esc(clip.thumbnail_url)+'" alt="" />'
|
|
646
|
+
: '<span class="rk-clips-noimg">No preview</span>';
|
|
647
|
+
var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
|
|
648
|
+
var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
|
|
649
|
+
var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
|
|
650
|
+
return '<article class="rk-clips-card rk-card-hover">'
|
|
651
|
+
+ '<button type="button" class="rk-clips-thumb" data-rk-clip-view="'+i+'" aria-label="Open raw">'+media
|
|
652
|
+
+ '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
|
|
653
|
+
+ '<span class="rk-clips-play" aria-hidden="true">'+${JSON.stringify(PLAY)}+'</span>'
|
|
654
|
+
+ '</button>'
|
|
655
|
+
+ '<div class="rk-clips-body">'
|
|
656
|
+
+ '<h3 class="rk-clips-title">'+esc(desc)+'</h3>'
|
|
657
|
+
+ (tags?'<div class="rk-clips-tags">'+tags+'</div>':'')
|
|
658
|
+
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(clip.source_filename)+'">'+esc(clip.source_filename||'')+'</span>'
|
|
659
|
+
+ '<a class="rk-clips-dl" href="'+dl+'" target="_blank" rel="noopener">Download</a></div>'
|
|
660
|
+
+ '</div></article>';
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── mouseover preview popover ────────────────────────────────────────────── */
|
|
664
|
+
var hoverTimer=null;
|
|
665
|
+
function hideHover(){
|
|
666
|
+
if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
|
|
667
|
+
if(!hoverEl)return;
|
|
668
|
+
hoverEl.hidden=true; hoverEl.classList.remove('is-visible');
|
|
669
|
+
if(hoverVid){ try{ hoverVid.pause(); }catch(e){} hoverVid.removeAttribute('src'); hoverVid.load&&hoverVid.load(); }
|
|
670
|
+
}
|
|
671
|
+
function positionHover(rect){
|
|
672
|
+
if(!hoverEl)return;
|
|
673
|
+
var gap=12, bw=hoverEl.offsetWidth||250, bh=hoverEl.offsetHeight||330;
|
|
674
|
+
var x=rect.right+gap;
|
|
675
|
+
if(x+bw>window.innerWidth-8) x=rect.left-gap-bw;
|
|
676
|
+
if(x<8) x=Math.max(8,Math.min(rect.left,window.innerWidth-bw-8));
|
|
677
|
+
var y=rect.top+rect.height/2-bh/2;
|
|
678
|
+
y=Math.max(8,Math.min(y,window.innerHeight-bh-8));
|
|
679
|
+
hoverEl.style.left=Math.round(x)+'px';
|
|
680
|
+
hoverEl.style.top=Math.round(y)+'px';
|
|
681
|
+
}
|
|
682
|
+
function showHover(clip,rect){
|
|
683
|
+
if(!hoverEl||!clip||(!clip.view_url&&!clip.thumbnail_url))return;
|
|
684
|
+
if(hoverCap){ hoverCap.textContent=clip.description||(clip.tags&&clip.tags.transcript)||clip.source_filename||'Raw clip'; }
|
|
685
|
+
if(hoverVid){
|
|
686
|
+
hoverVid.poster=clip.thumbnail_url||'';
|
|
687
|
+
if(clip.view_url){ hoverVid.setAttribute('src',clip.view_url); hoverVid.load&&hoverVid.load(); var p=hoverVid.play&&hoverVid.play(); if(p&&p.catch)p.catch(function(){}); }
|
|
688
|
+
else { hoverVid.removeAttribute('src'); }
|
|
689
|
+
}
|
|
690
|
+
hoverEl.hidden=false; hoverEl.classList.add('is-visible');
|
|
691
|
+
positionHover(rect);
|
|
692
|
+
}
|
|
693
|
+
function bindHover(){
|
|
694
|
+
if(!grid||!canHover)return;
|
|
695
|
+
Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb[data-rk-clip-view]')).forEach(function(thumb){
|
|
696
|
+
var idx=Number(thumb.getAttribute('data-rk-clip-view'));
|
|
697
|
+
thumb.addEventListener('mouseenter',function(){
|
|
698
|
+
var clip=VIEW[idx]; if(!clip)return;
|
|
699
|
+
var rect=thumb.getBoundingClientRect();
|
|
700
|
+
if(hoverTimer)clearTimeout(hoverTimer);
|
|
701
|
+
hoverTimer=setTimeout(function(){ showHover(clip,rect); },90);
|
|
702
|
+
});
|
|
703
|
+
thumb.addEventListener('mouseleave',hideHover);
|
|
320
704
|
});
|
|
321
705
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
706
|
+
if(hoverEl){
|
|
707
|
+
window.addEventListener('scroll',hideHover,true);
|
|
708
|
+
window.addEventListener('resize',hideHover);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ── breadcrumb + view rendering ──────────────────────────────────────────── */
|
|
712
|
+
function renderCrumbs(){
|
|
713
|
+
if(!crumbsEl)return;
|
|
714
|
+
var html='';
|
|
715
|
+
if(MODE==='root'){
|
|
716
|
+
html='<span class="rk-raws-crumb is-current">All raws</span>';
|
|
717
|
+
} else {
|
|
718
|
+
html='<button type="button" class="rk-raws-crumb rk-raws-crumb-link" data-crumb-root>'+${JSON.stringify(ICON_CHEVLEFT)}+'<span>All raws</span></button>';
|
|
719
|
+
html+='<span class="rk-raws-crumb-sep" aria-hidden="true">/</span>';
|
|
720
|
+
if(MODE==='folder'){
|
|
721
|
+
var f=folderById(CUR_FOLDER);
|
|
722
|
+
html+='<span class="rk-raws-crumb is-current">'+esc(f?f.name:'Folder')+' <span class="rk-raws-crumb-n">'+(f?f.clips.length:0)+'</span></span>';
|
|
723
|
+
} else {
|
|
724
|
+
html+='<span class="rk-raws-crumb is-current">Search '+String.fromCharCode(8220)+esc(LAST_Q)+String.fromCharCode(8221)+' <span class="rk-raws-crumb-n">'+VIEW.length+'</span></span>';
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
crumbsEl.innerHTML=html;
|
|
728
|
+
}
|
|
729
|
+
function folderById(id){ for(var i=0;i<FOLDERS.length;i++){ if(FOLDERS[i].id===id)return FOLDERS[i]; } return null; }
|
|
730
|
+
|
|
731
|
+
function paintRoot(){
|
|
732
|
+
MODE='root'; CUR_FOLDER=''; VIEW=[];
|
|
733
|
+
if(!grid)return;
|
|
734
|
+
buildFolders();
|
|
735
|
+
grid.classList.add('is-folders');
|
|
736
|
+
if(FOLDERS.length===0 && PENDING.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent=''; return; }
|
|
737
|
+
grid.innerHTML=PENDING.map(importCardHtml).join('')+FOLDERS.map(folderCardHtml).join('');
|
|
738
|
+
setGridEmpty(false);
|
|
739
|
+
renderCrumbs();
|
|
740
|
+
if(statusEl)statusEl.textContent=FOLDERS.length+' folder'+(FOLDERS.length===1?'':'s');
|
|
741
|
+
}
|
|
742
|
+
function paintFolder(id){
|
|
743
|
+
var f=folderById(id); if(!f){ paintRoot(); return; }
|
|
744
|
+
MODE='folder'; CUR_FOLDER=id; VIEW=f.clips;
|
|
745
|
+
if(!grid)return;
|
|
746
|
+
grid.classList.remove('is-folders');
|
|
747
|
+
grid.innerHTML=VIEW.map(card).join('');
|
|
748
|
+
setGridEmpty(VIEW.length===0);
|
|
749
|
+
renderCrumbs(); bindHover();
|
|
750
|
+
if(statusEl)statusEl.textContent='';
|
|
751
|
+
}
|
|
752
|
+
function paintSearch(results,q){
|
|
753
|
+
MODE='search'; LAST_Q=q; VIEW=results||[];
|
|
754
|
+
if(!grid)return;
|
|
755
|
+
grid.classList.remove('is-folders');
|
|
756
|
+
if(VIEW.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent='No raws match '+String.fromCharCode(8220)+q+String.fromCharCode(8221)+'.'; return; }
|
|
757
|
+
grid.innerHTML=VIEW.map(card).join('');
|
|
758
|
+
setGridEmpty(false);
|
|
759
|
+
renderCrumbs(); bindHover();
|
|
760
|
+
if(statusEl)statusEl.textContent='';
|
|
761
|
+
}
|
|
762
|
+
// Repaint whatever view is active (used after an optimistic import lands).
|
|
763
|
+
function repaint(){
|
|
764
|
+
if(MODE==='folder')paintFolder(CUR_FOLDER);
|
|
765
|
+
else if(MODE==='search')paintSearch(VIEW,LAST_Q);
|
|
766
|
+
else paintRoot();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* ── optimistic import lifecycle ─────────────────────────────────────────── */
|
|
770
|
+
function getPending(id){ for(var i=0;i<PENDING.length;i++){ if(PENDING[i].id===id)return PENDING[i]; } return null; }
|
|
771
|
+
function removeImport(id){ PENDING=PENDING.filter(function(p){ return p.id!==id; }); }
|
|
772
|
+
function updateImportCard(id){
|
|
773
|
+
var p=getPending(id); if(!p)return;
|
|
774
|
+
var el=grid&&grid.querySelector('[data-import-id="'+id+'"]');
|
|
775
|
+
if(el){ el.outerHTML=importCardHtml(p); } else { repaint(); }
|
|
776
|
+
}
|
|
777
|
+
function finishImport(id){ removeImport(id); loadFeed(); }
|
|
778
|
+
function failImport(id,msg){ var p=getPending(id); if(p){ p.error=msg; } updateImportCard(id); if(statusEl)statusEl.textContent=msg; }
|
|
779
|
+
function onScanResult(id,clip,res){
|
|
780
|
+
if(!res.ok){ failImport(id,(res.data&&res.data.error)||(clip?'Could not start the scan.':'Could not import that video.')); return; }
|
|
781
|
+
var d=res.data||{};
|
|
782
|
+
if(d.scan_id && d.status!=='complete'){
|
|
783
|
+
var p=getPending(id); if(p){ p.label=clip?'Mining clips…':'Importing…'; updateImportCard(id); }
|
|
784
|
+
pollImport(id,d.scan_id);
|
|
785
|
+
} else {
|
|
786
|
+
finishImport(id);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function pollImport(id,scanId){
|
|
790
|
+
var tries=0;
|
|
791
|
+
var t=setInterval(function(){
|
|
792
|
+
tries++;
|
|
793
|
+
if(tries>200){ clearInterval(t); var p=getPending(id); if(p){ p.error='Still working in the background — refresh shortly.'; updateImportCard(id); } return; }
|
|
794
|
+
fetch('/raws/scan/'+encodeURIComponent(scanId),{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
795
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
796
|
+
.then(function(d){
|
|
797
|
+
if(!d)return;
|
|
798
|
+
var src=d.source||{}; var status=src.status||(d.scan&&d.scan.status)||'running';
|
|
799
|
+
if(status==='complete'){ clearInterval(t); finishImport(id); }
|
|
800
|
+
else if(status==='failed'||status==='error'){ clearInterval(t); failImport(id,src.error||(d.scan&&d.scan.error)||'The source could not be processed.'); }
|
|
801
|
+
}).catch(function(){});
|
|
802
|
+
},4000);
|
|
803
|
+
}
|
|
804
|
+
if(grid){
|
|
805
|
+
grid.addEventListener('click',function(e){
|
|
806
|
+
var d=e.target&&e.target.closest?e.target.closest('[data-import-dismiss]'):null;
|
|
807
|
+
if(!d)return; e.preventDefault(); removeImport(d.getAttribute('data-import-dismiss')); repaint();
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* ── fullscreen scroll-snap viewer (click a raw → vertical deck) ──────────── */
|
|
812
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
813
|
+
var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
|
|
814
|
+
if(viewer&&vTrack){
|
|
815
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
|
|
816
|
+
function slideHtml(t,i){
|
|
817
|
+
var media=t.view_url
|
|
818
|
+
? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.view_url)+'"'+(t.thumbnail_url?' poster="'+attr(t.thumbnail_url)+'"':'')+' loop playsinline preload="none"></video>'
|
|
819
|
+
: '<img class="rk-library-viewer-media" src="'+attr(t.thumbnail_url||'')+'" alt="'+attr(t.description||'Raw clip')+'">';
|
|
820
|
+
var desc=t.description || (t.tags&&t.tags.transcript) || t.source_filename || 'Raw clip';
|
|
821
|
+
var range=fmtClock(t.start_time_sec)+'–'+fmtClock(t.end_time_sec);
|
|
822
|
+
var meta=(t.source_filename?esc(t.source_filename)+' · ':'')+esc(range);
|
|
823
|
+
var dl='/raws/'+encodeURIComponent(t.clip_id)+'/download';
|
|
824
|
+
return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
|
|
825
|
+
+'<figure class="rk-library-viewer-stage">'
|
|
826
|
+
+ media
|
|
827
|
+
+'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
|
|
828
|
+
+'<div class="rk-library-viewer-shade"></div>'
|
|
829
|
+
+'<div class="rk-library-viewer-info">'
|
|
830
|
+
+'<div class="rk-library-viewer-title">'+esc(desc)+'</div>'
|
|
831
|
+
+'<div class="rk-library-viewer-desc">'+meta+'</div>'
|
|
832
|
+
+'<div class="rk-library-viewer-actions"><a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+dl+'" target="_blank" rel="noopener">Download</a></div>'
|
|
833
|
+
+'</div>'
|
|
834
|
+
+'</figure>'
|
|
835
|
+
+'</section>';
|
|
836
|
+
}
|
|
837
|
+
function hydrate(slide){ var v=slide.querySelector('video[data-vsrc]'); if(!v||v.getAttribute('src'))return; v.preload='auto'; v.setAttribute('src',v.getAttribute('data-vsrc')); }
|
|
838
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
839
|
+
function setActive(idx){
|
|
840
|
+
vActive=idx;
|
|
841
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
842
|
+
if(Math.abs(i-idx)<=1) hydrate(s);
|
|
843
|
+
s.classList.remove('is-paused');
|
|
844
|
+
var v=s.querySelector('video'); if(!v)return;
|
|
845
|
+
if(i===idx){ v.muted=!vSound; var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){ v.muted=true; vSound=false; updateMuteUi(); var p2=v.play(); if(p2&&p2.catch)p2.catch(function(){}); }); }
|
|
846
|
+
else { v.pause(); }
|
|
847
|
+
});
|
|
848
|
+
updateMuteUi();
|
|
849
|
+
}
|
|
850
|
+
function step(d){ if(!vOpen)return; var n=Math.max(0,Math.min(vItems.length-1,vActive+d)); if(n===vActive)return; vTrack.scrollTo({top:n*vTrack.clientHeight,behavior:'smooth'}); }
|
|
851
|
+
function open(clickedIdx){
|
|
852
|
+
hideHover();
|
|
853
|
+
vItems=[]; var start=0;
|
|
854
|
+
VIEW.forEach(function(t,i){ if(!t.view_url&&!t.thumbnail_url)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
|
|
855
|
+
if(!vItems.length)return;
|
|
856
|
+
vTrack.innerHTML=vItems.map(slideHtml).join('');
|
|
857
|
+
vOpen=true; vSound=true; viewer.hidden=false;
|
|
858
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
859
|
+
if(vIO)vIO.disconnect();
|
|
860
|
+
vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting)return; var i=Number(en.target.getAttribute('data-vslide')); if(i!==vActive)setActive(i); }); },{root:vTrack,threshold:0.6}):null;
|
|
861
|
+
if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
862
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
863
|
+
setActive(start);
|
|
864
|
+
}
|
|
865
|
+
function close(){
|
|
866
|
+
if(!vOpen)return; vOpen=false; vActive=-1;
|
|
867
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
868
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
869
|
+
vTrack.innerHTML=''; viewer.hidden=true;
|
|
870
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
871
|
+
}
|
|
872
|
+
if(grid){
|
|
873
|
+
grid.addEventListener('click',function(e){
|
|
874
|
+
var b=e.target&&e.target.closest?e.target.closest('[data-rk-clip-view]'):null;
|
|
875
|
+
if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-clip-view')));
|
|
336
876
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
877
|
+
}
|
|
878
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
879
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
880
|
+
if(muteBtn)muteBtn.addEventListener('click',function(){ vSound=!vSound; updateMuteUi(); var s=vTrack.children[vActive]; var v=s&&s.querySelector('video'); if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch)pr.catch(function(){}); s.classList.remove('is-paused'); } } });
|
|
881
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
|
|
882
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
|
|
883
|
+
vTrack.addEventListener('click',function(e){
|
|
884
|
+
var t=e.target;
|
|
885
|
+
if(t&&t.closest&&t.closest('a'))return;
|
|
886
|
+
var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
|
|
887
|
+
if(media&&media.tagName==='VIDEO'){ var slide=media.closest('.rk-library-viewer-slide'); if(media.paused){ var pr=media.play(); if(pr&&pr.catch)pr.catch(function(){}); if(slide)slide.classList.remove('is-paused'); } else { media.pause(); if(slide)slide.classList.add('is-paused'); } return; }
|
|
888
|
+
if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
|
|
889
|
+
});
|
|
890
|
+
document.addEventListener('keydown',function(e){ if(!vOpen)return; if(e.key==='Escape'){ e.preventDefault(); close(); } else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); } else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); } });
|
|
891
|
+
window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Folder navigation (grid tile) + breadcrumb "All raws" back link.
|
|
895
|
+
if(grid){
|
|
896
|
+
grid.addEventListener('click',function(e){
|
|
897
|
+
var f=e.target&&e.target.closest?e.target.closest('[data-folder-id]'):null;
|
|
898
|
+
if(!f)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintFolder(f.getAttribute('data-folder-id'));
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if(crumbsEl){
|
|
902
|
+
crumbsEl.addEventListener('click',function(e){
|
|
903
|
+
var r=e.target&&e.target.closest?e.target.closest('[data-crumb-root]'):null;
|
|
904
|
+
if(!r)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintRoot();
|
|
340
905
|
});
|
|
341
906
|
}
|
|
907
|
+
|
|
908
|
+
function handleAuth(res){
|
|
909
|
+
if(res.status===401||res.status===403){
|
|
910
|
+
if(grid)grid.innerHTML='';
|
|
911
|
+
setGridEmpty(false);
|
|
912
|
+
if(statusEl)statusEl.innerHTML='Sign in to vidfarm to see your raws, then build a library with the Import source button or <code>vidfarm raws scan</code>.';
|
|
913
|
+
throw new Error('unauthorized');
|
|
914
|
+
}
|
|
915
|
+
return res.json();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function loadFeed(){
|
|
919
|
+
if(statusEl)statusEl.textContent='Loading your raws…';
|
|
920
|
+
fetch('/raws/feed?limit=250',{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
921
|
+
.then(handleAuth).then(function(d){
|
|
922
|
+
ALL=d.clips||[]; buildFolders(); syncPlaceholder();
|
|
923
|
+
// stay inside the current folder if it survived the refresh, else root
|
|
924
|
+
if(MODE==='folder' && folderById(CUR_FOLDER)) paintFolder(CUR_FOLDER);
|
|
925
|
+
else paintRoot();
|
|
926
|
+
}).catch(function(){});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function search(q){
|
|
930
|
+
if(statusEl)statusEl.textContent='Searching…';
|
|
931
|
+
fetch('/raws/search',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({query:q,limit:60})})
|
|
932
|
+
.then(handleAuth).then(function(d){ paintSearch(d.results||[],q); }).catch(function(){});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if(form){
|
|
936
|
+
form.addEventListener('submit',function(e){
|
|
937
|
+
e.preventDefault();
|
|
938
|
+
var q=(query&&query.value||'').trim();
|
|
939
|
+
if(q)search(q); else paintRoot();
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if(query){
|
|
943
|
+
// clearing the box returns to the folder root without a round-trip
|
|
944
|
+
query.addEventListener('input',function(){ if(!(query.value||'').trim() && MODE==='search'){ hideHover(); paintRoot(); } });
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ── Scan a source (real clip hunt) ─────────────────────────────────────
|
|
342
948
|
var dialog=root.getElementById('rkClipsDialog');
|
|
343
|
-
var
|
|
949
|
+
var openBtns=Array.prototype.slice.call(root.querySelectorAll('[data-rk-open-scan]'));
|
|
344
950
|
var cancelBtn=root.getElementById('rkClipsScanCancel');
|
|
345
951
|
var scanForm=root.getElementById('rkClipsScanForm');
|
|
346
|
-
|
|
952
|
+
var scanUrl=root.getElementById('rkScanUrl');
|
|
953
|
+
var scanPrompt=root.getElementById('rkScanPrompt');
|
|
954
|
+
var scanProvider=root.getElementById('rkScanProvider');
|
|
955
|
+
var scanTracer=root.getElementById('rkScanTracer');
|
|
956
|
+
var scanNote=root.getElementById('rkScanNote');
|
|
957
|
+
var scanSubmit=root.getElementById('rkClipsScanSubmit');
|
|
958
|
+
var scanClip=root.getElementById('rkScanClip');
|
|
959
|
+
var scanAdvanced=root.getElementById('rkClipsAdvanced');
|
|
960
|
+
var IMPORT_NOTE="We'll pull the whole video into your raws library.";
|
|
961
|
+
var CLIP_NOTE='AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.';
|
|
962
|
+
var SCAN_OPTS=null; // GET /raws/scan-options: which providers/keys can run a hunt
|
|
963
|
+
function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
|
|
964
|
+
function providerLabel(p){ return p==='gemini'?'Gemini':p==='openai'?'OpenAI':p==='openrouter'?'OpenRouter':p; }
|
|
965
|
+
// Can a clip hunt actually run? A saved provider key, a local agent CLI, or a
|
|
966
|
+
// linked cloud account all qualify. Unknown (not yet loaded) → don't block.
|
|
967
|
+
function clipReady(){
|
|
968
|
+
if(!SCAN_OPTS)return true;
|
|
969
|
+
if(SCAN_OPTS.default_provider)return true;
|
|
970
|
+
if(SCAN_OPTS.pipeline==='upstream'||SCAN_OPTS.upstream_backup)return true;
|
|
971
|
+
return (SCAN_OPTS.providers||[]).some(function(p){ return p.has_key; });
|
|
972
|
+
}
|
|
973
|
+
function applyScanOpts(){
|
|
974
|
+
if(!SCAN_OPTS||!scanProvider)return;
|
|
975
|
+
Array.prototype.slice.call(scanProvider.options).forEach(function(opt){
|
|
976
|
+
if(!opt.value)return; // leave "Auto" alone
|
|
977
|
+
var pr=null; (SCAN_OPTS.providers||[]).forEach(function(p){ if(p.provider===opt.value)pr=p; });
|
|
978
|
+
if(!pr)return;
|
|
979
|
+
opt.disabled=!pr.has_key;
|
|
980
|
+
opt.textContent=providerLabel(opt.value)+(pr.has_key?' — key saved':' — no key saved');
|
|
981
|
+
});
|
|
982
|
+
if(scanClip&&scanClip.checked)syncClipMode();
|
|
983
|
+
}
|
|
984
|
+
function loadScanOpts(){
|
|
985
|
+
fetch('/raws/scan-options',{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
986
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
987
|
+
.then(function(d){ if(d){ SCAN_OPTS=d; applyScanOpts(); } }).catch(function(){});
|
|
988
|
+
}
|
|
989
|
+
function syncClipMode(){
|
|
990
|
+
var on=!!(scanClip&&scanClip.checked);
|
|
991
|
+
if(scanAdvanced)scanAdvanced.hidden=!on;
|
|
992
|
+
if(scanSubmit)scanSubmit.innerHTML=(on?'Start scan':'Import video')+' <span class="rk-arrow">→</span>';
|
|
993
|
+
if(on&&!clipReady()){
|
|
994
|
+
setNote('No AI provider available — add a Gemini, OpenAI, or OpenRouter key in Settings to clip raws (or uncheck to import the whole video).','error');
|
|
995
|
+
} else {
|
|
996
|
+
setNote(on?CLIP_NOTE:IMPORT_NOTE);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if(scanClip)scanClip.addEventListener('change',syncClipMode);
|
|
1000
|
+
if(dialog){ openBtns.forEach(function(openBtn){ openBtn.addEventListener('click',function(){ if(!SCAN_OPTS)loadScanOpts(); syncClipMode(); if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal(); }); }); }
|
|
347
1001
|
if(cancelBtn&&dialog){ cancelBtn.addEventListener('click',function(){ if(dialog.open)dialog.close(); }); }
|
|
348
1002
|
if(dialog){
|
|
349
1003
|
dialog.addEventListener('click',function(e){
|
|
@@ -351,87 +1005,186 @@ const CLIPS_SCRIPT = `
|
|
|
351
1005
|
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();
|
|
352
1006
|
});
|
|
353
1007
|
}
|
|
354
|
-
if(scanForm){
|
|
1008
|
+
if(scanForm){
|
|
1009
|
+
scanForm.addEventListener('submit',function(e){
|
|
1010
|
+
e.preventDefault();
|
|
1011
|
+
var url=(scanUrl&&scanUrl.value||'').trim();
|
|
1012
|
+
var tracer=(scanTracer&&scanTracer.value||'').trim();
|
|
1013
|
+
var clip=!!(scanClip&&scanClip.checked);
|
|
1014
|
+
var prompt=clip?(scanPrompt&&scanPrompt.value||'').trim():'';
|
|
1015
|
+
var provider=clip?(scanProvider&&scanProvider.value||'').trim():'';
|
|
1016
|
+
if(!url){ setNote('Enter a video URL.','error'); return; }
|
|
1017
|
+
setNote(clip?'Starting scan…':'Importing video…');
|
|
1018
|
+
if(scanSubmit){ scanSubmit.disabled=true; scanSubmit.classList.add('is-loading'); }
|
|
1019
|
+
|
|
1020
|
+
var payload={source_url:url};
|
|
1021
|
+
if(clip){ payload.prompt=prompt; if(provider)payload.provider=provider; } else { payload.import_only=true; }
|
|
1022
|
+
if(tracer)payload.tracer=tracer;
|
|
1023
|
+
|
|
1024
|
+
// Fire the request now; hold it and only wire the result AFTER the modal
|
|
1025
|
+
// closes and the optimistic card lands (so a fast reply can't race ahead
|
|
1026
|
+
// of its own placeholder).
|
|
1027
|
+
var id='imp'+Date.now()+'-'+Math.floor(Math.random()*1e6);
|
|
1028
|
+
var req=fetch('/raws/scan',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(payload)})
|
|
1029
|
+
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); });
|
|
1030
|
+
|
|
1031
|
+
// brief spinner beat, then close and optimistically drop an "importing…" card
|
|
1032
|
+
setTimeout(function(){
|
|
1033
|
+
if(scanSubmit){ scanSubmit.disabled=false; scanSubmit.classList.remove('is-loading'); }
|
|
1034
|
+
if(dialog&&dialog.open)dialog.close();
|
|
1035
|
+
if(scanForm.reset)scanForm.reset();
|
|
1036
|
+
syncClipMode();
|
|
1037
|
+
PENDING.unshift({id:id,url:url,tracer:tracer,clip:clip,label:clip?'Starting scan…':'Importing…'});
|
|
1038
|
+
if(query)query.value='';
|
|
1039
|
+
paintRoot(); // surface the optimistic "importing…" card at the directory root
|
|
1040
|
+
if(statusEl)statusEl.textContent=(clip?'Mining a source…':'Importing a video…')+(tracer?' (tracer '+tracer+')':'');
|
|
1041
|
+
req.then(function(res){ onScanResult(id,clip,res); }).catch(function(){ failImport(id,'Network error — please try again.'); });
|
|
1042
|
+
},650);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
loadFeed();
|
|
355
1047
|
})();`;
|
|
356
|
-
export function renderReskinLibrary(tab = "approved") {
|
|
357
|
-
const
|
|
1048
|
+
export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPUT) {
|
|
1049
|
+
const isRaws = tab === "raws";
|
|
1050
|
+
const isLogs = tab === "logs";
|
|
1051
|
+
const isFiles = tab === "files";
|
|
1052
|
+
const tabDef = [
|
|
1053
|
+
{ key: "approved", label: "Approved", href: "/library/approved" },
|
|
1054
|
+
{ key: "raws", label: "Raws", href: "/library/raws" },
|
|
1055
|
+
{ key: "files", label: "Files", href: "/library/files" }
|
|
1056
|
+
];
|
|
358
1057
|
const tabs = `<div class="rk-library-tabs" role="tablist" aria-label="Library view">
|
|
359
|
-
|
|
360
|
-
<a class="rk-library-tab${isClips ? " is-active" : ""}" role="tab" ${isClips ? `aria-current="page"` : ""} href="/reskin/library/clips">Clips</a>
|
|
1058
|
+
${tabDef.map((t) => `<a class="rk-library-tab${t.key === tab ? " is-active" : ""}" role="tab"${t.key === tab ? ` aria-current="page"` : ""} href="${t.href}">${t.label}</a>`).join("\n ")}
|
|
361
1059
|
</div>`;
|
|
1060
|
+
// Logs tab reuses the real /job-runs history endpoint (client-fetched by the
|
|
1061
|
+
// folded-in job-runs script); the seed is empty so it loads live.
|
|
1062
|
+
const logsBoot = {
|
|
1063
|
+
loadEndpoint: "/job-runs/history?limit=250",
|
|
1064
|
+
accountId: input.currentAccountId || "",
|
|
1065
|
+
storageDriver: "s3",
|
|
1066
|
+
entries: []
|
|
1067
|
+
};
|
|
1068
|
+
const headSub = isLogs
|
|
1069
|
+
? "Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost."
|
|
1070
|
+
: isFiles
|
|
1071
|
+
? "Your whole workspace as one navigable directory — files, temp scratch, and mined raws. Search by meaning, name, or path."
|
|
1072
|
+
: isRaws
|
|
1073
|
+
? "Long-form videos mined into tagged, searchable short clips — every reaction, hook, and money shot, ready to drop into an edit."
|
|
1074
|
+
: "Every published cut you own — ready-to-post renders and scheduled drops. Open in the editor, share, download, or schedule.";
|
|
1075
|
+
const headCta = isRaws
|
|
1076
|
+
? `<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" data-rk-open-scan>Import source <span class="rk-arrow">→</span></button>`
|
|
1077
|
+
: isLogs || isFiles
|
|
1078
|
+
? ""
|
|
1079
|
+
: `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/editor/original/fork/new">New project <span class="rk-arrow">→</span></a>`;
|
|
1080
|
+
const panel = isLogs
|
|
1081
|
+
? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
|
|
1082
|
+
: isFiles
|
|
1083
|
+
? filesPanel(tabs)
|
|
1084
|
+
: isRaws
|
|
1085
|
+
? clipsPanel(tabs)
|
|
1086
|
+
: approvedPanel(input, tabs);
|
|
362
1087
|
const body = `
|
|
363
1088
|
<main class="rk-container-wide rk-page rk-library-page">
|
|
364
|
-
<div class="rk-spread rk-
|
|
1089
|
+
<div class="rk-spread rk-page-head rk-library-head">
|
|
365
1090
|
<div class="rk-stack rk-library-head-copy">
|
|
366
|
-
<span class="rk-badge">Your studio</span>
|
|
367
1091
|
<h1 class="rk-h1">Library</h1>
|
|
368
|
-
<p class="rk-sub">${
|
|
369
|
-
? "Long-form videos mined into tagged, searchable short clips — every reaction, hook, and money shot, ready to drop into an edit."
|
|
370
|
-
: "Every project you own — forked templates, in-progress edits, active renders, and ready-to-post cuts."}</p>
|
|
371
|
-
</div>
|
|
372
|
-
<div class="rk-row rk-wrap rk-library-head-cta">
|
|
373
|
-
<a class="rk-btn rk-btn-gold rk-btn-lg" href="/discover">New project <span class="rk-arrow">→</span></a>
|
|
1092
|
+
<p class="rk-sub">${headSub}</p>
|
|
374
1093
|
</div>
|
|
1094
|
+
${headCta}
|
|
375
1095
|
</div>
|
|
376
1096
|
|
|
377
|
-
${
|
|
378
|
-
|
|
379
|
-
${isClips ? clipsPanel() : approvedPanel()}
|
|
1097
|
+
${panel}
|
|
380
1098
|
</main>
|
|
381
|
-
${
|
|
1099
|
+
${isRaws ? CLIPS_DIALOG : ""}`;
|
|
382
1100
|
const pageCss = `
|
|
383
1101
|
/* .rk-page zeroes horizontal padding; the wide container == viewport at 1280 so
|
|
384
1102
|
margin:auto adds no gutter. Restore side gutters (longhand beats the shorthand). */
|
|
385
1103
|
.rk-library-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
386
|
-
|
|
387
|
-
.rk-library-head-
|
|
388
|
-
.rk-library-head-copy
|
|
389
|
-
.rk-library-
|
|
390
|
-
|
|
1104
|
+
/* compact head: title + sub left, small New project pinned top-right on the same row */
|
|
1105
|
+
.rk-library-head{align-items:flex-start}
|
|
1106
|
+
.rk-library-head-copy{gap:4px;margin:0;flex:1 1 auto;min-width:0}
|
|
1107
|
+
.rk-library-new-btn{flex:none;margin-top:2px}
|
|
1108
|
+
|
|
1109
|
+
/* topbar row: primary tab toggle + the tab's quick search share one line */
|
|
1110
|
+
.rk-library-topbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:22px}
|
|
1111
|
+
/* "Logs" — pulled out of the tab toggle; floats to the far right of the row.
|
|
1112
|
+
When a search bar is present it already grabs margin-left:auto, so Logs just
|
|
1113
|
+
trails it; when there's no search (Files tab) Logs floats right on its own. */
|
|
1114
|
+
.rk-library-logs-btn{flex:none;margin-left:auto;gap:7px}
|
|
1115
|
+
.rk-library-qsearch ~ .rk-library-logs-btn,#rkClipsSearch ~ .rk-library-logs-btn{margin-left:0}
|
|
1116
|
+
.rk-library-logs-btn svg{color:var(--rk-n-500)}
|
|
1117
|
+
@media(max-width:720px){.rk-library-logs-btn{margin-left:auto}}
|
|
391
1118
|
|
|
392
1119
|
/* primary tab toggle (Approved | Clips) */
|
|
393
|
-
.rk-library-tabs{display:inline-flex;padding:5px;gap:3px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-r-full)
|
|
1120
|
+
.rk-library-tabs{display:inline-flex;flex:none;padding:5px;gap:3px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-r-full)}
|
|
394
1121
|
.rk-library-tab{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;border-radius:var(--rk-r-full);font-size:14px;font-weight:600;color:var(--rk-n-600);transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
395
1122
|
.rk-library-tab span{font-size:15px}
|
|
396
1123
|
.rk-library-tab:hover{color:var(--rk-ink)}
|
|
397
1124
|
.rk-library-tab.is-active{background:var(--rk-ink);color:#fff;box-shadow:var(--rk-shadow-sm)}
|
|
398
1125
|
|
|
399
|
-
/* stats */
|
|
400
|
-
.rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;
|
|
1126
|
+
/* stats — sits right-aligned in the chips row */
|
|
1127
|
+
.rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;line-height:1.7}
|
|
401
1128
|
.rk-library-summary b{color:var(--rk-ink);font-weight:800;font-family:var(--rk-font-display);font-size:15px;letter-spacing:-.01em}
|
|
1129
|
+
.rk-library-toolbar .rk-library-summary{margin-left:auto}
|
|
402
1130
|
|
|
403
|
-
/* toolbar */
|
|
1131
|
+
/* toolbar (filter chips + count) */
|
|
404
1132
|
.rk-library-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:24px}
|
|
405
1133
|
.rk-library-chips{display:flex;flex-wrap:wrap;gap:4px}
|
|
406
1134
|
.rk-library-chip{display:inline-flex;align-items:center;padding:6px 13px;border-radius:var(--rk-r-full);border:1px solid transparent;background:transparent;color:var(--rk-text-muted);font-size:13px;font-weight:600;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
407
1135
|
.rk-library-chip:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
408
1136
|
.rk-library-chip.is-active{background:var(--rk-ink);color:#fff}
|
|
409
|
-
|
|
410
|
-
|
|
1137
|
+
/* approved quickfilter — same pill search as the Clips tab, right-aligned */
|
|
1138
|
+
.rk-library-qsearch{flex:1 1 220px;max-width:360px;margin-left:auto}
|
|
1139
|
+
.rk-library-qsearch .rk-clips-search-input:disabled{background:var(--rk-n-100);cursor:not-allowed}
|
|
1140
|
+
@media(max-width:720px){.rk-library-qsearch{max-width:none;margin-left:0;width:100%}}
|
|
411
1141
|
|
|
412
1142
|
/* grid + cards */
|
|
413
1143
|
.rk-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px}
|
|
414
1144
|
.rk-library-card{display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);overflow:hidden;box-shadow:var(--rk-shadow-card)}
|
|
415
1145
|
.rk-library-card[hidden]{display:none}
|
|
416
|
-
.rk-library-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden}
|
|
1146
|
+
.rk-library-poster{position:relative;display:block;width:100%;aspect-ratio:9/16;overflow:hidden;background:linear-gradient(158deg,#2c2c2c,#171717);border:0;padding:0;margin:0;cursor:pointer;text-align:left;font:inherit;color:inherit}
|
|
417
1147
|
.rk-library-poster::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 58%,rgba(0,0,0,.22));pointer-events:none}
|
|
418
|
-
.rk-library-
|
|
1148
|
+
.rk-library-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#050505}
|
|
1149
|
+
.rk-library-noprev{position:absolute;inset:0;display:grid;place-items:center;color:rgba(255,255,255,.72);font-size:12px;font-weight:600;letter-spacing:.01em}
|
|
419
1150
|
.rk-library-pstatus{position:absolute;top:12px;left:12px;z-index:2;box-shadow:var(--rk-shadow-sm)}
|
|
420
1151
|
.rk-library-dot{width:6px;height:6px;border-radius:var(--rk-r-full);background:currentColor;opacity:.9}
|
|
421
|
-
.rk-library-dot.is-pulse{animation:rk-library-pulse 1.2s var(--rk-ease) infinite}
|
|
422
1152
|
.rk-library-dur{position:absolute;bottom:12px;right:12px;z-index:2;padding:4px 9px;border-radius:var(--rk-r-full);background:rgba(23,23,23,.72);color:#fff;font-size:11px;font-weight:600;letter-spacing:.01em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
|
|
423
1153
|
.rk-library-play{position:absolute;top:50%;left:50%;z-index:2;width:52px;height:52px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.94);display:grid;place-items:center;color:var(--rk-ink);box-shadow:var(--rk-shadow-md);opacity:0;transform:translate(-50%,-50%) scale(.82);transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
424
1154
|
.rk-library-card:hover .rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}
|
|
1155
|
+
@media(hover:none){.rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}}
|
|
425
1156
|
.rk-library-play svg{margin-left:2px}
|
|
426
|
-
.rk-library-
|
|
427
|
-
.rk-library-prog-fill{display:block;height:100%;background:var(--rk-gold-500);border-radius:0 var(--rk-r-full) var(--rk-r-full) 0;box-shadow:0 0 8px rgba(252,185,0,.6);animation:rk-library-pulse 1.6s var(--rk-ease) infinite}
|
|
428
|
-
.rk-library-meta{display:grid;gap:10px;padding:16px 16px 18px}
|
|
1157
|
+
.rk-library-meta{display:grid;gap:9px;padding:14px 14px 15px}
|
|
429
1158
|
.rk-library-title{font-family:var(--rk-font-display);font-size:15px;font-weight:700;color:var(--rk-ink);line-height:1.24;letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.48em}
|
|
430
|
-
.rk-library-origin{display:flex;align-items:center;gap:
|
|
1159
|
+
.rk-library-origin{display:flex;align-items:center;gap:6px;min-width:0;font-size:12px;font-weight:500;color:var(--rk-text-muted)}
|
|
1160
|
+
.rk-library-origin-sep{color:var(--rk-text-faint);flex:none}
|
|
431
1161
|
.rk-library-origin-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
432
|
-
.rk-library-
|
|
433
|
-
|
|
434
|
-
.rk-library-
|
|
1162
|
+
.rk-library-time{font-size:12px;font-weight:600;color:var(--rk-text-faint);white-space:nowrap;flex:none}
|
|
1163
|
+
/* compact action row: labelled Schedule + icon-only Open/View/Download */
|
|
1164
|
+
.rk-library-actions{display:flex;align-items:center;gap:6px;margin-top:1px}
|
|
1165
|
+
.rk-library-act{flex:none;min-height:32px;padding:0 12px;font-size:12.5px;border-radius:var(--rk-r-full)}
|
|
1166
|
+
.rk-library-act-ic{width:32px;min-width:32px;padding:0;display:inline-flex;align-items:center;justify-content:center}
|
|
1167
|
+
.rk-library-act-ic svg{display:block}
|
|
1168
|
+
|
|
1169
|
+
/* right-floated "⋯" overflow menu (Copy ID / Delete-with-popconfirm) */
|
|
1170
|
+
.rk-library-menu{position:relative;margin-left:auto;flex:none}
|
|
1171
|
+
.rk-library-kebab{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);background:var(--rk-surface);color:var(--rk-n-600);cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
1172
|
+
.rk-library-kebab:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
1173
|
+
.rk-library-menu.is-open .rk-library-kebab{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border-strong)}
|
|
1174
|
+
.rk-library-kebab.is-ok{color:#1f7a44;border-color:#bfe6cd;background:#e8f7ee}
|
|
1175
|
+
.rk-library-menu-pop{position:absolute;right:0;bottom:calc(100% + 8px);z-index:30;min-width:172px;padding:6px;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-xl))}
|
|
1176
|
+
.rk-library-menu-pop[hidden]{display:none}
|
|
1177
|
+
.rk-library-menu-item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 11px;border:0;border-radius:var(--rk-r-lg,12px);background:none;color:var(--rk-ink);font:inherit;font-size:13px;font-weight:600;text-align:left;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
1178
|
+
.rk-library-menu-item svg{flex:none;color:var(--rk-n-500)}
|
|
1179
|
+
.rk-library-menu-item:hover{background:var(--rk-n-100)}
|
|
1180
|
+
.rk-library-menu-item.is-danger{color:var(--rk-red)}
|
|
1181
|
+
.rk-library-menu-item.is-danger svg{color:var(--rk-red)}
|
|
1182
|
+
.rk-library-menu-item.is-danger:hover{background:var(--rk-red-tint)}
|
|
1183
|
+
.rk-library-menu-confirm{padding:10px 11px 4px}
|
|
1184
|
+
.rk-library-menu-confirm[hidden]{display:none}
|
|
1185
|
+
.rk-library-menu-confirm-q{font-size:12.5px;font-weight:600;color:var(--rk-ink);margin:0 0 9px}
|
|
1186
|
+
.rk-library-menu-confirm-row{display:flex;justify-content:flex-end;gap:7px}
|
|
1187
|
+
.rk-library-menu-confirm-row .rk-btn{min-height:30px}
|
|
435
1188
|
|
|
436
1189
|
/* new-project tile */
|
|
437
1190
|
.rk-library-new{display:grid;grid-auto-flow:row;place-content:center;justify-items:center;gap:14px;text-align:center;padding:28px 22px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-3xl);background:var(--rk-n-50);color:var(--rk-text-muted);transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
@@ -447,69 +1200,187 @@ ${isClips ? CLIPS_DIALOG : ""}`;
|
|
|
447
1200
|
.rk-library-empty h3{font-size:var(--rk-text-xl)}
|
|
448
1201
|
.rk-library-empty p{max-width:42ch}
|
|
449
1202
|
|
|
450
|
-
@
|
|
451
|
-
|
|
1203
|
+
@media(prefers-reduced-motion:reduce){.rk-library-video{}}
|
|
1204
|
+
|
|
1205
|
+
/* ── fullscreen TikTok-style viewer (click a card → vertical scroll deck) ── */
|
|
1206
|
+
html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
|
|
1207
|
+
.rk-library-viewer{position:fixed;inset:0;z-index:120}
|
|
1208
|
+
.rk-library-viewer[hidden]{display:none}
|
|
1209
|
+
.rk-library-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
|
1210
|
+
.rk-library-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
|
|
1211
|
+
.rk-library-viewer-track::-webkit-scrollbar{display:none}
|
|
1212
|
+
.rk-library-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
|
|
1213
|
+
.rk-library-viewer-stage{position:relative;width:min(92vw,calc(93vh * 9 / 16));aspect-ratio:9/16;max-height:100%;margin:0;border-radius:var(--rk-r-3xl);overflow:hidden;background:#000;box-shadow:var(--rk-shadow-xl)}
|
|
1214
|
+
.rk-library-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
|
1215
|
+
video.rk-library-viewer-media{cursor:pointer}
|
|
1216
|
+
.rk-library-viewer-pause{position:absolute;inset:0;z-index:2;display:none;place-items:center;color:rgba(255,255,255,.92);pointer-events:none;filter:drop-shadow(0 4px 14px rgba(0,0,0,.55))}
|
|
1217
|
+
.rk-library-viewer-pause svg{width:64px;height:64px}
|
|
1218
|
+
.rk-library-viewer-slide.is-paused .rk-library-viewer-pause{display:grid}
|
|
1219
|
+
.rk-library-viewer-shade{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.22) 62%,transparent 100%)}
|
|
1220
|
+
.rk-library-viewer-info{position:absolute;left:0;right:0;bottom:0;z-index:3;padding:20px;display:grid;gap:8px;justify-items:start;pointer-events:none}
|
|
1221
|
+
.rk-library-viewer-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.12rem;line-height:1.2;letter-spacing:-.01em;color:#fff}
|
|
1222
|
+
.rk-library-viewer-desc{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
1223
|
+
.rk-library-viewer-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px;pointer-events:auto}
|
|
1224
|
+
.rk-library-viewer-cta{flex:none}
|
|
1225
|
+
.rk-library-viewer .rk-btn-ghost-light{background:rgba(255,255,255,.14);color:#fff;border-color:rgba(255,255,255,.24);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:none}
|
|
1226
|
+
.rk-library-viewer .rk-btn-ghost-light:hover{background:rgba(255,255,255,.26);border-color:rgba(255,255,255,.4)}
|
|
1227
|
+
.rk-library-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
|
|
1228
|
+
.rk-library-viewer-btn{width:44px;height:44px;border-radius:var(--rk-r-full);border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.12);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;display:grid;place-items:center;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
|
|
1229
|
+
.rk-library-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
|
|
1230
|
+
.rk-library-viewer-btn span{display:grid;place-items:center;line-height:0}
|
|
1231
|
+
.rk-library-viewer .rk-lvm-off{display:none}
|
|
1232
|
+
.rk-library-viewer.is-muted .rk-lvm-on{display:none}
|
|
1233
|
+
.rk-library-viewer.is-muted .rk-lvm-off{display:grid}
|
|
1234
|
+
.rk-library-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
|
|
1235
|
+
@media(hover:none){.rk-library-viewer-nav{display:none}}
|
|
1236
|
+
@media(max-width:640px){
|
|
1237
|
+
.rk-library-viewer-nav{display:none}
|
|
1238
|
+
.rk-library-viewer-slide{padding:0}
|
|
1239
|
+
.rk-library-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
|
|
1240
|
+
}
|
|
452
1241
|
|
|
453
1242
|
/* ── clips tab ── */
|
|
454
1243
|
.rk-clips-toolbar{display:grid;gap:16px;margin-bottom:24px}
|
|
455
|
-
.rk-clips-
|
|
456
|
-
.rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 340px;min-width:240px;margin:0}
|
|
1244
|
+
.rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 300px;min-width:240px;margin:0}
|
|
457
1245
|
.rk-clips-search-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
458
1246
|
.rk-clips-search-input{flex:1;padding-left:40px;border-radius:var(--rk-r-full)}
|
|
459
1247
|
.rk-clips-search-btn{flex:none}
|
|
460
1248
|
.rk-clips-scan{flex:none}
|
|
461
|
-
.rk-clips-chips{display:flex;flex-wrap:wrap;gap:4px}
|
|
462
|
-
.rk-clips-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 13px;border-radius:var(--rk-r-full);border:1px solid transparent;background:transparent;font-family:var(--rk-font-body);font-size:13px;font-weight:600;color:var(--rk-text-muted);cursor:pointer;white-space:nowrap;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
463
|
-
.rk-clips-chip:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
464
|
-
.rk-clips-chip.is-active{background:var(--rk-ink);color:#fff}
|
|
465
1249
|
.rk-clips-status{font-size:13px;color:var(--rk-text-muted);font-weight:500}
|
|
466
|
-
.rk-clips-
|
|
1250
|
+
.rk-clips-status code{font-family:var(--rk-font-mono,monospace);font-size:12px;background:var(--rk-n-100);padding:1px 5px;border-radius:6px}
|
|
1251
|
+
|
|
1252
|
+
/* ── raws file-directory explorer ── */
|
|
1253
|
+
/* breadcrumb bar (replaces the old count/summary double-row) */
|
|
1254
|
+
.rk-raws-bar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;min-height:30px;margin-bottom:18px}
|
|
1255
|
+
.rk-raws-crumbs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
|
1256
|
+
.rk-raws-crumb{display:inline-flex;align-items:center;gap:5px;font-size:14px;font-weight:600;color:var(--rk-text-muted);max-width:52ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1257
|
+
.rk-raws-crumb.is-current{color:var(--rk-ink);font-family:var(--rk-font-display);font-weight:800;letter-spacing:-.01em}
|
|
1258
|
+
.rk-raws-crumb-link{border:0;background:none;padding:4px 8px 4px 6px;margin-left:-6px;border-radius:var(--rk-r-full);cursor:pointer;font-family:inherit;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
1259
|
+
.rk-raws-crumb-link:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
1260
|
+
.rk-raws-crumb-link svg{flex:none}
|
|
1261
|
+
.rk-raws-crumb-sep{color:var(--rk-text-faint);font-weight:500}
|
|
1262
|
+
.rk-raws-crumb-n{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:19px;padding:0 6px;border-radius:var(--rk-r-full);background:var(--rk-n-100);color:var(--rk-text-muted);font-family:var(--rk-font-mono,monospace);font-size:11px;font-weight:700}
|
|
1263
|
+
.rk-raws-bar .rk-clips-status{margin-left:auto;text-align:right}
|
|
1264
|
+
|
|
1265
|
+
/* the explorer grid switches between clip cards (default) and folder tiles */
|
|
1266
|
+
.rk-raws-explorer{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
|
|
1267
|
+
.rk-raws-explorer.is-folders{grid-template-columns:repeat(auto-fill,minmax(228px,1fr))}
|
|
1268
|
+
|
|
1269
|
+
/* folder tile — 2×2 thumbnail mosaic + name + count */
|
|
1270
|
+
.rk-raws-folder{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card);cursor:pointer;text-align:left;font:inherit;color:inherit;padding:0;transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
|
|
1271
|
+
.rk-raws-folder:hover{transform:translateY(-3px);box-shadow:var(--rk-shadow-md);border-color:var(--rk-gold-600)}
|
|
1272
|
+
.rk-raws-folder-mosaic{position:relative;display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;gap:2px;aspect-ratio:4/3;background:var(--rk-n-100);overflow:hidden}
|
|
1273
|
+
.rk-raws-folder-mosaic.is-single{grid-template-columns:1fr}
|
|
1274
|
+
.rk-raws-folder-mosaic.is-empty{display:grid;grid-template-columns:1fr;place-items:center;color:var(--rk-n-400);background:linear-gradient(150deg,var(--rk-n-100),var(--rk-n-50))}
|
|
1275
|
+
.rk-raws-folder-mosaic.is-empty svg{width:38px;height:38px}
|
|
1276
|
+
.rk-raws-folder-cell{display:block;background-size:cover;background-position:center;background-repeat:no-repeat;background-color:#171717}
|
|
1277
|
+
.rk-raws-folder-meta{display:flex;align-items:center;gap:10px;padding:13px 14px}
|
|
1278
|
+
.rk-raws-folder-ic{flex:none;color:var(--rk-gold-700);display:flex}
|
|
1279
|
+
.rk-raws-folder-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1280
|
+
.rk-raws-folder-name{font-family:var(--rk-font-display);font-size:14px;font-weight:700;color:var(--rk-ink);letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1281
|
+
.rk-raws-folder-count{font-size:12px;font-weight:600;color:var(--rk-text-muted)}
|
|
1282
|
+
|
|
1283
|
+
/* mouseover preview popover (floating enlarged media) */
|
|
1284
|
+
.rk-raws-hover{position:fixed;z-index:90;width:250px;pointer-events:none;border-radius:var(--rk-r-2xl);overflow:hidden;background:#0d0d0d;border:1px solid rgba(255,255,255,.12);box-shadow:var(--rk-shadow-xl);opacity:0;transform:scale(.96);transition:opacity .12s var(--rk-ease),transform .12s var(--rk-ease)}
|
|
1285
|
+
.rk-raws-hover.is-visible{opacity:1;transform:scale(1)}
|
|
1286
|
+
.rk-raws-hover[hidden]{display:none}
|
|
1287
|
+
.rk-raws-hover-vid{display:block;width:100%;aspect-ratio:3/4;object-fit:cover;background:#0d0d0d}
|
|
1288
|
+
.rk-raws-hover-cap{padding:9px 12px 11px;font-size:12px;font-weight:600;line-height:1.35;color:rgba(255,255,255,.92);background:linear-gradient(180deg,#171717,#0d0d0d);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
1289
|
+
@media(hover:none){.rk-raws-hover{display:none!important}}
|
|
1290
|
+
|
|
1291
|
+
.rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
|
|
467
1292
|
.rk-clips-card{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card)}
|
|
468
|
-
.rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center}
|
|
469
|
-
.rk-clips-
|
|
470
|
-
.rk-clips-
|
|
471
|
-
.rk-clips-
|
|
472
|
-
.rk-clips-
|
|
1293
|
+
.rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
|
|
1294
|
+
.rk-clips-thumb-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1295
|
+
.rk-clips-thumb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none}
|
|
1296
|
+
.rk-clips-noimg{color:rgba(255,255,255,.68);font-size:12px;font-weight:600;letter-spacing:.01em}
|
|
1297
|
+
.rk-clips-dur{position:absolute;left:10px;bottom:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums;box-shadow:var(--rk-shadow-xs);z-index:2}
|
|
1298
|
+
.rk-clips-play{position:absolute;right:10px;top:10px;width:34px;height:34px;display:grid;place-items:center;padding:0 0 0 2px;border:0;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:13px;cursor:pointer;opacity:0;transform:scale(.85);box-shadow:var(--rk-shadow-sm);z-index:2;transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
473
1299
|
.rk-clips-card:hover .rk-clips-play{opacity:1;transform:scale(1)}
|
|
1300
|
+
@media(hover:none){.rk-clips-play{opacity:1;transform:scale(1)}}
|
|
474
1301
|
.rk-clips-play:hover{background:var(--rk-gold-500)}
|
|
475
1302
|
.rk-clips-body{display:flex;flex-direction:column;gap:10px;flex:1;padding:16px}
|
|
476
|
-
.rk-clips-title{font-family:var(--rk-font-display);font-size:
|
|
477
|
-
.rk-clips-
|
|
1303
|
+
.rk-clips-title{font-family:var(--rk-font-display);font-size:14px;font-weight:700;line-height:1.34;color:var(--rk-ink);letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
1304
|
+
.rk-clips-tags{display:flex;flex-wrap:wrap;gap:5px}
|
|
1305
|
+
.rk-clips-tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--rk-r-full);background:var(--rk-gold-tint);color:var(--rk-n-700);font-size:11px;font-weight:600;white-space:nowrap}
|
|
478
1306
|
.rk-clips-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;border-top:1px solid var(--rk-border);padding-top:12px}
|
|
479
1307
|
.rk-clips-src{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--rk-text-muted);font-size:12px;font-weight:500}
|
|
480
|
-
.rk-clips-dl{flex:none;color:var(--rk-gold-700);font-size:12px;font-weight:700}
|
|
1308
|
+
.rk-clips-dl{flex:none;color:var(--rk-gold-700);font-size:12px;font-weight:700;background:none;border:0;padding:0;font-family:inherit;cursor:pointer}
|
|
481
1309
|
.rk-clips-dl:hover{color:var(--rk-gold-500)}
|
|
482
|
-
|
|
483
|
-
.rk-clips-
|
|
484
|
-
|
|
485
|
-
.rk-clips-
|
|
486
|
-
.rk-clips-
|
|
487
|
-
|
|
488
|
-
.rk-clips-
|
|
1310
|
+
/* the whole thumb is a button that opens the scroll-snap viewer */
|
|
1311
|
+
button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer}
|
|
1312
|
+
/* optimistic "importing…" placeholder card */
|
|
1313
|
+
.rk-clips-importing .rk-clips-thumb{aspect-ratio:3/4}
|
|
1314
|
+
.rk-clips-importing-spin{width:36px;height:36px;border-radius:var(--rk-r-full);border:3px solid rgba(255,255,255,.26);border-top-color:#fff;animation:rk-clips-spin .8s linear infinite}
|
|
1315
|
+
@keyframes rk-clips-spin{to{transform:rotate(360deg)}}
|
|
1316
|
+
.rk-clips-importing.is-error .rk-clips-thumb{background:linear-gradient(150deg,#5b2b23,#3a1a15)}
|
|
1317
|
+
.rk-clips-importing-x{color:rgba(255,255,255,.9);display:grid;place-items:center}
|
|
1318
|
+
.rk-clips-importing.is-error .rk-clips-title{color:#8d3b2f}
|
|
1319
|
+
.rk-clips-importing-tag{color:var(--rk-text-faint)!important;cursor:default}
|
|
1320
|
+
/* submit spinner while the request is in flight, before the modal closes */
|
|
1321
|
+
.rk-clips-dialog .rk-btn.is-loading{position:relative;color:transparent!important;pointer-events:none}
|
|
1322
|
+
.rk-clips-dialog .rk-btn.is-loading::after{content:"";position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:var(--rk-r-full);border:2px solid rgba(0,0,0,.32);border-top-color:var(--rk-ink);animation:rk-clips-spin .7s linear infinite}
|
|
489
1323
|
.rk-clips-empty{display:grid;place-items:center;gap:10px;text-align:center;padding:64px 20px;background:#fff;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-3xl)}
|
|
490
1324
|
.rk-clips-empty[hidden]{display:none}
|
|
491
1325
|
.rk-clips-empty h3{font-size:var(--rk-text-xl)}
|
|
492
|
-
.rk-clips-dialog{width:min(34rem,calc(100vw - 2rem));padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl)}
|
|
1326
|
+
.rk-clips-dialog{width:min(34rem,calc(100vw - 2rem));max-height:calc(100dvh - 2rem);padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl);overflow:hidden}
|
|
493
1327
|
.rk-clips-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
|
|
494
|
-
|
|
1328
|
+
/* scroll the form (not the page) so Cancel / Import stay reachable when the
|
|
1329
|
+
"Clip raws" advanced block expands on short / landscape-phone viewports */
|
|
1330
|
+
.rk-clips-dialog-form{display:grid;gap:16px;margin:0;padding:28px;max-height:calc(100dvh - 2rem);overflow-y:auto}
|
|
495
1331
|
.rk-clips-dialog-grid{gap:14px}
|
|
1332
|
+
.rk-clips-toggle{display:flex;gap:10px;align-items:flex-start;cursor:pointer;font-size:.92rem;line-height:1.4;color:var(--rk-n-700);background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);padding:12px 14px}
|
|
1333
|
+
.rk-clips-toggle input{margin-top:2px;flex:0 0 auto;accent-color:var(--rk-gold);width:16px;height:16px;cursor:pointer}
|
|
1334
|
+
.rk-clips-toggle b{color:var(--rk-ink)}
|
|
1335
|
+
.rk-clips-advanced{display:grid;gap:16px}
|
|
1336
|
+
.rk-clips-advanced[hidden]{display:none}
|
|
496
1337
|
.rk-clips-dialog-note{background:var(--rk-gold-tint);border-radius:var(--rk-r-xl);padding:10px 13px;color:var(--rk-n-700)}
|
|
1338
|
+
.rk-clips-dialog-note[data-tone="error"]{background:#fdecea;color:#8d3b2f}
|
|
1339
|
+
.rk-clips-dialog-note[data-tone="success"]{background:#e8f7ee;color:#1f7a44}
|
|
497
1340
|
.rk-clips-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
1341
|
+
/* ── files tab: full-page mount of the shared directory-explorer component ──
|
|
1342
|
+
The component (.rk-dir + .rk-aichat-* rows) is tuned for the narrow chat
|
|
1343
|
+
drawer; on a full page we wrap it in a card and scale the search, crumbs and
|
|
1344
|
+
rows up so it reads as a first-class file explorer. Scoped to
|
|
1345
|
+
.rk-files-explorer so the drawer instance is untouched. */
|
|
1346
|
+
.rk-files-explorer{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);overflow:hidden;min-height:60vh;display:flex;flex-direction:column}
|
|
1347
|
+
.rk-files-explorer .rk-dir{flex:1;min-height:60vh}
|
|
1348
|
+
.rk-files-explorer .rk-dir-search{padding:18px 20px 12px}
|
|
1349
|
+
.rk-files-explorer .rk-dir-search-input{font-size:15px;padding:12px 16px;border-radius:var(--rk-r-full)}
|
|
1350
|
+
.rk-files-explorer .rk-aichat-crumbs,.rk-files-explorer .rk-dir-crumbs{padding:4px 22px 12px;gap:2px;border-bottom:1px solid var(--rk-border)}
|
|
1351
|
+
.rk-files-explorer .rk-aichat-crumb{font-size:14px;padding:4px 9px}
|
|
1352
|
+
.rk-files-explorer .rk-aichat-crumb-sep{font-size:14px}
|
|
1353
|
+
.rk-files-explorer .rk-dir-newfolder{font-size:12.5px;padding:5px 11px}
|
|
1354
|
+
.rk-files-explorer .rk-dir-selbar{margin:12px 18px 0;padding:9px 14px}
|
|
1355
|
+
.rk-files-explorer .rk-aichat-files-body,.rk-files-explorer .rk-dir-body{padding:12px 14px 20px;gap:3px}
|
|
1356
|
+
.rk-files-explorer .rk-aichat-frow{padding:11px 12px;border-radius:var(--rk-r-xl)}
|
|
1357
|
+
.rk-files-explorer .rk-aichat-fic{width:44px;height:44px;border-radius:var(--rk-r-lg)}
|
|
1358
|
+
.rk-files-explorer .rk-aichat-fname{font-size:14.5px}
|
|
1359
|
+
.rk-files-explorer .rk-aichat-fmeta{font-size:12.5px}
|
|
1360
|
+
.rk-files-explorer .rk-dir-open{font-size:12.5px}
|
|
1361
|
+
.rk-files-explorer .rk-dir-kebab{width:30px;height:30px}
|
|
1362
|
+
.rk-files-explorer .rk-aichat-drop{margin:0 18px 18px}
|
|
1363
|
+
.rk-files-explorer .rk-aichat-fstate{padding:48px 24px;text-align:center}
|
|
498
1364
|
@media(max-width:620px){
|
|
499
1365
|
.rk-clips-search{flex:1 1 100%}
|
|
500
1366
|
.rk-clips-scan{flex:1 1 100%}
|
|
501
1367
|
.rk-clips-search-input{font-size:16px}
|
|
502
|
-
.rk-clips-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
503
|
-
.rk-
|
|
1368
|
+
.rk-clips-grid,.rk-raws-explorer{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
1369
|
+
.rk-raws-explorer.is-folders{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
1370
|
+
.rk-files-explorer .rk-dir-search-input{font-size:16px}
|
|
504
1371
|
}
|
|
505
1372
|
`;
|
|
1373
|
+
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : isFiles ? " · Files" : "";
|
|
506
1374
|
return reskinDocument({
|
|
507
|
-
title: `vidfarm reskin — Library${
|
|
508
|
-
description: "Reskinned vidfarm library: your
|
|
1375
|
+
title: `vidfarm reskin — Library${tabTitle}`,
|
|
1376
|
+
description: "Reskinned vidfarm library: your published cuts, mined raws library, unified file directory, and job run history.",
|
|
509
1377
|
activeSlug: "library",
|
|
510
|
-
|
|
1378
|
+
account: { name: input.name, email: input.email },
|
|
1379
|
+
pageCss: pageCss + (isLogs ? "\n" + JOBRUNS_PAGE_CSS : ""),
|
|
511
1380
|
body,
|
|
512
|
-
script
|
|
1381
|
+
// Files tab needs no page script — the auto-mounted directory-explorer
|
|
1382
|
+
// component (base module on every full-chrome page) owns all its behaviour.
|
|
1383
|
+
script: isFiles ? undefined : isLogs ? JOBRUNS_SCRIPT : isRaws ? CLIPS_SCRIPT : APPROVED_SCRIPT
|
|
513
1384
|
});
|
|
514
1385
|
}
|
|
515
1386
|
//# sourceMappingURL=library-page.js.map
|