@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) 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 +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -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 +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  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 +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,890 +0,0 @@
1
- // /tools/clipper — the third creative-tool twin. Where /tools/image does masked
2
- // image edits and /tools/video does time-scoped video edits, /tools/clipper cuts
3
- // EXACT subranges out of a source video (or audio) into the raws library. It
4
- // plays the source straight from its resolved RapidAPI/direct URL (nothing is
5
- // uploaded to preview), lets the user pick an in/out point down to sub-second
6
- // precision (HH:MM:SS:NNNN) on ONE unified timeline shared with the playhead,
7
- // shows a live estimate of the clip length, and on "Clip Media" POSTs to
8
- // /raws/clip-range which trims just that window and lands one raw. It is a
9
- // MULTI-CLIP surface — the source stays loaded so you can grab clip after clip,
10
- // each grouped under the session's tracer/folder and listed in the right-hand
11
- // history card, with a past-sessions slide-in grouped by tracer (like /tools/*).
12
- //
13
- // Layout: one big LEFT column (source URL + compact ingest above the preview;
14
- // video preview; unified in/out timeline + <advanced> below) and a RIGHT history
15
- // card styled to match /tools/image. Works for audio sources too.
16
- //
17
- // Data flow:
18
- // • load source → POST /raws/resolve-source { source_url } → { preview_url, ... }
19
- // • clip media → POST /raws/clip-range { preview_url, start_sec, end_sec, tracer, folder_path, name, notes, audio_only }
20
- // • session list → GET /raws/feed?folder=<slug> (this session's clips)
21
- // • past sessions → GET /raws/feed?limit=200 grouped by folder_path/tracer
22
- import { reskinDocument } from "./document.js";
23
- import { renderInpaintModeTabs, INPAINT_TABS_CSS } from "./inpaint-page.js";
24
- const UPLOAD_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`;
25
- const GEAR_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.8 3.4 9.4 2h1.2l.6 1.4 1.2.5 1.4-.6.9.9-.6 1.4.5 1.2 1.4.6v1.2l-1.4.6-.5 1.2.6 1.4-.9.9-1.4-.6-1.2.5-.6 1.4H9.4l-.6-1.4-1.2-.5-1.4.6-.9-.9.6-1.4-.5-1.2L4 8.6V7.4l1.4-.6.5-1.2-.6-1.4.9-.9 1.4.6 1.2-.5Z"/><circle cx="10" cy="8" r="2.4"/></svg>`;
26
- const HISTORY_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 10a6.8 6.8 0 1 1 2 4.8"/><path d="M3.2 14.4v-3.6h3.6"/><path d="M10 6.2V10l2.6 1.6"/></svg>`;
27
- const REFRESH_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"/><path d="M16.5 4.2v4.2h-4.2"/></svg>`;
28
- const PLUS_ICON = `<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg>`;
29
- const CLIP_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg>`;
30
- const PLAY_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z"/></svg>`;
31
- const PAUSE_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>`;
32
- const CLOSE_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg>`;
33
- const FOLDER_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 5.5A1.5 1.5 0 0 1 4 4h3.2l1.4 1.6H16a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 16 15.6H4a1.5 1.5 0 0 1-1.5-1.5Z"/></svg>`;
34
- const CHEVRON_ICON = `<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 8 4 4 4-4"/></svg>`;
35
- const AUDIO_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10v4"/><path d="M8 6v12"/><path d="M12 3v18"/><path d="M16 7v10"/><path d="M20 10v4"/></svg>`;
36
- export function renderReskinInpaintClipper(input = {}) {
37
- const boot = { accountId: input.accountId || "", isLoggedIn: Boolean(input.isLoggedIn) };
38
- const body = `
39
- <main class="rk-container-wide rk-page rk-inpaint-page rk-clp-page">
40
- <div class="rk-page-head rk-inpaint-head rk-clp-head">
41
- <div class="rk-inpaint-head-text">
42
- ${renderInpaintModeTabs("clipper")}
43
- <h1 class="rk-h1">Clip Media</h1>
44
- <p class="rk-sub">Paste (or upload) a video or audio source, scrub to the exact moment, and drag the <strong>in</strong> and <strong>out</strong> edges on the timeline (HH:MM:SS:NNNN). Only the selected subrange is saved to your raws — grab as many clips as you like from the same source.</p>
45
- </div>
46
- <div class="rk-inpaint-toolbar">
47
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-clp-start-new>${PLUS_ICON} Start New</button>
48
- <button type="button" class="rk-ip-iconbtn" data-clp-settings-button aria-label="Settings" title="Settings">${GEAR_ICON}</button>
49
- <button type="button" class="rk-ip-iconbtn" data-clp-history-toggle aria-label="Past sessions" title="Past sessions">${HISTORY_ICON}</button>
50
- </div>
51
- </div>
52
-
53
- <div class="rk-clp-workbench">
54
- <!-- LEFT: source (top) → preview → timeline + advanced (bottom) -->
55
- <section class="rk-clp-main" aria-label="Clip controls">
56
- <!-- Source, above the preview -->
57
- <div class="rk-clp-sourcebar">
58
- <label class="rk-label" for="rkClpUrl">Source URL <span class="rk-hint">video or audio</span></label>
59
- <div class="rk-clp-urlrow">
60
- <input class="rk-input" id="rkClpUrl" data-clp-url-input type="url" placeholder="https://youtube.com/watch?v=… or https://…/media.mp4" autocomplete="off" spellcheck="false" />
61
- <button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-clp-loadbtn" data-clp-url-load>Load</button>
62
- <span class="rk-clp-or">or</span>
63
- <button type="button" class="rk-ip-iconbtn rk-clp-ingestbtn" data-clp-upload title="Upload from your computer" aria-label="Upload">${UPLOAD_ICON}</button>
64
- <button type="button" class="rk-ip-iconbtn rk-clp-ingestbtn" data-clp-attach title="Attach from your Files" aria-label="From Files">${FOLDER_ICON}</button>
65
- </div>
66
- <input data-clp-file type="file" accept="video/*,audio/*" hidden />
67
- <span class="rk-hint rk-clp-sourcehint">TikTok, YouTube, Instagram, X, or a direct link — we play it straight from the source; nothing is saved until you clip.</span>
68
- </div>
69
-
70
- <!-- Preview -->
71
- <div class="rk-clp-stage" data-clp-stage>
72
- <div class="rk-clp-empty" data-clp-empty>
73
- <span class="rk-clp-empty-ic" aria-hidden="true">${CLIP_ICON}</span>
74
- <p>Load media to start clipping.</p>
75
- <span>Paste a URL and hit <strong>Load</strong>, or upload a file.</span>
76
- </div>
77
- <div class="rk-clp-player" data-clp-player hidden>
78
- <div class="rk-clp-videowrap" data-clp-videowrap>
79
- <video data-clp-video playsinline preload="auto"></video>
80
- <div class="rk-clp-audioposter" data-clp-audio-poster hidden>
81
- <span class="rk-clp-audioposter-ic" aria-hidden="true">${AUDIO_ICON}</span>
82
- <span>Audio source</span>
83
- </div>
84
- </div>
85
- </div>
86
- </div>
87
-
88
- <!-- Transport + unified timeline (only when a source is loaded) -->
89
- <div class="rk-clp-controls" data-clp-controls hidden>
90
- <div class="rk-clp-transport">
91
- <button type="button" class="rk-ip-iconbtn rk-clp-playpause" data-clp-playpause aria-label="Play / pause">${PLAY_ICON}</button>
92
- <input class="rk-clp-tc rk-clp-tc-now" data-clp-timecode type="text" inputmode="numeric" value="00:00:00:0000" aria-label="Current time" />
93
- <span class="rk-clp-duration" data-clp-duration>/ 00:00:00:0000</span>
94
- <div class="rk-clp-spacer"></div>
95
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-xs" data-clp-whole title="Select the whole source">Whole</button>
96
- </div>
97
-
98
- <!-- ONE timeline: playhead + draggable in/out edges + selected subrange -->
99
- <div class="rk-clp-timeline" data-clp-timeline>
100
- <div class="rk-clp-tl-track" data-clp-tl-track>
101
- <div class="rk-clp-tl-sel" data-clp-tl-sel></div>
102
- <div class="rk-clp-tl-playhead" data-clp-tl-playhead aria-hidden="true"></div>
103
- <button type="button" class="rk-clp-tl-handle rk-clp-tl-handle-in" data-clp-tl-in aria-label="In point"><span></span></button>
104
- <button type="button" class="rk-clp-tl-handle rk-clp-tl-handle-out" data-clp-tl-out aria-label="Out point"><span></span></button>
105
- </div>
106
- </div>
107
-
108
- <div class="rk-clp-inout">
109
- <label class="rk-clp-field">
110
- <span class="rk-clp-field-label">In</span>
111
- <input class="rk-input rk-clp-tc" data-clp-start-input type="text" inputmode="numeric" value="00:00:00:0000" />
112
- <button type="button" class="rk-clp-tinybtn" data-clp-set-start title="Set in-point to the playhead">Set to playhead</button>
113
- </label>
114
- <label class="rk-clp-field">
115
- <span class="rk-clp-field-label">Out</span>
116
- <input class="rk-input rk-clp-tc" data-clp-end-input type="text" inputmode="numeric" value="00:00:00:0000" />
117
- <button type="button" class="rk-clp-tinybtn" data-clp-set-end title="Set out-point to the playhead">Set to playhead</button>
118
- </label>
119
- <div class="rk-clp-lenbox">
120
- <span class="rk-clp-field-label">Length</span>
121
- <strong data-clp-length>0:00.0</strong>
122
- </div>
123
- </div>
124
- <div class="rk-clp-rangenote" data-clp-range-note hidden></div>
125
-
126
- <!-- Advanced (collapsed) -->
127
- <div class="rk-clp-advanced" data-clp-advanced>
128
- <button type="button" class="rk-clp-adv-toggle" data-clp-adv-toggle aria-expanded="false">
129
- <span class="rk-clp-adv-chev" aria-hidden="true">${CHEVRON_ICON}</span>
130
- <span>Advanced</span>
131
- <span class="rk-hint rk-clp-adv-hint">name · notes · audio only</span>
132
- </button>
133
- <div class="rk-clp-adv-body" data-clp-adv-body hidden>
134
- <label class="rk-label" for="rkClpName">Filename <span class="rk-hint">optional</span></label>
135
- <input class="rk-input" id="rkClpName" data-clp-name type="text" placeholder="hook-01" autocomplete="off" />
136
- <label class="rk-label" for="rkClpNotes" style="margin-top:11px">Notes <span class="rk-hint">helps you find this clip later by search</span></label>
137
- <textarea class="rk-input rk-clp-notes" id="rkClpNotes" data-clp-notes rows="2" placeholder="What's in this clip / how you'll use it — indexed for search."></textarea>
138
- <label class="rk-label" for="rkClpFolder" style="margin-top:11px">Save to raws folder <span class="rk-hint">optional</span></label>
139
- <input class="rk-input" id="rkClpFolder" data-clp-folder type="text" placeholder="defaults to the session tracer" autocomplete="off" spellcheck="false" />
140
- <label class="rk-clp-audio-toggle">
141
- <input type="checkbox" data-clp-audio-only />
142
- <span class="rk-clp-audio-text">
143
- <strong>Clip audio only</strong>
144
- <span class="rk-hint">Save just the sound (no video) — e.g. grab a track from a copyright-free sound video. Your choice is remembered.</span>
145
- </span>
146
- </label>
147
- </div>
148
- </div>
149
-
150
- <button type="button" class="rk-btn rk-btn-gold rk-clp-savebtn" data-clp-save>
151
- ${CLIP_ICON}<span data-clp-save-label>Clip Media</span>
152
- </button>
153
- <div class="rk-clp-status" data-clp-status role="status"></div>
154
- </div>
155
- </section>
156
-
157
- <!-- RIGHT: this-session history card -->
158
- <aside class="rk-clp-side" aria-label="Clips this session">
159
- <div class="rk-clp-side-card">
160
- <div class="rk-clp-side-head">
161
- <h2 class="rk-inpaint-h2">This session</h2>
162
- <button type="button" class="rk-ip-iconbtn rk-ip-iconbtn-sm" data-clp-session-refresh aria-label="Refresh" title="Refresh">${REFRESH_ICON}</button>
163
- </div>
164
- <div class="rk-clp-session-list" data-clp-session-list>
165
- <div class="rk-clp-session-empty">No clips yet — set an in/out point and clip your first media.</div>
166
- </div>
167
- <a class="rk-clp-librarylink" href="/library/raws">Open your raws library →</a>
168
- </div>
169
- </aside>
170
- </div>
171
-
172
- <!-- Settings modal -->
173
- <div class="rk-ip-settingsmodal" data-clp-settings hidden>
174
- <div class="rk-ip-settingsmodal-panel" role="dialog" aria-modal="true" aria-label="Clipper settings">
175
- <div class="rk-ip-settingsmodal-head">
176
- <span class="rk-ip-settingsmodal-title">Settings</span>
177
- <button type="button" class="rk-ip-iconbtn" data-clp-settings-close aria-label="Close">${CLOSE_ICON}</button>
178
- </div>
179
- <div class="rk-ip-settingsmodal-body">
180
- <label class="rk-ip-setting">
181
- <span class="rk-label">Session name <span class="rk-ip-setting-note">(tracer — groups this session's clips)</span></span>
182
- <input class="rk-ip-input" id="rkClpTracer" data-clp-tracer type="text" placeholder="auto — named from the source" autocomplete="off" spellcheck="false" />
183
- </label>
184
- <span class="rk-hint">Every clip you save in this session is grouped under this tracer — and lands in a matching <code>/raws</code> folder.</span>
185
- </div>
186
- </div>
187
- </div>
188
-
189
- <!-- Past sessions slide-in -->
190
- <aside class="rk-ip-sessions rk-clp-sessions" data-clp-sessions aria-label="Past clipping sessions">
191
- <div class="rk-ip-sessions-head">
192
- <span class="rk-ip-sessions-title">Past sessions</span>
193
- <button type="button" class="rk-ip-iconbtn" data-clp-sessions-close aria-label="Close">${CLOSE_ICON}</button>
194
- </div>
195
- <div class="rk-ip-sessions-list" data-clp-sessions-list></div>
196
- </aside>
197
- <div class="rk-ip-sessions-backdrop" data-clp-sessions-backdrop hidden></div>
198
-
199
- <script type="application/json" data-clp-boot>${JSON.stringify(boot)}</script>
200
- </main>`;
201
- const pageCss = `
202
- ${INPAINT_TABS_CSS}
203
- .rk-clp-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter);padding-bottom:44px}
204
-
205
- /* shared page-head toolbar (Start New + gear + history) — top-right, like /tools/image */
206
- .rk-inpaint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
207
- .rk-inpaint-head-text{min-width:0;flex:1 1 340px}
208
- .rk-inpaint-toolbar{flex:none;display:flex;align-items:center;gap:8px;padding-top:2px}
209
- .rk-inpaint-startnew{gap:6px}
210
- .rk-inpaint-h2{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:800;color:var(--rk-ink);letter-spacing:-.01em;margin:0}
211
-
212
- /* shared icon buttons */
213
- .rk-ip-iconbtn{display:grid;place-items:center;width:34px;height:34px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:#fff;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)}
214
- .rk-ip-iconbtn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
215
- .rk-ip-iconbtn:disabled{opacity:.45;cursor:not-allowed;background:var(--rk-n-50);color:var(--rk-n-400);border-color:var(--rk-border)}
216
- .rk-ip-iconbtn-sm{width:28px;height:28px}
217
-
218
- /* workbench grid — one big left column + right history card */
219
- .rk-clp-workbench{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:20px;align-items:start}
220
- .rk-clp-main{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);padding:18px;display:grid;gap:16px;align-content:start}
221
-
222
- /* source bar (above the preview) */
223
- .rk-clp-sourcebar{display:grid;gap:8px}
224
- .rk-clp-urlrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
225
- .rk-clp-urlrow .rk-input{flex:1 1 240px;min-width:0}
226
- .rk-clp-loadbtn{flex:none}
227
- .rk-clp-or{flex:none;font-size:11.5px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--rk-text-faint)}
228
- .rk-clp-ingestbtn{flex:none}
229
- .rk-clp-sourcehint{line-height:1.4}
230
- .rk-clp-loadbtn[data-loading="true"]{opacity:.7;pointer-events:none}
231
- .rk-clp-sourcebar.is-loading .rk-clp-ingestbtn,.rk-clp-sourcebar.is-loading .rk-input{opacity:.5;pointer-events:none}
232
-
233
- /* preview stage */
234
- .rk-clp-stage{min-height:300px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
235
- .rk-clp-empty{padding:44px 24px;text-align:center;color:var(--rk-text-muted)}
236
- .rk-clp-empty[hidden]{display:none}
237
- .rk-clp-empty-ic{display:inline-grid;place-items:center;color:var(--rk-n-400);margin-bottom:10px}
238
- .rk-clp-empty-ic svg{width:30px;height:30px}
239
- .rk-clp-empty p{font-size:15px;font-weight:800;color:var(--rk-ink);margin:0 0 4px}
240
- .rk-clp-empty span{font-size:12.5px}
241
- .rk-clp-player{width:100%}
242
- .rk-clp-player[hidden]{display:none}
243
- .rk-clp-videowrap{width:100%;background:#000;display:flex;align-items:center;justify-content:center;position:relative;min-height:300px;max-height:58vh}
244
- .rk-clp-videowrap video{width:100%;max-height:58vh;display:block}
245
- .rk-clp-audioposter{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:#fff;background:linear-gradient(135deg,#2a2622,#1b1815)}
246
- .rk-clp-audioposter[hidden]{display:none}
247
- .rk-clp-audioposter-ic{display:grid;place-items:center;color:var(--rk-gold-500,#f0b429)}
248
- .rk-clp-audioposter-ic svg{width:34px;height:34px}
249
- .rk-clp-audioposter span{font-size:13px;font-weight:700;letter-spacing:.02em}
250
-
251
- /* transport row */
252
- .rk-clp-controls[hidden]{display:none}
253
- .rk-clp-controls{display:grid;gap:13px}
254
- .rk-clp-transport{display:flex;align-items:center;gap:10px}
255
- .rk-clp-spacer{flex:1}
256
- .rk-clp-tc{font-variant-numeric:tabular-nums;letter-spacing:.02em;font-weight:700}
257
- .rk-clp-tc-now{width:132px;padding:6px 9px;font-size:12.5px}
258
- .rk-clp-duration{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:12.5px;color:var(--rk-text-muted)}
259
- .rk-btn-xs{padding:4px 11px;font-size:11.5px}
260
-
261
- /* unified timeline */
262
- .rk-clp-timeline{padding:16px 12px 8px}
263
- .rk-clp-tl-track{position:relative;height:12px;border-radius:999px;background:var(--rk-n-200,#e7e2da);cursor:pointer;touch-action:none}
264
- .rk-clp-tl-sel{position:absolute;top:0;bottom:0;left:0;width:100%;background:var(--rk-gold-500,#f0b429);border-radius:999px;box-shadow:inset 0 0 0 1px var(--rk-gold-600,#dd9a1f)}
265
- .rk-clp-tl-playhead{position:absolute;top:-4px;bottom:-4px;left:0;width:2px;background:var(--rk-ink);border-radius:2px;transform:translateX(-1px);pointer-events:none;z-index:3}
266
- .rk-clp-tl-playhead::after{content:"";position:absolute;top:-3px;left:50%;width:9px;height:9px;background:var(--rk-ink);border:2px solid #fff;border-radius:50%;transform:translateX(-50%)}
267
- .rk-clp-tl-handle{position:absolute;top:50%;width:16px;height:26px;padding:0;border:0;background:transparent;cursor:ew-resize;transform:translate(-50%,-50%);z-index:4;touch-action:none}
268
- .rk-clp-tl-handle span{display:block;width:14px;height:26px;margin:0 auto;border-radius:5px;background:#fff;border:2px solid var(--rk-gold-600,#dd9a1f);box-shadow:var(--rk-shadow-sm),0 0 0 1px rgba(0,0,0,.04)}
269
- .rk-clp-tl-handle:hover span,.rk-clp-tl-handle.is-drag span{border-color:var(--rk-gold-700);background:var(--rk-gold-tint)}
270
- .rk-clp-tl-handle::before{content:"";position:absolute;top:50%;left:50%;width:2px;height:12px;background:var(--rk-gold-700,#c98a1a);border-radius:2px;transform:translate(-50%,-50%);opacity:.7}
271
-
272
- /* in / out inputs */
273
- .rk-clp-inout{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap}
274
- .rk-clp-field{display:flex;flex-direction:column;gap:5px;flex:1 1 150px;min-width:130px}
275
- .rk-clp-field-label{font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-muted)}
276
- .rk-clp-tinybtn{align-self:flex-start;border:0;background:transparent;color:var(--rk-gold-ink,#9a6b12);font-size:11px;font-weight:700;cursor:pointer;padding:1px 2px;border-radius:6px}
277
- .rk-clp-tinybtn:hover{text-decoration:underline}
278
- .rk-clp-lenbox{display:flex;flex-direction:column;gap:5px;flex:0 0 auto;padding-bottom:2px}
279
- .rk-clp-lenbox strong{font-size:14px;color:var(--rk-ink);font-variant-numeric:tabular-nums}
280
- .rk-clp-rangenote{font-size:12px;font-weight:600;color:#b42318}
281
- .rk-clp-rangenote[hidden]{display:none}
282
-
283
- /* advanced */
284
- .rk-clp-advanced{border-top:1px solid var(--rk-border);padding-top:12px}
285
- .rk-clp-adv-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:2px;border:0;background:transparent;cursor:pointer;font:inherit;font-size:13px;font-weight:800;color:var(--rk-ink);text-align:left}
286
- .rk-clp-adv-chev{display:grid;place-items:center;color:var(--rk-text-muted);transition:transform var(--rk-dur) var(--rk-ease)}
287
- .rk-clp-adv-toggle[aria-expanded="true"] .rk-clp-adv-chev{transform:rotate(180deg)}
288
- .rk-clp-adv-hint{font-weight:600;margin-left:auto}
289
- .rk-clp-adv-body{margin-top:12px}
290
- .rk-clp-adv-body[hidden]{display:none}
291
- .rk-clp-notes{min-height:52px;resize:vertical;line-height:1.45}
292
- .rk-clp-audio-toggle{display:flex;gap:9px;align-items:flex-start;margin-top:14px;cursor:pointer}
293
- .rk-clp-audio-toggle input{margin-top:2px;flex:none;width:16px;height:16px;accent-color:var(--rk-gold-700);cursor:pointer}
294
- .rk-clp-audio-text{display:flex;flex-direction:column;gap:2px;line-height:1.35}
295
- .rk-clp-audio-text strong{font-size:13px;color:var(--rk-ink);font-weight:800}
296
- .rk-clp-audio-text .rk-hint{font-size:11.5px}
297
-
298
- /* clip button + status */
299
- .rk-clp-savebtn{width:100%;justify-content:center;gap:8px;padding:13px}
300
- .rk-clp-savebtn[data-loading="true"]{opacity:.7;pointer-events:none}
301
- .rk-clp-savebtn:disabled{opacity:.5;cursor:not-allowed}
302
- .rk-clp-status{font-size:12.5px;font-weight:600;min-height:16px;line-height:1.5}
303
- .rk-clp-status[data-tone="error"]{color:#b42318}
304
- .rk-clp-status[data-tone="success"]{color:#1f7a44}
305
-
306
- /* right: this-session history card (matches /tools/image) */
307
- .rk-clp-side{position:sticky;top:16px}
308
- .rk-clp-side-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);padding:18px;display:grid;gap:12px;align-content:start}
309
- .rk-clp-side-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
310
- .rk-clp-session-list{display:flex;flex-direction:column;gap:9px;max-height:62vh;overflow:auto}
311
- .rk-clp-session-empty{font-size:12.5px;color:var(--rk-text-faint);padding:14px 4px;line-height:1.5;text-align:center}
312
- .rk-clp-clip{display:flex;gap:10px;align-items:center;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50);text-decoration:none}
313
- .rk-clp-clip:hover{border-color:var(--rk-gold-600)}
314
- .rk-clp-clip-thumb{flex:none;width:60px;height:40px;border-radius:var(--rk-r-lg);object-fit:cover;background:var(--rk-n-200,#e7e2da)}
315
- .rk-clp-clip-meta{min-width:0;flex:1;display:grid;gap:3px}
316
- .rk-clp-clip-title{font-size:12.5px;font-weight:700;color:var(--rk-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
317
- .rk-clp-clip-sub{font-size:11px;color:var(--rk-text-muted);font-variant-numeric:tabular-nums}
318
- .rk-clp-librarylink{font-size:12.5px;font-weight:700;color:var(--rk-gold-ink,#9a6b12);text-decoration:none}
319
- .rk-clp-librarylink:hover{text-decoration:underline}
320
-
321
- /* settings modal (shared look with /tools/image) */
322
- .rk-ip-settingsmodal{position:fixed;inset:0;z-index:70;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;animation:rk-clp-fade .16s var(--rk-ease)}
323
- .rk-ip-settingsmodal[hidden]{display:none}
324
- .rk-ip-settingsmodal-panel{width:min(440px,100%);background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
325
- .rk-ip-settingsmodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
326
- .rk-ip-settingsmodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
327
- .rk-ip-settingsmodal-body{padding:16px 18px;display:grid;gap:10px}
328
- .rk-ip-setting{display:grid;gap:6px}
329
- .rk-ip-setting-note{font-weight:600;color:var(--rk-text-faint)}
330
- .rk-ip-input{width:100%;padding:9px 11px;font:inherit;font-size:13px;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
331
- .rk-ip-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
332
- @keyframes rk-clp-fade{from{opacity:0}to{opacity:1}}
333
-
334
- /* past-sessions left slide-in panel (shared look with /tools/image) */
335
- .rk-ip-sessions{position:fixed;left:0;top:0;bottom:0;z-index:96;width:min(320px,86vw);display:flex;flex-direction:column;background:var(--rk-surface);border-right:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xl,var(--rk-shadow-card));transform:translateX(-100%);transition:transform var(--rk-dur) var(--rk-ease)}
336
- .rk-ip-sessions.is-open{transform:none}
337
- .rk-ip-sessions-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 14px 10px;border-bottom:1px solid var(--rk-border)}
338
- .rk-ip-sessions-title{font-family:var(--rk-font-display);font-weight:800;font-size:14px;color:var(--rk-ink)}
339
- .rk-ip-sessions-list{flex:1;min-height:0;overflow-y:auto;padding:8px;display:grid;gap:4px;align-content:start}
340
- .rk-ip-sessions-empty{font-size:12.5px;color:var(--rk-text-faint);padding:16px 8px;text-align:center;line-height:1.4}
341
- .rk-ip-session{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;padding:7px;border:1px solid transparent;border-radius:var(--rk-r-lg);background:transparent;cursor:pointer;text-align:left;font:inherit;width:100%}
342
- .rk-ip-session:hover{background:var(--rk-n-50)}
343
- .rk-ip-session.is-active{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
344
- .rk-ip-session-thumb{width:44px;height:44px;border:1px solid var(--rk-border);border-radius:var(--rk-r-md,8px);object-fit:cover;background:var(--rk-n-200)}
345
- .rk-ip-session-thumb.is-empty{display:grid;place-items:center;color:var(--rk-text-faint)}
346
- .rk-ip-session-meta{min-width:0;display:grid;gap:3px}
347
- .rk-ip-session-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
348
- .rk-ip-session-sub{min-width:0;color:var(--rk-text-muted);font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
349
- .rk-ip-sessions-backdrop{position:fixed;inset:0;z-index:95;background:rgba(28,25,23,.34)}
350
- .rk-ip-sessions-backdrop[hidden]{display:none}
351
-
352
- @media(max-width:960px){.rk-clp-workbench{grid-template-columns:1fr}.rk-clp-side{position:static}}
353
- `;
354
- return reskinDocument({
355
- title: "vidfarm — Clip Media",
356
- description: "Cut exact subranges out of any video or audio straight into your raws library — preview from the source, drag a precise in/out point, and save only the clip.",
357
- activeSlug: "inpaint",
358
- pageCss,
359
- body,
360
- script: CLIPPER_SCRIPT
361
- });
362
- }
363
- // Self-contained client runtime (plain string, no runtime imports). Uses string
364
- // concatenation (not template literals) so no `${}` needs escaping inside the
365
- // outer TS template literal.
366
- const CLIPPER_SCRIPT = `
367
- (function(){
368
- var bootEl = document.querySelector("[data-clp-boot]");
369
- var boot = {}; try { boot = JSON.parse(bootEl && bootEl.textContent || "{}"); } catch(_){}
370
- var AUDIO_ONLY_KEY = "vf_clipper_audio_only";
371
-
372
- var sourceBar = document.querySelector(".rk-clp-sourcebar");
373
- var urlInput = document.querySelector("[data-clp-url-input]");
374
- var loadBtn = document.querySelector("[data-clp-url-load]");
375
- var uploadBtn = document.querySelector("[data-clp-upload]");
376
- var attachBtn = document.querySelector("[data-clp-attach]");
377
- var fileInput = document.querySelector("[data-clp-file]");
378
- var emptyState = document.querySelector("[data-clp-empty]");
379
- var player = document.querySelector("[data-clp-player]");
380
- var video = document.querySelector("[data-clp-video]");
381
- var audioPoster = document.querySelector("[data-clp-audio-poster]");
382
- var controls = document.querySelector("[data-clp-controls]");
383
- var playpause = document.querySelector("[data-clp-playpause]");
384
- var timecodeNow = document.querySelector("[data-clp-timecode]");
385
- var durationEl = document.querySelector("[data-clp-duration]");
386
- var track = document.querySelector("[data-clp-tl-track]");
387
- var selEl = document.querySelector("[data-clp-tl-sel]");
388
- var playhead = document.querySelector("[data-clp-tl-playhead]");
389
- var handleIn = document.querySelector("[data-clp-tl-in]");
390
- var handleOut = document.querySelector("[data-clp-tl-out]");
391
- var startInput = document.querySelector("[data-clp-start-input]");
392
- var endInput = document.querySelector("[data-clp-end-input]");
393
- var setStartBtn = document.querySelector("[data-clp-set-start]");
394
- var setEndBtn = document.querySelector("[data-clp-set-end]");
395
- var wholeBtn = document.querySelector("[data-clp-whole]");
396
- var lengthEl = document.querySelector("[data-clp-length]");
397
- var rangeNote = document.querySelector("[data-clp-range-note]");
398
- var advToggle = document.querySelector("[data-clp-adv-toggle]");
399
- var advBody = document.querySelector("[data-clp-adv-body]");
400
- var nameInput = document.querySelector("[data-clp-name]");
401
- var notesInput = document.querySelector("[data-clp-notes]");
402
- var folderInput = document.querySelector("[data-clp-folder]");
403
- var audioOnlyInput = document.querySelector("[data-clp-audio-only]");
404
- var saveBtn = document.querySelector("[data-clp-save]");
405
- var saveLabel = document.querySelector("[data-clp-save-label]");
406
- var statusEl = document.querySelector("[data-clp-status]");
407
- var sessionList = document.querySelector("[data-clp-session-list]");
408
- var sessionRefresh = document.querySelector("[data-clp-session-refresh]");
409
- var startNewBtn = document.querySelector("[data-clp-start-new]");
410
- var settingsBtn = document.querySelector("[data-clp-settings-button]");
411
- var settingsModal = document.querySelector("[data-clp-settings]");
412
- var settingsCloses = document.querySelectorAll("[data-clp-settings-close]");
413
- var tracerInput = document.querySelector("[data-clp-tracer]");
414
- var sessionsToggle = document.querySelector("[data-clp-history-toggle]");
415
- var sessionsPanel = document.querySelector("[data-clp-sessions]");
416
- var sessionsList = document.querySelector("[data-clp-sessions-list]");
417
- var sessionsClose = document.querySelector("[data-clp-sessions-close]");
418
- var sessionsBackdrop = document.querySelector("[data-clp-sessions-backdrop]");
419
-
420
- var PLAY_SVG = ${JSON.stringify(PLAY_ICON)};
421
- var PAUSE_SVG = ${JSON.stringify(PAUSE_ICON)};
422
-
423
- // ── state ──
424
- var sourceUrl = ""; // the original pasted/handoff URL
425
- var previewUrl = ""; // resolved direct media URL (what <video> plays)
426
- var sourceName = "";
427
- var sourceObjectUrl = ""; // object URL for a local upload
428
- var uploadTempFileId = ""; // temp_file_id when the source was uploaded
429
- var isAudioSource = false; // heuristic: source looks like audio
430
- var durationSec = 0;
431
- var startSec = 0;
432
- var endSec = 0;
433
- var sessionTracer = "";
434
- var tracerAutoPending = true;
435
- var dragging = null; // "in" | "out" | null
436
-
437
- function esc(t){ var d=document.createElement("div"); d.textContent=(t==null?"":String(t)); return d.innerHTML; }
438
- function pad(n, w){ n=String(Math.max(0, Math.floor(n))); while(n.length<w) n="0"+n; return n; }
439
- function absoluteUrl(u){ try { return new URL(u, window.location.origin).toString(); } catch(_){ return u; } }
440
- function parseJson(r){ return r.text().then(function(t){ try { return t?JSON.parse(t):{}; } catch(_){ return {}; } }); }
441
-
442
- // HH:MM:SS:NNNN where NNNN = ten-thousandths of a second (4 digits).
443
- function fmtTC(sec){
444
- if(!isFinite(sec) || sec < 0) sec = 0;
445
- var whole = Math.floor(sec);
446
- var frac = Math.round((sec - whole) * 10000);
447
- if(frac >= 10000){ frac -= 10000; whole += 1; }
448
- var h = Math.floor(whole / 3600);
449
- var m = Math.floor((whole % 3600) / 60);
450
- var s = whole % 60;
451
- return pad(h,2)+":"+pad(m,2)+":"+pad(s,2)+":"+pad(frac,4);
452
- }
453
- function parseTC(str){
454
- if(str == null) return NaN;
455
- var parts = String(str).trim().split(":").map(function(p){ return p.trim(); });
456
- if(!parts.length) return NaN;
457
- var h=0,m=0,s=0,frac=0;
458
- if(parts.length === 4){ h=+parts[0]; m=+parts[1]; s=+parts[2]; }
459
- else if(parts.length === 3){ m=+parts[0]; s=+parts[1]; }
460
- else if(parts.length === 2){ s=+parts[0]; }
461
- else { s=+parts[0]; }
462
- if([h,m,s].some(function(n){ return isNaN(n); })) return NaN;
463
- if(parts.length >= 2){ var last = parts[parts.length-1]||""; var div = Math.pow(10, last.length||4); frac = (+last)/div; if(isNaN(frac)) frac=0; }
464
- return h*3600 + m*60 + s + frac;
465
- }
466
- function fmtLen(sec){
467
- if(!isFinite(sec) || sec < 0) sec = 0;
468
- var m = Math.floor(sec/60);
469
- var s = sec - m*60;
470
- return m+":"+pad(Math.floor(s),2)+"."+pad(Math.round((s-Math.floor(s))*10),1);
471
- }
472
- function slug(v){ return String(v||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60); }
473
- function looksAudio(u){ return /\\.(mp3|m4a|aac|wav|ogg|oga|flac|opus)(\\?|#|$)/i.test(String(u||"")); }
474
-
475
- function setStatus(msg, tone){ if(!statusEl) return; statusEl.textContent = msg||""; if(tone) statusEl.setAttribute("data-tone", tone); else statusEl.removeAttribute("data-tone"); }
476
- function setRangeNote(msg){ if(!rangeNote) return; if(msg){ rangeNote.textContent = msg; rangeNote.hidden = false; } else { rangeNote.hidden = true; } }
477
-
478
- // ── tracer / session id ──
479
- function tracerSuffix(){ return Math.random().toString(36).slice(2, 6); }
480
- function nameTracerFromSeed(seed){
481
- var base = slug(seed) || "clipper";
482
- var t = base + "-" + tracerSuffix();
483
- sessionTracer = t; tracerAutoPending = false;
484
- if(tracerInput) tracerInput.value = t;
485
- return t;
486
- }
487
- function resolveTracer(){
488
- if(tracerInput && tracerInput.value.trim()){ sessionTracer = tracerInput.value.trim(); tracerAutoPending = false; return sessionTracer; }
489
- return sessionTracer;
490
- }
491
- // Always return a concrete session id — never leave the tracer unset at save time.
492
- function ensureTracer(){
493
- var t = resolveTracer();
494
- if(tracerAutoPending || !t){ t = nameTracerFromSeed(sourceName || "clip"); }
495
- return t;
496
- }
497
- function currentFolder(){
498
- var f = (folderInput && folderInput.value.trim()) || "";
499
- return f || slug(resolveTracer() || sessionTracer || "");
500
- }
501
-
502
- // ── source loading ──
503
- function revealPlayer(){ if(emptyState) emptyState.hidden = true; if(player) player.hidden = false; if(controls) controls.hidden = false; }
504
- function clearSourceObjectUrl(){ if(sourceObjectUrl){ try { URL.revokeObjectURL(sourceObjectUrl); } catch(_){} sourceObjectUrl=""; } }
505
- function applyAudioPoster(){
506
- // Once a frame is decoded (readyState>=2) a real video reports videoWidth>0;
507
- // a frameless source (audio) never does → treat it as audio and force the
508
- // audio-only clip mode (a "video" trim of a frameless source is meaningless).
509
- if(video && video.readyState >= 2 && (!video.videoWidth || !video.videoHeight)){
510
- isAudioSource = true;
511
- if(audioOnlyInput && !audioOnlyInput.checked){ audioOnlyInput.checked = true; refreshSaveLabel(); }
512
- }
513
- var showPoster = isAudioSource || (video && video.readyState >= 1 && (!video.videoWidth || !video.videoHeight));
514
- if(audioPoster) audioPoster.hidden = !showPoster;
515
- }
516
-
517
- function playSrc(url){
518
- if(!video) return;
519
- try { video.pause(); } catch(_){}
520
- video.src = url;
521
- video.load();
522
- revealPlayer();
523
- applyAudioPoster();
524
- }
525
-
526
- // Toggle the ingest controls off while a URL is resolving.
527
- function setSourceLoading(on){
528
- if(sourceBar) sourceBar.classList.toggle("is-loading", !!on);
529
- if(loadBtn){ loadBtn.setAttribute("data-loading", on?"true":"false"); loadBtn.disabled = !!on; loadBtn.textContent = on ? "Loading\\u2026" : "Load"; }
530
- if(uploadBtn) uploadBtn.disabled = !!on;
531
- if(attachBtn) attachBtn.disabled = !!on;
532
- }
533
-
534
- // Resolve a pasted/handoff URL to a direct preview media URL, then play it.
535
- function loadSourceUrl(rawUrl, name){
536
- var u = String(rawUrl||"").trim();
537
- if(!u){ setStatus("Enter a media URL.", "error"); return; }
538
- if(!/^https?:\\/\\//i.test(u)){ setStatus("That doesn't look like a valid URL.", "error"); return; }
539
- sourceUrl = u; sourceName = name || ""; uploadTempFileId = ""; clearSourceObjectUrl();
540
- isAudioSource = looksAudio(u);
541
- setSourceLoading(true);
542
- setStatus("Resolving source\\u2026");
543
- fetch("/raws/resolve-source", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ source_url: u }) })
544
- .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
545
- .then(function(res){
546
- if(!res.ok || !res.p.preview_url) throw new Error(res.p.error || "Could not load that source.");
547
- previewUrl = res.p.preview_url;
548
- if(res.p.title && !sourceName) sourceName = res.p.title;
549
- if(looksAudio(previewUrl)) isAudioSource = true;
550
- if(tracerAutoPending) nameTracerFromSeed(sourceName || "clip");
551
- playSrc(previewUrl);
552
- setStatus("Loaded \\u2014 drag the in/out edges on the timeline.", "success");
553
- })
554
- .catch(function(err){ setStatus(err && err.message ? err.message : "Could not load that source.", "error"); })
555
- .then(function(){ setSourceLoading(false); });
556
- }
557
-
558
- function loadLocalFile(file){
559
- if(!file) return;
560
- clearSourceObjectUrl();
561
- sourceObjectUrl = URL.createObjectURL(file);
562
- previewUrl = ""; sourceName = file.name || "upload.mp4"; sourceUrl = ""; uploadTempFileId = "";
563
- isAudioSource = /^audio\\//i.test(file.type||"") || looksAudio(file.name);
564
- if(tracerAutoPending) nameTracerFromSeed(sourceName);
565
- playSrc(sourceObjectUrl);
566
- setStatus("Uploading source in the background\\u2026");
567
- // The <video> plays the local object URL immediately; in parallel push the
568
- // file to /temp so /raws/clip-range has a server-side source to trim.
569
- var fd = new FormData();
570
- fd.append("file", file);
571
- fetch("/api/v1/user/me/temporary-files/upload", { method:"POST", credentials:"same-origin", body: fd })
572
- .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
573
- .then(function(res){
574
- var id = res.p && (res.p.id || res.p.file_id || (res.p.file && res.p.file.id));
575
- if(res.ok && id){ uploadTempFileId = String(id); setStatus("Source ready \\u2014 drag the in/out edges on the timeline.", "success"); }
576
- else { setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work. " + ((res.p && res.p.error)||""), "error"); }
577
- })
578
- .catch(function(){ setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work.", "error"); });
579
- }
580
-
581
- function openAttach(){
582
- if(window.__vidfarmDirectory && typeof window.__vidfarmDirectory.pick === "function"){
583
- window.__vidfarmDirectory.pick({ select:"file" }).then(function(picked){
584
- if(!picked) return;
585
- var url = picked.view_url || picked.url || picked.path;
586
- if(url){ loadSourceUrl(absoluteUrl(url), picked.name || picked.file_name || ""); }
587
- }).catch(function(){});
588
- } else {
589
- setStatus("File browser unavailable here \\u2014 paste a URL or upload instead.", "error");
590
- }
591
- }
592
-
593
- // ── range + timeline sync ──
594
- function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
595
- function pctOf(sec){ return durationSec > 0 ? clamp(sec/durationSec, 0, 1) : 0; }
596
- function renderTimeline(){
597
- if(!durationSec || durationSec <= 0){
598
- if(selEl){ selEl.style.left="0"; selEl.style.width="100%"; }
599
- if(handleIn) handleIn.style.left="0%";
600
- if(handleOut) handleOut.style.left="100%";
601
- return;
602
- }
603
- var a = pctOf(startSec)*100, b = pctOf(endSec)*100;
604
- if(selEl){ selEl.style.left = a+"%"; selEl.style.width = Math.max(0, b-a)+"%"; }
605
- if(handleIn) handleIn.style.left = a+"%";
606
- if(handleOut) handleOut.style.left = b+"%";
607
- }
608
- function renderPlayhead(){
609
- if(!playhead) return;
610
- playhead.style.left = (pctOf(video ? (video.currentTime||0) : 0)*100)+"%";
611
- }
612
- function refreshRangeUI(sync){
613
- if(sync !== "start" && startInput && document.activeElement !== startInput) startInput.value = fmtTC(startSec);
614
- if(sync !== "end" && endInput && document.activeElement !== endInput) endInput.value = fmtTC(endSec);
615
- var len = endSec - startSec;
616
- if(lengthEl) lengthEl.textContent = fmtLen(Math.max(0, len)) + " (" + Math.max(0, len).toFixed(2) + "s)";
617
- renderTimeline();
618
- var valid = len > 0.02 && (durationSec <= 0 || (startSec >= 0 && endSec <= durationSec + 0.01));
619
- if(!valid){ setRangeNote(len <= 0 ? "Out point must be after the in point." : "Range is outside the source."); }
620
- else setRangeNote("");
621
- if(saveBtn) saveBtn.disabled = !valid;
622
- }
623
- function setStart(sec){ startSec = clamp(sec, 0, durationSec>0?durationSec:sec); if(endSec < startSec) endSec = Math.min(durationSec>0?durationSec:startSec, startSec + 0.1); refreshRangeUI(); }
624
- function setEnd(sec){ endSec = clamp(sec, 0, durationSec>0?durationSec:sec); if(startSec > endSec) startSec = Math.max(0, endSec - 0.1); refreshRangeUI(); }
625
-
626
- // Seek to a time and reflect the playhead / timecode.
627
- function seekTo(sec){ if(!video || durationSec<=0) return; video.currentTime = clamp(sec, 0, durationSec); syncNow(); }
628
-
629
- function secFromPointer(clientX){
630
- if(!track || durationSec<=0) return 0;
631
- var r = track.getBoundingClientRect();
632
- var pct = r.width>0 ? clamp((clientX - r.left)/r.width, 0, 1) : 0;
633
- return pct * durationSec;
634
- }
635
-
636
- // Timeline pointer interactions: click track = seek; drag handle = set in/out.
637
- function onTrackPointerDown(e){
638
- if(durationSec<=0) return;
639
- var t = e.target;
640
- if(t === handleIn || (handleIn && handleIn.contains(t))){ beginDrag("in", e); return; }
641
- if(t === handleOut || (handleOut && handleOut.contains(t))){ beginDrag("out", e); return; }
642
- seekTo(secFromPointer(e.clientX));
643
- }
644
- function beginDrag(which, e){
645
- dragging = which;
646
- var h = which==="in" ? handleIn : handleOut;
647
- if(h) h.classList.add("is-drag");
648
- e.preventDefault();
649
- window.addEventListener("pointermove", onDragMove);
650
- window.addEventListener("pointerup", onDragEnd);
651
- }
652
- function onDragMove(e){
653
- if(!dragging) return;
654
- var sec = secFromPointer(e.clientX);
655
- if(dragging==="in"){ setStart(sec); seekTo(sec); }
656
- else { setEnd(sec); seekTo(sec); }
657
- }
658
- function onDragEnd(){
659
- if(handleIn) handleIn.classList.remove("is-drag");
660
- if(handleOut) handleOut.classList.remove("is-drag");
661
- dragging = null;
662
- window.removeEventListener("pointermove", onDragMove);
663
- window.removeEventListener("pointerup", onDragEnd);
664
- }
665
-
666
- // ── playback ──
667
- function syncNow(){
668
- if(timecodeNow && document.activeElement !== timecodeNow) timecodeNow.value = fmtTC(video ? (video.currentTime||0) : 0);
669
- renderPlayhead();
670
- }
671
- function tick(){ if(video && !video.paused && !video.ended){ syncNow(); requestAnimationFrame(tick); } }
672
- function setPlayIcon(){ if(playpause) playpause.innerHTML = (video && !video.paused) ? PAUSE_SVG : PLAY_SVG; }
673
-
674
- if(video){
675
- video.addEventListener("loadedmetadata", function(){
676
- durationSec = video.duration || 0;
677
- startSec = 0; endSec = durationSec || 0;
678
- if(durationEl) durationEl.textContent = "/ " + fmtTC(durationSec);
679
- applyAudioPoster();
680
- refreshRangeUI(); renderPlayhead();
681
- });
682
- video.addEventListener("loadeddata", applyAudioPoster);
683
- video.addEventListener("timeupdate", syncNow);
684
- video.addEventListener("play", function(){ setPlayIcon(); tick(); });
685
- video.addEventListener("pause", setPlayIcon);
686
- video.addEventListener("ended", setPlayIcon);
687
- video.addEventListener("error", function(){ setStatus("The browser couldn't play this source. Try a direct file URL or upload it.", "error"); });
688
- }
689
-
690
- // ── wire controls ──
691
- if(loadBtn) loadBtn.addEventListener("click", function(){ loadSourceUrl(urlInput && urlInput.value, ""); });
692
- if(urlInput) urlInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); loadSourceUrl(urlInput.value, ""); } });
693
- if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ if(!uploadBtn.disabled) fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadLocalFile(fileInput.files[0]); }); }
694
- if(attachBtn) attachBtn.addEventListener("click", function(){ if(!attachBtn.disabled) openAttach(); });
695
- if(playpause) playpause.addEventListener("click", function(){ if(!video) return; if(video.paused) video.play(); else video.pause(); });
696
- if(timecodeNow) timecodeNow.addEventListener("change", function(){ var t = parseTC(timecodeNow.value); if(isFinite(t)) seekTo(t); });
697
-
698
- if(track) track.addEventListener("pointerdown", onTrackPointerDown);
699
- if(startInput) startInput.addEventListener("change", function(){ var t = parseTC(startInput.value); if(isFinite(t)) setStart(t); else refreshRangeUI(); });
700
- if(endInput) endInput.addEventListener("change", function(){ var t = parseTC(endInput.value); if(isFinite(t)) setEnd(t); else refreshRangeUI(); });
701
- if(setStartBtn) setStartBtn.addEventListener("click", function(){ setStart(video ? (video.currentTime||0) : 0); });
702
- if(setEndBtn) setEndBtn.addEventListener("click", function(){ setEnd(video ? (video.currentTime||0) : 0); });
703
- if(wholeBtn) wholeBtn.addEventListener("click", function(){ startSec = 0; endSec = durationSec || 0; refreshRangeUI(); });
704
-
705
- // ── advanced toggle + sticky audio-only ──
706
- if(advToggle && advBody){
707
- advToggle.addEventListener("click", function(){
708
- var open = advBody.hidden;
709
- advBody.hidden = !open;
710
- advToggle.setAttribute("aria-expanded", open ? "true" : "false");
711
- });
712
- }
713
- function loadStickyAudioOnly(){
714
- var v = false; try { v = localStorage.getItem(AUDIO_ONLY_KEY) === "1"; } catch(_){}
715
- if(audioOnlyInput) audioOnlyInput.checked = v;
716
- // If it was previously enabled, reveal Advanced so the choice is visible.
717
- if(v && advBody && advToggle && advBody.hidden){ advBody.hidden = false; advToggle.setAttribute("aria-expanded","true"); }
718
- }
719
- if(audioOnlyInput){
720
- audioOnlyInput.addEventListener("change", function(){
721
- try { localStorage.setItem(AUDIO_ONLY_KEY, audioOnlyInput.checked ? "1" : "0"); } catch(_){}
722
- refreshSaveLabel();
723
- });
724
- }
725
-
726
- // ── clip media ──
727
- function isAudioOnly(){ return !!(audioOnlyInput && audioOnlyInput.checked); }
728
- function restingSaveLabel(){ return isAudioOnly() ? "Clip Audio" : "Clip Media"; }
729
- function refreshSaveLabel(){ if(saveLabel && saveBtn && saveBtn.getAttribute("data-loading") !== "true") saveLabel.textContent = restingSaveLabel(); }
730
- function setSaving(on){ if(saveBtn){ saveBtn.setAttribute("data-loading", on?"true":"false"); saveBtn.disabled = on; } if(saveLabel) saveLabel.textContent = on ? "Saving\\u2026" : restingSaveLabel(); }
731
- function saveClip(){
732
- if(!video){ return; }
733
- var len = endSec - startSec;
734
- if(!(len > 0.02)){ setStatus("Set an in point and a later out point first.", "error"); return; }
735
- if(!previewUrl && !sourceUrl && !uploadTempFileId){ setStatus("Load a source first.", "error"); return; }
736
- var tracer = ensureTracer();
737
- var folder = currentFolder();
738
- var payload = { start_sec: Number(startSec.toFixed(3)), end_sec: Number(endSec.toFixed(3)), tracer: tracer, session_id: tracer };
739
- if(isAudioOnly()) payload.audio_only = true;
740
- if(folder) payload.folder_path = folder;
741
- if(nameInput && nameInput.value.trim()) payload.name = nameInput.value.trim();
742
- if(notesInput && notesInput.value.trim()) payload.notes = notesInput.value.trim();
743
- // Prefer the already-resolved direct URL (no re-resolve); else the uploaded
744
- // temp file; else the raw source URL (server resolves + bills).
745
- if(previewUrl) payload.preview_url = previewUrl;
746
- if(uploadTempFileId) payload.temp_file_id = uploadTempFileId;
747
- if(sourceUrl) payload.source_url = sourceUrl;
748
- var audioOnly = isAudioOnly();
749
- setSaving(true); setStatus(audioOnly ? "Extracting and saving the audio\\u2026" : "Trimming and saving your clip\\u2026");
750
- fetch("/raws/clip-range", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify(payload) })
751
- .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
752
- .then(function(res){
753
- if(!res.ok || !res.p.clip_id) throw new Error(res.p.error || "Could not save that clip.");
754
- setStatus("Saved \\u2014 " + (audioOnly ? "audio" : "clip") + " added to your raws (" + (res.p.folder_path||folder) + ").", "success");
755
- prependSessionClip(res.p);
756
- loadSession();
757
- })
758
- .catch(function(err){ setStatus(err && err.message ? err.message : "Could not save that clip.", "error"); })
759
- .then(function(){ setSaving(false); });
760
- }
761
- if(saveBtn) saveBtn.addEventListener("click", saveClip);
762
-
763
- // ── this-session clip list ──
764
- function clipRow(clip){
765
- var a = document.createElement("a");
766
- a.className = "rk-clp-clip"; a.href = clip.view_url ? absoluteUrl(clip.view_url) : "/library/raws"; a.target = "_blank"; a.rel = "noopener";
767
- var thumb;
768
- if(clip.thumbnail_url){ thumb = document.createElement("img"); thumb.className = "rk-clp-clip-thumb"; thumb.loading = "lazy"; thumb.src = absoluteUrl(clip.thumbnail_url); thumb.alt = ""; }
769
- else { thumb = document.createElement("span"); thumb.className = "rk-clp-clip-thumb"; }
770
- var meta = document.createElement("div"); meta.className = "rk-clp-clip-meta";
771
- var title = document.createElement("div"); title.className = "rk-clp-clip-title"; title.textContent = clip.description || clip.name || clip.clip_id || "clip";
772
- var sub = document.createElement("div"); sub.className = "rk-clp-clip-sub";
773
- var d = Number(clip.duration_sec || 0);
774
- var kind = clip.media_kind === "audio" ? "audio" : "clip";
775
- sub.textContent = (d>0 ? d.toFixed(2)+"s" : "") + (d>0 ? " · " : "") + kind;
776
- meta.appendChild(title); meta.appendChild(sub); a.appendChild(thumb); a.appendChild(meta);
777
- return a;
778
- }
779
- function prependSessionClip(clip){
780
- if(!sessionList) return;
781
- var empty = sessionList.querySelector(".rk-clp-session-empty"); if(empty) empty.remove();
782
- sessionList.insertBefore(clipRow(clip), sessionList.firstChild);
783
- }
784
- function loadSession(){
785
- if(!sessionList) return;
786
- var folder = currentFolder();
787
- if(!folder){ return; }
788
- var endpoint = new URL("/raws/feed", window.location.origin);
789
- endpoint.searchParams.set("folder", folder); endpoint.searchParams.set("limit", "200");
790
- fetch(endpoint, { credentials:"same-origin", headers:{ Accept:"application/json" } })
791
- .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
792
- .then(function(res){
793
- if(!res.ok) return;
794
- var clips = (res.p && res.p.clips) || [];
795
- sessionList.innerHTML = "";
796
- if(!clips.length){ var e=document.createElement("div"); e.className="rk-clp-session-empty"; e.textContent="No clips yet — set an in/out point and clip your first media."; sessionList.appendChild(e); return; }
797
- clips.forEach(function(c){ sessionList.appendChild(clipRow(c)); });
798
- }).catch(function(){});
799
- }
800
- if(sessionRefresh) sessionRefresh.addEventListener("click", loadSession);
801
-
802
- // ── past sessions (grouped by folder/tracer) ──
803
- function openSessions(){ if(sessionsPanel) sessionsPanel.classList.add("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = false; loadPastSessions(); }
804
- function closeSessions(){ if(sessionsPanel) sessionsPanel.classList.remove("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = true; }
805
- function relTime(v){ var then=Date.parse(v||0); if(!then) return ""; var s=Math.max(0,Math.floor((Date.now()-then)/1000)); if(s<60) return "just now"; var m=Math.floor(s/60); if(m<60) return m+"m ago"; var h=Math.floor(m/60); if(h<24) return h+"h ago"; var d=Math.floor(h/24); if(d<7) return d+"d ago"; return new Date(then).toLocaleDateString(); }
806
- function loadPastSessions(){
807
- if(!sessionsList) return;
808
- sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">Loading\\u2026</div>";
809
- var endpoint = new URL("/raws/feed", window.location.origin); endpoint.searchParams.set("limit", "200");
810
- fetch(endpoint, { credentials:"same-origin", headers:{ Accept:"application/json" } })
811
- .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
812
- .then(function(res){
813
- var clips = (res.p && res.p.clips) || [];
814
- var groups = {}, order = [];
815
- clips.forEach(function(c){
816
- var key = (c.folder_path || c.tracer || "").trim(); if(!key) return;
817
- if(!groups[key]){ groups[key] = { key:key, thumb:"", count:0, createdAt:c.created_at||"" }; order.push(key); }
818
- var g = groups[key]; g.count++;
819
- if(!g.thumb && c.thumbnail_url) g.thumb = absoluteUrl(c.thumbnail_url);
820
- if(Date.parse(c.created_at||0) > Date.parse(g.createdAt||0)) g.createdAt = c.created_at;
821
- });
822
- var list = order.map(function(k){ return groups[k]; }).sort(function(a,b){ return Date.parse(b.createdAt||0) - Date.parse(a.createdAt||0); });
823
- sessionsList.innerHTML = "";
824
- if(!list.length){ sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">No past clipping sessions yet.</div>"; return; }
825
- list.forEach(function(g){
826
- var row = document.createElement("button"); row.type="button"; row.className="rk-ip-session" + (g.key === currentFolder() ? " is-active" : "");
827
- if(g.thumb){ var im=document.createElement("img"); im.className="rk-ip-session-thumb"; im.src=g.thumb; im.alt=""; row.appendChild(im); }
828
- else { var ph=document.createElement("span"); ph.className="rk-ip-session-thumb is-empty"; row.appendChild(ph); }
829
- var meta=document.createElement("div"); meta.className="rk-ip-session-meta";
830
- var title=document.createElement("div"); title.className="rk-ip-session-title"; title.textContent=g.key;
831
- var sub=document.createElement("div"); sub.className="rk-ip-session-sub"; sub.textContent = g.count + " clip" + (g.count===1?"":"s") + (relTime(g.createdAt)?" \\u00b7 "+relTime(g.createdAt):"");
832
- meta.appendChild(title); meta.appendChild(sub); row.appendChild(meta);
833
- row.addEventListener("click", function(){ switchToSession(g.key); });
834
- sessionsList.appendChild(row);
835
- });
836
- }).catch(function(){ if(sessionsList) sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">Unable to load sessions.</div>"; });
837
- }
838
- function switchToSession(key){
839
- if(!key) return;
840
- sessionTracer = key; tracerAutoPending = false;
841
- if(tracerInput) tracerInput.value = key;
842
- if(folderInput) folderInput.value = key;
843
- closeSessions(); loadSession();
844
- setStatus("Opened session " + key + " \\u2014 load a source to add more clips.", "");
845
- }
846
-
847
- // ── settings / sessions open-close ──
848
- if(settingsBtn) settingsBtn.addEventListener("click", function(){ if(settingsModal) settingsModal.hidden = false; });
849
- Array.prototype.forEach.call(settingsCloses, function(el){ el.addEventListener("click", function(){ if(settingsModal) settingsModal.hidden = true; }); });
850
- if(sessionsToggle) sessionsToggle.addEventListener("click", openSessions);
851
- if(sessionsClose) sessionsClose.addEventListener("click", closeSessions);
852
- if(sessionsBackdrop) sessionsBackdrop.addEventListener("click", closeSessions);
853
- if(settingsModal) settingsModal.addEventListener("click", function(e){ if(e.target === settingsModal) settingsModal.hidden = true; });
854
- document.addEventListener("keydown", function(e){ if(e.key === "Escape"){ if(settingsModal && !settingsModal.hidden) settingsModal.hidden = true; closeSessions(); } });
855
-
856
- // ── start new ──
857
- function resetWorkspace(){
858
- clearSourceObjectUrl();
859
- sourceUrl=""; previewUrl=""; sourceName=""; uploadTempFileId=""; isAudioSource=false; durationSec=0; startSec=0; endSec=0;
860
- try { video.pause(); } catch(_){} if(video){ video.removeAttribute("src"); video.load(); }
861
- if(emptyState) emptyState.hidden = false; if(player) player.hidden = true;
862
- if(controls) controls.hidden = true; if(audioPoster) audioPoster.hidden = true;
863
- if(urlInput) urlInput.value=""; if(nameInput) nameInput.value=""; if(notesInput) notesInput.value=""; if(folderInput) folderInput.value="";
864
- setSourceLoading(false);
865
- refreshSaveLabel();
866
- if(sessionList) sessionList.innerHTML = "<div class=\\"rk-clp-session-empty\\">No clips yet — set an in/out point and clip your first media.</div>";
867
- setStatus(""); setRangeNote("");
868
- }
869
- if(startNewBtn) startNewBtn.addEventListener("click", function(){
870
- sessionTracer=""; tracerAutoPending=true; if(tracerInput) tracerInput.value="";
871
- clearUrlParams(); resetWorkspace();
872
- setStatus("Started a new session.", "");
873
- });
874
-
875
- // ── URL param preload ──
876
- function firstParam(names){ var p=new URLSearchParams(window.location.search); for(var i=0;i<names.length;i++){ var v=p.get(names[i]); if(v && v.trim()) return v.trim(); } return ""; }
877
- function clearUrlParams(){ var u=new URL(window.location.href); ["source_url","source_video_url","video_url","url","source_video_name","video_name","file_name","tracer","tracer_id","trace"].forEach(function(k){ u.searchParams.delete(k); }); window.history.replaceState({}, "", u.toString()); }
878
- function initFromUrl(){
879
- var t = firstParam(["tracer","tracer_id","trace"]);
880
- if(t){ sessionTracer=t; tracerAutoPending=false; if(tracerInput) tracerInput.value=t; if(folderInput && !folderInput.value) folderInput.value=slug(t); }
881
- var src = firstParam(["source_url","source_video_url","video_url","url"]);
882
- if(src){ if(urlInput) urlInput.value = src; loadSourceUrl(src, firstParam(["source_video_name","video_name","file_name"])); }
883
- if(t) loadSession();
884
- }
885
-
886
- loadStickyAudioOnly();
887
- refreshSaveLabel();
888
- initFromUrl();
889
- })();`;
890
- //# sourceMappingURL=inpaint-clipper-page.js.map