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