@mevdragon/vidfarm-devcli 0.18.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 +41 -4
- package/SKILL.director.md +17 -3
- package/SKILL.platform.md +3 -3
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1550 -187
- package/dist/src/cli.js +227 -7
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +52 -9
- 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 +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +319 -72
- package/dist/src/reskin/discover-page.js +486 -31
- package/dist/src/reskin/document.js +1171 -384
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-page.js +10 -4
- package/dist/src/reskin/library-page.js +918 -220
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +337 -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
|
@@ -37,6 +37,26 @@ const EMPTY_LIBRARY_INPUT = {
|
|
|
37
37
|
};
|
|
38
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
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>`;
|
|
40
60
|
/* ──────────────────────────── Approved (projects) tab ─────────────────────── */
|
|
41
61
|
const STATUS = {
|
|
42
62
|
ready: { short: "Ready", pill: "rk-pill-green" },
|
|
@@ -68,7 +88,13 @@ function downloadAsset(post) {
|
|
|
68
88
|
const v = post.media.find((a) => isVideoAsset(a));
|
|
69
89
|
return v || primaryAsset(post);
|
|
70
90
|
}
|
|
71
|
-
|
|
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)}`;
|
|
97
|
+
}
|
|
72
98
|
return post.share_url && post.share_url.length
|
|
73
99
|
? post.share_url
|
|
74
100
|
: `/approved/posts/${encodeURIComponent(post.post_id)}`;
|
|
@@ -89,11 +115,8 @@ function posterMedia(post) {
|
|
|
89
115
|
}
|
|
90
116
|
return `<span class="rk-library-noprev">No preview</span>`;
|
|
91
117
|
}
|
|
92
|
-
function projectCard(post, accountId) {
|
|
118
|
+
function projectCard(post, accountId, index) {
|
|
93
119
|
const s = statusInfo(post.status);
|
|
94
|
-
const href = editorHref(post, accountId) || shareHref(post);
|
|
95
|
-
const dl = downloadAsset(post);
|
|
96
|
-
const canDownload = dl && isVideoAsset(dl);
|
|
97
120
|
const title = post.title || "Published render";
|
|
98
121
|
const subLine = post.tracer
|
|
99
122
|
? post.tracer
|
|
@@ -102,29 +125,44 @@ function projectCard(post, accountId) {
|
|
|
102
125
|
: "Ready-to-post cut";
|
|
103
126
|
const searchHay = [title, post.caption, post.tracer, post.template_id].filter(Boolean).join(" ").toLowerCase();
|
|
104
127
|
const editHref = editorHref(post, accountId);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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).
|
|
108
132
|
const actions = [
|
|
109
|
-
|
|
110
|
-
`<a class="rk-btn rk-btn-ghost rk-
|
|
111
|
-
canDownload ? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(dl.url)}" download="${rkEscape(dl.file_name || "render.mp4")}">Download</a>` : ""
|
|
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>` : ""
|
|
112
135
|
].filter(Boolean).join("");
|
|
113
|
-
|
|
114
|
-
|
|
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)}`
|
|
140
|
+
: "";
|
|
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)}">
|
|
115
157
|
${posterMedia(post)}
|
|
116
158
|
<span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
|
|
117
159
|
<span class="rk-library-dur">9:16</span>
|
|
118
160
|
<span class="rk-library-play">${PLAY}</span>
|
|
119
|
-
</
|
|
161
|
+
</button>
|
|
120
162
|
<div class="rk-library-meta">
|
|
121
163
|
<h3 class="rk-library-title">${rkEscape(title)}</h3>
|
|
122
|
-
<div class="rk-library-origin"><span class="rk-library-origin-txt">${rkEscape(subLine)}</span></div>
|
|
123
|
-
<div class="rk-
|
|
124
|
-
<span class="rk-library-time">${rkEscape(fmtDate(post.created_at))}</span>
|
|
125
|
-
${primaryAction}
|
|
126
|
-
</div>
|
|
127
|
-
<div class="rk-library-actions">${actions}</div>
|
|
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>
|
|
128
166
|
</div>
|
|
129
167
|
</article>`;
|
|
130
168
|
}
|
|
@@ -133,108 +171,137 @@ function approvedPanel(input, tabs) {
|
|
|
133
171
|
const accountId = input.currentAccountId;
|
|
134
172
|
// The quickfilter searchbar mirrors the Clips tab's pill search — always
|
|
135
173
|
// rendered so the topbar reads the same on both tabs (disabled when empty).
|
|
174
|
+
const qCount = posts.length;
|
|
136
175
|
const searchBar = `<label class="rk-clips-search rk-library-qsearch">
|
|
137
176
|
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
138
177
|
<input class="rk-input rk-clips-search-input" data-rk-search type="search"
|
|
139
|
-
placeholder="Search
|
|
178
|
+
placeholder="Search ${qCount} approved post${qCount === 1 ? "" : "s"}…" autocomplete="off" aria-label="Search approved posts"${posts.length ? "" : " disabled"} />
|
|
140
179
|
</label>`;
|
|
141
180
|
if (posts.length === 0) {
|
|
142
181
|
return `
|
|
143
182
|
<div class="rk-library-topbar">
|
|
144
183
|
${tabs}
|
|
145
184
|
${searchBar}
|
|
185
|
+
${LOGS_BTN}
|
|
146
186
|
</div>
|
|
147
187
|
|
|
148
188
|
<div class="rk-library-empty is-visible">
|
|
149
189
|
<h3>No published cuts yet</h3>
|
|
150
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>
|
|
151
|
-
<a class="rk-btn rk-btn-gold" href="/
|
|
191
|
+
<a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
|
|
152
192
|
</div>`;
|
|
153
193
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
212
|
return `
|
|
173
213
|
<div class="rk-library-topbar">
|
|
174
214
|
${tabs}
|
|
175
215
|
${searchBar}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<div class="rk-library-toolbar">
|
|
179
|
-
<div class="rk-library-chips" role="tablist" aria-label="Filter by status">${chips}</div>
|
|
180
|
-
<div class="rk-library-summary"><b>${posts.length}</b> published cut${posts.length === 1 ? "" : "s"} · <b>${readyCount}</b> ready to post · <b>${scheduledCount}</b> scheduled</div>
|
|
216
|
+
${LOGS_BTN}
|
|
181
217
|
</div>
|
|
182
218
|
|
|
183
219
|
<div class="rk-library-grid" data-rk-grid>
|
|
184
|
-
${newTile}
|
|
185
220
|
${cards}
|
|
186
221
|
</div>
|
|
187
222
|
|
|
188
223
|
<div class="rk-library-empty" data-rk-empty>
|
|
189
224
|
<h3>Nothing here yet</h3>
|
|
190
|
-
<p class="rk-muted">No cuts match
|
|
191
|
-
<a class="rk-btn rk-btn-gold" href="/
|
|
192
|
-
</div
|
|
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}`;
|
|
193
231
|
}
|
|
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>
|
|
241
|
+
</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>
|
|
245
|
+
</div>
|
|
246
|
+
</div>`;
|
|
194
247
|
/* ──────────────────────────────── Clips tab ───────────────────────────────── */
|
|
195
248
|
// The clip library is NOT part of the library route's input — the client fetches
|
|
196
249
|
// the REAL endpoints on load: GET /clips/feed (library), POST /clips/search
|
|
197
250
|
// (hybrid semantic search), POST /clips/scan + GET /clips/scan/:id (mine a
|
|
198
251
|
// source), download via /clips/:clipId/download.
|
|
199
|
-
// Filter chips double as real one-tap semantic searches (each hits /clips/search
|
|
200
|
-
// with its phrase); "All clips" reloads the feed.
|
|
201
|
-
const CLIP_PRESETS = [
|
|
202
|
-
{ slug: "all", label: "All raws", q: "" },
|
|
203
|
-
{ slug: "reactions", label: "Reactions", q: "someone reacting, a surprised or emotional reaction close-up" },
|
|
204
|
-
{ slug: "hooks", label: "Hooks & hot takes", q: "a strong opening hook or hot take, talking to camera" },
|
|
205
|
-
{ slug: "money", label: "Money shots", q: "money, cash, coins or payment in close-up" },
|
|
206
|
-
{ slug: "talking", label: "Talking head", q: "a person talking to the camera, medium shot" },
|
|
207
|
-
{ slug: "broll", label: "B-roll", q: "b-roll cutaway footage with no one talking" },
|
|
208
|
-
{ slug: "establishing", label: "Establishing", q: "a wide establishing shot of a place" }
|
|
209
|
-
];
|
|
210
252
|
function clipsPanel(tabs) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
214
258
|
return `
|
|
215
259
|
<div class="rk-library-topbar">
|
|
216
260
|
${tabs}
|
|
217
|
-
<form class="rk-clips-search" id="rkClipsSearch">
|
|
261
|
+
<form class="rk-clips-search rk-library-qsearch" id="rkClipsSearch">
|
|
218
262
|
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
219
263
|
<input class="rk-input rk-clips-search-input" id="rkClipsQuery" type="search"
|
|
220
|
-
placeholder="Search
|
|
221
|
-
<button class="rk-btn rk-btn-ink rk-btn-sm rk-clips-search-btn" type="submit">Search</button>
|
|
264
|
+
placeholder="Search raw clips…" autocomplete="off" aria-label="Search raw clips" />
|
|
222
265
|
</form>
|
|
266
|
+
${LOGS_BTN}
|
|
223
267
|
</div>
|
|
224
268
|
|
|
225
|
-
<div class="rk-
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
<div class="rk-library-summary" id="rkClipsSummary">Loading your raws library…</div>
|
|
229
|
-
</div>
|
|
230
|
-
<div class="rk-clips-status" id="rkClipsStatus">Loading your raws…</div>
|
|
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>
|
|
231
272
|
</div>
|
|
232
273
|
|
|
233
|
-
<div class="rk-
|
|
274
|
+
<div class="rk-raws-explorer" id="rkClipsGrid"></div>
|
|
234
275
|
|
|
235
276
|
<div class="rk-clips-empty" id="rkClipsEmpty" hidden>
|
|
236
|
-
<h3>No
|
|
277
|
+
<h3>No raws yet</h3>
|
|
237
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>
|
|
238
305
|
</div>`;
|
|
239
306
|
}
|
|
240
307
|
const CLIPS_DIALOG = `
|
|
@@ -242,19 +309,27 @@ const CLIPS_DIALOG = `
|
|
|
242
309
|
<form class="rk-clips-dialog-form" id="rkClipsScanForm" method="dialog">
|
|
243
310
|
<div class="rk-card-head">
|
|
244
311
|
<span class="rk-eyebrow">Import source</span>
|
|
245
|
-
<h2>
|
|
246
|
-
<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>
|
|
247
314
|
</div>
|
|
248
315
|
<div class="rk-field">
|
|
249
316
|
<label class="rk-label" for="rkScanUrl">Video URL</label>
|
|
250
317
|
<input class="rk-input" id="rkScanUrl" name="url" type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" />
|
|
251
318
|
</div>
|
|
252
319
|
<div class="rk-field">
|
|
253
|
-
<label class="rk-label" for="
|
|
254
|
-
<
|
|
255
|
-
<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" />
|
|
256
322
|
</div>
|
|
257
|
-
<
|
|
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>
|
|
258
333
|
<div class="rk-field">
|
|
259
334
|
<label class="rk-label" for="rkScanProvider">AI provider</label>
|
|
260
335
|
<select class="rk-select" id="rkScanProvider" name="provider">
|
|
@@ -264,15 +339,11 @@ const CLIPS_DIALOG = `
|
|
|
264
339
|
<option value="openrouter">OpenRouter</option>
|
|
265
340
|
</select>
|
|
266
341
|
</div>
|
|
267
|
-
<div class="rk-field">
|
|
268
|
-
<label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
|
|
269
|
-
<input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
|
|
270
|
-
</div>
|
|
271
342
|
</div>
|
|
272
|
-
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">
|
|
343
|
+
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">We'll pull the whole video into your raws library.</p>
|
|
273
344
|
<div class="rk-clips-dialog-actions">
|
|
274
345
|
<button type="button" class="rk-btn rk-btn-ghost" id="rkClipsScanCancel">Cancel</button>
|
|
275
|
-
<button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">
|
|
346
|
+
<button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Import video <span class="rk-arrow">→</span></button>
|
|
276
347
|
</div>
|
|
277
348
|
</form>
|
|
278
349
|
</dialog>`;
|
|
@@ -280,35 +351,28 @@ const CLIPS_DIALOG = `
|
|
|
280
351
|
const APPROVED_SCRIPT = `
|
|
281
352
|
(function(){
|
|
282
353
|
var root=document;
|
|
283
|
-
var grid=root.querySelector('[data-rk-grid]');
|
|
284
354
|
var empty=root.querySelector('[data-rk-empty]');
|
|
285
|
-
var chips=Array.prototype.slice.call(root.querySelectorAll('[data-rk-filter]'));
|
|
286
|
-
var newTile=root.querySelector('[data-rk-newtile]');
|
|
287
355
|
var search=root.querySelector('[data-rk-search]');
|
|
356
|
+
var grid=root.querySelector('[data-rk-grid]');
|
|
288
357
|
var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
|
|
289
|
-
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
|
+
}
|
|
290
363
|
function apply(){
|
|
291
364
|
var q=(search&&search.value||'').trim().toLowerCase();
|
|
292
365
|
var shown=0;
|
|
293
366
|
cards.forEach(function(card){
|
|
294
|
-
|
|
295
|
-
var
|
|
296
|
-
var visible=okStatus&&okSearch;
|
|
367
|
+
if(card.getAttribute('data-removed'))return;
|
|
368
|
+
var visible=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
|
|
297
369
|
card.hidden=!visible;
|
|
298
370
|
if(visible)shown++;
|
|
299
371
|
});
|
|
300
|
-
if(newTile)newTile.hidden=!(activeFilter===''&&!q);
|
|
301
372
|
if(empty)empty.classList.toggle('is-visible',shown===0);
|
|
302
373
|
}
|
|
303
|
-
chips.forEach(function(btn){
|
|
304
|
-
btn.addEventListener('click',function(){
|
|
305
|
-
activeFilter=btn.getAttribute('data-rk-filter')||'';
|
|
306
|
-
chips.forEach(function(b){b.classList.toggle('is-active',b===btn)});
|
|
307
|
-
apply();
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
374
|
if(search)search.addEventListener('input',apply);
|
|
311
|
-
// hover-to-play the real MP4 previews
|
|
375
|
+
// hover-to-play the real MP4 previews on the grid cards
|
|
312
376
|
root.querySelectorAll('video.rk-library-video').forEach(function(v){
|
|
313
377
|
var card=v.closest('.rk-library-card');
|
|
314
378
|
if(!card)return;
|
|
@@ -316,76 +380,529 @@ const APPROVED_SCRIPT = `
|
|
|
316
380
|
card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
|
|
317
381
|
});
|
|
318
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
|
+
}
|
|
319
545
|
})();`;
|
|
320
546
|
const CLIPS_SCRIPT = `
|
|
321
547
|
(function(){
|
|
322
548
|
var root=document;
|
|
323
549
|
var grid=root.getElementById('rkClipsGrid');
|
|
324
550
|
var statusEl=root.getElementById('rkClipsStatus');
|
|
325
|
-
var
|
|
551
|
+
var crumbsEl=root.getElementById('rkRawsCrumbs');
|
|
326
552
|
var empty=root.getElementById('rkClipsEmpty');
|
|
327
|
-
var chips=root.getElementById('rkClipsChips');
|
|
328
553
|
var form=root.getElementById('rkClipsSearch');
|
|
329
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}
|
|
330
568
|
|
|
331
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')+"'"; }
|
|
332
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; }
|
|
333
573
|
function topTags(tags){
|
|
334
574
|
if(!tags||typeof tags!=='object') return [];
|
|
335
575
|
var out=[];
|
|
336
|
-
|
|
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); }); });
|
|
337
579
|
if(tags.energy) out.push('energy:'+tags.energy);
|
|
338
|
-
return out.slice(0,
|
|
580
|
+
return out.slice(0,5);
|
|
339
581
|
}
|
|
340
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);
|
|
598
|
+
});
|
|
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
|
+
}
|
|
341
640
|
|
|
342
|
-
function card(clip){
|
|
641
|
+
function card(clip,i){
|
|
343
642
|
var range=fmtClock(clip.start_time_sec)+'–'+fmtClock(clip.end_time_sec);
|
|
344
643
|
var dur=(clip.duration_sec!=null)?(Number(clip.duration_sec).toFixed(1)+'s'):'';
|
|
345
644
|
var media=clip.thumbnail_url
|
|
346
645
|
? '<img class="rk-clips-thumb-img" loading="lazy" src="'+esc(clip.thumbnail_url)+'" alt="" />'
|
|
347
646
|
: '<span class="rk-clips-noimg">No preview</span>';
|
|
348
|
-
var vid=clip.view_url ? '<video class="rk-clips-thumb-vid" muted loop playsinline preload="none" src="'+esc(clip.view_url)+'"></video>' : '';
|
|
349
647
|
var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
|
|
350
648
|
var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
|
|
351
649
|
var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
|
|
352
650
|
return '<article class="rk-clips-card rk-card-hover">'
|
|
353
|
-
+ '<
|
|
651
|
+
+ '<button type="button" class="rk-clips-thumb" data-rk-clip-view="'+i+'" aria-label="Open raw">'+media
|
|
354
652
|
+ '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
|
|
355
|
-
+ '<
|
|
356
|
-
+ '</
|
|
653
|
+
+ '<span class="rk-clips-play" aria-hidden="true">'+${JSON.stringify(PLAY)}+'</span>'
|
|
654
|
+
+ '</button>'
|
|
357
655
|
+ '<div class="rk-clips-body">'
|
|
358
656
|
+ '<h3 class="rk-clips-title">'+esc(desc)+'</h3>'
|
|
359
657
|
+ (tags?'<div class="rk-clips-tags">'+tags+'</div>':'')
|
|
360
|
-
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+
|
|
361
|
-
+ '<a class="rk-clips-dl" href="'+dl+'">Download</a></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>'
|
|
362
660
|
+ '</div></article>';
|
|
363
661
|
}
|
|
364
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
|
+
}
|
|
365
693
|
function bindHover(){
|
|
366
|
-
if(!grid)return;
|
|
367
|
-
Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb')).forEach(function(thumb){
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
704
|
});
|
|
373
705
|
}
|
|
706
|
+
if(hoverEl){
|
|
707
|
+
window.addEventListener('scroll',hideHover,true);
|
|
708
|
+
window.addEventListener('resize',hideHover);
|
|
709
|
+
}
|
|
374
710
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
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('');
|
|
384
738
|
setGridEmpty(false);
|
|
385
|
-
|
|
386
|
-
if(
|
|
387
|
-
|
|
388
|
-
|
|
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')));
|
|
876
|
+
});
|
|
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();
|
|
905
|
+
});
|
|
389
906
|
}
|
|
390
907
|
|
|
391
908
|
function handleAuth(res){
|
|
@@ -393,7 +910,6 @@ const CLIPS_SCRIPT = `
|
|
|
393
910
|
if(grid)grid.innerHTML='';
|
|
394
911
|
setGridEmpty(false);
|
|
395
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>.';
|
|
396
|
-
if(summaryEl)summaryEl.textContent='Sign in to view your raws library';
|
|
397
913
|
throw new Error('unauthorized');
|
|
398
914
|
}
|
|
399
915
|
return res.json();
|
|
@@ -401,37 +917,31 @@ const CLIPS_SCRIPT = `
|
|
|
401
917
|
|
|
402
918
|
function loadFeed(){
|
|
403
919
|
if(statusEl)statusEl.textContent='Loading your raws…';
|
|
404
|
-
fetch('/raws/feed?limit=
|
|
405
|
-
.then(handleAuth).then(function(d){
|
|
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(){});
|
|
406
927
|
}
|
|
407
928
|
|
|
408
|
-
function search(q
|
|
929
|
+
function search(q){
|
|
409
930
|
if(statusEl)statusEl.textContent='Searching…';
|
|
410
931
|
fetch('/raws/search',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({query:q,limit:60})})
|
|
411
|
-
.then(handleAuth).then(function(d){
|
|
412
|
-
var lbl=(d.semantic?'semantic ':'')+'results for '+String.fromCharCode(8220)+(label||q)+String.fromCharCode(8221)+' — ';
|
|
413
|
-
render(d.results,lbl);
|
|
414
|
-
}).catch(function(){});
|
|
932
|
+
.then(handleAuth).then(function(d){ paintSearch(d.results||[],q); }).catch(function(){});
|
|
415
933
|
}
|
|
416
934
|
|
|
417
935
|
if(form){
|
|
418
936
|
form.addEventListener('submit',function(e){
|
|
419
937
|
e.preventDefault();
|
|
420
938
|
var q=(query&&query.value||'').trim();
|
|
421
|
-
if(
|
|
422
|
-
if(q)search(q,null); else { var all=chips&&chips.querySelector('[data-filter="all"]'); if(all)all.classList.add('is-active'); loadFeed(); }
|
|
939
|
+
if(q)search(q); else paintRoot();
|
|
423
940
|
});
|
|
424
941
|
}
|
|
425
|
-
if(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if(!btn)return;
|
|
429
|
-
chips.querySelectorAll('.rk-clips-chip').forEach(function(b){b.classList.toggle('is-active',b===btn);});
|
|
430
|
-
var slug=btn.getAttribute('data-filter');
|
|
431
|
-
var q=btn.getAttribute('data-q')||'';
|
|
432
|
-
if(query)query.value='';
|
|
433
|
-
if(slug==='all'||!q)loadFeed(); else search(q,btn.textContent.trim());
|
|
434
|
-
});
|
|
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(); } });
|
|
435
945
|
}
|
|
436
946
|
|
|
437
947
|
// ── Scan a source (real clip hunt) ─────────────────────────────────────
|
|
@@ -445,9 +955,49 @@ const CLIPS_SCRIPT = `
|
|
|
445
955
|
var scanTracer=root.getElementById('rkScanTracer');
|
|
446
956
|
var scanNote=root.getElementById('rkScanNote');
|
|
447
957
|
var scanSubmit=root.getElementById('rkClipsScanSubmit');
|
|
448
|
-
var
|
|
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
|
|
449
963
|
function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
|
|
450
|
-
|
|
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(); }); }); }
|
|
451
1001
|
if(cancelBtn&&dialog){ cancelBtn.addEventListener('click',function(){ if(dialog.open)dialog.close(); }); }
|
|
452
1002
|
if(dialog){
|
|
453
1003
|
dialog.addEventListener('click',function(e){
|
|
@@ -455,49 +1005,41 @@ const CLIPS_SCRIPT = `
|
|
|
455
1005
|
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();
|
|
456
1006
|
});
|
|
457
1007
|
}
|
|
458
|
-
function pollScan(scanId){
|
|
459
|
-
if(scanPoll)clearInterval(scanPoll);
|
|
460
|
-
var tries=0;
|
|
461
|
-
scanPoll=setInterval(function(){
|
|
462
|
-
tries++;
|
|
463
|
-
if(tries>200){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Still scanning in the background — check back shortly.'; return; }
|
|
464
|
-
fetch('/raws/scan/'+encodeURIComponent(scanId),{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
465
|
-
.then(function(r){ return r.ok?r.json():null; })
|
|
466
|
-
.then(function(d){
|
|
467
|
-
if(!d)return;
|
|
468
|
-
var src=d.source||{};
|
|
469
|
-
var status=src.status||(d.scan&&d.scan.status)||'running';
|
|
470
|
-
if(status==='complete'){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Import complete — refreshing your raws…'; loadFeed(); }
|
|
471
|
-
else if(status==='failed'||status==='error'){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Import failed: '+(src.error||(d.scan&&d.scan.error)||'the source could not be scanned.'); }
|
|
472
|
-
else if(statusEl){ statusEl.textContent='Scanning source… ('+status+')'; }
|
|
473
|
-
}).catch(function(){});
|
|
474
|
-
},4000);
|
|
475
|
-
}
|
|
476
1008
|
if(scanForm){
|
|
477
1009
|
scanForm.addEventListener('submit',function(e){
|
|
478
1010
|
e.preventDefault();
|
|
479
1011
|
var url=(scanUrl&&scanUrl.value||'').trim();
|
|
480
|
-
var prompt=(scanPrompt&&scanPrompt.value||'').trim();
|
|
481
1012
|
var tracer=(scanTracer&&scanTracer.value||'').trim();
|
|
482
|
-
var
|
|
1013
|
+
var clip=!!(scanClip&&scanClip.checked);
|
|
1014
|
+
var prompt=clip?(scanPrompt&&scanPrompt.value||'').trim():'';
|
|
1015
|
+
var provider=clip?(scanProvider&&scanProvider.value||'').trim():'';
|
|
483
1016
|
if(!url){ setNote('Enter a video URL.','error'); return; }
|
|
484
|
-
setNote('Starting scan…');
|
|
485
|
-
if(scanSubmit)scanSubmit.disabled=true;
|
|
486
|
-
|
|
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; }
|
|
487
1022
|
if(tracer)payload.tracer=tracer;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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);
|
|
501
1043
|
});
|
|
502
1044
|
}
|
|
503
1045
|
|
|
@@ -506,10 +1048,11 @@ const CLIPS_SCRIPT = `
|
|
|
506
1048
|
export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPUT) {
|
|
507
1049
|
const isRaws = tab === "raws";
|
|
508
1050
|
const isLogs = tab === "logs";
|
|
1051
|
+
const isFiles = tab === "files";
|
|
509
1052
|
const tabDef = [
|
|
510
1053
|
{ key: "approved", label: "Approved", href: "/library/approved" },
|
|
511
1054
|
{ key: "raws", label: "Raws", href: "/library/raws" },
|
|
512
|
-
{ key: "
|
|
1055
|
+
{ key: "files", label: "Files", href: "/library/files" }
|
|
513
1056
|
];
|
|
514
1057
|
const tabs = `<div class="rk-library-tabs" role="tablist" aria-label="Library view">
|
|
515
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 ")}
|
|
@@ -524,19 +1067,23 @@ export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPU
|
|
|
524
1067
|
};
|
|
525
1068
|
const headSub = isLogs
|
|
526
1069
|
? "Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost."
|
|
527
|
-
:
|
|
528
|
-
? "
|
|
529
|
-
:
|
|
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.";
|
|
530
1075
|
const headCta = isRaws
|
|
531
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>`
|
|
532
|
-
: isLogs
|
|
1077
|
+
: isLogs || isFiles
|
|
533
1078
|
? ""
|
|
534
|
-
: `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/
|
|
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>`;
|
|
535
1080
|
const panel = isLogs
|
|
536
1081
|
? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
|
|
537
|
-
:
|
|
538
|
-
?
|
|
539
|
-
:
|
|
1082
|
+
: isFiles
|
|
1083
|
+
? filesPanel(tabs)
|
|
1084
|
+
: isRaws
|
|
1085
|
+
? clipsPanel(tabs)
|
|
1086
|
+
: approvedPanel(input, tabs);
|
|
540
1087
|
const body = `
|
|
541
1088
|
<main class="rk-container-wide rk-page rk-library-page">
|
|
542
1089
|
<div class="rk-spread rk-page-head rk-library-head">
|
|
@@ -561,6 +1108,13 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
561
1108
|
|
|
562
1109
|
/* topbar row: primary tab toggle + the tab's quick search share one line */
|
|
563
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}}
|
|
564
1118
|
|
|
565
1119
|
/* primary tab toggle (Approved | Clips) */
|
|
566
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)}
|
|
@@ -572,7 +1126,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
572
1126
|
/* stats — sits right-aligned in the chips row */
|
|
573
1127
|
.rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;line-height:1.7}
|
|
574
1128
|
.rk-library-summary b{color:var(--rk-ink);font-weight:800;font-family:var(--rk-font-display);font-size:15px;letter-spacing:-.01em}
|
|
575
|
-
.rk-library-toolbar .rk-library-summary
|
|
1129
|
+
.rk-library-toolbar .rk-library-summary{margin-left:auto}
|
|
576
1130
|
|
|
577
1131
|
/* toolbar (filter chips + count) */
|
|
578
1132
|
.rk-library-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:24px}
|
|
@@ -589,7 +1143,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
589
1143
|
.rk-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px}
|
|
590
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)}
|
|
591
1145
|
.rk-library-card[hidden]{display:none}
|
|
592
|
-
.rk-library-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:linear-gradient(158deg,#2c2c2c,#171717)}
|
|
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}
|
|
593
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}
|
|
594
1148
|
.rk-library-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#050505}
|
|
595
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}
|
|
@@ -598,16 +1152,39 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
598
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)}
|
|
599
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)}
|
|
600
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)}}
|
|
601
1156
|
.rk-library-play svg{margin-left:2px}
|
|
602
|
-
.rk-library-meta{display:grid;gap:
|
|
1157
|
+
.rk-library-meta{display:grid;gap:9px;padding:14px 14px 15px}
|
|
603
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}
|
|
604
|
-
.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}
|
|
605
1161
|
.rk-library-origin-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
606
|
-
.rk-library-
|
|
607
|
-
|
|
608
|
-
.rk-library-
|
|
609
|
-
.rk-library-
|
|
610
|
-
.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}
|
|
611
1188
|
|
|
612
1189
|
/* new-project tile */
|
|
613
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)}
|
|
@@ -625,21 +1202,93 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
625
1202
|
|
|
626
1203
|
@media(prefers-reduced-motion:reduce){.rk-library-video{}}
|
|
627
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
|
+
}
|
|
1241
|
+
|
|
628
1242
|
/* ── clips tab ── */
|
|
629
1243
|
.rk-clips-toolbar{display:grid;gap:16px;margin-bottom:24px}
|
|
630
|
-
.rk-clips-chipsrow{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
|
|
631
1244
|
.rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 300px;min-width:240px;margin:0}
|
|
632
1245
|
.rk-clips-search-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
633
1246
|
.rk-clips-search-input{flex:1;padding-left:40px;border-radius:var(--rk-r-full)}
|
|
634
1247
|
.rk-clips-search-btn{flex:none}
|
|
635
1248
|
.rk-clips-scan{flex:none}
|
|
636
|
-
.rk-clips-chips{display:flex;flex-wrap:wrap;gap:4px}
|
|
637
|
-
.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)}
|
|
638
|
-
.rk-clips-chip:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
639
|
-
.rk-clips-chip.is-active{background:var(--rk-ink);color:#fff}
|
|
640
1249
|
.rk-clips-status{font-size:13px;color:var(--rk-text-muted);font-weight:500}
|
|
641
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}
|
|
642
|
-
|
|
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}
|
|
643
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)}
|
|
644
1293
|
.rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
|
|
645
1294
|
.rk-clips-thumb-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
@@ -648,6 +1297,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
648
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}
|
|
649
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)}
|
|
650
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)}}
|
|
651
1301
|
.rk-clips-play:hover{background:var(--rk-gold-500)}
|
|
652
1302
|
.rk-clips-body{display:flex;flex-direction:column;gap:10px;flex:1;padding:16px}
|
|
653
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}
|
|
@@ -655,34 +1305,82 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
655
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}
|
|
656
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}
|
|
657
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}
|
|
658
|
-
.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}
|
|
659
1309
|
.rk-clips-dl:hover{color:var(--rk-gold-500)}
|
|
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}
|
|
660
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)}
|
|
661
1324
|
.rk-clips-empty[hidden]{display:none}
|
|
662
1325
|
.rk-clips-empty h3{font-size:var(--rk-text-xl)}
|
|
663
|
-
.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}
|
|
664
1327
|
.rk-clips-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
|
|
665
|
-
|
|
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}
|
|
666
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}
|
|
667
1337
|
.rk-clips-dialog-note{background:var(--rk-gold-tint);border-radius:var(--rk-r-xl);padding:10px 13px;color:var(--rk-n-700)}
|
|
668
1338
|
.rk-clips-dialog-note[data-tone="error"]{background:#fdecea;color:#8d3b2f}
|
|
669
1339
|
.rk-clips-dialog-note[data-tone="success"]{background:#e8f7ee;color:#1f7a44}
|
|
670
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}
|
|
671
1364
|
@media(max-width:620px){
|
|
672
1365
|
.rk-clips-search{flex:1 1 100%}
|
|
673
1366
|
.rk-clips-scan{flex:1 1 100%}
|
|
674
1367
|
.rk-clips-search-input{font-size:16px}
|
|
675
|
-
.rk-clips-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
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}
|
|
676
1371
|
}
|
|
677
1372
|
`;
|
|
678
|
-
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : "";
|
|
1373
|
+
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : isFiles ? " · Files" : "";
|
|
679
1374
|
return reskinDocument({
|
|
680
1375
|
title: `vidfarm reskin — Library${tabTitle}`,
|
|
681
|
-
description: "Reskinned vidfarm library: your published cuts, mined raws library, and job run history.",
|
|
1376
|
+
description: "Reskinned vidfarm library: your published cuts, mined raws library, unified file directory, and job run history.",
|
|
682
1377
|
activeSlug: "library",
|
|
1378
|
+
account: { name: input.name, email: input.email },
|
|
683
1379
|
pageCss: pageCss + (isLogs ? "\n" + JOBRUNS_PAGE_CSS : ""),
|
|
684
1380
|
body,
|
|
685
|
-
|
|
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
|
|
686
1384
|
});
|
|
687
1385
|
}
|
|
688
1386
|
//# sourceMappingURL=library-page.js.map
|