@mevdragon/vidfarm-devcli 0.16.0 → 0.18.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/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/vidfarm-media/SKILL.md +4 -3
- package/.agents/skills/vidfarm-media/references/tts.md +20 -1
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/account-pages-legacy.js +2 -2
- package/dist/src/app.js +721 -101
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/editor-chat.js +2 -2
- package/dist/src/frontend/homepage-client.js +162 -2
- package/dist/src/frontend/homepage-store.js +30 -1
- package/dist/src/frontend/homepage-view.js +111 -4
- package/dist/src/homepage.js +184 -1
- package/dist/src/landing-page.js +367 -0
- package/dist/src/primitive-registry.js +278 -2
- package/dist/src/reskin/agency-page.js +299 -0
- package/dist/src/reskin/calendar-page.js +567 -0
- package/dist/src/reskin/chat-page.js +607 -0
- package/dist/src/reskin/discover-page.js +1096 -0
- package/dist/src/reskin/document.js +663 -0
- package/dist/src/reskin/help-page.js +356 -0
- package/dist/src/reskin/index-page.js +62 -0
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +477 -0
- package/dist/src/reskin/library-page.js +688 -0
- package/dist/src/reskin/login-page.js +262 -0
- package/dist/src/reskin/pricing-page.js +388 -0
- package/dist/src/reskin/settings-page.js +687 -0
- package/dist/src/reskin/theme.js +362 -0
- package/dist/src/services/serverless-records.js +54 -0
- package/dist/src/services/swipe-customize.js +434 -0
- package/package.json +1 -1
- package/public/assets/homepage-client-app.js +22 -22
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// /reskin/library — the director's own studio, now MIGRATED to real production
|
|
2
|
+
// data (mirrors src/reskin/settings-page.ts). This is ONE page with two
|
|
3
|
+
// server-routed tabs at distinct URLs:
|
|
4
|
+
// /reskin/library/approved → your REAL published cuts (ready + scheduled)
|
|
5
|
+
// /reskin/library/clips → your REAL mined clip library (client-fetched)
|
|
6
|
+
//
|
|
7
|
+
// The renderer accepts the SAME input object the live `/library` route already
|
|
8
|
+
// builds — `Parameters<typeof renderLibraryPage>[0]` from ../account-pages.js —
|
|
9
|
+
// so wiring the route is just "fetch real data + call this". Every action points
|
|
10
|
+
// at a real endpoint:
|
|
11
|
+
// • Open → /editor/:templateId (the real editor, account-scoped)
|
|
12
|
+
// • View post → post.share_url (the real share/preview page)
|
|
13
|
+
// • Download → the primary video asset's real MP4 url
|
|
14
|
+
// • Schedule → /reskin/calendar (the real scheduling surface)
|
|
15
|
+
// • Clips tab → GET /clips/feed, POST /clips/search, POST /clips/scan (real)
|
|
16
|
+
//
|
|
17
|
+
// Follow-up: the live library ships an editorChat / chat-dock runtime
|
|
18
|
+
// (buildLibraryEditorChatBoot → editorChat). That is intentionally OMITTED here
|
|
19
|
+
// for now — this pass focuses on real projects + real clip search. `editorChat`
|
|
20
|
+
// is accepted in the input but not yet rendered.
|
|
21
|
+
//
|
|
22
|
+
// Design stays farmville (shared rk-* primitives + a page-scoped rk-library-* /
|
|
23
|
+
// rk-clips-* CSS block); only the data + actions became real.
|
|
24
|
+
import { reskinDocument, rkEscape } from "./document.js";
|
|
25
|
+
import { jobRunsPanel, JOBRUNS_PAGE_CSS, JOBRUNS_SCRIPT } from "./job-runs-page.js";
|
|
26
|
+
// A safe default so the current route (`renderReskinLibrary("approved")`, no
|
|
27
|
+
// input) keeps working until app.ts is updated to pass real data (mirror the
|
|
28
|
+
// live `/library` route). Once wired, the parent passes the real object.
|
|
29
|
+
const EMPTY_LIBRARY_INPUT = {
|
|
30
|
+
userId: "",
|
|
31
|
+
currentAccountId: "",
|
|
32
|
+
name: null,
|
|
33
|
+
email: "",
|
|
34
|
+
posts: [],
|
|
35
|
+
schedule: { channels: [], connectHref: "/settings?tab=channels&connect=flockposter" },
|
|
36
|
+
editorChat: null
|
|
37
|
+
};
|
|
38
|
+
const PLAY = `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13l11-6.5-11-6.5Z"/></svg>`;
|
|
39
|
+
const SEARCH_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
|
|
40
|
+
/* ──────────────────────────── Approved (projects) tab ─────────────────────── */
|
|
41
|
+
const STATUS = {
|
|
42
|
+
ready: { short: "Ready", pill: "rk-pill-green" },
|
|
43
|
+
scheduled: { short: "Scheduled", pill: "rk-pill-violet" }
|
|
44
|
+
};
|
|
45
|
+
function statusInfo(status) {
|
|
46
|
+
return STATUS[status] || { short: status ? status.charAt(0).toUpperCase() + status.slice(1) : "Ready", pill: "rk-pill" };
|
|
47
|
+
}
|
|
48
|
+
function fmtDate(iso) {
|
|
49
|
+
const t = Date.parse(iso);
|
|
50
|
+
if (Number.isNaN(t))
|
|
51
|
+
return iso || "";
|
|
52
|
+
return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
53
|
+
}
|
|
54
|
+
function isVideoAsset(a) {
|
|
55
|
+
return a.kind === "video" || String(a.content_type || "").startsWith("video");
|
|
56
|
+
}
|
|
57
|
+
function isImageAsset(a) {
|
|
58
|
+
return a.kind === "image" || String(a.content_type || "").startsWith("image");
|
|
59
|
+
}
|
|
60
|
+
// primaryAsset mirrors the live library: role="primary" → first video → media[0].
|
|
61
|
+
function primaryAsset(post) {
|
|
62
|
+
return post.media.find((a) => a.role === "primary")
|
|
63
|
+
|| post.media.find((a) => isVideoAsset(a))
|
|
64
|
+
|| post.media[0]
|
|
65
|
+
|| null;
|
|
66
|
+
}
|
|
67
|
+
function downloadAsset(post) {
|
|
68
|
+
const v = post.media.find((a) => isVideoAsset(a));
|
|
69
|
+
return v || primaryAsset(post);
|
|
70
|
+
}
|
|
71
|
+
function shareHref(post) {
|
|
72
|
+
return post.share_url && post.share_url.length
|
|
73
|
+
? post.share_url
|
|
74
|
+
: `/approved/posts/${encodeURIComponent(post.post_id)}`;
|
|
75
|
+
}
|
|
76
|
+
function editorHref(post, accountId) {
|
|
77
|
+
if (!post.template_id)
|
|
78
|
+
return null;
|
|
79
|
+
const q = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
|
|
80
|
+
return `/editor/${encodeURIComponent(post.template_id)}${q}`;
|
|
81
|
+
}
|
|
82
|
+
function posterMedia(post) {
|
|
83
|
+
const asset = post.media.find((a) => isVideoAsset(a)) || post.media.find((a) => isImageAsset(a)) || primaryAsset(post);
|
|
84
|
+
if (asset && isVideoAsset(asset)) {
|
|
85
|
+
return `<video class="rk-library-video" muted loop playsinline preload="metadata" src="${rkEscape(asset.url)}#t=0.1"></video>`;
|
|
86
|
+
}
|
|
87
|
+
if (asset && isImageAsset(asset)) {
|
|
88
|
+
return `<img class="rk-library-video" loading="lazy" src="${rkEscape(asset.url)}" alt="${rkEscape(post.title || "Published render")}" />`;
|
|
89
|
+
}
|
|
90
|
+
return `<span class="rk-library-noprev">No preview</span>`;
|
|
91
|
+
}
|
|
92
|
+
function projectCard(post, accountId) {
|
|
93
|
+
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
|
+
const title = post.title || "Published render";
|
|
98
|
+
const subLine = post.tracer
|
|
99
|
+
? post.tracer
|
|
100
|
+
: post.template_id
|
|
101
|
+
? `Composition ${post.template_id}`
|
|
102
|
+
: "Ready-to-post cut";
|
|
103
|
+
const searchHay = [title, post.caption, post.tracer, post.template_id].filter(Boolean).join(" ").toLowerCase();
|
|
104
|
+
const editHref = editorHref(post, accountId);
|
|
105
|
+
const primaryAction = post.status === "scheduled"
|
|
106
|
+
? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="/calendar">Reschedule</a>`
|
|
107
|
+
: `<a class="rk-btn rk-btn-gold rk-btn-sm" href="/calendar">Schedule <span class="rk-arrow">→</span></a>`;
|
|
108
|
+
const actions = [
|
|
109
|
+
editHref ? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(editHref)}">Open in editor</a>` : "",
|
|
110
|
+
`<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(shareHref(post))}" target="_blank" rel="noreferrer">View post</a>`,
|
|
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>` : ""
|
|
112
|
+
].filter(Boolean).join("");
|
|
113
|
+
return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}">
|
|
114
|
+
<a class="rk-library-poster" href="${rkEscape(href)}" aria-label="${rkEscape(title)}">
|
|
115
|
+
${posterMedia(post)}
|
|
116
|
+
<span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
|
|
117
|
+
<span class="rk-library-dur">9:16</span>
|
|
118
|
+
<span class="rk-library-play">${PLAY}</span>
|
|
119
|
+
</a>
|
|
120
|
+
<div class="rk-library-meta">
|
|
121
|
+
<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-spread rk-library-foot">
|
|
124
|
+
<span class="rk-library-time">${rkEscape(fmtDate(post.created_at))}</span>
|
|
125
|
+
${primaryAction}
|
|
126
|
+
</div>
|
|
127
|
+
<div class="rk-library-actions">${actions}</div>
|
|
128
|
+
</div>
|
|
129
|
+
</article>`;
|
|
130
|
+
}
|
|
131
|
+
function approvedPanel(input, tabs) {
|
|
132
|
+
const posts = input.posts;
|
|
133
|
+
const accountId = input.currentAccountId;
|
|
134
|
+
// The quickfilter searchbar mirrors the Clips tab's pill search — always
|
|
135
|
+
// rendered so the topbar reads the same on both tabs (disabled when empty).
|
|
136
|
+
const searchBar = `<label class="rk-clips-search rk-library-qsearch">
|
|
137
|
+
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
138
|
+
<input class="rk-input rk-clips-search-input" data-rk-search type="search"
|
|
139
|
+
placeholder="Search cuts — title, caption, or tracer…" autocomplete="off" aria-label="Search cuts"${posts.length ? "" : " disabled"} />
|
|
140
|
+
</label>`;
|
|
141
|
+
if (posts.length === 0) {
|
|
142
|
+
return `
|
|
143
|
+
<div class="rk-library-topbar">
|
|
144
|
+
${tabs}
|
|
145
|
+
${searchBar}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="rk-library-empty is-visible">
|
|
149
|
+
<h3>No published cuts yet</h3>
|
|
150
|
+
<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="/discover">Start a project <span class="rk-arrow">→</span></a>
|
|
152
|
+
</div>`;
|
|
153
|
+
}
|
|
154
|
+
const readyCount = posts.filter((p) => p.status === "ready").length;
|
|
155
|
+
const scheduledCount = posts.filter((p) => p.status === "scheduled").length;
|
|
156
|
+
const chipDefs = [
|
|
157
|
+
["", "All"],
|
|
158
|
+
["ready", "Ready to post"],
|
|
159
|
+
["scheduled", "Scheduled"]
|
|
160
|
+
];
|
|
161
|
+
const chips = chipDefs
|
|
162
|
+
.map(([value, label], i) => `<button type="button" class="rk-library-chip${i === 0 ? " is-active" : ""}" data-rk-filter="${value}">${rkEscape(label)}</button>`)
|
|
163
|
+
.join("");
|
|
164
|
+
const newTile = `<a class="rk-library-new" data-rk-newtile href="/discover">
|
|
165
|
+
<span class="rk-tile rk-tile-lg rk-tile-gold rk-library-new-plus">+</span>
|
|
166
|
+
<div>
|
|
167
|
+
<div class="rk-library-new-title">New project</div>
|
|
168
|
+
<div class="rk-library-new-sub">Fork a template or start from a prompt</div>
|
|
169
|
+
</div>
|
|
170
|
+
</a>`;
|
|
171
|
+
const cards = posts.map((p) => projectCard(p, accountId)).join("");
|
|
172
|
+
return `
|
|
173
|
+
<div class="rk-library-topbar">
|
|
174
|
+
${tabs}
|
|
175
|
+
${searchBar}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="rk-library-toolbar">
|
|
179
|
+
<div class="rk-library-chips" role="tablist" aria-label="Filter by status">${chips}</div>
|
|
180
|
+
<div class="rk-library-summary"><b>${posts.length}</b> published cut${posts.length === 1 ? "" : "s"} · <b>${readyCount}</b> ready to post · <b>${scheduledCount}</b> scheduled</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="rk-library-grid" data-rk-grid>
|
|
184
|
+
${newTile}
|
|
185
|
+
${cards}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="rk-library-empty" data-rk-empty>
|
|
189
|
+
<h3>Nothing here yet</h3>
|
|
190
|
+
<p class="rk-muted">No cuts match this filter. Try another status or spin up a fresh render.</p>
|
|
191
|
+
<a class="rk-btn rk-btn-gold" href="/discover">Start a project <span class="rk-arrow">→</span></a>
|
|
192
|
+
</div>`;
|
|
193
|
+
}
|
|
194
|
+
/* ──────────────────────────────── Clips tab ───────────────────────────────── */
|
|
195
|
+
// The clip library is NOT part of the library route's input — the client fetches
|
|
196
|
+
// the REAL endpoints on load: GET /clips/feed (library), POST /clips/search
|
|
197
|
+
// (hybrid semantic search), POST /clips/scan + GET /clips/scan/:id (mine a
|
|
198
|
+
// 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
|
+
function clipsPanel(tabs) {
|
|
211
|
+
const filterChips = CLIP_PRESETS
|
|
212
|
+
.map((f, i) => `<button type="button" class="rk-clips-chip${i === 0 ? " is-active" : ""}" data-filter="${rkEscape(f.slug)}" data-q="${rkEscape(f.q)}">${rkEscape(f.label)}</button>`)
|
|
213
|
+
.join("");
|
|
214
|
+
return `
|
|
215
|
+
<div class="rk-library-topbar">
|
|
216
|
+
${tabs}
|
|
217
|
+
<form class="rk-clips-search" id="rkClipsSearch">
|
|
218
|
+
<span class="rk-clips-search-ic">${SEARCH_ICON}</span>
|
|
219
|
+
<input class="rk-input rk-clips-search-input" id="rkClipsQuery" type="search"
|
|
220
|
+
placeholder="Search raws — e.g. "someone looks confused after reading a message"" autocomplete="off" />
|
|
221
|
+
<button class="rk-btn rk-btn-ink rk-btn-sm rk-clips-search-btn" type="submit">Search</button>
|
|
222
|
+
</form>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div class="rk-clips-toolbar">
|
|
226
|
+
<div class="rk-clips-chipsrow">
|
|
227
|
+
<div class="rk-clips-chips" id="rkClipsChips">${filterChips}</div>
|
|
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>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="rk-clips-grid" id="rkClipsGrid"></div>
|
|
234
|
+
|
|
235
|
+
<div class="rk-clips-empty" id="rkClipsEmpty" hidden>
|
|
236
|
+
<h3>No clips yet</h3>
|
|
237
|
+
<p class="rk-muted">Click <b>Import source</b> to mine a long video into tagged, searchable short clips.</p>
|
|
238
|
+
</div>`;
|
|
239
|
+
}
|
|
240
|
+
const CLIPS_DIALOG = `
|
|
241
|
+
<dialog class="rk-clips-dialog" id="rkClipsDialog">
|
|
242
|
+
<form class="rk-clips-dialog-form" id="rkClipsScanForm" method="dialog">
|
|
243
|
+
<div class="rk-card-head">
|
|
244
|
+
<span class="rk-eyebrow">Import source</span>
|
|
245
|
+
<h2>Mine a video into clips</h2>
|
|
246
|
+
<p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll scan it and add the matching moments to your raws library.</p>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="rk-field">
|
|
249
|
+
<label class="rk-label" for="rkScanUrl">Video URL</label>
|
|
250
|
+
<input class="rk-input" id="rkScanUrl" name="url" type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" />
|
|
251
|
+
</div>
|
|
252
|
+
<div class="rk-field">
|
|
253
|
+
<label class="rk-label" for="rkScanPrompt">What to clip for</label>
|
|
254
|
+
<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>
|
|
255
|
+
<span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="rk-grid rk-grid-2 rk-clips-dialog-grid">
|
|
258
|
+
<div class="rk-field">
|
|
259
|
+
<label class="rk-label" for="rkScanProvider">AI provider</label>
|
|
260
|
+
<select class="rk-select" id="rkScanProvider" name="provider">
|
|
261
|
+
<option value="">Auto — use my saved keys</option>
|
|
262
|
+
<option value="gemini">Gemini</option>
|
|
263
|
+
<option value="openai">OpenAI</option>
|
|
264
|
+
<option value="openrouter">OpenRouter</option>
|
|
265
|
+
</select>
|
|
266
|
+
</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
|
+
</div>
|
|
272
|
+
<p class="rk-hint rk-clips-dialog-note" id="rkScanNote">AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.</p>
|
|
273
|
+
<div class="rk-clips-dialog-actions">
|
|
274
|
+
<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">Start scan <span class="rk-arrow">→</span></button>
|
|
276
|
+
</div>
|
|
277
|
+
</form>
|
|
278
|
+
</dialog>`;
|
|
279
|
+
/* ──────────────────────────────── Page shell ──────────────────────────────── */
|
|
280
|
+
const APPROVED_SCRIPT = `
|
|
281
|
+
(function(){
|
|
282
|
+
var root=document;
|
|
283
|
+
var grid=root.querySelector('[data-rk-grid]');
|
|
284
|
+
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
|
+
var search=root.querySelector('[data-rk-search]');
|
|
288
|
+
var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
|
|
289
|
+
var activeFilter='';
|
|
290
|
+
function apply(){
|
|
291
|
+
var q=(search&&search.value||'').trim().toLowerCase();
|
|
292
|
+
var shown=0;
|
|
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);
|
|
302
|
+
}
|
|
303
|
+
chips.forEach(function(btn){
|
|
304
|
+
btn.addEventListener('click',function(){
|
|
305
|
+
activeFilter=btn.getAttribute('data-rk-filter')||'';
|
|
306
|
+
chips.forEach(function(b){b.classList.toggle('is-active',b===btn)});
|
|
307
|
+
apply();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
if(search)search.addEventListener('input',apply);
|
|
311
|
+
// hover-to-play the real MP4 previews
|
|
312
|
+
root.querySelectorAll('video.rk-library-video').forEach(function(v){
|
|
313
|
+
var card=v.closest('.rk-library-card');
|
|
314
|
+
if(!card)return;
|
|
315
|
+
card.addEventListener('mouseenter',function(){ if(v.play){var p=v.play(); if(p&&p.catch)p.catch(function(){});} });
|
|
316
|
+
card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
|
|
317
|
+
});
|
|
318
|
+
apply();
|
|
319
|
+
})();`;
|
|
320
|
+
const CLIPS_SCRIPT = `
|
|
321
|
+
(function(){
|
|
322
|
+
var root=document;
|
|
323
|
+
var grid=root.getElementById('rkClipsGrid');
|
|
324
|
+
var statusEl=root.getElementById('rkClipsStatus');
|
|
325
|
+
var summaryEl=root.getElementById('rkClipsSummary');
|
|
326
|
+
var empty=root.getElementById('rkClipsEmpty');
|
|
327
|
+
var chips=root.getElementById('rkClipsChips');
|
|
328
|
+
var form=root.getElementById('rkClipsSearch');
|
|
329
|
+
var query=root.getElementById('rkClipsQuery');
|
|
330
|
+
|
|
331
|
+
function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
|
|
332
|
+
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
|
+
function topTags(tags){
|
|
334
|
+
if(!tags||typeof tags!=='object') return [];
|
|
335
|
+
var out=[];
|
|
336
|
+
['subject','action','emotion','composition'].forEach(function(k){ (tags[k]||[]).slice(0,1).forEach(function(v){ out.push(v); }); });
|
|
337
|
+
if(tags.energy) out.push('energy:'+tags.energy);
|
|
338
|
+
return out.slice(0,4);
|
|
339
|
+
}
|
|
340
|
+
function setGridEmpty(show){ if(empty)empty.hidden=!show; if(grid)grid.style.display=show?'none':''; }
|
|
341
|
+
|
|
342
|
+
function card(clip){
|
|
343
|
+
var range=fmtClock(clip.start_time_sec)+'–'+fmtClock(clip.end_time_sec);
|
|
344
|
+
var dur=(clip.duration_sec!=null)?(Number(clip.duration_sec).toFixed(1)+'s'):'';
|
|
345
|
+
var media=clip.thumbnail_url
|
|
346
|
+
? '<img class="rk-clips-thumb-img" loading="lazy" src="'+esc(clip.thumbnail_url)+'" alt="" />'
|
|
347
|
+
: '<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
|
+
var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
|
|
350
|
+
var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
|
|
351
|
+
var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
|
|
352
|
+
return '<article class="rk-clips-card rk-card-hover">'
|
|
353
|
+
+ '<div class="rk-clips-thumb">'+media+vid
|
|
354
|
+
+ '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
|
|
355
|
+
+ '<button type="button" class="rk-clips-play" aria-label="Preview clip">'+String.fromCharCode(9654)+'</button>'
|
|
356
|
+
+ '</div>'
|
|
357
|
+
+ '<div class="rk-clips-body">'
|
|
358
|
+
+ '<h3 class="rk-clips-title">'+esc(desc)+'</h3>'
|
|
359
|
+
+ (tags?'<div class="rk-clips-tags">'+tags+'</div>':'')
|
|
360
|
+
+ '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+esc(clip.source_filename)+'">'+esc(clip.source_filename||'')+'</span>'
|
|
361
|
+
+ '<a class="rk-clips-dl" href="'+dl+'">Download</a></div>'
|
|
362
|
+
+ '</div></article>';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function bindHover(){
|
|
366
|
+
if(!grid)return;
|
|
367
|
+
Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb')).forEach(function(thumb){
|
|
368
|
+
var v=thumb.querySelector('.rk-clips-thumb-vid');
|
|
369
|
+
if(!v)return;
|
|
370
|
+
thumb.addEventListener('mouseenter',function(){ v.style.display='block'; var p=v.play&&v.play(); if(p&&p.catch)p.catch(function(){}); });
|
|
371
|
+
thumb.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); v.style.display='none'; });
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function render(clips,label){
|
|
376
|
+
clips=clips||[];
|
|
377
|
+
if(clips.length===0){
|
|
378
|
+
if(statusEl)statusEl.textContent=label||'';
|
|
379
|
+
if(summaryEl)summaryEl.innerHTML='<b>0</b> clips in your library';
|
|
380
|
+
if(grid)grid.innerHTML='';
|
|
381
|
+
setGridEmpty(true);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
setGridEmpty(false);
|
|
385
|
+
if(statusEl)statusEl.textContent=(label||'')+clips.length+' clip'+(clips.length===1?'':'s');
|
|
386
|
+
if(summaryEl)summaryEl.innerHTML='<b>'+clips.length+'</b> clip'+(clips.length===1?'':'s')+' shown';
|
|
387
|
+
if(grid)grid.innerHTML=clips.map(card).join('');
|
|
388
|
+
bindHover();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function handleAuth(res){
|
|
392
|
+
if(res.status===401||res.status===403){
|
|
393
|
+
if(grid)grid.innerHTML='';
|
|
394
|
+
setGridEmpty(false);
|
|
395
|
+
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
|
+
throw new Error('unauthorized');
|
|
398
|
+
}
|
|
399
|
+
return res.json();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function loadFeed(){
|
|
403
|
+
if(statusEl)statusEl.textContent='Loading your raws…';
|
|
404
|
+
fetch('/raws/feed?limit=120',{credentials:'same-origin',headers:{Accept:'application/json'}})
|
|
405
|
+
.then(handleAuth).then(function(d){ render(d.clips,''); }).catch(function(){});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function search(q,label){
|
|
409
|
+
if(statusEl)statusEl.textContent='Searching…';
|
|
410
|
+
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(){});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if(form){
|
|
418
|
+
form.addEventListener('submit',function(e){
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
var q=(query&&query.value||'').trim();
|
|
421
|
+
if(chips)chips.querySelectorAll('.rk-clips-chip').forEach(function(b){b.classList.remove('is-active');});
|
|
422
|
+
if(q)search(q,null); else { var all=chips&&chips.querySelector('[data-filter="all"]'); if(all)all.classList.add('is-active'); loadFeed(); }
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if(chips){
|
|
426
|
+
chips.addEventListener('click',function(e){
|
|
427
|
+
var btn=e.target.closest('.rk-clips-chip');
|
|
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
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Scan a source (real clip hunt) ─────────────────────────────────────
|
|
438
|
+
var dialog=root.getElementById('rkClipsDialog');
|
|
439
|
+
var openBtns=Array.prototype.slice.call(root.querySelectorAll('[data-rk-open-scan]'));
|
|
440
|
+
var cancelBtn=root.getElementById('rkClipsScanCancel');
|
|
441
|
+
var scanForm=root.getElementById('rkClipsScanForm');
|
|
442
|
+
var scanUrl=root.getElementById('rkScanUrl');
|
|
443
|
+
var scanPrompt=root.getElementById('rkScanPrompt');
|
|
444
|
+
var scanProvider=root.getElementById('rkScanProvider');
|
|
445
|
+
var scanTracer=root.getElementById('rkScanTracer');
|
|
446
|
+
var scanNote=root.getElementById('rkScanNote');
|
|
447
|
+
var scanSubmit=root.getElementById('rkClipsScanSubmit');
|
|
448
|
+
var scanPoll=null;
|
|
449
|
+
function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
|
|
450
|
+
if(dialog){ openBtns.forEach(function(openBtn){ openBtn.addEventListener('click',function(){ setNote('AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.'); if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal(); }); }); }
|
|
451
|
+
if(cancelBtn&&dialog){ cancelBtn.addEventListener('click',function(){ if(dialog.open)dialog.close(); }); }
|
|
452
|
+
if(dialog){
|
|
453
|
+
dialog.addEventListener('click',function(e){
|
|
454
|
+
var r=dialog.getBoundingClientRect();
|
|
455
|
+
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();
|
|
456
|
+
});
|
|
457
|
+
}
|
|
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
|
+
if(scanForm){
|
|
477
|
+
scanForm.addEventListener('submit',function(e){
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
var url=(scanUrl&&scanUrl.value||'').trim();
|
|
480
|
+
var prompt=(scanPrompt&&scanPrompt.value||'').trim();
|
|
481
|
+
var tracer=(scanTracer&&scanTracer.value||'').trim();
|
|
482
|
+
var provider=(scanProvider&&scanProvider.value||'').trim();
|
|
483
|
+
if(!url){ setNote('Enter a video URL.','error'); return; }
|
|
484
|
+
setNote('Starting scan…');
|
|
485
|
+
if(scanSubmit)scanSubmit.disabled=true;
|
|
486
|
+
var payload={source_url:url,prompt:prompt};
|
|
487
|
+
if(tracer)payload.tracer=tracer;
|
|
488
|
+
if(provider)payload.provider=provider;
|
|
489
|
+
fetch('/raws/scan',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(payload)})
|
|
490
|
+
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); })
|
|
491
|
+
.then(function(res){
|
|
492
|
+
if(scanSubmit)scanSubmit.disabled=false;
|
|
493
|
+
if(!res.ok){ setNote((res.data&&res.data.error)||'Could not start the scan.','error'); return; }
|
|
494
|
+
setNote('Scan started — this can take a few minutes.','success');
|
|
495
|
+
if(statusEl)statusEl.textContent='Scanning source…'+(res.data&&res.data.tracer?' (tracer '+res.data.tracer+')':'');
|
|
496
|
+
if(dialog&&dialog.open)dialog.close();
|
|
497
|
+
if(scanForm.reset)scanForm.reset();
|
|
498
|
+
if(res.data&&res.data.scan_id)pollScan(res.data.scan_id);
|
|
499
|
+
})
|
|
500
|
+
.catch(function(){ if(scanSubmit)scanSubmit.disabled=false; setNote('Network error — please try again.','error'); });
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
loadFeed();
|
|
505
|
+
})();`;
|
|
506
|
+
export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPUT) {
|
|
507
|
+
const isRaws = tab === "raws";
|
|
508
|
+
const isLogs = tab === "logs";
|
|
509
|
+
const tabDef = [
|
|
510
|
+
{ key: "approved", label: "Approved", href: "/library/approved" },
|
|
511
|
+
{ key: "raws", label: "Raws", href: "/library/raws" },
|
|
512
|
+
{ key: "logs", label: "Logs", href: "/library/logs" }
|
|
513
|
+
];
|
|
514
|
+
const tabs = `<div class="rk-library-tabs" role="tablist" aria-label="Library view">
|
|
515
|
+
${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 ")}
|
|
516
|
+
</div>`;
|
|
517
|
+
// Logs tab reuses the real /job-runs history endpoint (client-fetched by the
|
|
518
|
+
// folded-in job-runs script); the seed is empty so it loads live.
|
|
519
|
+
const logsBoot = {
|
|
520
|
+
loadEndpoint: "/job-runs/history?limit=250",
|
|
521
|
+
accountId: input.currentAccountId || "",
|
|
522
|
+
storageDriver: "s3",
|
|
523
|
+
entries: []
|
|
524
|
+
};
|
|
525
|
+
const headSub = isLogs
|
|
526
|
+
? "Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost."
|
|
527
|
+
: isRaws
|
|
528
|
+
? "Long-form videos mined into tagged, searchable short clips — every reaction, hook, and money shot, ready to drop into an edit."
|
|
529
|
+
: "Every published cut you own — ready-to-post renders and scheduled drops. Open in the editor, share, download, or schedule.";
|
|
530
|
+
const headCta = isRaws
|
|
531
|
+
? `<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
|
|
533
|
+
? ""
|
|
534
|
+
: `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/discover">New project <span class="rk-arrow">→</span></a>`;
|
|
535
|
+
const panel = isLogs
|
|
536
|
+
? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
|
|
537
|
+
: isRaws
|
|
538
|
+
? clipsPanel(tabs)
|
|
539
|
+
: approvedPanel(input, tabs);
|
|
540
|
+
const body = `
|
|
541
|
+
<main class="rk-container-wide rk-page rk-library-page">
|
|
542
|
+
<div class="rk-spread rk-page-head rk-library-head">
|
|
543
|
+
<div class="rk-stack rk-library-head-copy">
|
|
544
|
+
<h1 class="rk-h1">Library</h1>
|
|
545
|
+
<p class="rk-sub">${headSub}</p>
|
|
546
|
+
</div>
|
|
547
|
+
${headCta}
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
${panel}
|
|
551
|
+
</main>
|
|
552
|
+
${isRaws ? CLIPS_DIALOG : ""}`;
|
|
553
|
+
const pageCss = `
|
|
554
|
+
/* .rk-page zeroes horizontal padding; the wide container == viewport at 1280 so
|
|
555
|
+
margin:auto adds no gutter. Restore side gutters (longhand beats the shorthand). */
|
|
556
|
+
.rk-library-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
|
|
557
|
+
/* compact head: title + sub left, small New project pinned top-right on the same row */
|
|
558
|
+
.rk-library-head{align-items:flex-start}
|
|
559
|
+
.rk-library-head-copy{gap:4px;margin:0;flex:1 1 auto;min-width:0}
|
|
560
|
+
.rk-library-new-btn{flex:none;margin-top:2px}
|
|
561
|
+
|
|
562
|
+
/* topbar row: primary tab toggle + the tab's quick search share one line */
|
|
563
|
+
.rk-library-topbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:22px}
|
|
564
|
+
|
|
565
|
+
/* primary tab toggle (Approved | Clips) */
|
|
566
|
+
.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)}
|
|
567
|
+
.rk-library-tab{display:inline-flex;align-items:center;gap:7px;padding:9px 20px;border-radius:var(--rk-r-full);font-size:14px;font-weight:600;color:var(--rk-n-600);transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
568
|
+
.rk-library-tab span{font-size:15px}
|
|
569
|
+
.rk-library-tab:hover{color:var(--rk-ink)}
|
|
570
|
+
.rk-library-tab.is-active{background:var(--rk-ink);color:#fff;box-shadow:var(--rk-shadow-sm)}
|
|
571
|
+
|
|
572
|
+
/* stats — sits right-aligned in the chips row */
|
|
573
|
+
.rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;line-height:1.7}
|
|
574
|
+
.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,.rk-clips-chipsrow .rk-library-summary{margin-left:auto}
|
|
576
|
+
|
|
577
|
+
/* toolbar (filter chips + count) */
|
|
578
|
+
.rk-library-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:24px}
|
|
579
|
+
.rk-library-chips{display:flex;flex-wrap:wrap;gap:4px}
|
|
580
|
+
.rk-library-chip{display:inline-flex;align-items:center;padding:6px 13px;border-radius:var(--rk-r-full);border:1px solid transparent;background:transparent;color:var(--rk-text-muted);font-size:13px;font-weight:600;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
|
|
581
|
+
.rk-library-chip:hover{background:var(--rk-n-100);color:var(--rk-ink)}
|
|
582
|
+
.rk-library-chip.is-active{background:var(--rk-ink);color:#fff}
|
|
583
|
+
/* approved quickfilter — same pill search as the Clips tab, right-aligned */
|
|
584
|
+
.rk-library-qsearch{flex:1 1 220px;max-width:360px;margin-left:auto}
|
|
585
|
+
.rk-library-qsearch .rk-clips-search-input:disabled{background:var(--rk-n-100);cursor:not-allowed}
|
|
586
|
+
@media(max-width:720px){.rk-library-qsearch{max-width:none;margin-left:0;width:100%}}
|
|
587
|
+
|
|
588
|
+
/* grid + cards */
|
|
589
|
+
.rk-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px}
|
|
590
|
+
.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
|
+
.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)}
|
|
593
|
+
.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
|
+
.rk-library-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#050505}
|
|
595
|
+
.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}
|
|
596
|
+
.rk-library-pstatus{position:absolute;top:12px;left:12px;z-index:2;box-shadow:var(--rk-shadow-sm)}
|
|
597
|
+
.rk-library-dot{width:6px;height:6px;border-radius:var(--rk-r-full);background:currentColor;opacity:.9}
|
|
598
|
+
.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
|
+
.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
|
+
.rk-library-card:hover .rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}
|
|
601
|
+
.rk-library-play svg{margin-left:2px}
|
|
602
|
+
.rk-library-meta{display:grid;gap:10px;padding:16px 16px 18px}
|
|
603
|
+
.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:8px;min-width:0;font-size:12.5px;font-weight:500;color:var(--rk-text-muted)}
|
|
605
|
+
.rk-library-origin-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
606
|
+
.rk-library-foot{margin-top:2px;gap:10px}
|
|
607
|
+
.rk-library-foot .rk-btn{flex:none}
|
|
608
|
+
.rk-library-time{font-size:12px;font-weight:500;color:var(--rk-text-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
609
|
+
.rk-library-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}
|
|
610
|
+
.rk-library-actions .rk-btn{flex:none}
|
|
611
|
+
|
|
612
|
+
/* new-project tile */
|
|
613
|
+
.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)}
|
|
614
|
+
.rk-library-new:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);transform:translateY(-2px)}
|
|
615
|
+
.rk-library-new-plus{font-family:var(--rk-font-display);font-weight:800}
|
|
616
|
+
.rk-library-new-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink);letter-spacing:-.01em}
|
|
617
|
+
.rk-library-new-sub{font-size:12.5px;color:var(--rk-text-muted);margin-top:3px;max-width:15ch}
|
|
618
|
+
.rk-library-new[hidden]{display:none}
|
|
619
|
+
|
|
620
|
+
/* empty state */
|
|
621
|
+
.rk-library-empty{display:none;place-items:center;gap:12px;text-align:center;padding:64px 24px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-3xl);background:var(--rk-n-50)}
|
|
622
|
+
.rk-library-empty.is-visible{display:grid}
|
|
623
|
+
.rk-library-empty h3{font-size:var(--rk-text-xl)}
|
|
624
|
+
.rk-library-empty p{max-width:42ch}
|
|
625
|
+
|
|
626
|
+
@media(prefers-reduced-motion:reduce){.rk-library-video{}}
|
|
627
|
+
|
|
628
|
+
/* ── clips tab ── */
|
|
629
|
+
.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
|
+
.rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 300px;min-width:240px;margin:0}
|
|
632
|
+
.rk-clips-search-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
|
|
633
|
+
.rk-clips-search-input{flex:1;padding-left:40px;border-radius:var(--rk-r-full)}
|
|
634
|
+
.rk-clips-search-btn{flex:none}
|
|
635
|
+
.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
|
+
.rk-clips-status{font-size:13px;color:var(--rk-text-muted);font-weight:500}
|
|
641
|
+
.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
|
+
.rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:22px}
|
|
643
|
+
.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
|
+
.rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
|
|
645
|
+
.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}
|
|
647
|
+
.rk-clips-noimg{color:rgba(255,255,255,.68);font-size:12px;font-weight:600;letter-spacing:.01em}
|
|
648
|
+
.rk-clips-dur{position:absolute;left:10px;bottom:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums;box-shadow:var(--rk-shadow-xs);z-index:2}
|
|
649
|
+
.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
|
+
.rk-clips-card:hover .rk-clips-play{opacity:1;transform:scale(1)}
|
|
651
|
+
.rk-clips-play:hover{background:var(--rk-gold-500)}
|
|
652
|
+
.rk-clips-body{display:flex;flex-direction:column;gap:10px;flex:1;padding:16px}
|
|
653
|
+
.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}
|
|
654
|
+
.rk-clips-tags{display:flex;flex-wrap:wrap;gap:5px}
|
|
655
|
+
.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
|
+
.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
|
+
.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}
|
|
659
|
+
.rk-clips-dl:hover{color:var(--rk-gold-500)}
|
|
660
|
+
.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
|
+
.rk-clips-empty[hidden]{display:none}
|
|
662
|
+
.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)}
|
|
664
|
+
.rk-clips-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
|
|
665
|
+
.rk-clips-dialog-form{display:grid;gap:16px;margin:0;padding:28px}
|
|
666
|
+
.rk-clips-dialog-grid{gap:14px}
|
|
667
|
+
.rk-clips-dialog-note{background:var(--rk-gold-tint);border-radius:var(--rk-r-xl);padding:10px 13px;color:var(--rk-n-700)}
|
|
668
|
+
.rk-clips-dialog-note[data-tone="error"]{background:#fdecea;color:#8d3b2f}
|
|
669
|
+
.rk-clips-dialog-note[data-tone="success"]{background:#e8f7ee;color:#1f7a44}
|
|
670
|
+
.rk-clips-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
671
|
+
@media(max-width:620px){
|
|
672
|
+
.rk-clips-search{flex:1 1 100%}
|
|
673
|
+
.rk-clips-scan{flex:1 1 100%}
|
|
674
|
+
.rk-clips-search-input{font-size:16px}
|
|
675
|
+
.rk-clips-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
676
|
+
}
|
|
677
|
+
`;
|
|
678
|
+
const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : "";
|
|
679
|
+
return reskinDocument({
|
|
680
|
+
title: `vidfarm reskin — Library${tabTitle}`,
|
|
681
|
+
description: "Reskinned vidfarm library: your published cuts, mined raws library, and job run history.",
|
|
682
|
+
activeSlug: "library",
|
|
683
|
+
pageCss: pageCss + (isLogs ? "\n" + JOBRUNS_PAGE_CSS : ""),
|
|
684
|
+
body,
|
|
685
|
+
script: isLogs ? JOBRUNS_SCRIPT : isRaws ? CLIPS_SCRIPT : APPROVED_SCRIPT
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
//# sourceMappingURL=library-page.js.map
|