@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0
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/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- 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 +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- 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 +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- 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 +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -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 +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- 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/public/assets/placeholders/scene-placeholder.png +0 -0
- 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,49 @@ 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
|
+
// Folder identity for the client-side folder view: the real folder_path if the
|
|
156
|
+
// post has one, else its tracer (a synthetic group that materializes a folder
|
|
157
|
+
// when renamed). data-poster fuels the folder-tile mosaic.
|
|
158
|
+
const folder = (post.folder_path || "").replace(/^\/+|\/+$/g, "");
|
|
159
|
+
const posterUrl = (post.media.find((a) => isVideoAsset(a)) || post.media.find((a) => isImageAsset(a)) || primaryAsset(post))?.url || "";
|
|
160
|
+
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)}" data-folder="${rkEscape(folder)}" data-tracer="${rkEscape(post.tracer || "")}" data-poster="${rkEscape(posterUrl)}">
|
|
161
|
+
<button type="button" class="rk-library-poster" data-rk-view="${index}" aria-label="Play ${rkEscape(title)}">
|
|
115
162
|
${posterMedia(post)}
|
|
116
163
|
<span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
|
|
117
164
|
<span class="rk-library-dur">9:16</span>
|
|
118
165
|
<span class="rk-library-play">${PLAY}</span>
|
|
119
|
-
</
|
|
166
|
+
</button>
|
|
120
167
|
<div class="rk-library-meta">
|
|
121
168
|
<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>
|
|
169
|
+
<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>
|
|
170
|
+
<div class="rk-library-actions">${actions}${menu}</div>
|
|
128
171
|
</div>
|
|
129
172
|
</article>`;
|
|
130
173
|
}
|
|
@@ -133,108 +176,143 @@ function approvedPanel(input, tabs) {
|
|
|
133
176
|
const accountId = input.currentAccountId;
|
|
134
177
|
// The quickfilter searchbar mirrors the Clips tab's pill search — always
|
|
135
178
|
// rendered so the topbar reads the same on both tabs (disabled when empty).
|
|
179
|
+
const qCount = posts.length;
|
|
136
180
|
const searchBar = `<label class="rk-clips-search rk-library-qsearch">
|
|
137
181
|
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
138
182
|
<input class="rk-input rk-clips-search-input" data-rk-search type="search"
|
|
139
|
-
placeholder="Search
|
|
183
|
+
placeholder="Search ${qCount} approved post${qCount === 1 ? "" : "s"}…" autocomplete="off" aria-label="Search approved posts"${posts.length ? "" : " disabled"} />
|
|
140
184
|
</label>`;
|
|
141
185
|
if (posts.length === 0) {
|
|
142
186
|
return `
|
|
143
187
|
<div class="rk-library-topbar">
|
|
144
188
|
${tabs}
|
|
145
189
|
${searchBar}
|
|
190
|
+
${LOGS_BTN}
|
|
146
191
|
</div>
|
|
147
192
|
|
|
148
193
|
<div class="rk-library-empty is-visible">
|
|
149
194
|
<h3>No published cuts yet</h3>
|
|
150
195
|
<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="/
|
|
196
|
+
<a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
|
|
152
197
|
</div>`;
|
|
153
198
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
199
|
+
const cards = posts.map((p, i) => projectCard(p, accountId, i)).join("");
|
|
200
|
+
// Data the client viewer needs to build the vertical scroll deck (order MUST
|
|
201
|
+
// match the cards' data-rk-view index). Videos play in the modal; image-only
|
|
202
|
+
// posts show their poster still.
|
|
203
|
+
const viewerData = posts.map((p) => {
|
|
204
|
+
const asset = primaryAsset(p);
|
|
205
|
+
const isVideo = Boolean(asset && isVideoAsset(asset));
|
|
206
|
+
const poster = p.media.find((a) => isImageAsset(a));
|
|
207
|
+
return {
|
|
208
|
+
title: p.title || "Published render",
|
|
209
|
+
sub: p.tracer || (p.template_id ? `Composition ${p.template_id}` : "Ready-to-post cut"),
|
|
210
|
+
url: asset ? asset.url : "",
|
|
211
|
+
is_video: isVideo,
|
|
212
|
+
poster: poster ? poster.url : "",
|
|
213
|
+
post_href: shareHref(p, accountId),
|
|
214
|
+
editor_href: editorHref(p, accountId)
|
|
215
|
+
};
|
|
216
|
+
});
|
|
172
217
|
return `
|
|
173
218
|
<div class="rk-library-topbar">
|
|
174
219
|
${tabs}
|
|
175
220
|
${searchBar}
|
|
221
|
+
${LOGS_BTN}
|
|
176
222
|
</div>
|
|
177
223
|
|
|
178
|
-
<div class="rk-library-
|
|
179
|
-
<
|
|
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>
|
|
224
|
+
<div class="rk-raws-bar rk-library-foldbar" data-rk-approved-bar data-rk-account="${rkEscape(accountId)}" hidden>
|
|
225
|
+
<nav class="rk-raws-crumbs" data-rk-approved-crumbs aria-label="Breadcrumb"></nav>
|
|
181
226
|
</div>
|
|
182
227
|
|
|
183
228
|
<div class="rk-library-grid" data-rk-grid>
|
|
184
|
-
${newTile}
|
|
185
229
|
${cards}
|
|
186
230
|
</div>
|
|
187
231
|
|
|
188
232
|
<div class="rk-library-empty" data-rk-empty>
|
|
189
233
|
<h3>Nothing here yet</h3>
|
|
190
|
-
<p class="rk-muted">No cuts match
|
|
191
|
-
<a class="rk-btn rk-btn-gold" href="/
|
|
192
|
-
</div
|
|
234
|
+
<p class="rk-muted">No cuts match your search. Try a different term or spin up a fresh render.</p>
|
|
235
|
+
<a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<script type="application/json" data-rk-library-data>${JSON.stringify(viewerData).replace(/</g, "\\u003c")}</script>
|
|
239
|
+
${LIBRARY_VIEWER_HTML}`;
|
|
193
240
|
}
|
|
241
|
+
// Fullscreen TikTok-style viewer shell — ported from the /discover reskin so the
|
|
242
|
+
// library modal reads identically. The client fills [data-viewer-track] on open.
|
|
243
|
+
const LIBRARY_VIEWER_HTML = `
|
|
244
|
+
<div class="rk-library-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Published cut viewer">
|
|
245
|
+
<div class="rk-library-viewer-scrim" data-viewer-close></div>
|
|
246
|
+
<div class="rk-library-viewer-track" data-viewer-track></div>
|
|
247
|
+
<div class="rk-library-viewer-ui">
|
|
248
|
+
<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>
|
|
249
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="rk-library-viewer-nav">
|
|
252
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-prev aria-label="Previous cut">${CHEV_UP_ICON}</button>
|
|
253
|
+
<button type="button" class="rk-library-viewer-btn" data-viewer-next aria-label="Next cut">${CHEV_DOWN_ICON}</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>`;
|
|
194
256
|
/* ──────────────────────────────── Clips tab ───────────────────────────────── */
|
|
195
257
|
// The clip library is NOT part of the library route's input — the client fetches
|
|
196
258
|
// the REAL endpoints on load: GET /clips/feed (library), POST /clips/search
|
|
197
259
|
// (hybrid semantic search), POST /clips/scan + GET /clips/scan/:id (mine a
|
|
198
260
|
// 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
261
|
function clipsPanel(tabs) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
262
|
+
// Raws render as a file-directory explorer: raws are foldered by their source
|
|
263
|
+
// video (source_video_id / source_filename). The root shows folders; opening a
|
|
264
|
+
// folder shows its raws as big scannable thumbnails; typing a query flips to a
|
|
265
|
+
// flat semantic-search results view. The clip count lives in the search
|
|
266
|
+
// placeholder (kept in sync by the client) — no separate summary/count row.
|
|
214
267
|
return `
|
|
215
268
|
<div class="rk-library-topbar">
|
|
216
269
|
${tabs}
|
|
217
|
-
<form class="rk-clips-search" id="rkClipsSearch">
|
|
270
|
+
<form class="rk-clips-search rk-library-qsearch" id="rkClipsSearch">
|
|
218
271
|
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
219
272
|
<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>
|
|
273
|
+
placeholder="Search raw clips…" autocomplete="off" aria-label="Search raw clips" />
|
|
222
274
|
</form>
|
|
275
|
+
${LOGS_BTN}
|
|
223
276
|
</div>
|
|
224
277
|
|
|
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>
|
|
278
|
+
<div class="rk-raws-bar">
|
|
279
|
+
<nav class="rk-raws-crumbs" id="rkRawsCrumbs" aria-label="Breadcrumb"></nav>
|
|
280
|
+
<div class="rk-clips-status" id="rkClipsStatus"></div>
|
|
231
281
|
</div>
|
|
232
282
|
|
|
233
|
-
<div class="rk-
|
|
283
|
+
<div class="rk-raws-explorer" id="rkClipsGrid"></div>
|
|
234
284
|
|
|
235
285
|
<div class="rk-clips-empty" id="rkClipsEmpty" hidden>
|
|
236
|
-
<h3>No
|
|
286
|
+
<h3>No raws yet</h3>
|
|
237
287
|
<p class="rk-muted">Click <b>Import source</b> to mine a long video into tagged, searchable short clips.</p>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
${LIBRARY_VIEWER_HTML}`;
|
|
291
|
+
}
|
|
292
|
+
/* ──────────────────────────────── Files tab ───────────────────────────────── */
|
|
293
|
+
// The Files tab is the full-page mount of the reusable file-directory explorer
|
|
294
|
+
// (src/frontend/file-directory.ts). It opens at the virtual root so the three
|
|
295
|
+
// backends read as folders — Files / Temp / Raws — with the component's own
|
|
296
|
+
// unified search (semantic vector + substring + absolute-path, mode=auto),
|
|
297
|
+
// breadcrumb navigation, multi-select, and drag-drop upload. The bundle
|
|
298
|
+
// (/assets/file-directory-app.js) already ships on every full-chrome reskin
|
|
299
|
+
// page and auto-mounts any `[data-rk-directory]`, so no page script is needed;
|
|
300
|
+
// preview routes through the chrome's `window.__rkDirectoryHost.onPreview`.
|
|
301
|
+
function filesPanel(tabs, cloudAvailable = false, initialPath) {
|
|
302
|
+
// `?path=` deep-links straight into a folder (e.g. /library/files?path=/approved/swipes).
|
|
303
|
+
// The client's parseDirectoryPath only accepts the known roots, so any junk or
|
|
304
|
+
// traversal falls back to the virtual root "/" — safe to reflect verbatim.
|
|
305
|
+
const initialAttr = initialPath && initialPath.trim()
|
|
306
|
+
? ` data-initial-path="${rkEscape(initialPath.trim())}"`
|
|
307
|
+
: "";
|
|
308
|
+
return `
|
|
309
|
+
<div class="rk-library-topbar">
|
|
310
|
+
${tabs}
|
|
311
|
+
${LOGS_BTN}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div class="rk-files-explorer">
|
|
315
|
+
<div data-rk-directory data-mode="standalone" data-show-upload="true" data-show-multiselect="true" data-primary-click="preview"${cloudAvailable ? ` data-cloud-available="true"` : ""}${initialAttr}></div>
|
|
238
316
|
</div>`;
|
|
239
317
|
}
|
|
240
318
|
const CLIPS_DIALOG = `
|
|
@@ -242,19 +320,27 @@ const CLIPS_DIALOG = `
|
|
|
242
320
|
<form class="rk-clips-dialog-form" id="rkClipsScanForm" method="dialog">
|
|
243
321
|
<div class="rk-card-head">
|
|
244
322
|
<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
|
|
323
|
+
<h2>Import a video</h2>
|
|
324
|
+
<p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll open the Clipper to preview it and save an exact subrange to your raws.</p>
|
|
247
325
|
</div>
|
|
248
326
|
<div class="rk-field">
|
|
249
327
|
<label class="rk-label" for="rkScanUrl">Video URL</label>
|
|
250
328
|
<input class="rk-input" id="rkScanUrl" name="url" type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" />
|
|
251
329
|
</div>
|
|
252
330
|
<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>
|
|
331
|
+
<label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
|
|
332
|
+
<input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
|
|
256
333
|
</div>
|
|
257
|
-
<
|
|
334
|
+
<label class="rk-clips-toggle" for="rkScanClip">
|
|
335
|
+
<input type="checkbox" id="rkScanClip" name="clip" />
|
|
336
|
+
<span><b>Clip raws with AI</b> — auto-mine this video into matching moments, instead of hand-picking a subrange in the Clipper.</span>
|
|
337
|
+
</label>
|
|
338
|
+
<div class="rk-clips-advanced" id="rkClipsAdvanced" hidden>
|
|
339
|
+
<div class="rk-field">
|
|
340
|
+
<label class="rk-label" for="rkScanPrompt">What to clip for</label>
|
|
341
|
+
<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>
|
|
342
|
+
<span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
|
|
343
|
+
</div>
|
|
258
344
|
<div class="rk-field">
|
|
259
345
|
<label class="rk-label" for="rkScanProvider">AI provider</label>
|
|
260
346
|
<select class="rk-select" id="rkScanProvider" name="provider">
|
|
@@ -264,15 +350,11 @@ const CLIPS_DIALOG = `
|
|
|
264
350
|
<option value="openrouter">OpenRouter</option>
|
|
265
351
|
</select>
|
|
266
352
|
</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
353
|
</div>
|
|
272
|
-
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">
|
|
354
|
+
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">We'll pull the whole video into your raws library.</p>
|
|
273
355
|
<div class="rk-clips-dialog-actions">
|
|
274
356
|
<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">
|
|
357
|
+
<button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Open clipper <span class="rk-arrow">→</span></button>
|
|
276
358
|
</div>
|
|
277
359
|
</form>
|
|
278
360
|
</dialog>`;
|
|
@@ -280,112 +362,773 @@ const CLIPS_DIALOG = `
|
|
|
280
362
|
const APPROVED_SCRIPT = `
|
|
281
363
|
(function(){
|
|
282
364
|
var root=document;
|
|
283
|
-
var grid=root.querySelector('[data-rk-grid]');
|
|
284
365
|
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
366
|
var search=root.querySelector('[data-rk-search]');
|
|
367
|
+
var grid=root.querySelector('[data-rk-grid]');
|
|
368
|
+
var foldBar=root.querySelector('[data-rk-approved-bar]');
|
|
369
|
+
var crumbsEl=root.querySelector('[data-rk-approved-crumbs]');
|
|
370
|
+
var account=(foldBar&&foldBar.getAttribute('data-rk-account'))||'';
|
|
288
371
|
var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
|
|
289
|
-
var
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
cards.forEach(function(card){
|
|
294
|
-
var okStatus=activeFilter===''||card.getAttribute('data-status')===activeFilter;
|
|
295
|
-
var okSearch=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
|
|
296
|
-
var visible=okStatus&&okSearch;
|
|
297
|
-
card.hidden=!visible;
|
|
298
|
-
if(visible)shown++;
|
|
299
|
-
});
|
|
300
|
-
if(newTile)newTile.hidden=!(activeFilter===''&&!q);
|
|
301
|
-
if(empty)empty.classList.toggle('is-visible',shown===0);
|
|
372
|
+
var total=cards.length;
|
|
373
|
+
function syncPlaceholder(){
|
|
374
|
+
if(!search)return;
|
|
375
|
+
search.setAttribute('placeholder','Search '+total+' approved post'+(total===1?'':'s')+String.fromCharCode(8230));
|
|
302
376
|
}
|
|
303
|
-
|
|
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
|
-
if(search)search.addEventListener('input',apply);
|
|
311
|
-
// hover-to-play the real MP4 previews
|
|
377
|
+
// hover-to-play the real MP4 previews on the grid cards
|
|
312
378
|
root.querySelectorAll('video.rk-library-video').forEach(function(v){
|
|
313
379
|
var card=v.closest('.rk-library-card');
|
|
314
380
|
if(!card)return;
|
|
315
381
|
card.addEventListener('mouseenter',function(){ if(v.play){var p=v.play(); if(p&&p.catch)p.catch(function(){});} });
|
|
316
382
|
card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
|
|
317
383
|
});
|
|
384
|
+
|
|
385
|
+
/* ── folder view (mirrors the Raws tab): group cards by folder_path, fall back
|
|
386
|
+
to tracer for legacy posts; drill into a folder; search flattens. ─────── */
|
|
387
|
+
function cssUrl(u){ return "'"+String(u||'').replace(/['")\\\\]/g,'').replace(/\\s/g,'%20')+"'"; }
|
|
388
|
+
function activeCards(){ return cards.filter(function(c){ return !c.getAttribute('data-removed'); }); }
|
|
389
|
+
var FOLDERS=[], LOOSE=[], MODE='root', CUR='';
|
|
390
|
+
function metaOf(card){
|
|
391
|
+
var fp=(card.getAttribute('data-folder')||'').replace(/^\\/+|\\/+$/g,'');
|
|
392
|
+
var tr=card.getAttribute('data-tracer')||'';
|
|
393
|
+
var key = fp ? ('fp:'+fp) : (tr ? ('tr:'+tr) : '');
|
|
394
|
+
return { card:card, fp:fp, tr:tr, key:key, poster:card.getAttribute('data-poster')||'', postId:card.getAttribute('data-post-id')||'' };
|
|
395
|
+
}
|
|
396
|
+
function buildFolders(){
|
|
397
|
+
var map={}; FOLDERS=[]; LOOSE=[];
|
|
398
|
+
activeCards().forEach(function(card){
|
|
399
|
+
var m=metaOf(card);
|
|
400
|
+
if(!m.key){ LOOSE.push(m); return; }
|
|
401
|
+
var f=map[m.key];
|
|
402
|
+
if(!f){
|
|
403
|
+
f={ id:m.key, folderPath:m.fp||null, tracer:m.tr, name:(m.fp||m.tr||'Untitled'), items:[] };
|
|
404
|
+
map[m.key]=f; FOLDERS.push(f);
|
|
405
|
+
}
|
|
406
|
+
f.items.push(m);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function folderById(id){ for(var i=0;i<FOLDERS.length;i++){ if(FOLDERS[i].id===id)return FOLDERS[i]; } return null; }
|
|
410
|
+
function clearTiles(){ Array.prototype.slice.call(grid.querySelectorAll('[data-approved-tile]')).forEach(function(t){ t.parentNode&&t.parentNode.removeChild(t); }); }
|
|
411
|
+
function folderTileEl(f){
|
|
412
|
+
var thumbs=f.items.filter(function(m){ return m.poster; }).slice(0,4);
|
|
413
|
+
var mosaic = thumbs.length
|
|
414
|
+
? '<span class="rk-raws-folder-mosaic'+(thumbs.length===1?' is-single':'')+'">'+thumbs.map(function(m){ return '<span class="rk-raws-folder-cell" style="background-image:url('+cssUrl(m.poster)+')"></span>'; }).join('')+'</span>'
|
|
415
|
+
: '<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
|
|
416
|
+
var n=f.items.length;
|
|
417
|
+
var canonPath='/approved'+(f.folderPath?'/'+f.folderPath:(f.tracer?'/'+f.tracer:''));
|
|
418
|
+
var html='<div class="rk-raws-folder-wrap" data-approved-tile>'
|
|
419
|
+
+ '<button type="button" class="rk-raws-folder rk-card-hover" data-approved-folder="'+attr(f.id)+'">'
|
|
420
|
+
+ mosaic
|
|
421
|
+
+ '<span class="rk-raws-folder-meta"><span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
|
|
422
|
+
+ '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(f.name)+'">'+esc(f.name)+'</span>'
|
|
423
|
+
+ '<span class="rk-raws-folder-count">'+n+' post'+(n===1?'':'s')+'</span></span></span>'
|
|
424
|
+
+ '</button>'
|
|
425
|
+
+ '<div class="rk-raws-folder-menu" data-folder-menu>'
|
|
426
|
+
+ '<button type="button" class="rk-raws-folder-kebab" data-approved-fkebab aria-label="Folder actions" aria-haspopup="true" aria-expanded="false">'+${JSON.stringify(ICON_MORE)}+'</button>'
|
|
427
|
+
+ '<div class="rk-raws-folder-pop" data-folder-pop hidden role="menu">'
|
|
428
|
+
+ '<button type="button" role="menuitem" data-approved-frename="'+attr(f.id)+'">Rename folder</button>'
|
|
429
|
+
+ '<button type="button" role="menuitem" data-folder-copy="'+attr(canonPath)+'">Copy path</button>'
|
|
430
|
+
+ '</div></div>'
|
|
431
|
+
+ '</div>';
|
|
432
|
+
var tmp=document.createElement('div'); tmp.innerHTML=html; return tmp.firstChild;
|
|
433
|
+
}
|
|
434
|
+
function renderCrumbs(){
|
|
435
|
+
if(!crumbsEl)return;
|
|
436
|
+
if(MODE==='root'){ crumbsEl.innerHTML='<span class="rk-raws-crumb is-current">All posts</span>'; return; }
|
|
437
|
+
var html='<button type="button" class="rk-raws-crumb rk-raws-crumb-link" data-approved-crumb-root>'+${JSON.stringify(ICON_CHEVLEFT)}+'<span>All posts</span></button>'
|
|
438
|
+
+ '<span class="rk-raws-crumb-sep" aria-hidden="true">/</span>';
|
|
439
|
+
if(MODE==='folder'){ var f=folderById(CUR); html+='<span class="rk-raws-crumb is-current">'+esc(f?f.name:'Folder')+' <span class="rk-raws-crumb-n">'+(f?f.items.length:0)+'</span></span>'; }
|
|
440
|
+
else { html+='<span class="rk-raws-crumb is-current">Search</span>'; }
|
|
441
|
+
crumbsEl.innerHTML=html;
|
|
442
|
+
}
|
|
443
|
+
function showBar(show){ if(foldBar)foldBar.hidden=!show; }
|
|
444
|
+
function setVisible(pred){ var shown=0; activeCards().forEach(function(c){ var v=pred(metaOf(c)); c.hidden=!v; if(v)shown++; }); return shown; }
|
|
445
|
+
function paintRoot(){
|
|
446
|
+
MODE='root'; CUR=''; clearTiles(); buildFolders();
|
|
447
|
+
// folder tiles first (in order, before the cards), then loose cards remain.
|
|
448
|
+
var frag=document.createDocumentFragment();
|
|
449
|
+
FOLDERS.forEach(function(f){ frag.appendChild(folderTileEl(f)); });
|
|
450
|
+
grid.insertBefore(frag,grid.firstChild);
|
|
451
|
+
var looseShown=setVisible(function(m){ return !m.key; });
|
|
452
|
+
showBar(FOLDERS.length>0);
|
|
453
|
+
renderCrumbs();
|
|
454
|
+
if(empty)empty.classList.toggle('is-visible',FOLDERS.length===0&&looseShown===0);
|
|
455
|
+
}
|
|
456
|
+
function paintFolder(id){
|
|
457
|
+
var f=folderById(id); if(!f){ paintRoot(); return; }
|
|
458
|
+
MODE='folder'; CUR=id; clearTiles(); showBar(true); renderCrumbs();
|
|
459
|
+
var shown=setVisible(function(m){ return m.key===id; });
|
|
460
|
+
if(empty)empty.classList.toggle('is-visible',shown===0);
|
|
461
|
+
}
|
|
462
|
+
function paintSearch(q){
|
|
463
|
+
MODE='search'; clearTiles(); showBar(false);
|
|
464
|
+
var shown=setVisible(function(m){ return (m.card.getAttribute('data-title')||'').indexOf(q)!==-1; });
|
|
465
|
+
if(empty)empty.classList.toggle('is-visible',shown===0);
|
|
466
|
+
}
|
|
467
|
+
function repaint(){ if(MODE==='folder'&&folderById(CUR))paintFolder(CUR); else paintRoot(); }
|
|
468
|
+
function apply(){
|
|
469
|
+
var q=(search&&search.value||'').trim().toLowerCase();
|
|
470
|
+
if(q)paintSearch(q); else repaint();
|
|
471
|
+
}
|
|
472
|
+
if(search)search.addEventListener('input',apply);
|
|
473
|
+
|
|
474
|
+
/* ── folder tile clicks: open / rename / copy-path + breadcrumb back ──────── */
|
|
475
|
+
function closeFolderMenus(except){
|
|
476
|
+
Array.prototype.slice.call(grid.querySelectorAll('[data-folder-menu]')).forEach(function(mn){
|
|
477
|
+
if(mn===except)return;
|
|
478
|
+
var pop=mn.querySelector('[data-folder-pop]'); if(pop)pop.hidden=true;
|
|
479
|
+
var kb=mn.querySelector('[data-approved-fkebab]'); if(kb)kb.setAttribute('aria-expanded','false');
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function copyText(txt,ok){ try{ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(txt); } else { var ta=document.createElement('textarea'); ta.value=txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } if(ok)ok(); }catch(e){} }
|
|
483
|
+
function slugFolder(v){ return String(v||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60); }
|
|
484
|
+
function renameApprovedFolder(id){
|
|
485
|
+
var f=folderById(id); if(!f)return;
|
|
486
|
+
var current=f.folderPath?f.folderPath.split('/').pop():(f.tracer||f.name);
|
|
487
|
+
var next=(window.prompt('Rename folder',current||'')||'').trim();
|
|
488
|
+
if(!next||next===current)return;
|
|
489
|
+
if(f.folderPath){
|
|
490
|
+
fetch('/api/v1/user/me/directory/rename',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({path:'/approved/'+f.folderPath,new_name:next})})
|
|
491
|
+
.then(function(r){ return r.ok?r.json():r.json().catch(function(){return{};}).then(function(d){ throw new Error((d&&d.error)||'Rename failed'); }); })
|
|
492
|
+
.then(function(){ window.location.reload(); })
|
|
493
|
+
.catch(function(err){ window.alert((err&&err.message)||'Rename failed'); });
|
|
494
|
+
} else {
|
|
495
|
+
// synthetic tracer group → materialize a real folder by moving each post.
|
|
496
|
+
if(!account){ window.alert('Cannot rename this group here.'); return; }
|
|
497
|
+
var target=slugFolder(next); if(!target){ window.alert('Pick a folder name with letters or numbers.'); return; }
|
|
498
|
+
Promise.all(f.items.map(function(m){
|
|
499
|
+
return fetch('/u/'+encodeURIComponent(account)+'/approved/posts/'+encodeURIComponent(m.postId),{method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({folder_path:target})}).then(function(r){ return r.ok; });
|
|
500
|
+
})).then(function(){ window.location.reload(); }).catch(function(){ window.alert('Rename failed'); });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if(grid){
|
|
504
|
+
grid.addEventListener('click',function(e){
|
|
505
|
+
var t=e.target;
|
|
506
|
+
var kb=t&&t.closest?t.closest('[data-approved-fkebab]'):null;
|
|
507
|
+
if(kb){ e.preventDefault(); e.stopPropagation(); var mn=kb.closest('[data-folder-menu]'); var pop=mn&&mn.querySelector('[data-folder-pop]'); var willOpen=pop&&pop.hidden; closeFolderMenus(mn); if(pop){ pop.hidden=!willOpen; kb.setAttribute('aria-expanded',willOpen?'true':'false'); } return; }
|
|
508
|
+
var rn=t&&t.closest?t.closest('[data-approved-frename]'):null;
|
|
509
|
+
if(rn){ e.preventDefault(); closeFolderMenus(null); renameApprovedFolder(rn.getAttribute('data-approved-frename')); return; }
|
|
510
|
+
var cp=t&&t.closest?t.closest('[data-folder-copy]'):null;
|
|
511
|
+
if(cp){ e.preventDefault(); closeFolderMenus(null); copyText(cp.getAttribute('data-folder-copy')); return; }
|
|
512
|
+
var fo=t&&t.closest?t.closest('[data-approved-folder]'):null;
|
|
513
|
+
if(fo){ e.preventDefault(); closeFolderMenus(null); if(search)search.value=''; paintFolder(fo.getAttribute('data-approved-folder')); return; }
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if(crumbsEl){
|
|
517
|
+
crumbsEl.addEventListener('click',function(e){
|
|
518
|
+
var r=e.target&&e.target.closest?e.target.closest('[data-approved-crumb-root]'):null;
|
|
519
|
+
if(!r)return; e.preventDefault(); if(search)search.value=''; paintRoot();
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
document.addEventListener('click',function(e){ var t=e.target; if(!(t&&t.closest&&t.closest('[data-folder-menu]')))closeFolderMenus(null); });
|
|
523
|
+
|
|
318
524
|
apply();
|
|
525
|
+
|
|
526
|
+
// ── per-card "⋯" overflow menu: Copy ID + Delete (popconfirm) ────────────
|
|
527
|
+
function closeAllMenus(except){
|
|
528
|
+
root.querySelectorAll('[data-rk-menu].is-open').forEach(function(m){
|
|
529
|
+
if(m===except)return;
|
|
530
|
+
m.classList.remove('is-open');
|
|
531
|
+
var pop=m.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=true;
|
|
532
|
+
var cf=m.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
|
|
533
|
+
var btn=m.querySelector('[data-rk-menu-btn]'); if(btn)btn.setAttribute('aria-expanded','false');
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function flashKebab(menu,text){
|
|
537
|
+
var btn=menu&&menu.querySelector('[data-rk-menu-btn]'); if(!btn)return;
|
|
538
|
+
btn.classList.add('is-ok'); var prev=btn.getAttribute('title')||'';
|
|
539
|
+
btn.setAttribute('title',text||'Done');
|
|
540
|
+
setTimeout(function(){ btn.classList.remove('is-ok'); btn.setAttribute('title',prev); },1100);
|
|
541
|
+
}
|
|
542
|
+
document.addEventListener('click',function(e){
|
|
543
|
+
var t=e.target;
|
|
544
|
+
var btn=t&&t.closest?t.closest('[data-rk-menu-btn]'):null;
|
|
545
|
+
if(btn){
|
|
546
|
+
e.preventDefault(); e.stopPropagation();
|
|
547
|
+
var menu=btn.closest('[data-rk-menu]');
|
|
548
|
+
var open=menu.classList.contains('is-open');
|
|
549
|
+
closeAllMenus(open?null:menu);
|
|
550
|
+
if(open){ menu.classList.remove('is-open'); btn.setAttribute('aria-expanded','false'); return; }
|
|
551
|
+
menu.classList.add('is-open'); btn.setAttribute('aria-expanded','true');
|
|
552
|
+
var pop=menu.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=false;
|
|
553
|
+
var cf=menu.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Copy ID
|
|
557
|
+
var copy=t&&t.closest?t.closest('[data-rk-copy-id]'):null;
|
|
558
|
+
if(copy){
|
|
559
|
+
e.preventDefault();
|
|
560
|
+
var id=copy.getAttribute('data-rk-copy-id')||'';
|
|
561
|
+
var menuC=copy.closest('[data-rk-menu]');
|
|
562
|
+
var done=function(){ flashKebab(menuC,'Copied ID'); closeAllMenus(null); };
|
|
563
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(id).then(done,done); }
|
|
564
|
+
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(); }
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Delete → reveal popconfirm
|
|
568
|
+
var del=t&&t.closest?t.closest('[data-rk-del]'):null;
|
|
569
|
+
if(del){
|
|
570
|
+
e.preventDefault();
|
|
571
|
+
var m2=del.closest('[data-rk-menu]');
|
|
572
|
+
var cf2=m2.querySelector('[data-rk-confirm]'); if(cf2)cf2.hidden=false;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Cancel popconfirm
|
|
576
|
+
var no=t&&t.closest?t.closest('[data-rk-confirm-no]'):null;
|
|
577
|
+
if(no){ e.preventDefault(); var mN=no.closest('[data-rk-menu]'); closeAllMenus(null); void mN; return; }
|
|
578
|
+
// Confirm delete → real DELETE
|
|
579
|
+
var yes=t&&t.closest?t.closest('[data-rk-confirm-yes]'):null;
|
|
580
|
+
if(yes){
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
var m3=yes.closest('[data-rk-menu]');
|
|
583
|
+
var card=m3.closest('.rk-library-card');
|
|
584
|
+
var delBtn=m3.querySelector('[data-rk-del]');
|
|
585
|
+
var url=delBtn&&delBtn.getAttribute('data-rk-del')||'';
|
|
586
|
+
if(!url){ window.alert('Cannot delete this post.'); return; }
|
|
587
|
+
yes.disabled=true; yes.classList.add('is-loading');
|
|
588
|
+
fetch(url,{method:'DELETE',credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
589
|
+
.then(function(r){ if(!r.ok)throw new Error('delete failed'); return r.json().catch(function(){return {};}); })
|
|
590
|
+
.then(function(){
|
|
591
|
+
if(card){ card.setAttribute('data-removed','1'); card.hidden=true; card.parentNode&&card.parentNode.removeChild(card); }
|
|
592
|
+
total=Math.max(0,total-1); syncPlaceholder(); apply();
|
|
593
|
+
})
|
|
594
|
+
.catch(function(){ yes.disabled=false; yes.classList.remove('is-loading'); window.alert('Could not delete that post. Please try again.'); });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// click elsewhere closes any open menu
|
|
598
|
+
if(!(t&&t.closest&&t.closest('[data-rk-menu]')))closeAllMenus(null);
|
|
599
|
+
});
|
|
600
|
+
document.addEventListener('keydown',function(e){ if(e.key==='Escape')closeAllMenus(null); });
|
|
601
|
+
|
|
602
|
+
// ── TikTok-style viewer modal (mirrors the /discover reskin viewer) ──────
|
|
603
|
+
var DATA=[]; try{ var el=root.querySelector('[data-rk-library-data]'); if(el) DATA=JSON.parse(el.textContent||'[]'); }catch(e){ DATA=[]; }
|
|
604
|
+
function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
|
|
605
|
+
function attr(t){ return esc(t).replace(/"/g,'"'); }
|
|
606
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
607
|
+
var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
|
|
608
|
+
if(viewer&&vTrack){
|
|
609
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
|
|
610
|
+
function slideHtml(t,i){
|
|
611
|
+
var media=t.is_video
|
|
612
|
+
? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.url)+'"'+(t.poster?' poster="'+attr(t.poster)+'"':'')+' loop playsinline preload="none"></video>'
|
|
613
|
+
: '<img class="rk-library-viewer-media" src="'+attr(t.url||t.poster)+'" alt="'+attr(t.title||'Published cut')+'">';
|
|
614
|
+
var cta=t.editor_href
|
|
615
|
+
? '<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+attr(t.editor_href)+'" target="_blank" rel="noreferrer">Open in editor</a>'
|
|
616
|
+
: '';
|
|
617
|
+
var view=t.post_href
|
|
618
|
+
? '<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>'
|
|
619
|
+
: '';
|
|
620
|
+
return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
|
|
621
|
+
+'<figure class="rk-library-viewer-stage">'
|
|
622
|
+
+ media
|
|
623
|
+
+'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
|
|
624
|
+
+'<div class="rk-library-viewer-shade"></div>'
|
|
625
|
+
+'<div class="rk-library-viewer-info">'
|
|
626
|
+
+'<div class="rk-library-viewer-title">'+esc(t.title)+'</div>'
|
|
627
|
+
+(t.sub?'<div class="rk-library-viewer-desc">'+esc(t.sub)+'</div>':'')
|
|
628
|
+
+'<div class="rk-library-viewer-actions">'+cta+view+'</div>'
|
|
629
|
+
+'</div>'
|
|
630
|
+
+'</figure>'
|
|
631
|
+
+'</section>';
|
|
632
|
+
}
|
|
633
|
+
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')); }
|
|
634
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
635
|
+
function setActive(idx){
|
|
636
|
+
vActive=idx;
|
|
637
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
638
|
+
if(Math.abs(i-idx)<=1) hydrate(s);
|
|
639
|
+
s.classList.remove('is-paused');
|
|
640
|
+
var v=s.querySelector('video'); if(!v)return;
|
|
641
|
+
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(){}); }); }
|
|
642
|
+
else { v.pause(); }
|
|
643
|
+
});
|
|
644
|
+
updateMuteUi();
|
|
645
|
+
}
|
|
646
|
+
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'}); }
|
|
647
|
+
function open(clickedIdx){
|
|
648
|
+
vItems=[]; var start=0;
|
|
649
|
+
DATA.forEach(function(t,i){ if(!t.url&&!t.poster)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
|
|
650
|
+
if(!vItems.length)return;
|
|
651
|
+
vTrack.innerHTML=vItems.map(slideHtml).join('');
|
|
652
|
+
vOpen=true; vSound=true; viewer.hidden=false;
|
|
653
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
654
|
+
if(vIO)vIO.disconnect();
|
|
655
|
+
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;
|
|
656
|
+
if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
657
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
658
|
+
setActive(start);
|
|
659
|
+
}
|
|
660
|
+
function close(){
|
|
661
|
+
if(!vOpen)return; vOpen=false; vActive=-1;
|
|
662
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
663
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
664
|
+
vTrack.innerHTML=''; viewer.hidden=true;
|
|
665
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
666
|
+
}
|
|
667
|
+
// open from a card poster
|
|
668
|
+
document.addEventListener('click',function(e){
|
|
669
|
+
var b=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
|
|
670
|
+
if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-view')));
|
|
671
|
+
});
|
|
672
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
673
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
674
|
+
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'); } } });
|
|
675
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
|
|
676
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
|
|
677
|
+
vTrack.addEventListener('click',function(e){
|
|
678
|
+
var t=e.target;
|
|
679
|
+
if(t&&t.closest&&t.closest('a'))return;
|
|
680
|
+
var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
|
|
681
|
+
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; }
|
|
682
|
+
if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
|
|
683
|
+
});
|
|
684
|
+
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); } });
|
|
685
|
+
window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
|
|
686
|
+
}
|
|
319
687
|
})();`;
|
|
320
688
|
const CLIPS_SCRIPT = `
|
|
321
689
|
(function(){
|
|
322
690
|
var root=document;
|
|
323
691
|
var grid=root.getElementById('rkClipsGrid');
|
|
324
692
|
var statusEl=root.getElementById('rkClipsStatus');
|
|
325
|
-
var
|
|
693
|
+
var crumbsEl=root.getElementById('rkRawsCrumbs');
|
|
326
694
|
var empty=root.getElementById('rkClipsEmpty');
|
|
327
|
-
var chips=root.getElementById('rkClipsChips');
|
|
328
695
|
var form=root.getElementById('rkClipsSearch');
|
|
329
696
|
var query=root.getElementById('rkClipsQuery');
|
|
697
|
+
var canHover=!(window.matchMedia&&window.matchMedia('(hover:none)').matches);
|
|
698
|
+
|
|
699
|
+
// ── file-directory state ────────────────────────────────────────────────
|
|
700
|
+
var ALL=[]; // every raw in the library
|
|
701
|
+
var FOLDERS=[]; // [{id,name,clips:[...]}] grouped by source video
|
|
702
|
+
var VIEW=[]; // clips currently on screen (drives viewer + hover, by index)
|
|
703
|
+
var MODE='root'; // 'root' (folders) | 'folder' | 'search'
|
|
704
|
+
var CUR_FOLDER=''; // active folder id when MODE==='folder'
|
|
705
|
+
var LAST_Q=''; // active query when MODE==='search'
|
|
706
|
+
var PENDING=[]; // optimistic import cards: {id,url,tracer,clip,label,error}
|
|
330
707
|
|
|
331
708
|
function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
|
|
709
|
+
function attr(t){ return esc(t).replace(/"/g,'"'); }
|
|
710
|
+
function cssUrl(u){ return "'"+String(u||'').replace(/['")\\\\]/g,'').replace(/\\s/g,'%20')+"'"; }
|
|
332
711
|
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
712
|
function topTags(tags){
|
|
334
713
|
if(!tags||typeof tags!=='object') return [];
|
|
335
714
|
var out=[];
|
|
336
|
-
|
|
715
|
+
// content_type leads — it's the shot-kind filter the chips key off.
|
|
716
|
+
(tags.content_type||[]).slice(0,2).forEach(function(v){ out.push(v.replace(/_/g,' ')); });
|
|
717
|
+
['subject','action','emotion'].forEach(function(k){ (tags[k]||[]).slice(0,1).forEach(function(v){ out.push(v); }); });
|
|
337
718
|
if(tags.energy) out.push('energy:'+tags.energy);
|
|
338
|
-
return out.slice(0,
|
|
719
|
+
return out.slice(0,5);
|
|
339
720
|
}
|
|
340
721
|
function setGridEmpty(show){ if(empty)empty.hidden=!show; if(grid)grid.style.display=show?'none':''; }
|
|
722
|
+
function syncPlaceholder(){
|
|
723
|
+
if(!query)return;
|
|
724
|
+
var n=ALL.length;
|
|
725
|
+
query.setAttribute('placeholder','Search '+n+' raw clip'+(n===1?'':'s')+String.fromCharCode(8230));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function normFp(v){ return (v==null?'':String(v)).replace(/^\\/+|\\/+$/g,''); }
|
|
729
|
+
function slugFolder(v){ return String(v||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60); }
|
|
730
|
+
|
|
731
|
+
// Group raws into REAL directory folders keyed on each clip's folder_path
|
|
732
|
+
// (canonical /raws/<folder_path>) — the same folders the file directory and the
|
|
733
|
+
// AI agent see, so a folder can be renamed and clips moved between them. Legacy
|
|
734
|
+
// clips with no folder_path fall back to a synthetic source-derived group; those
|
|
735
|
+
// aren't backed by a stored folder yet, so renaming one MATERIALIZES a folder by
|
|
736
|
+
// moving its clips (see renameFolder). First-seen order is preserved.
|
|
737
|
+
function buildFolders(){
|
|
738
|
+
var map={}; FOLDERS=[];
|
|
739
|
+
ALL.forEach(function(c){
|
|
740
|
+
var fp=normFp(c.folder_path);
|
|
741
|
+
var key,f;
|
|
742
|
+
if(fp){ key='fp:'+fp; }
|
|
743
|
+
else { key='src:'+String(c.source_video_id||c.source_filename||'ungrouped'); }
|
|
744
|
+
f=map[key];
|
|
745
|
+
if(!f){
|
|
746
|
+
f={ id:key, folderPath:fp||null, name:fp||(c.source_filename||'Untitled source'), clips:[] };
|
|
747
|
+
map[key]=f; FOLDERS.push(f);
|
|
748
|
+
}
|
|
749
|
+
f.clips.push(c);
|
|
750
|
+
});
|
|
751
|
+
// Float the full-length original video (role:"source") to the top of its
|
|
752
|
+
// folder so it reads as the reference the mined clips were cut from.
|
|
753
|
+
FOLDERS.forEach(function(f){
|
|
754
|
+
f.clips.sort(function(a,b){ return (b.is_source_original?1:0)-(a.is_source_original?1:0); });
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Optimistic "importing…" placeholder card (spinner, or an error state with a
|
|
759
|
+
// Dismiss button). Kept in PENDING and re-prepended on every root repaint so a
|
|
760
|
+
// feed reload never wipes a still-running import.
|
|
761
|
+
function importCardHtml(p){
|
|
762
|
+
var label=p.error?'Import failed':(p.label||'Importing…');
|
|
763
|
+
var sub=p.error?p.error:(p.url||'');
|
|
764
|
+
var badge=p.error
|
|
765
|
+
? '<button type="button" class="rk-clips-dl" data-import-dismiss="'+attr(p.id)+'">Dismiss</button>'
|
|
766
|
+
: '<span class="rk-clips-dl rk-clips-importing-tag">'+(p.clip?'Mining':'Importing')+'</span>';
|
|
767
|
+
return '<article class="rk-clips-card rk-clips-importing'+(p.error?' is-error':'')+'" data-import-id="'+attr(p.id)+'">'
|
|
768
|
+
+ '<div class="rk-clips-thumb">'
|
|
769
|
+
+ (p.error?'<span class="rk-clips-importing-x">'+${JSON.stringify(CROSS_ICON)}+'</span>':'<span class="rk-clips-importing-spin" aria-hidden="true"></span>')
|
|
770
|
+
+ '</div>'
|
|
771
|
+
+ '<div class="rk-clips-body">'
|
|
772
|
+
+ '<h3 class="rk-clips-title">'+esc(label)+'</h3>'
|
|
773
|
+
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(sub)+'">'+esc(sub)+'</span>'+badge+'</div>'
|
|
774
|
+
+ '</div></article>';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Folder tile — a 2×2 thumbnail mosaic + name + clip count.
|
|
778
|
+
function folderCardHtml(folder){
|
|
779
|
+
var thumbs=folder.clips.filter(function(c){ return c.thumbnail_url; }).slice(0,4);
|
|
780
|
+
var mosaic;
|
|
781
|
+
if(thumbs.length){
|
|
782
|
+
mosaic='<span class="rk-raws-folder-mosaic'+(thumbs.length===1?' is-single':'')+'">'
|
|
783
|
+
+ thumbs.map(function(c){ return '<span class="rk-raws-folder-cell" style="background-image:url('+cssUrl(c.thumbnail_url)+')"></span>'; }).join('')
|
|
784
|
+
+ '</span>';
|
|
785
|
+
} else {
|
|
786
|
+
mosaic='<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
|
|
787
|
+
}
|
|
788
|
+
// Count MINED clips only — the full-length original is a reference raw, not a
|
|
789
|
+
// mined scene — and note the presence of a full video alongside the count.
|
|
790
|
+
var hasFull=folder.clips.some(function(c){ return c.is_source_original; });
|
|
791
|
+
var n=folder.clips.filter(function(c){ return !c.is_source_original; }).length;
|
|
792
|
+
var countTxt=n+' clip'+(n===1?'':'s')+(hasFull?' · full video':'');
|
|
793
|
+
var canonPath='/raws'+(folder.folderPath?'/'+folder.folderPath:'');
|
|
794
|
+
// Kebab: rename the folder + copy its canonical /raws path. Sits OUTSIDE the
|
|
795
|
+
// open-folder button (buttons can't nest); its own click is caught before the
|
|
796
|
+
// tile's navigation handler.
|
|
797
|
+
var menu='<div class="rk-raws-folder-menu" data-folder-menu>'
|
|
798
|
+
+ '<button type="button" class="rk-raws-folder-kebab" data-folder-kebab aria-label="Folder actions" aria-haspopup="true" aria-expanded="false">'+${JSON.stringify(ICON_MORE)}+'</button>'
|
|
799
|
+
+ '<div class="rk-raws-folder-pop" data-folder-pop hidden role="menu">'
|
|
800
|
+
+ '<button type="button" role="menuitem" data-folder-rename="'+attr(folder.id)+'">Rename folder</button>'
|
|
801
|
+
+ '<button type="button" role="menuitem" data-folder-copy="'+attr(canonPath)+'">Copy path</button>'
|
|
802
|
+
+ '</div></div>';
|
|
803
|
+
return '<div class="rk-raws-folder-wrap">'
|
|
804
|
+
+ '<button type="button" class="rk-raws-folder rk-card-hover" data-folder-id="'+attr(folder.id)+'">'
|
|
805
|
+
+ mosaic
|
|
806
|
+
+ '<span class="rk-raws-folder-meta">'
|
|
807
|
+
+ '<span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
|
|
808
|
+
+ '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(folder.name)+'">'+esc(folder.name)+'</span>'
|
|
809
|
+
+ '<span class="rk-raws-folder-count">'+countTxt+'</span></span>'
|
|
810
|
+
+ '</span></button>'
|
|
811
|
+
+ menu
|
|
812
|
+
+ '</div>';
|
|
813
|
+
}
|
|
341
814
|
|
|
342
|
-
function card(clip){
|
|
815
|
+
function card(clip,i){
|
|
343
816
|
var range=fmtClock(clip.start_time_sec)+'–'+fmtClock(clip.end_time_sec);
|
|
344
817
|
var dur=(clip.duration_sec!=null)?(Number(clip.duration_sec).toFixed(1)+'s'):'';
|
|
345
818
|
var media=clip.thumbnail_url
|
|
346
819
|
? '<img class="rk-clips-thumb-img" loading="lazy" src="'+esc(clip.thumbnail_url)+'" alt="" />'
|
|
347
820
|
: '<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
821
|
var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
|
|
350
822
|
var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
|
|
351
823
|
var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
|
|
352
|
-
|
|
353
|
-
|
|
824
|
+
// The full-length original a hunt was mined from gets a distinct badge.
|
|
825
|
+
var fullBadge=clip.is_source_original?'<span class="rk-clips-fullbadge">Full video</span>':'';
|
|
826
|
+
return '<article class="rk-clips-card rk-card-hover'+(clip.is_source_original?' is-source-original':'')+'">'
|
|
827
|
+
+ '<button type="button" class="rk-clips-thumb" data-rk-clip-view="'+i+'" aria-label="Open raw">'+media
|
|
828
|
+
+ '<video class="rk-clips-thumb-vid" muted loop playsinline preload="none"'+(clip.thumbnail_url?' poster="'+attr(clip.thumbnail_url)+'"':'')+'></video>'
|
|
829
|
+
+ fullBadge
|
|
354
830
|
+ '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
|
|
355
|
-
+ '<
|
|
356
|
-
+ '</
|
|
831
|
+
+ '<span class="rk-clips-play" aria-hidden="true">'+${JSON.stringify(PLAY)}+'</span>'
|
|
832
|
+
+ '</button>'
|
|
357
833
|
+ '<div class="rk-clips-body">'
|
|
358
834
|
+ '<h3 class="rk-clips-title">'+esc(desc)+'</h3>'
|
|
359
835
|
+ (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>'
|
|
836
|
+
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(clip.source_filename)+'">'+esc(clip.source_filename||'')+'</span>'
|
|
837
|
+
+ '<a class="rk-clips-dl" href="'+dl+'" target="_blank" rel="noopener">Download</a></div>'
|
|
362
838
|
+ '</div></article>';
|
|
363
839
|
}
|
|
364
840
|
|
|
841
|
+
/* ── mouseover: play the clip in place inside its own card ────────────────── */
|
|
842
|
+
var hoverTimer=null;
|
|
843
|
+
var playingThumb=null; // the thumb whose inline <video> is currently active
|
|
844
|
+
function stopInline(thumb){
|
|
845
|
+
if(!thumb)return;
|
|
846
|
+
var vid=thumb.querySelector('.rk-clips-thumb-vid');
|
|
847
|
+
if(vid){ try{ vid.pause(); }catch(e){} vid.removeAttribute('src'); vid.load&&vid.load(); }
|
|
848
|
+
thumb.classList.remove('is-playing');
|
|
849
|
+
}
|
|
850
|
+
function hideHover(){
|
|
851
|
+
if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
|
|
852
|
+
if(playingThumb){ stopInline(playingThumb); playingThumb=null; }
|
|
853
|
+
}
|
|
854
|
+
function playInline(thumb,clip){
|
|
855
|
+
if(!thumb||!clip||!clip.view_url)return;
|
|
856
|
+
if(playingThumb&&playingThumb!==thumb){ stopInline(playingThumb); }
|
|
857
|
+
playingThumb=thumb;
|
|
858
|
+
var vid=thumb.querySelector('.rk-clips-thumb-vid');
|
|
859
|
+
if(!vid)return;
|
|
860
|
+
if(clip.thumbnail_url) vid.poster=clip.thumbnail_url;
|
|
861
|
+
vid.setAttribute('src',clip.view_url); vid.load&&vid.load();
|
|
862
|
+
thumb.classList.add('is-playing');
|
|
863
|
+
var p=vid.play&&vid.play(); if(p&&p.catch)p.catch(function(){});
|
|
864
|
+
}
|
|
365
865
|
function bindHover(){
|
|
366
|
-
if(!grid)return;
|
|
367
|
-
Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb')).forEach(function(thumb){
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
866
|
+
if(!grid||!canHover)return;
|
|
867
|
+
Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb[data-rk-clip-view]')).forEach(function(thumb){
|
|
868
|
+
var idx=Number(thumb.getAttribute('data-rk-clip-view'));
|
|
869
|
+
thumb.addEventListener('mouseenter',function(){
|
|
870
|
+
var clip=VIEW[idx]; if(!clip)return;
|
|
871
|
+
if(hoverTimer)clearTimeout(hoverTimer);
|
|
872
|
+
hoverTimer=setTimeout(function(){ playInline(thumb,clip); },90);
|
|
873
|
+
});
|
|
874
|
+
thumb.addEventListener('mouseleave',function(){
|
|
875
|
+
if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
|
|
876
|
+
stopInline(thumb);
|
|
877
|
+
if(playingThumb===thumb) playingThumb=null;
|
|
878
|
+
});
|
|
372
879
|
});
|
|
373
880
|
}
|
|
881
|
+
window.addEventListener('scroll',hideHover,true);
|
|
374
882
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
883
|
+
/* ── breadcrumb + view rendering ──────────────────────────────────────────── */
|
|
884
|
+
function renderCrumbs(){
|
|
885
|
+
if(!crumbsEl)return;
|
|
886
|
+
var html='';
|
|
887
|
+
if(MODE==='root'){
|
|
888
|
+
html='<span class="rk-raws-crumb is-current">All raws</span>';
|
|
889
|
+
} else {
|
|
890
|
+
html='<button type="button" class="rk-raws-crumb rk-raws-crumb-link" data-crumb-root>'+${JSON.stringify(ICON_CHEVLEFT)}+'<span>All raws</span></button>';
|
|
891
|
+
html+='<span class="rk-raws-crumb-sep" aria-hidden="true">/</span>';
|
|
892
|
+
if(MODE==='folder'){
|
|
893
|
+
var f=folderById(CUR_FOLDER);
|
|
894
|
+
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>';
|
|
895
|
+
} else {
|
|
896
|
+
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>';
|
|
897
|
+
}
|
|
383
898
|
}
|
|
899
|
+
crumbsEl.innerHTML=html;
|
|
900
|
+
}
|
|
901
|
+
function folderById(id){ for(var i=0;i<FOLDERS.length;i++){ if(FOLDERS[i].id===id)return FOLDERS[i]; } return null; }
|
|
902
|
+
|
|
903
|
+
function paintRoot(){
|
|
904
|
+
MODE='root'; CUR_FOLDER=''; VIEW=[];
|
|
905
|
+
if(!grid)return;
|
|
906
|
+
buildFolders();
|
|
907
|
+
grid.classList.add('is-folders');
|
|
908
|
+
if(FOLDERS.length===0 && PENDING.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent=''; return; }
|
|
909
|
+
grid.innerHTML=PENDING.map(importCardHtml).join('')+FOLDERS.map(folderCardHtml).join('');
|
|
384
910
|
setGridEmpty(false);
|
|
385
|
-
|
|
386
|
-
if(
|
|
387
|
-
|
|
388
|
-
|
|
911
|
+
renderCrumbs();
|
|
912
|
+
if(statusEl)statusEl.textContent=FOLDERS.length+' folder'+(FOLDERS.length===1?'':'s');
|
|
913
|
+
}
|
|
914
|
+
function paintFolder(id){
|
|
915
|
+
var f=folderById(id); if(!f){ paintRoot(); return; }
|
|
916
|
+
MODE='folder'; CUR_FOLDER=id; VIEW=f.clips;
|
|
917
|
+
if(!grid)return;
|
|
918
|
+
grid.classList.remove('is-folders');
|
|
919
|
+
grid.innerHTML=VIEW.map(card).join('');
|
|
920
|
+
setGridEmpty(VIEW.length===0);
|
|
921
|
+
renderCrumbs(); bindHover();
|
|
922
|
+
if(statusEl)statusEl.textContent='';
|
|
923
|
+
}
|
|
924
|
+
function paintSearch(results,q){
|
|
925
|
+
MODE='search'; LAST_Q=q; VIEW=results||[];
|
|
926
|
+
if(!grid)return;
|
|
927
|
+
grid.classList.remove('is-folders');
|
|
928
|
+
if(VIEW.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent='No raws match '+String.fromCharCode(8220)+q+String.fromCharCode(8221)+'.'; return; }
|
|
929
|
+
grid.innerHTML=VIEW.map(card).join('');
|
|
930
|
+
setGridEmpty(false);
|
|
931
|
+
renderCrumbs(); bindHover();
|
|
932
|
+
if(statusEl)statusEl.textContent='';
|
|
933
|
+
}
|
|
934
|
+
// Repaint whatever view is active (used after an optimistic import lands).
|
|
935
|
+
function repaint(){
|
|
936
|
+
if(MODE==='folder')paintFolder(CUR_FOLDER);
|
|
937
|
+
else if(MODE==='search')paintSearch(VIEW,LAST_Q);
|
|
938
|
+
else paintRoot();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* ── optimistic import lifecycle ─────────────────────────────────────────── */
|
|
942
|
+
function getPending(id){ for(var i=0;i<PENDING.length;i++){ if(PENDING[i].id===id)return PENDING[i]; } return null; }
|
|
943
|
+
function removeImport(id){ PENDING=PENDING.filter(function(p){ return p.id!==id; }); }
|
|
944
|
+
function updateImportCard(id){
|
|
945
|
+
var p=getPending(id); if(!p)return;
|
|
946
|
+
var el=grid&&grid.querySelector('[data-import-id="'+id+'"]');
|
|
947
|
+
if(el){ el.outerHTML=importCardHtml(p); } else { repaint(); }
|
|
948
|
+
}
|
|
949
|
+
function finishImport(id){ removeImport(id); loadFeed(); }
|
|
950
|
+
function failImport(id,msg){ var p=getPending(id); if(p){ p.error=msg; } updateImportCard(id); if(statusEl)statusEl.textContent=msg; }
|
|
951
|
+
function onScanResult(id,clip,res){
|
|
952
|
+
if(!res.ok){ failImport(id,(res.data&&res.data.error)||(clip?'Could not start the scan.':'Could not import that video.')); return; }
|
|
953
|
+
var d=res.data||{};
|
|
954
|
+
if(d.scan_id && d.status!=='complete'){
|
|
955
|
+
var p=getPending(id); if(p){ p.label=clip?'Mining clips…':'Importing…'; updateImportCard(id); }
|
|
956
|
+
pollImport(id,d.scan_id);
|
|
957
|
+
} else {
|
|
958
|
+
finishImport(id);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function pollImport(id,scanId){
|
|
962
|
+
var tries=0;
|
|
963
|
+
var t=setInterval(function(){
|
|
964
|
+
tries++;
|
|
965
|
+
if(tries>200){ clearInterval(t); var p=getPending(id); if(p){ p.error='Still working in the background — refresh shortly.'; updateImportCard(id); } return; }
|
|
966
|
+
fetch('/raws/scan/'+encodeURIComponent(scanId),{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
967
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
968
|
+
.then(function(d){
|
|
969
|
+
if(!d)return;
|
|
970
|
+
var src=d.source||{}; var status=src.status||(d.scan&&d.scan.status)||'running';
|
|
971
|
+
if(status==='complete'){ clearInterval(t); finishImport(id); }
|
|
972
|
+
else if(status==='failed'||status==='error'){ clearInterval(t); failImport(id,src.error||(d.scan&&d.scan.error)||'The source could not be processed.'); }
|
|
973
|
+
}).catch(function(){});
|
|
974
|
+
},4000);
|
|
975
|
+
}
|
|
976
|
+
if(grid){
|
|
977
|
+
grid.addEventListener('click',function(e){
|
|
978
|
+
var d=e.target&&e.target.closest?e.target.closest('[data-import-dismiss]'):null;
|
|
979
|
+
if(!d)return; e.preventDefault(); removeImport(d.getAttribute('data-import-dismiss')); repaint();
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/* ── fullscreen scroll-snap viewer (click a raw → vertical deck) ──────────── */
|
|
984
|
+
var viewer=root.querySelector('[data-rk-viewer]');
|
|
985
|
+
var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
|
|
986
|
+
if(viewer&&vTrack){
|
|
987
|
+
var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
|
|
988
|
+
function slideHtml(t,i){
|
|
989
|
+
var media=t.view_url
|
|
990
|
+
? '<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>'
|
|
991
|
+
: '<img class="rk-library-viewer-media" src="'+attr(t.thumbnail_url||'')+'" alt="'+attr(t.description||'Raw clip')+'">';
|
|
992
|
+
var desc=t.description || (t.tags&&t.tags.transcript) || t.source_filename || 'Raw clip';
|
|
993
|
+
var range=fmtClock(t.start_time_sec)+'–'+fmtClock(t.end_time_sec);
|
|
994
|
+
var meta=(t.source_filename?esc(t.source_filename)+' · ':'')+esc(range);
|
|
995
|
+
var dl='/raws/'+encodeURIComponent(t.clip_id)+'/download';
|
|
996
|
+
return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
|
|
997
|
+
+'<figure class="rk-library-viewer-stage">'
|
|
998
|
+
+ media
|
|
999
|
+
+'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
|
|
1000
|
+
+'<div class="rk-library-viewer-shade"></div>'
|
|
1001
|
+
+'<div class="rk-library-viewer-info">'
|
|
1002
|
+
+'<div class="rk-library-viewer-title">'+esc(desc)+'</div>'
|
|
1003
|
+
+'<div class="rk-library-viewer-desc">'+meta+'</div>'
|
|
1004
|
+
+'<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>'
|
|
1005
|
+
+'</div>'
|
|
1006
|
+
+'</figure>'
|
|
1007
|
+
+'</section>';
|
|
1008
|
+
}
|
|
1009
|
+
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')); }
|
|
1010
|
+
function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
|
|
1011
|
+
function setActive(idx){
|
|
1012
|
+
vActive=idx;
|
|
1013
|
+
[].forEach.call(vTrack.children,function(s,i){
|
|
1014
|
+
if(Math.abs(i-idx)<=1) hydrate(s);
|
|
1015
|
+
s.classList.remove('is-paused');
|
|
1016
|
+
var v=s.querySelector('video'); if(!v)return;
|
|
1017
|
+
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(){}); }); }
|
|
1018
|
+
else { v.pause(); }
|
|
1019
|
+
});
|
|
1020
|
+
updateMuteUi();
|
|
1021
|
+
}
|
|
1022
|
+
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'}); }
|
|
1023
|
+
function open(clickedIdx){
|
|
1024
|
+
hideHover();
|
|
1025
|
+
vItems=[]; var start=0;
|
|
1026
|
+
VIEW.forEach(function(t,i){ if(!t.view_url&&!t.thumbnail_url)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
|
|
1027
|
+
if(!vItems.length)return;
|
|
1028
|
+
vTrack.innerHTML=vItems.map(slideHtml).join('');
|
|
1029
|
+
vOpen=true; vSound=true; viewer.hidden=false;
|
|
1030
|
+
document.documentElement.classList.add('rk-noscroll');
|
|
1031
|
+
if(vIO)vIO.disconnect();
|
|
1032
|
+
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;
|
|
1033
|
+
if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
|
|
1034
|
+
vTrack.scrollTop=start*vTrack.clientHeight;
|
|
1035
|
+
setActive(start);
|
|
1036
|
+
}
|
|
1037
|
+
function close(){
|
|
1038
|
+
if(!vOpen)return; vOpen=false; vActive=-1;
|
|
1039
|
+
if(vIO){ vIO.disconnect(); vIO=null; }
|
|
1040
|
+
vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
|
|
1041
|
+
vTrack.innerHTML=''; viewer.hidden=true;
|
|
1042
|
+
document.documentElement.classList.remove('rk-noscroll');
|
|
1043
|
+
}
|
|
1044
|
+
if(grid){
|
|
1045
|
+
grid.addEventListener('click',function(e){
|
|
1046
|
+
var b=e.target&&e.target.closest?e.target.closest('[data-rk-clip-view]'):null;
|
|
1047
|
+
if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-clip-view')));
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
|
|
1051
|
+
var muteBtn=viewer.querySelector('[data-viewer-mute]');
|
|
1052
|
+
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'); } } });
|
|
1053
|
+
var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
|
|
1054
|
+
var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
|
|
1055
|
+
vTrack.addEventListener('click',function(e){
|
|
1056
|
+
var t=e.target;
|
|
1057
|
+
if(t&&t.closest&&t.closest('a'))return;
|
|
1058
|
+
var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
|
|
1059
|
+
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; }
|
|
1060
|
+
if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
|
|
1061
|
+
});
|
|
1062
|
+
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); } });
|
|
1063
|
+
window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* ── folder rename / copy-path (kebab on each folder tile) ────────────────── */
|
|
1067
|
+
function closeFolderMenus(except){
|
|
1068
|
+
if(!grid)return;
|
|
1069
|
+
Array.prototype.slice.call(grid.querySelectorAll('[data-folder-menu]')).forEach(function(m){
|
|
1070
|
+
if(m===except)return;
|
|
1071
|
+
var pop=m.querySelector('[data-folder-pop]'); if(pop)pop.hidden=true;
|
|
1072
|
+
var kb=m.querySelector('[data-folder-kebab]'); if(kb)kb.setAttribute('aria-expanded','false');
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function copyPath(path){
|
|
1076
|
+
try{
|
|
1077
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(path); }
|
|
1078
|
+
else { var ta=document.createElement('textarea'); ta.value=path; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }
|
|
1079
|
+
if(statusEl)statusEl.textContent='Copied '+path;
|
|
1080
|
+
}catch(e){ if(statusEl)statusEl.textContent=path; }
|
|
1081
|
+
}
|
|
1082
|
+
// Rename a raws folder. Real folders (folder_path set) go through the directory
|
|
1083
|
+
// rename endpoint (re-points every clip + nested subfolders server-side). Legacy
|
|
1084
|
+
// source-groups (no folder_path) have no stored folder to rename, so we
|
|
1085
|
+
// MATERIALIZE one by moving each clip into /raws/<slug> via PATCH /raws/:id.
|
|
1086
|
+
function renameFolder(folderId){
|
|
1087
|
+
var f=folderById(folderId); if(!f)return;
|
|
1088
|
+
var current=f.folderPath?(f.folderPath.split('/').pop()):f.name;
|
|
1089
|
+
var next=(window.prompt('Rename folder',current||'')||'').trim();
|
|
1090
|
+
if(!next||next===current)return;
|
|
1091
|
+
if(statusEl)statusEl.textContent='Renaming…';
|
|
1092
|
+
if(f.folderPath){
|
|
1093
|
+
fetch('/api/v1/user/me/directory/rename',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({path:'/raws/'+f.folderPath,new_name:next})})
|
|
1094
|
+
.then(function(r){ return r.ok?r.json():r.json().catch(function(){return{};}).then(function(d){ throw new Error((d&&d.error)||'Rename failed'); }); })
|
|
1095
|
+
.then(function(){ loadFeed(); })
|
|
1096
|
+
.catch(function(err){ if(statusEl)statusEl.textContent=(err&&err.message)||'Rename failed'; });
|
|
1097
|
+
} else {
|
|
1098
|
+
var target=slugFolder(next); if(!target){ if(statusEl)statusEl.textContent='Pick a folder name with letters or numbers.'; return; }
|
|
1099
|
+
Promise.all(f.clips.map(function(c){
|
|
1100
|
+
return fetch('/raws/'+encodeURIComponent(c.clip_id),{method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({folder_path:target})}).then(function(r){ return r.ok; });
|
|
1101
|
+
})).then(function(oks){
|
|
1102
|
+
if(oks.some(function(ok){ return !ok; })){ if(statusEl)statusEl.textContent='Some clips could not be moved — try again.'; }
|
|
1103
|
+
loadFeed();
|
|
1104
|
+
}).catch(function(){ if(statusEl)statusEl.textContent='Rename failed'; loadFeed(); });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Folder navigation (grid tile) + breadcrumb "All raws" back link.
|
|
1109
|
+
if(grid){
|
|
1110
|
+
grid.addEventListener('click',function(e){
|
|
1111
|
+
var tgt=e.target;
|
|
1112
|
+
// kebab toggle
|
|
1113
|
+
var kb=tgt&&tgt.closest?tgt.closest('[data-folder-kebab]'):null;
|
|
1114
|
+
if(kb){ e.preventDefault(); e.stopPropagation(); var menu=kb.closest('[data-folder-menu]'); var pop=menu&&menu.querySelector('[data-folder-pop]'); var willOpen=pop&&pop.hidden; closeFolderMenus(menu); if(pop){ pop.hidden=!willOpen; kb.setAttribute('aria-expanded',willOpen?'true':'false'); } return; }
|
|
1115
|
+
var rn=tgt&&tgt.closest?tgt.closest('[data-folder-rename]'):null;
|
|
1116
|
+
if(rn){ e.preventDefault(); closeFolderMenus(null); renameFolder(rn.getAttribute('data-folder-rename')); return; }
|
|
1117
|
+
var cp=tgt&&tgt.closest?tgt.closest('[data-folder-copy]'):null;
|
|
1118
|
+
if(cp){ e.preventDefault(); closeFolderMenus(null); copyPath(cp.getAttribute('data-folder-copy')); return; }
|
|
1119
|
+
var f=tgt&&tgt.closest?tgt.closest('[data-folder-id]'):null;
|
|
1120
|
+
if(!f)return; e.preventDefault(); closeFolderMenus(null); hideHover(); if(query)query.value=''; paintFolder(f.getAttribute('data-folder-id'));
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
document.addEventListener('click',function(e){
|
|
1124
|
+
var t=e.target;
|
|
1125
|
+
if(!(t&&t.closest&&t.closest('[data-folder-menu]')))closeFolderMenus(null);
|
|
1126
|
+
});
|
|
1127
|
+
if(crumbsEl){
|
|
1128
|
+
crumbsEl.addEventListener('click',function(e){
|
|
1129
|
+
var r=e.target&&e.target.closest?e.target.closest('[data-crumb-root]'):null;
|
|
1130
|
+
if(!r)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintRoot();
|
|
1131
|
+
});
|
|
389
1132
|
}
|
|
390
1133
|
|
|
391
1134
|
function handleAuth(res){
|
|
@@ -393,7 +1136,6 @@ const CLIPS_SCRIPT = `
|
|
|
393
1136
|
if(grid)grid.innerHTML='';
|
|
394
1137
|
setGridEmpty(false);
|
|
395
1138
|
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
1139
|
throw new Error('unauthorized');
|
|
398
1140
|
}
|
|
399
1141
|
return res.json();
|
|
@@ -401,37 +1143,31 @@ const CLIPS_SCRIPT = `
|
|
|
401
1143
|
|
|
402
1144
|
function loadFeed(){
|
|
403
1145
|
if(statusEl)statusEl.textContent='Loading your raws…';
|
|
404
|
-
fetch('/raws/feed?limit=
|
|
405
|
-
.then(handleAuth).then(function(d){
|
|
1146
|
+
fetch('/raws/feed?limit=250',{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
1147
|
+
.then(handleAuth).then(function(d){
|
|
1148
|
+
ALL=d.clips||[]; buildFolders(); syncPlaceholder();
|
|
1149
|
+
// stay inside the current folder if it survived the refresh, else root
|
|
1150
|
+
if(MODE==='folder' && folderById(CUR_FOLDER)) paintFolder(CUR_FOLDER);
|
|
1151
|
+
else paintRoot();
|
|
1152
|
+
}).catch(function(){});
|
|
406
1153
|
}
|
|
407
1154
|
|
|
408
|
-
function search(q
|
|
1155
|
+
function search(q){
|
|
409
1156
|
if(statusEl)statusEl.textContent='Searching…';
|
|
410
1157
|
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(){});
|
|
1158
|
+
.then(handleAuth).then(function(d){ paintSearch(d.results||[],q); }).catch(function(){});
|
|
415
1159
|
}
|
|
416
1160
|
|
|
417
1161
|
if(form){
|
|
418
1162
|
form.addEventListener('submit',function(e){
|
|
419
1163
|
e.preventDefault();
|
|
420
1164
|
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(); }
|
|
1165
|
+
if(q)search(q); else paintRoot();
|
|
423
1166
|
});
|
|
424
1167
|
}
|
|
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
|
-
});
|
|
1168
|
+
if(query){
|
|
1169
|
+
// clearing the box returns to the folder root without a round-trip
|
|
1170
|
+
query.addEventListener('input',function(){ if(!(query.value||'').trim() && MODE==='search'){ hideHover(); paintRoot(); } });
|
|
435
1171
|
}
|
|
436
1172
|
|
|
437
1173
|
// ── Scan a source (real clip hunt) ─────────────────────────────────────
|
|
@@ -445,9 +1181,49 @@ const CLIPS_SCRIPT = `
|
|
|
445
1181
|
var scanTracer=root.getElementById('rkScanTracer');
|
|
446
1182
|
var scanNote=root.getElementById('rkScanNote');
|
|
447
1183
|
var scanSubmit=root.getElementById('rkClipsScanSubmit');
|
|
448
|
-
var
|
|
1184
|
+
var scanClip=root.getElementById('rkScanClip');
|
|
1185
|
+
var scanAdvanced=root.getElementById('rkClipsAdvanced');
|
|
1186
|
+
var IMPORT_NOTE="We'll open the Clipper so you can preview the video and save an exact subrange (or the whole thing) to your raws.";
|
|
1187
|
+
var CLIP_NOTE='AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.';
|
|
1188
|
+
var SCAN_OPTS=null; // GET /raws/scan-options: which providers/keys can run a hunt
|
|
449
1189
|
function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
|
|
450
|
-
|
|
1190
|
+
function providerLabel(p){ return p==='gemini'?'Gemini':p==='openai'?'OpenAI':p==='openrouter'?'OpenRouter':p; }
|
|
1191
|
+
// Can a clip hunt actually run? A saved provider key, a local agent CLI, or a
|
|
1192
|
+
// linked cloud account all qualify. Unknown (not yet loaded) → don't block.
|
|
1193
|
+
function clipReady(){
|
|
1194
|
+
if(!SCAN_OPTS)return true;
|
|
1195
|
+
if(SCAN_OPTS.default_provider)return true;
|
|
1196
|
+
if(SCAN_OPTS.pipeline==='upstream'||SCAN_OPTS.upstream_backup)return true;
|
|
1197
|
+
return (SCAN_OPTS.providers||[]).some(function(p){ return p.has_key; });
|
|
1198
|
+
}
|
|
1199
|
+
function applyScanOpts(){
|
|
1200
|
+
if(!SCAN_OPTS||!scanProvider)return;
|
|
1201
|
+
Array.prototype.slice.call(scanProvider.options).forEach(function(opt){
|
|
1202
|
+
if(!opt.value)return; // leave "Auto" alone
|
|
1203
|
+
var pr=null; (SCAN_OPTS.providers||[]).forEach(function(p){ if(p.provider===opt.value)pr=p; });
|
|
1204
|
+
if(!pr)return;
|
|
1205
|
+
opt.disabled=!pr.has_key;
|
|
1206
|
+
opt.textContent=providerLabel(opt.value)+(pr.has_key?' — key saved':' — no key saved');
|
|
1207
|
+
});
|
|
1208
|
+
if(scanClip&&scanClip.checked)syncClipMode();
|
|
1209
|
+
}
|
|
1210
|
+
function loadScanOpts(){
|
|
1211
|
+
fetch('/raws/scan-options',{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
1212
|
+
.then(function(r){ return r.ok?r.json():null; })
|
|
1213
|
+
.then(function(d){ if(d){ SCAN_OPTS=d; applyScanOpts(); } }).catch(function(){});
|
|
1214
|
+
}
|
|
1215
|
+
function syncClipMode(){
|
|
1216
|
+
var on=!!(scanClip&&scanClip.checked);
|
|
1217
|
+
if(scanAdvanced)scanAdvanced.hidden=!on;
|
|
1218
|
+
if(scanSubmit)scanSubmit.innerHTML=(on?'Start scan':'Open clipper')+' <span class="rk-arrow">→</span>';
|
|
1219
|
+
if(on&&!clipReady()){
|
|
1220
|
+
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');
|
|
1221
|
+
} else {
|
|
1222
|
+
setNote(on?CLIP_NOTE:IMPORT_NOTE);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if(scanClip)scanClip.addEventListener('change',syncClipMode);
|
|
1226
|
+
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
1227
|
if(cancelBtn&&dialog){ cancelBtn.addEventListener('click',function(){ if(dialog.open)dialog.close(); }); }
|
|
452
1228
|
if(dialog){
|
|
453
1229
|
dialog.addEventListener('click',function(e){
|
|
@@ -455,49 +1231,50 @@ const CLIPS_SCRIPT = `
|
|
|
455
1231
|
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();
|
|
456
1232
|
});
|
|
457
1233
|
}
|
|
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
1234
|
if(scanForm){
|
|
477
1235
|
scanForm.addEventListener('submit',function(e){
|
|
478
1236
|
e.preventDefault();
|
|
479
1237
|
var url=(scanUrl&&scanUrl.value||'').trim();
|
|
480
|
-
var prompt=(scanPrompt&&scanPrompt.value||'').trim();
|
|
481
1238
|
var tracer=(scanTracer&&scanTracer.value||'').trim();
|
|
482
|
-
var
|
|
1239
|
+
var clip=!!(scanClip&&scanClip.checked);
|
|
1240
|
+
var prompt=clip?(scanPrompt&&scanPrompt.value||'').trim():'';
|
|
1241
|
+
var provider=clip?(scanProvider&&scanProvider.value||'').trim():'';
|
|
483
1242
|
if(!url){ setNote('Enter a video URL.','error'); return; }
|
|
1243
|
+
// Whole-video (non-AI) path now opens the Clipper so the user can pick an
|
|
1244
|
+
// exact subrange (or save the whole thing) with a live video preview.
|
|
1245
|
+
if(!clip){
|
|
1246
|
+
var clp=new URL('/tools/clipper', window.location.origin);
|
|
1247
|
+
clp.searchParams.set('source_url', url);
|
|
1248
|
+
if(tracer) clp.searchParams.set('tracer', tracer);
|
|
1249
|
+
window.location.href=clp.pathname+clp.search;
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
484
1252
|
setNote('Starting scan…');
|
|
485
|
-
if(scanSubmit)scanSubmit.disabled=true;
|
|
486
|
-
|
|
1253
|
+
if(scanSubmit){ scanSubmit.disabled=true; scanSubmit.classList.add('is-loading'); }
|
|
1254
|
+
|
|
1255
|
+
var payload={source_url:url};
|
|
1256
|
+
if(clip){ payload.prompt=prompt; if(provider)payload.provider=provider; } else { payload.import_only=true; }
|
|
487
1257
|
if(tracer)payload.tracer=tracer;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1258
|
+
|
|
1259
|
+
// Fire the request now; hold it and only wire the result AFTER the modal
|
|
1260
|
+
// closes and the optimistic card lands (so a fast reply can't race ahead
|
|
1261
|
+
// of its own placeholder).
|
|
1262
|
+
var id='imp'+Date.now()+'-'+Math.floor(Math.random()*1e6);
|
|
1263
|
+
var req=fetch('/raws/scan',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(payload)})
|
|
1264
|
+
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); });
|
|
1265
|
+
|
|
1266
|
+
// brief spinner beat, then close and optimistically drop an "importing…" card
|
|
1267
|
+
setTimeout(function(){
|
|
1268
|
+
if(scanSubmit){ scanSubmit.disabled=false; scanSubmit.classList.remove('is-loading'); }
|
|
1269
|
+
if(dialog&&dialog.open)dialog.close();
|
|
1270
|
+
if(scanForm.reset)scanForm.reset();
|
|
1271
|
+
syncClipMode();
|
|
1272
|
+
PENDING.unshift({id:id,url:url,tracer:tracer,clip:clip,label:clip?'Starting scan…':'Importing…'});
|
|
1273
|
+
if(query)query.value='';
|
|
1274
|
+
paintRoot(); // surface the optimistic "importing…" card at the directory root
|
|
1275
|
+
if(statusEl)statusEl.textContent=(clip?'Mining a source…':'Importing a video…')+(tracer?' (tracer '+tracer+')':'');
|
|
1276
|
+
req.then(function(res){ onScanResult(id,clip,res); }).catch(function(){ failImport(id,'Network error — please try again.'); });
|
|
1277
|
+
},650);
|
|
501
1278
|
});
|
|
502
1279
|
}
|
|
503
1280
|
|
|
@@ -506,10 +1283,11 @@ const CLIPS_SCRIPT = `
|
|
|
506
1283
|
export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPUT) {
|
|
507
1284
|
const isRaws = tab === "raws";
|
|
508
1285
|
const isLogs = tab === "logs";
|
|
1286
|
+
const isFiles = tab === "files";
|
|
509
1287
|
const tabDef = [
|
|
510
1288
|
{ key: "approved", label: "Approved", href: "/library/approved" },
|
|
511
1289
|
{ key: "raws", label: "Raws", href: "/library/raws" },
|
|
512
|
-
{ key: "
|
|
1290
|
+
{ key: "files", label: "Files", href: "/library/files" }
|
|
513
1291
|
];
|
|
514
1292
|
const tabs = `<div class="rk-library-tabs" role="tablist" aria-label="Library view">
|
|
515
1293
|
${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 +1302,23 @@ export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPU
|
|
|
524
1302
|
};
|
|
525
1303
|
const headSub = isLogs
|
|
526
1304
|
? "Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost."
|
|
527
|
-
:
|
|
528
|
-
? "
|
|
529
|
-
:
|
|
1305
|
+
: isFiles
|
|
1306
|
+
? "Your whole workspace as one navigable directory — files, temp scratch, and mined raws. Search by meaning, name, or path."
|
|
1307
|
+
: isRaws
|
|
1308
|
+
? "Long-form videos mined into tagged, searchable short clips — every reaction, hook, and money shot, ready to drop into an edit."
|
|
1309
|
+
: "Every published cut you own — ready-to-post renders and scheduled drops. Open in the editor, share, download, or schedule.";
|
|
530
1310
|
const headCta = isRaws
|
|
531
1311
|
? `<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
|
|
1312
|
+
: isLogs || isFiles
|
|
533
1313
|
? ""
|
|
534
|
-
: `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/
|
|
1314
|
+
: `<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
1315
|
const panel = isLogs
|
|
536
1316
|
? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
|
|
537
|
-
:
|
|
538
|
-
?
|
|
539
|
-
:
|
|
1317
|
+
: isFiles
|
|
1318
|
+
? filesPanel(tabs, Boolean(input.cloudSpace), input.initialFilesPath)
|
|
1319
|
+
: isRaws
|
|
1320
|
+
? clipsPanel(tabs)
|
|
1321
|
+
: approvedPanel(input, tabs);
|
|
540
1322
|
const body = `
|
|
541
1323
|
<main class="rk-container-wide rk-page rk-library-page">
|
|
542
1324
|
<div class="rk-spread rk-page-head rk-library-head">
|
|
@@ -561,6 +1343,13 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
561
1343
|
|
|
562
1344
|
/* topbar row: primary tab toggle + the tab's quick search share one line */
|
|
563
1345
|
.rk-library-topbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:22px}
|
|
1346
|
+
/* "Logs" — pulled out of the tab toggle; floats to the far right of the row.
|
|
1347
|
+
When a search bar is present it already grabs margin-left:auto, so Logs just
|
|
1348
|
+
trails it; when there's no search (Files tab) Logs floats right on its own. */
|
|
1349
|
+
.rk-library-logs-btn{flex:none;margin-left:auto;gap:7px}
|
|
1350
|
+
.rk-library-qsearch ~ .rk-library-logs-btn,#rkClipsSearch ~ .rk-library-logs-btn{margin-left:0}
|
|
1351
|
+
.rk-library-logs-btn svg{color:var(--rk-n-500)}
|
|
1352
|
+
@media(max-width:720px){.rk-library-logs-btn{margin-left:auto}}
|
|
564
1353
|
|
|
565
1354
|
/* primary tab toggle (Approved | Clips) */
|
|
566
1355
|
.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 +1361,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
572
1361
|
/* stats — sits right-aligned in the chips row */
|
|
573
1362
|
.rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;line-height:1.7}
|
|
574
1363
|
.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
|
|
1364
|
+
.rk-library-toolbar .rk-library-summary{margin-left:auto}
|
|
576
1365
|
|
|
577
1366
|
/* toolbar (filter chips + count) */
|
|
578
1367
|
.rk-library-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:24px}
|
|
@@ -589,7 +1378,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
589
1378
|
.rk-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px}
|
|
590
1379
|
.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
1380
|
.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)}
|
|
1381
|
+
.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
1382
|
.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
1383
|
.rk-library-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#050505}
|
|
595
1384
|
.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 +1387,39 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
598
1387
|
.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
1388
|
.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
1389
|
.rk-library-card:hover .rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}
|
|
1390
|
+
@media(hover:none){.rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}}
|
|
601
1391
|
.rk-library-play svg{margin-left:2px}
|
|
602
|
-
.rk-library-meta{display:grid;gap:
|
|
1392
|
+
.rk-library-meta{display:grid;gap:9px;padding:14px 14px 15px}
|
|
603
1393
|
.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:
|
|
1394
|
+
.rk-library-origin{display:flex;align-items:center;gap:6px;min-width:0;font-size:12px;font-weight:500;color:var(--rk-text-muted)}
|
|
1395
|
+
.rk-library-origin-sep{color:var(--rk-text-faint);flex:none}
|
|
605
1396
|
.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-
|
|
1397
|
+
.rk-library-time{font-size:12px;font-weight:600;color:var(--rk-text-faint);white-space:nowrap;flex:none}
|
|
1398
|
+
/* compact action row: labelled Schedule + icon-only Open/View/Download */
|
|
1399
|
+
.rk-library-actions{display:flex;align-items:center;gap:6px;margin-top:1px}
|
|
1400
|
+
.rk-library-act{flex:none;min-height:32px;padding:0 12px;font-size:12.5px;border-radius:var(--rk-r-full)}
|
|
1401
|
+
.rk-library-act-ic{width:32px;min-width:32px;padding:0;display:inline-flex;align-items:center;justify-content:center}
|
|
1402
|
+
.rk-library-act-ic svg{display:block}
|
|
1403
|
+
|
|
1404
|
+
/* right-floated "⋯" overflow menu (Copy ID / Delete-with-popconfirm) */
|
|
1405
|
+
.rk-library-menu{position:relative;margin-left:auto;flex:none}
|
|
1406
|
+
.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)}
|
|
1407
|
+
.rk-library-kebab:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
1408
|
+
.rk-library-menu.is-open .rk-library-kebab{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border-strong)}
|
|
1409
|
+
.rk-library-kebab.is-ok{color:#1f7a44;border-color:#bfe6cd;background:#e8f7ee}
|
|
1410
|
+
.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))}
|
|
1411
|
+
.rk-library-menu-pop[hidden]{display:none}
|
|
1412
|
+
.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)}
|
|
1413
|
+
.rk-library-menu-item svg{flex:none;color:var(--rk-n-500)}
|
|
1414
|
+
.rk-library-menu-item:hover{background:var(--rk-n-100)}
|
|
1415
|
+
.rk-library-menu-item.is-danger{color:var(--rk-red)}
|
|
1416
|
+
.rk-library-menu-item.is-danger svg{color:var(--rk-red)}
|
|
1417
|
+
.rk-library-menu-item.is-danger:hover{background:var(--rk-red-tint)}
|
|
1418
|
+
.rk-library-menu-confirm{padding:10px 11px 4px}
|
|
1419
|
+
.rk-library-menu-confirm[hidden]{display:none}
|
|
1420
|
+
.rk-library-menu-confirm-q{font-size:12.5px;font-weight:600;color:var(--rk-ink);margin:0 0 9px}
|
|
1421
|
+
.rk-library-menu-confirm-row{display:flex;justify-content:flex-end;gap:7px}
|
|
1422
|
+
.rk-library-menu-confirm-row .rk-btn{min-height:30px}
|
|
611
1423
|
|
|
612
1424
|
/* new-project tile */
|
|
613
1425
|
.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,29 +1437,109 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
625
1437
|
|
|
626
1438
|
@media(prefers-reduced-motion:reduce){.rk-library-video{}}
|
|
627
1439
|
|
|
1440
|
+
/* ── fullscreen TikTok-style viewer (click a card → vertical scroll deck) ── */
|
|
1441
|
+
html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
|
|
1442
|
+
.rk-library-viewer{position:fixed;inset:0;z-index:120}
|
|
1443
|
+
.rk-library-viewer[hidden]{display:none}
|
|
1444
|
+
.rk-library-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
|
1445
|
+
.rk-library-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
|
|
1446
|
+
.rk-library-viewer-track::-webkit-scrollbar{display:none}
|
|
1447
|
+
.rk-library-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
|
|
1448
|
+
.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)}
|
|
1449
|
+
.rk-library-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
|
1450
|
+
video.rk-library-viewer-media{cursor:pointer}
|
|
1451
|
+
.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))}
|
|
1452
|
+
.rk-library-viewer-pause svg{width:64px;height:64px}
|
|
1453
|
+
.rk-library-viewer-slide.is-paused .rk-library-viewer-pause{display:grid}
|
|
1454
|
+
.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%)}
|
|
1455
|
+
.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}
|
|
1456
|
+
.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}
|
|
1457
|
+
.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}
|
|
1458
|
+
.rk-library-viewer-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px;pointer-events:auto}
|
|
1459
|
+
.rk-library-viewer-cta{flex:none}
|
|
1460
|
+
.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}
|
|
1461
|
+
.rk-library-viewer .rk-btn-ghost-light:hover{background:rgba(255,255,255,.26);border-color:rgba(255,255,255,.4)}
|
|
1462
|
+
.rk-library-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
|
|
1463
|
+
.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)}
|
|
1464
|
+
.rk-library-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
|
|
1465
|
+
.rk-library-viewer-btn span{display:grid;place-items:center;line-height:0}
|
|
1466
|
+
.rk-library-viewer .rk-lvm-off{display:none}
|
|
1467
|
+
.rk-library-viewer.is-muted .rk-lvm-on{display:none}
|
|
1468
|
+
.rk-library-viewer.is-muted .rk-lvm-off{display:grid}
|
|
1469
|
+
.rk-library-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
|
|
1470
|
+
@media(hover:none){.rk-library-viewer-nav{display:none}}
|
|
1471
|
+
@media(max-width:640px){
|
|
1472
|
+
.rk-library-viewer-nav{display:none}
|
|
1473
|
+
.rk-library-viewer-slide{padding:0}
|
|
1474
|
+
.rk-library-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
628
1477
|
/* ── clips tab ── */
|
|
629
1478
|
.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
1479
|
.rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 300px;min-width:240px;margin:0}
|
|
632
1480
|
.rk-clips-search-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
633
1481
|
.rk-clips-search-input{flex:1;padding-left:40px;border-radius:var(--rk-r-full)}
|
|
634
1482
|
.rk-clips-search-btn{flex:none}
|
|
635
1483
|
.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
1484
|
.rk-clips-status{font-size:13px;color:var(--rk-text-muted);font-weight:500}
|
|
641
1485
|
.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
|
-
|
|
1486
|
+
|
|
1487
|
+
/* ── raws file-directory explorer ── */
|
|
1488
|
+
/* breadcrumb bar (replaces the old count/summary double-row) */
|
|
1489
|
+
.rk-raws-bar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;min-height:30px;margin-bottom:18px}
|
|
1490
|
+
.rk-raws-crumbs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
|
1491
|
+
.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}
|
|
1492
|
+
.rk-raws-crumb.is-current{color:var(--rk-ink);font-family:var(--rk-font-display);font-weight:800;letter-spacing:-.01em}
|
|
1493
|
+
.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)}
|
|
1494
|
+
.rk-raws-crumb-link:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
1495
|
+
.rk-raws-crumb-link svg{flex:none}
|
|
1496
|
+
.rk-raws-crumb-sep{color:var(--rk-text-faint);font-weight:500}
|
|
1497
|
+
.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}
|
|
1498
|
+
.rk-raws-bar .rk-clips-status{margin-left:auto;text-align:right}
|
|
1499
|
+
|
|
1500
|
+
/* the explorer grid switches between clip cards (default) and folder tiles */
|
|
1501
|
+
.rk-raws-explorer{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
|
|
1502
|
+
.rk-raws-explorer.is-folders{grid-template-columns:repeat(auto-fill,minmax(228px,1fr))}
|
|
1503
|
+
|
|
1504
|
+
/* folder tile — 2×2 thumbnail mosaic + name + count */
|
|
1505
|
+
.rk-raws-folder-wrap{position:relative;display:flex}
|
|
1506
|
+
.rk-raws-folder-wrap>.rk-raws-folder{flex:1;min-width:0}
|
|
1507
|
+
.rk-raws-folder-menu{position:absolute;top:10px;right:10px;z-index:4}
|
|
1508
|
+
.rk-raws-folder-kebab{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);cursor:pointer;opacity:0;transform:scale(.85);box-shadow:var(--rk-shadow-sm);transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
1509
|
+
.rk-raws-folder-wrap:hover .rk-raws-folder-kebab,.rk-raws-folder-menu [aria-expanded="true"]{opacity:1;transform:scale(1)}
|
|
1510
|
+
.rk-raws-folder-kebab:hover{background:#fff}
|
|
1511
|
+
@media(hover:none){.rk-raws-folder-kebab{opacity:1;transform:scale(1)}}
|
|
1512
|
+
.rk-raws-folder-pop{position:absolute;top:36px;right:0;min-width:168px;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);box-shadow:var(--rk-shadow-lg);padding:6px;display:flex;flex-direction:column;gap:2px}
|
|
1513
|
+
.rk-raws-folder-pop[hidden]{display:none}
|
|
1514
|
+
.rk-raws-folder-pop button{appearance:none;border:0;background:none;font:inherit;text-align:left;padding:8px 10px;border-radius:var(--rk-r-md);color:var(--rk-ink);font-size:13px;font-weight:600;cursor:pointer}
|
|
1515
|
+
.rk-raws-folder-pop button:hover{background:var(--rk-n-100)}
|
|
1516
|
+
.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)}
|
|
1517
|
+
.rk-raws-folder:hover{transform:translateY(-3px);box-shadow:var(--rk-shadow-md);border-color:var(--rk-gold-600)}
|
|
1518
|
+
.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}
|
|
1519
|
+
.rk-raws-folder-mosaic.is-single{grid-template-columns:1fr}
|
|
1520
|
+
.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))}
|
|
1521
|
+
.rk-raws-folder-mosaic.is-empty svg{width:38px;height:38px}
|
|
1522
|
+
.rk-raws-folder-cell{display:block;background-size:cover;background-position:center;background-repeat:no-repeat;background-color:#171717}
|
|
1523
|
+
.rk-raws-folder-meta{display:flex;align-items:center;gap:10px;padding:13px 14px}
|
|
1524
|
+
.rk-raws-folder-ic{flex:none;color:var(--rk-gold-700);display:flex}
|
|
1525
|
+
.rk-raws-folder-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1526
|
+
.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}
|
|
1527
|
+
.rk-raws-folder-count{font-size:12px;font-weight:600;color:var(--rk-text-muted)}
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
.rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
|
|
643
1531
|
.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
1532
|
.rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
|
|
645
1533
|
.rk-clips-thumb-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
646
|
-
.rk-clips-thumb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none}
|
|
1534
|
+
.rk-clips-thumb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none;z-index:1}
|
|
1535
|
+
.rk-clips-thumb.is-playing .rk-clips-thumb-vid{display:block}
|
|
1536
|
+
.rk-clips-thumb.is-playing .rk-clips-play{opacity:0!important}
|
|
647
1537
|
.rk-clips-noimg{color:rgba(255,255,255,.68);font-size:12px;font-weight:600;letter-spacing:.01em}
|
|
648
1538
|
.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}
|
|
1539
|
+
.rk-clips-fullbadge{position:absolute;left:10px;top:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:var(--rk-ink);color:#fff;font-size:11px;font-weight:700;letter-spacing:.01em;box-shadow:var(--rk-shadow-xs);z-index:3}
|
|
649
1540
|
.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
1541
|
.rk-clips-card:hover .rk-clips-play{opacity:1;transform:scale(1)}
|
|
1542
|
+
@media(hover:none){.rk-clips-play{opacity:1;transform:scale(1)}}
|
|
651
1543
|
.rk-clips-play:hover{background:var(--rk-gold-500)}
|
|
652
1544
|
.rk-clips-body{display:flex;flex-direction:column;gap:10px;flex:1;padding:16px}
|
|
653
1545
|
.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 +1547,82 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
|
|
|
655
1547
|
.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
1548
|
.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
1549
|
.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}
|
|
1550
|
+
.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
1551
|
.rk-clips-dl:hover{color:var(--rk-gold-500)}
|
|
1552
|
+
/* the whole thumb is a button that opens the scroll-snap viewer */
|
|
1553
|
+
button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer}
|
|
1554
|
+
/* optimistic "importing…" placeholder card */
|
|
1555
|
+
.rk-clips-importing .rk-clips-thumb{aspect-ratio:3/4}
|
|
1556
|
+
.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}
|
|
1557
|
+
@keyframes rk-clips-spin{to{transform:rotate(360deg)}}
|
|
1558
|
+
.rk-clips-importing.is-error .rk-clips-thumb{background:linear-gradient(150deg,#5b2b23,#3a1a15)}
|
|
1559
|
+
.rk-clips-importing-x{color:rgba(255,255,255,.9);display:grid;place-items:center}
|
|
1560
|
+
.rk-clips-importing.is-error .rk-clips-title{color:#8d3b2f}
|
|
1561
|
+
.rk-clips-importing-tag{color:var(--rk-text-faint)!important;cursor:default}
|
|
1562
|
+
/* submit spinner while the request is in flight, before the modal closes */
|
|
1563
|
+
.rk-clips-dialog .rk-btn.is-loading{position:relative;color:transparent!important;pointer-events:none}
|
|
1564
|
+
.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
1565
|
.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
1566
|
.rk-clips-empty[hidden]{display:none}
|
|
662
1567
|
.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)}
|
|
1568
|
+
.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
1569
|
.rk-clips-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
|
|
665
|
-
|
|
1570
|
+
/* scroll the form (not the page) so Cancel / Import stay reachable when the
|
|
1571
|
+
"Clip raws" advanced block expands on short / landscape-phone viewports */
|
|
1572
|
+
.rk-clips-dialog-form{display:grid;gap:16px;margin:0;padding:28px;max-height:calc(100dvh - 2rem);overflow-y:auto}
|
|
666
1573
|
.rk-clips-dialog-grid{gap:14px}
|
|
1574
|
+
.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}
|
|
1575
|
+
.rk-clips-toggle input{margin-top:2px;flex:0 0 auto;accent-color:var(--rk-gold);width:16px;height:16px;cursor:pointer}
|
|
1576
|
+
.rk-clips-toggle b{color:var(--rk-ink)}
|
|
1577
|
+
.rk-clips-advanced{display:grid;gap:16px}
|
|
1578
|
+
.rk-clips-advanced[hidden]{display:none}
|
|
667
1579
|
.rk-clips-dialog-note{background:var(--rk-gold-tint);border-radius:var(--rk-r-xl);padding:10px 13px;color:var(--rk-n-700)}
|
|
668
1580
|
.rk-clips-dialog-note[data-tone="error"]{background:#fdecea;color:#8d3b2f}
|
|
669
1581
|
.rk-clips-dialog-note[data-tone="success"]{background:#e8f7ee;color:#1f7a44}
|
|
670
1582
|
.rk-clips-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
1583
|
+
/* ── files tab: full-page mount of the shared directory-explorer component ──
|
|
1584
|
+
The component (.rk-dir + .rk-aichat-* rows) is tuned for the narrow chat
|
|
1585
|
+
drawer; on a full page we wrap it in a card and scale the search, crumbs and
|
|
1586
|
+
rows up so it reads as a first-class file explorer. Scoped to
|
|
1587
|
+
.rk-files-explorer so the drawer instance is untouched. */
|
|
1588
|
+
.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}
|
|
1589
|
+
.rk-files-explorer .rk-dir{flex:1;min-height:60vh}
|
|
1590
|
+
.rk-files-explorer .rk-dir-search{padding:18px 20px 12px}
|
|
1591
|
+
.rk-files-explorer .rk-dir-search-input{font-size:15px;padding:12px 16px;border-radius:var(--rk-r-full)}
|
|
1592
|
+
.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)}
|
|
1593
|
+
.rk-files-explorer .rk-aichat-crumb{font-size:14px;padding:4px 9px}
|
|
1594
|
+
.rk-files-explorer .rk-aichat-crumb-sep{font-size:14px}
|
|
1595
|
+
.rk-files-explorer .rk-dir-newfolder{font-size:12.5px;padding:5px 11px}
|
|
1596
|
+
.rk-files-explorer .rk-dir-selbar{margin:12px 18px 0;padding:9px 14px}
|
|
1597
|
+
.rk-files-explorer .rk-aichat-files-body,.rk-files-explorer .rk-dir-body{padding:12px 14px 20px;gap:3px}
|
|
1598
|
+
.rk-files-explorer .rk-aichat-frow{padding:11px 12px;border-radius:var(--rk-r-xl)}
|
|
1599
|
+
.rk-files-explorer .rk-aichat-fic{width:44px;height:44px;border-radius:var(--rk-r-lg)}
|
|
1600
|
+
.rk-files-explorer .rk-aichat-fname{font-size:14.5px}
|
|
1601
|
+
.rk-files-explorer .rk-aichat-fmeta{font-size:12.5px}
|
|
1602
|
+
.rk-files-explorer .rk-dir-open{font-size:12.5px}
|
|
1603
|
+
.rk-files-explorer .rk-dir-kebab{width:30px;height:30px}
|
|
1604
|
+
.rk-files-explorer .rk-aichat-drop{margin:0 18px 18px}
|
|
1605
|
+
.rk-files-explorer .rk-aichat-fstate{padding:48px 24px;text-align:center}
|
|
671
1606
|
@media(max-width:620px){
|
|
672
1607
|
.rk-clips-search{flex:1 1 100%}
|
|
673
1608
|
.rk-clips-scan{flex:1 1 100%}
|
|
674
1609
|
.rk-clips-search-input{font-size:16px}
|
|
675
|
-
.rk-clips-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
1610
|
+
.rk-clips-grid,.rk-raws-explorer{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
1611
|
+
.rk-raws-explorer.is-folders{grid-template-columns:repeat(2,minmax(0,1fr))}
|
|
1612
|
+
.rk-files-explorer .rk-dir-search-input{font-size:16px}
|
|
676
1613
|
}
|
|
677
1614
|
`;
|
|
678
|
-
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : "";
|
|
1615
|
+
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : isFiles ? " · Files" : "";
|
|
679
1616
|
return reskinDocument({
|
|
680
1617
|
title: `vidfarm reskin — Library${tabTitle}`,
|
|
681
|
-
description: "Reskinned vidfarm library: your published cuts, mined raws library, and job run history.",
|
|
1618
|
+
description: "Reskinned vidfarm library: your published cuts, mined raws library, unified file directory, and job run history.",
|
|
682
1619
|
activeSlug: "library",
|
|
1620
|
+
account: { name: input.name, email: input.email },
|
|
683
1621
|
pageCss: pageCss + (isLogs ? "\n" + JOBRUNS_PAGE_CSS : ""),
|
|
684
1622
|
body,
|
|
685
|
-
|
|
1623
|
+
// Files tab needs no page script — the auto-mounted directory-explorer
|
|
1624
|
+
// component (base module on every full-chrome page) owns all its behaviour.
|
|
1625
|
+
script: isFiles ? undefined : isLogs ? JOBRUNS_SCRIPT : isRaws ? CLIPS_SCRIPT : APPROVED_SCRIPT
|
|
686
1626
|
});
|
|
687
1627
|
}
|
|
688
1628
|
//# sourceMappingURL=library-page.js.map
|