@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.
Files changed (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +476 -260
  14. package/dist/src/cli.js +362 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21603
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -828
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3654
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. 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,'&quot;'); }
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,'&quot;'); }
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