@mevdragon/vidfarm-devcli 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +174 -16
- package/SKILL.platform.md +7 -3
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/dist/src/app.js +1513 -145
- package/dist/src/cli.js +1365 -105
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clips.js +3 -0
- package/dist/src/devcli/composition-edit.js +396 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +208 -1
- package/dist/src/editor-chat.js +33 -17
- package/dist/src/frontend/file-directory.js +271 -20
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +6 -3
- package/dist/src/page-shell.js +1 -1
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/chat-page.js +103 -15
- package/dist/src/reskin/discover-page.js +247 -10
- package/dist/src/reskin/document.js +147 -10
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -452
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +324 -82
- package/dist/src/reskin/theme.js +36 -11
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/hunt.js +2 -0
- package/dist/src/services/clip-records.js +28 -0
- package/dist/src/services/file-directory.js +6 -3
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +24 -2
- package/package.json +1 -1
- package/public/assets/file-directory-app.js +2 -2
- package/public/assets/homepage-client-app.js +1 -1
- package/public/assets/page-runtime-client-app.js +2 -2
- package/public/assets/placeholders/scene-placeholder.png +0 -0
|
@@ -0,0 +1,649 @@
|
|
|
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 into the raws library. It plays the
|
|
4
|
+
// source straight from its resolved RapidAPI/direct URL (nothing is uploaded to
|
|
5
|
+
// preview), lets the user pick an in/out point down to sub-second precision
|
|
6
|
+
// (HH:MM:SS:NNNN), shows a live estimate of the clip length, and on "Save clip"
|
|
7
|
+
// POSTs to /raws/clip-range which trims just that window and lands one raw. It is
|
|
8
|
+
// a MULTI-CLIP surface — the source stays loaded so you can grab clip after clip,
|
|
9
|
+
// each grouped under the session's tracer/folder and listed in "Clips this
|
|
10
|
+
// session", with a past-sessions slide-in grouped by tracer (like /tools/video).
|
|
11
|
+
//
|
|
12
|
+
// Data flow:
|
|
13
|
+
// • load source → POST /raws/resolve-source { source_url } → { preview_url, ... }
|
|
14
|
+
// • save clip → POST /raws/clip-range { preview_url, start_sec, end_sec, tracer, folder_path, name }
|
|
15
|
+
// • session list → GET /raws/feed?folder=<slug> (this session's clips)
|
|
16
|
+
// • past sessions → GET /raws/feed?limit=200 grouped by folder_path/tracer
|
|
17
|
+
import { reskinDocument } from "./document.js";
|
|
18
|
+
import { renderInpaintModeTabs, INPAINT_TABS_CSS } from "./inpaint-page.js";
|
|
19
|
+
const UPLOAD_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>`;
|
|
20
|
+
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>`;
|
|
21
|
+
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>`;
|
|
22
|
+
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>`;
|
|
23
|
+
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>`;
|
|
24
|
+
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>`;
|
|
25
|
+
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>`;
|
|
26
|
+
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>`;
|
|
27
|
+
const FOLDER_ICON = `<svg width="15" height="15" 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>`;
|
|
28
|
+
export function renderReskinInpaintClipper(input = {}) {
|
|
29
|
+
const boot = { accountId: input.accountId || "", isLoggedIn: Boolean(input.isLoggedIn) };
|
|
30
|
+
const body = `
|
|
31
|
+
<main class="rk-container-wide rk-page rk-inpaint-page rk-clp-page">
|
|
32
|
+
<div class="rk-page-head rk-inpaint-head">
|
|
33
|
+
<div class="rk-inpaint-head-text">
|
|
34
|
+
${renderInpaintModeTabs("clipper")}
|
|
35
|
+
<h1 class="rk-h1">Clip a Video</h1>
|
|
36
|
+
<p class="rk-sub">Paste (or upload) a video, scrub to the exact moment, and set a precise <strong>in</strong> and <strong>out</strong> point (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>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="rk-inpaint-toolbar">
|
|
39
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-clp-start-new>${PLUS_ICON} Start New</button>
|
|
40
|
+
<button type="button" class="rk-ip-iconbtn" data-clp-settings-button aria-label="Settings" title="Settings">${GEAR_ICON}</button>
|
|
41
|
+
<button type="button" class="rk-ip-iconbtn" data-clp-history-toggle aria-label="Past sessions" title="Past sessions">${HISTORY_ICON}</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="rk-inpaint-workbench rk-clp-workbench">
|
|
46
|
+
<!-- LEFT: source + subrange controls -->
|
|
47
|
+
<section class="rk-inpaint-panel" aria-label="Clip controls">
|
|
48
|
+
<div class="rk-inpaint-block">
|
|
49
|
+
<label class="rk-label" for="rkClpUrl">Source video URL</label>
|
|
50
|
+
<div class="rk-clp-urlrow">
|
|
51
|
+
<input class="rk-input" id="rkClpUrl" data-clp-url-input type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" spellcheck="false" />
|
|
52
|
+
<button type="button" class="rk-btn rk-btn-gold rk-btn-sm" data-clp-url-load>Load</button>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="rk-clp-ingest">
|
|
55
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-clp-upload>${UPLOAD_ICON}<span>Upload</span></button>
|
|
56
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-clp-attach>${FOLDER_ICON}<span>From Files</span></button>
|
|
57
|
+
</div>
|
|
58
|
+
<input data-clp-file type="file" accept="video/mp4,video/quicktime,video/webm,video/x-matroska,video/*" hidden />
|
|
59
|
+
<span class="rk-hint">TikTok, YouTube, Instagram, X, or a direct link — we play it straight from the source; nothing is saved until you clip.</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="rk-inpaint-block rk-clp-rangeblock" data-clp-range-controls hidden>
|
|
63
|
+
<div class="rk-inpaint-brushrow">
|
|
64
|
+
<label class="rk-label">Subrange</label>
|
|
65
|
+
<button type="button" class="rk-btn rk-btn-ghost rk-btn-xs" data-clp-whole title="Select the whole video">Whole video</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="rk-clp-inout">
|
|
69
|
+
<div class="rk-clp-field">
|
|
70
|
+
<div class="rk-clp-field-head">
|
|
71
|
+
<span class="rk-clp-field-label">In</span>
|
|
72
|
+
<button type="button" class="rk-clp-tinybtn" data-clp-set-start title="Set in-point to the playhead">Set to playhead</button>
|
|
73
|
+
</div>
|
|
74
|
+
<input class="rk-input rk-clp-tc" data-clp-start-input type="text" inputmode="numeric" placeholder="00:00:00:0000" value="00:00:00:0000" />
|
|
75
|
+
<input class="rk-clp-slider" data-clp-start-slider type="range" min="0" max="1000" step="1" value="0" aria-label="In point" />
|
|
76
|
+
</div>
|
|
77
|
+
<div class="rk-clp-field">
|
|
78
|
+
<div class="rk-clp-field-head">
|
|
79
|
+
<span class="rk-clp-field-label">Out</span>
|
|
80
|
+
<button type="button" class="rk-clp-tinybtn" data-clp-set-end title="Set out-point to the playhead">Set to playhead</button>
|
|
81
|
+
</div>
|
|
82
|
+
<input class="rk-input rk-clp-tc" data-clp-end-input type="text" inputmode="numeric" placeholder="00:00:00:0000" value="00:00:00:0000" />
|
|
83
|
+
<input class="rk-clp-slider" data-clp-end-slider type="range" min="0" max="1000" step="1" value="1000" aria-label="Out point" />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="rk-clp-estimate" data-clp-estimate>Clip length — <strong data-clp-length>0:00.0</strong></div>
|
|
88
|
+
<div class="rk-clp-rangenote" data-clp-range-note hidden></div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="rk-inpaint-block rk-clp-saveblock" data-clp-save-block hidden>
|
|
92
|
+
<label class="rk-label" for="rkClpName">Clip name <span class="rk-hint">optional</span></label>
|
|
93
|
+
<input class="rk-input" id="rkClpName" data-clp-name type="text" placeholder="hook-01" autocomplete="off" />
|
|
94
|
+
<label class="rk-label" for="rkClpFolder" style="margin-top:10px">Save to raws folder <span class="rk-hint">optional</span></label>
|
|
95
|
+
<input class="rk-input" id="rkClpFolder" data-clp-folder type="text" placeholder="defaults to the session tracer" autocomplete="off" spellcheck="false" />
|
|
96
|
+
<button type="button" class="rk-btn rk-btn-gold rk-clp-savebtn" data-clp-save>
|
|
97
|
+
${CLIP_ICON}<span data-clp-save-label>Save clip to raws</span>
|
|
98
|
+
</button>
|
|
99
|
+
<div class="rk-clp-status" data-clp-status role="status"></div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<!-- CENTER: video preview + timeline -->
|
|
104
|
+
<section class="rk-inpaint-stage rk-clp-stage" aria-label="Video preview">
|
|
105
|
+
<div class="rk-clp-empty" data-clp-empty>
|
|
106
|
+
<div class="rk-clp-empty-inner">
|
|
107
|
+
${CLIP_ICON}
|
|
108
|
+
<p>Load a video to start clipping.</p>
|
|
109
|
+
<span>Paste a URL and hit <strong>Load</strong>, or upload a file.</span>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="rk-clp-player" data-clp-player hidden>
|
|
113
|
+
<div class="rk-clp-videowrap">
|
|
114
|
+
<video data-clp-video playsinline preload="auto"></video>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="rk-clp-transport">
|
|
117
|
+
<button type="button" class="rk-ip-iconbtn rk-clp-playpause" data-clp-playpause aria-label="Play / pause">${PLAY_ICON}</button>
|
|
118
|
+
<div class="rk-clp-scrubwrap">
|
|
119
|
+
<input class="rk-clp-scrub" data-clp-scrub type="range" min="0" max="1000" step="1" value="0" aria-label="Scrub" />
|
|
120
|
+
<div class="rk-clp-rangebar" data-clp-rangebar aria-hidden="true"><span class="rk-clp-rangefill" data-clp-range-fill></span></div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="rk-clp-time">
|
|
123
|
+
<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" />
|
|
124
|
+
<span class="rk-clp-duration" data-clp-duration>/ 00:00:00:0000</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
|
|
130
|
+
<!-- RIGHT: clips this session -->
|
|
131
|
+
<aside class="rk-inpaint-side rk-clp-side" aria-label="Clips this session">
|
|
132
|
+
<div class="rk-clp-side-head">
|
|
133
|
+
<span class="rk-eyebrow">Clips this session</span>
|
|
134
|
+
<button type="button" class="rk-ip-iconbtn rk-ip-iconbtn-sm" data-clp-session-refresh aria-label="Refresh" title="Refresh">${HISTORY_ICON}</button>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="rk-clp-session-list" data-clp-session-list>
|
|
137
|
+
<div class="rk-clp-session-empty">No clips yet — set an in/out point and save your first clip.</div>
|
|
138
|
+
</div>
|
|
139
|
+
<a class="rk-clp-librarylink" href="/library/raws">Open your raws library →</a>
|
|
140
|
+
</aside>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Settings modal -->
|
|
144
|
+
<div class="rk-ip-settingsmodal" data-clp-settings hidden>
|
|
145
|
+
<div class="rk-ip-settingsmodal-backdrop" data-clp-settings-close></div>
|
|
146
|
+
<div class="rk-ip-settingsmodal-card" role="dialog" aria-modal="true" aria-label="Clipper settings">
|
|
147
|
+
<div class="rk-ip-settingsmodal-head">
|
|
148
|
+
<h2>Session settings</h2>
|
|
149
|
+
<button type="button" class="rk-ip-iconbtn" data-clp-settings-close aria-label="Close">${CLOSE_ICON}</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="rk-field">
|
|
152
|
+
<label class="rk-label" for="rkClpTracer">Session name (tracer)</label>
|
|
153
|
+
<input class="rk-input" id="rkClpTracer" data-clp-tracer type="text" placeholder="auto — named from the source" autocomplete="off" spellcheck="false" />
|
|
154
|
+
<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>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Past sessions slide-in -->
|
|
160
|
+
<aside class="rk-ip-sessions rk-clp-sessions" data-clp-sessions aria-label="Past clipping sessions">
|
|
161
|
+
<div class="rk-ip-sessions-head">
|
|
162
|
+
<span class="rk-eyebrow">Past sessions</span>
|
|
163
|
+
<button type="button" class="rk-ip-iconbtn" data-clp-sessions-close aria-label="Close">${CLOSE_ICON}</button>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="rk-ip-sessions-list" data-clp-sessions-list></div>
|
|
166
|
+
</aside>
|
|
167
|
+
<div class="rk-ip-sessions-backdrop" data-clp-sessions-backdrop hidden></div>
|
|
168
|
+
|
|
169
|
+
<script type="application/json" data-clp-boot>${JSON.stringify(boot)}</script>
|
|
170
|
+
</main>`;
|
|
171
|
+
const pageCss = `
|
|
172
|
+
${INPAINT_TABS_CSS}
|
|
173
|
+
.rk-clp-page{padding-bottom:40px}
|
|
174
|
+
.rk-clp-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 300px;gap:18px;align-items:start}
|
|
175
|
+
.rk-clp-urlrow{display:flex;gap:8px}
|
|
176
|
+
.rk-clp-urlrow .rk-input{flex:1;min-width:0}
|
|
177
|
+
.rk-clp-ingest{display:flex;gap:8px;margin-top:8px}
|
|
178
|
+
.rk-clp-ingest .rk-btn{flex:1;gap:6px}
|
|
179
|
+
.rk-clp-rangeblock .rk-clp-inout{display:flex;flex-direction:column;gap:14px;margin-top:6px}
|
|
180
|
+
.rk-clp-field{display:flex;flex-direction:column;gap:6px}
|
|
181
|
+
.rk-clp-field-head{display:flex;align-items:center;justify-content:space-between}
|
|
182
|
+
.rk-clp-field-label{font-size:12px;font-weight:800;letter-spacing:.02em;color:var(--rk-ink)}
|
|
183
|
+
.rk-clp-tinybtn{border:0;background:transparent;color:var(--rk-gold-ink,#9a6b12);font-size:11px;font-weight:700;cursor:pointer;padding:2px 4px;border-radius:6px}
|
|
184
|
+
.rk-clp-tinybtn:hover{background:var(--rk-n-100)}
|
|
185
|
+
.rk-clp-tc{font-variant-numeric:tabular-nums;letter-spacing:.02em;font-weight:700}
|
|
186
|
+
.rk-clp-slider,.rk-clp-scrub{width:100%;accent-color:var(--rk-gold,#e2a63a)}
|
|
187
|
+
.rk-clp-estimate{margin-top:12px;font-size:13px;color:var(--rk-text-muted)}
|
|
188
|
+
.rk-clp-estimate strong{color:var(--rk-ink);font-variant-numeric:tabular-nums}
|
|
189
|
+
.rk-clp-rangenote{margin-top:8px;font-size:12px;font-weight:600;color:#b42318}
|
|
190
|
+
.rk-clp-savebtn{width:100%;justify-content:center;gap:8px;margin-top:12px}
|
|
191
|
+
.rk-clp-savebtn[data-loading="true"]{opacity:.7;pointer-events:none}
|
|
192
|
+
.rk-clp-status{margin-top:8px;font-size:12.5px;font-weight:600;min-height:16px}
|
|
193
|
+
.rk-clp-status[data-tone="error"]{color:#b42318}
|
|
194
|
+
.rk-clp-status[data-tone="success"]{color:#15803d}
|
|
195
|
+
.rk-clp-stage{min-height:360px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-radius-lg,18px);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
|
|
196
|
+
.rk-clp-empty{padding:40px;text-align:center;color:var(--rk-text-muted)}
|
|
197
|
+
.rk-clp-empty-inner svg{width:34px;height:34px;opacity:.5;margin-bottom:10px}
|
|
198
|
+
.rk-clp-empty-inner p{font-size:15px;font-weight:700;color:var(--rk-ink);margin:0 0 4px}
|
|
199
|
+
.rk-clp-empty-inner span{font-size:12.5px}
|
|
200
|
+
.rk-clp-player{width:100%;display:flex;flex-direction:column;gap:10px;padding:12px}
|
|
201
|
+
.rk-clp-videowrap{width:100%;background:#000;border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;max-height:56vh}
|
|
202
|
+
.rk-clp-videowrap video{width:100%;max-height:56vh;display:block}
|
|
203
|
+
.rk-clp-transport{display:flex;align-items:center;gap:12px;padding:2px 4px}
|
|
204
|
+
.rk-clp-playpause{flex:none}
|
|
205
|
+
.rk-clp-scrubwrap{flex:1;min-width:0;position:relative;display:flex;flex-direction:column;gap:4px}
|
|
206
|
+
.rk-clp-rangebar{position:relative;height:5px;border-radius:999px;background:var(--rk-n-200,#e7e2da);overflow:hidden}
|
|
207
|
+
.rk-clp-rangefill{position:absolute;top:0;bottom:0;left:0;width:100%;background:var(--rk-gold,#e2a63a);border-radius:999px}
|
|
208
|
+
.rk-clp-time{flex:none;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--rk-text-muted)}
|
|
209
|
+
.rk-clp-tc-now{width:118px;padding:5px 8px;font-size:12px}
|
|
210
|
+
.rk-clp-duration{font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
211
|
+
.rk-clp-side{position:sticky;top:16px}
|
|
212
|
+
.rk-clp-side-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
|
213
|
+
.rk-ip-iconbtn-sm{width:28px;height:28px}
|
|
214
|
+
.rk-clp-session-list{display:flex;flex-direction:column;gap:8px;max-height:60vh;overflow:auto}
|
|
215
|
+
.rk-clp-session-empty{font-size:12.5px;color:var(--rk-text-muted);padding:10px 2px;line-height:1.5}
|
|
216
|
+
.rk-clp-clip{display:flex;gap:10px;align-items:center;padding:6px;border:1px solid var(--rk-border);border-radius:12px;background:var(--rk-surface);text-decoration:none}
|
|
217
|
+
.rk-clp-clip:hover{border-color:var(--rk-gold,#e2a63a)}
|
|
218
|
+
.rk-clp-clip-thumb{flex:none;width:64px;height:40px;border-radius:8px;object-fit:cover;background:var(--rk-n-200,#e7e2da)}
|
|
219
|
+
.rk-clp-clip-meta{min-width:0;flex:1}
|
|
220
|
+
.rk-clp-clip-title{font-size:12.5px;font-weight:700;color:var(--rk-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
221
|
+
.rk-clp-clip-sub{font-size:11px;color:var(--rk-text-muted);font-variant-numeric:tabular-nums}
|
|
222
|
+
.rk-clp-librarylink{display:inline-block;margin-top:12px;font-size:12.5px;font-weight:700;color:var(--rk-gold-ink,#9a6b12);text-decoration:none}
|
|
223
|
+
.rk-clp-librarylink:hover{text-decoration:underline}
|
|
224
|
+
.rk-btn-xs{padding:3px 9px;font-size:11px}
|
|
225
|
+
@media (max-width: 1080px){ .rk-clp-workbench{grid-template-columns:1fr;} .rk-clp-side{position:static} }
|
|
226
|
+
`;
|
|
227
|
+
return reskinDocument({
|
|
228
|
+
title: "vidfarm — Clip a Video",
|
|
229
|
+
description: "Cut exact subranges out of any video straight into your raws library — preview from the source, pick a precise in/out point, and save only the clip.",
|
|
230
|
+
activeSlug: "inpaint",
|
|
231
|
+
pageCss,
|
|
232
|
+
body,
|
|
233
|
+
script: CLIPPER_SCRIPT
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Self-contained client runtime (plain string, no runtime imports). Uses string
|
|
237
|
+
// concatenation (not template literals) so no `${}` needs escaping inside the
|
|
238
|
+
// outer TS template literal.
|
|
239
|
+
const CLIPPER_SCRIPT = `
|
|
240
|
+
(function(){
|
|
241
|
+
var bootEl = document.querySelector("[data-clp-boot]");
|
|
242
|
+
var boot = {}; try { boot = JSON.parse(bootEl && bootEl.textContent || "{}"); } catch(_){}
|
|
243
|
+
|
|
244
|
+
var urlInput = document.querySelector("[data-clp-url-input]");
|
|
245
|
+
var loadBtn = document.querySelector("[data-clp-url-load]");
|
|
246
|
+
var uploadBtn = document.querySelector("[data-clp-upload]");
|
|
247
|
+
var attachBtn = document.querySelector("[data-clp-attach]");
|
|
248
|
+
var fileInput = document.querySelector("[data-clp-file]");
|
|
249
|
+
var emptyState = document.querySelector("[data-clp-empty]");
|
|
250
|
+
var player = document.querySelector("[data-clp-player]");
|
|
251
|
+
var video = document.querySelector("[data-clp-video]");
|
|
252
|
+
var playpause = document.querySelector("[data-clp-playpause]");
|
|
253
|
+
var scrub = document.querySelector("[data-clp-scrub]");
|
|
254
|
+
var timecodeNow = document.querySelector("[data-clp-timecode]");
|
|
255
|
+
var durationEl = document.querySelector("[data-clp-duration]");
|
|
256
|
+
var rangeControls = document.querySelector("[data-clp-range-controls]");
|
|
257
|
+
var startInput = document.querySelector("[data-clp-start-input]");
|
|
258
|
+
var endInput = document.querySelector("[data-clp-end-input]");
|
|
259
|
+
var startSlider = document.querySelector("[data-clp-start-slider]");
|
|
260
|
+
var endSlider = document.querySelector("[data-clp-end-slider]");
|
|
261
|
+
var setStartBtn = document.querySelector("[data-clp-set-start]");
|
|
262
|
+
var setEndBtn = document.querySelector("[data-clp-set-end]");
|
|
263
|
+
var wholeBtn = document.querySelector("[data-clp-whole]");
|
|
264
|
+
var lengthEl = document.querySelector("[data-clp-length]");
|
|
265
|
+
var rangeNote = document.querySelector("[data-clp-range-note]");
|
|
266
|
+
var rangeFill = document.querySelector("[data-clp-range-fill]");
|
|
267
|
+
var saveBlock = document.querySelector("[data-clp-save-block]");
|
|
268
|
+
var nameInput = document.querySelector("[data-clp-name]");
|
|
269
|
+
var folderInput = document.querySelector("[data-clp-folder]");
|
|
270
|
+
var saveBtn = document.querySelector("[data-clp-save]");
|
|
271
|
+
var saveLabel = document.querySelector("[data-clp-save-label]");
|
|
272
|
+
var statusEl = document.querySelector("[data-clp-status]");
|
|
273
|
+
var sessionList = document.querySelector("[data-clp-session-list]");
|
|
274
|
+
var sessionRefresh = document.querySelector("[data-clp-session-refresh]");
|
|
275
|
+
var startNewBtn = document.querySelector("[data-clp-start-new]");
|
|
276
|
+
var settingsBtn = document.querySelector("[data-clp-settings-button]");
|
|
277
|
+
var settingsModal = document.querySelector("[data-clp-settings]");
|
|
278
|
+
var settingsCloses = document.querySelectorAll("[data-clp-settings-close]");
|
|
279
|
+
var tracerInput = document.querySelector("[data-clp-tracer]");
|
|
280
|
+
var sessionsToggle = document.querySelector("[data-clp-history-toggle]");
|
|
281
|
+
var sessionsPanel = document.querySelector("[data-clp-sessions]");
|
|
282
|
+
var sessionsList = document.querySelector("[data-clp-sessions-list]");
|
|
283
|
+
var sessionsClose = document.querySelector("[data-clp-sessions-close]");
|
|
284
|
+
var sessionsBackdrop = document.querySelector("[data-clp-sessions-backdrop]");
|
|
285
|
+
|
|
286
|
+
var PLAY_SVG = ${JSON.stringify(PLAY_ICON)};
|
|
287
|
+
var PAUSE_SVG = ${JSON.stringify(PAUSE_ICON)};
|
|
288
|
+
|
|
289
|
+
// ── state ──
|
|
290
|
+
var sourceUrl = ""; // the original pasted/handoff URL
|
|
291
|
+
var previewUrl = ""; // resolved direct media URL (what <video> plays)
|
|
292
|
+
var sourceName = "";
|
|
293
|
+
var sourceObjectUrl = ""; // object URL for a local upload
|
|
294
|
+
var uploadTempFileId = ""; // temp_file_id when the source was uploaded
|
|
295
|
+
var durationSec = 0;
|
|
296
|
+
var startSec = 0;
|
|
297
|
+
var endSec = 0;
|
|
298
|
+
var sessionTracer = "";
|
|
299
|
+
var tracerAutoPending = true;
|
|
300
|
+
|
|
301
|
+
function nowIso(){ return new Date().toISOString(); }
|
|
302
|
+
function esc(t){ var d=document.createElement("div"); d.textContent=(t==null?"":String(t)); return d.innerHTML; }
|
|
303
|
+
function pad(n, w){ n=String(Math.max(0, Math.floor(n))); while(n.length<w) n="0"+n; return n; }
|
|
304
|
+
function absoluteUrl(u){ try { return new URL(u, window.location.origin).toString(); } catch(_){ return u; } }
|
|
305
|
+
function parseJson(r){ return r.text().then(function(t){ try { return t?JSON.parse(t):{}; } catch(_){ return {}; } }); }
|
|
306
|
+
|
|
307
|
+
// HH:MM:SS:NNNN where NNNN = ten-thousandths of a second (4 digits).
|
|
308
|
+
function fmtTC(sec){
|
|
309
|
+
if(!isFinite(sec) || sec < 0) sec = 0;
|
|
310
|
+
var whole = Math.floor(sec);
|
|
311
|
+
var frac = Math.round((sec - whole) * 10000);
|
|
312
|
+
if(frac >= 10000){ frac -= 10000; whole += 1; }
|
|
313
|
+
var h = Math.floor(whole / 3600);
|
|
314
|
+
var m = Math.floor((whole % 3600) / 60);
|
|
315
|
+
var s = whole % 60;
|
|
316
|
+
return pad(h,2)+":"+pad(m,2)+":"+pad(s,2)+":"+pad(frac,4);
|
|
317
|
+
}
|
|
318
|
+
function parseTC(str){
|
|
319
|
+
if(str == null) return NaN;
|
|
320
|
+
var parts = String(str).trim().split(":").map(function(p){ return p.trim(); });
|
|
321
|
+
if(!parts.length) return NaN;
|
|
322
|
+
var h=0,m=0,s=0,frac=0;
|
|
323
|
+
if(parts.length === 4){ h=+parts[0]; m=+parts[1]; s=+parts[2]; frac=+parts[3]; }
|
|
324
|
+
else if(parts.length === 3){ m=+parts[0]; s=+parts[1]; frac=+parts[2]; }
|
|
325
|
+
else if(parts.length === 2){ s=+parts[0]; frac=+parts[1]; }
|
|
326
|
+
else { s=+parts[0]; }
|
|
327
|
+
if([h,m,s,frac].some(function(n){ return isNaN(n); })) return NaN;
|
|
328
|
+
// Frac field: 4 digits = ten-thousandths; be forgiving of shorter entries.
|
|
329
|
+
var fracDivisor = Math.pow(10, String(parts[parts.length-1]||"").length || 4);
|
|
330
|
+
if(parts.length >= 2){ frac = (+parts[parts.length-1]) / fracDivisor; } else { frac = 0; }
|
|
331
|
+
return h*3600 + m*60 + s + frac;
|
|
332
|
+
}
|
|
333
|
+
function fmtLen(sec){
|
|
334
|
+
if(!isFinite(sec) || sec < 0) sec = 0;
|
|
335
|
+
var m = Math.floor(sec/60);
|
|
336
|
+
var s = sec - m*60;
|
|
337
|
+
return m+":"+pad(Math.floor(s),2)+"."+pad(Math.round((s-Math.floor(s))*10),1);
|
|
338
|
+
}
|
|
339
|
+
function slug(v){ return String(v||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60); }
|
|
340
|
+
|
|
341
|
+
function setStatus(msg, tone){ if(!statusEl) return; statusEl.textContent = msg||""; if(tone) statusEl.setAttribute("data-tone", tone); else statusEl.removeAttribute("data-tone"); }
|
|
342
|
+
function setRangeNote(msg){ if(!rangeNote) return; if(msg){ rangeNote.textContent = msg; rangeNote.hidden = false; } else { rangeNote.hidden = true; } }
|
|
343
|
+
|
|
344
|
+
// ── tracer ──
|
|
345
|
+
function tracerSuffix(){ return Math.random().toString(36).slice(2, 6); }
|
|
346
|
+
function nameTracerFromSeed(seed){
|
|
347
|
+
var base = slug(seed) || "clipper";
|
|
348
|
+
var t = base + "-" + tracerSuffix();
|
|
349
|
+
sessionTracer = t; tracerAutoPending = false;
|
|
350
|
+
if(tracerInput) tracerInput.value = t;
|
|
351
|
+
return t;
|
|
352
|
+
}
|
|
353
|
+
function resolveTracer(){
|
|
354
|
+
if(tracerInput && tracerInput.value.trim()){ sessionTracer = tracerInput.value.trim(); tracerAutoPending = false; return sessionTracer; }
|
|
355
|
+
return sessionTracer;
|
|
356
|
+
}
|
|
357
|
+
function currentFolder(){
|
|
358
|
+
var f = (folderInput && folderInput.value.trim()) || "";
|
|
359
|
+
return f || slug(resolveTracer() || sessionTracer || "");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── source loading ──
|
|
363
|
+
function revealPlayer(){ if(emptyState) emptyState.hidden = true; if(player) player.hidden = false; if(rangeControls) rangeControls.hidden = false; if(saveBlock) saveBlock.hidden = false; }
|
|
364
|
+
function clearSourceObjectUrl(){ if(sourceObjectUrl){ try { URL.revokeObjectURL(sourceObjectUrl); } catch(_){} sourceObjectUrl=""; } }
|
|
365
|
+
|
|
366
|
+
function playSrc(url){
|
|
367
|
+
if(!video) return;
|
|
368
|
+
try { video.pause(); } catch(_){}
|
|
369
|
+
video.src = url;
|
|
370
|
+
video.load();
|
|
371
|
+
revealPlayer();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Resolve a pasted/handoff URL to a direct preview media URL, then play it.
|
|
375
|
+
function loadSourceUrl(rawUrl, name){
|
|
376
|
+
var u = String(rawUrl||"").trim();
|
|
377
|
+
if(!u){ setStatus("Enter a video URL.", "error"); return; }
|
|
378
|
+
if(!/^https?:\\/\\//i.test(u)){ setStatus("That doesn't look like a valid URL.", "error"); return; }
|
|
379
|
+
sourceUrl = u; sourceName = name || ""; uploadTempFileId = ""; clearSourceObjectUrl();
|
|
380
|
+
if(loadBtn){ loadBtn.disabled = true; loadBtn.textContent = "Loading\\u2026"; }
|
|
381
|
+
setStatus("Resolving source\\u2026");
|
|
382
|
+
fetch("/raws/resolve-source", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ source_url: u }) })
|
|
383
|
+
.then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
|
|
384
|
+
.then(function(res){
|
|
385
|
+
if(!res.ok || !res.p.preview_url) throw new Error(res.p.error || "Could not load that video.");
|
|
386
|
+
previewUrl = res.p.preview_url;
|
|
387
|
+
if(res.p.title && !sourceName) sourceName = res.p.title;
|
|
388
|
+
if(tracerAutoPending) nameTracerFromSeed(sourceName || "clip");
|
|
389
|
+
playSrc(previewUrl);
|
|
390
|
+
setStatus("Loaded \\u2014 scrub and set your in/out points.", "success");
|
|
391
|
+
})
|
|
392
|
+
.catch(function(err){ setStatus(err && err.message ? err.message : "Could not load that video.", "error"); })
|
|
393
|
+
.then(function(){ if(loadBtn){ loadBtn.disabled = false; loadBtn.textContent = "Load"; } });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function loadLocalFile(file){
|
|
397
|
+
if(!file) return;
|
|
398
|
+
clearSourceObjectUrl();
|
|
399
|
+
sourceObjectUrl = URL.createObjectURL(file);
|
|
400
|
+
previewUrl = ""; sourceName = file.name || "upload.mp4"; sourceUrl = ""; uploadTempFileId = "";
|
|
401
|
+
if(tracerAutoPending) nameTracerFromSeed(sourceName);
|
|
402
|
+
playSrc(sourceObjectUrl);
|
|
403
|
+
setStatus("Uploading source in the background\\u2026");
|
|
404
|
+
// The <video> plays the local object URL immediately; in parallel push the
|
|
405
|
+
// file to /temp so /raws/clip-range has a server-side source to trim.
|
|
406
|
+
var fd = new FormData();
|
|
407
|
+
fd.append("file", file);
|
|
408
|
+
fetch("/api/v1/user/me/temporary-files/upload", { method:"POST", credentials:"same-origin", body: fd })
|
|
409
|
+
.then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
|
|
410
|
+
.then(function(res){
|
|
411
|
+
var id = res.p && (res.p.id || res.p.file_id || (res.p.file && res.p.file.id));
|
|
412
|
+
if(res.ok && id){ uploadTempFileId = String(id); setStatus("Source ready \\u2014 set your in/out points.", "success"); }
|
|
413
|
+
else { setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work. " + ((res.p && res.p.error)||""), "error"); }
|
|
414
|
+
})
|
|
415
|
+
.catch(function(){ setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work.", "error"); });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function openAttach(){
|
|
419
|
+
if(window.__vidfarmDirectory && typeof window.__vidfarmDirectory.pick === "function"){
|
|
420
|
+
window.__vidfarmDirectory.pick({ select:"file" }).then(function(picked){
|
|
421
|
+
if(!picked) return;
|
|
422
|
+
var url = picked.view_url || picked.url || picked.path;
|
|
423
|
+
if(url){ loadSourceUrl(absoluteUrl(url), picked.name || picked.file_name || ""); }
|
|
424
|
+
}).catch(function(){});
|
|
425
|
+
} else {
|
|
426
|
+
setStatus("File browser unavailable here \\u2014 paste a URL or upload instead.", "error");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── range sync ──
|
|
431
|
+
function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
|
|
432
|
+
function updateFill(){
|
|
433
|
+
if(!rangeFill || durationSec <= 0){ if(rangeFill){ rangeFill.style.left="0"; rangeFill.style.width="0"; } return; }
|
|
434
|
+
var a = clamp(startSec/durationSec, 0, 1) * 100;
|
|
435
|
+
var b = clamp(endSec/durationSec, 0, 1) * 100;
|
|
436
|
+
rangeFill.style.left = a + "%";
|
|
437
|
+
rangeFill.style.width = Math.max(0, b - a) + "%";
|
|
438
|
+
}
|
|
439
|
+
function refreshRangeUI(sync){
|
|
440
|
+
if(sync !== "start" && startInput && document.activeElement !== startInput) startInput.value = fmtTC(startSec);
|
|
441
|
+
if(sync !== "end" && endInput && document.activeElement !== endInput) endInput.value = fmtTC(endSec);
|
|
442
|
+
if(durationSec > 0){
|
|
443
|
+
if(startSlider && document.activeElement !== startSlider) startSlider.value = String(Math.round((startSec/durationSec)*1000));
|
|
444
|
+
if(endSlider && document.activeElement !== endSlider) endSlider.value = String(Math.round((endSec/durationSec)*1000));
|
|
445
|
+
}
|
|
446
|
+
var len = endSec - startSec;
|
|
447
|
+
if(lengthEl) lengthEl.textContent = fmtLen(Math.max(0, len)) + " (" + Math.max(0, len).toFixed(2) + "s)";
|
|
448
|
+
updateFill();
|
|
449
|
+
var valid = len > 0.02 && (durationSec <= 0 || (startSec >= 0 && endSec <= durationSec + 0.01));
|
|
450
|
+
if(!valid){ setRangeNote(len <= 0 ? "Out point must be after the in point." : "Range is outside the video."); }
|
|
451
|
+
else setRangeNote("");
|
|
452
|
+
if(saveBtn) saveBtn.disabled = !valid;
|
|
453
|
+
}
|
|
454
|
+
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(); }
|
|
455
|
+
function setEnd(sec){ endSec = clamp(sec, 0, durationSec>0?durationSec:sec); if(startSec > endSec) startSec = Math.max(0, endSec - 0.1); refreshRangeUI(); }
|
|
456
|
+
|
|
457
|
+
// ── playback ──
|
|
458
|
+
function syncNow(){
|
|
459
|
+
var t = video.currentTime || 0;
|
|
460
|
+
if(scrub && document.activeElement !== scrub && durationSec > 0) scrub.value = String(Math.round((t/durationSec)*1000));
|
|
461
|
+
if(timecodeNow && document.activeElement !== timecodeNow) timecodeNow.value = fmtTC(t);
|
|
462
|
+
}
|
|
463
|
+
function tick(){ if(video && !video.paused && !video.ended){ syncNow(); requestAnimationFrame(tick); } }
|
|
464
|
+
function setPlayIcon(){ if(playpause) playpause.innerHTML = (video && !video.paused) ? PAUSE_SVG : PLAY_SVG; }
|
|
465
|
+
|
|
466
|
+
if(video){
|
|
467
|
+
video.addEventListener("loadedmetadata", function(){
|
|
468
|
+
durationSec = video.duration || 0;
|
|
469
|
+
startSec = 0; endSec = durationSec || 0;
|
|
470
|
+
if(durationEl) durationEl.textContent = "/ " + fmtTC(durationSec);
|
|
471
|
+
refreshRangeUI();
|
|
472
|
+
});
|
|
473
|
+
video.addEventListener("timeupdate", syncNow);
|
|
474
|
+
video.addEventListener("play", function(){ setPlayIcon(); tick(); });
|
|
475
|
+
video.addEventListener("pause", setPlayIcon);
|
|
476
|
+
video.addEventListener("ended", setPlayIcon);
|
|
477
|
+
video.addEventListener("error", function(){ setStatus("The browser couldn't play this source. Try a direct .mp4 URL or upload the file.", "error"); });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── wire controls ──
|
|
481
|
+
if(loadBtn) loadBtn.addEventListener("click", function(){ loadSourceUrl(urlInput && urlInput.value, ""); });
|
|
482
|
+
if(urlInput) urlInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); loadSourceUrl(urlInput.value, ""); } });
|
|
483
|
+
if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadLocalFile(fileInput.files[0]); }); }
|
|
484
|
+
if(attachBtn) attachBtn.addEventListener("click", openAttach);
|
|
485
|
+
if(playpause) playpause.addEventListener("click", function(){ if(!video) return; if(video.paused) video.play(); else video.pause(); });
|
|
486
|
+
if(scrub) scrub.addEventListener("input", function(){ if(durationSec > 0){ video.currentTime = (scrub.value/1000)*durationSec; syncNow(); } });
|
|
487
|
+
if(timecodeNow) timecodeNow.addEventListener("change", function(){ var t = parseTC(timecodeNow.value); if(isFinite(t)){ video.currentTime = clamp(t, 0, durationSec||t); syncNow(); } });
|
|
488
|
+
|
|
489
|
+
if(startSlider) startSlider.addEventListener("input", function(){ if(durationSec>0) setStart((startSlider.value/1000)*durationSec); });
|
|
490
|
+
if(endSlider) endSlider.addEventListener("input", function(){ if(durationSec>0) setEnd((endSlider.value/1000)*durationSec); });
|
|
491
|
+
if(startInput) startInput.addEventListener("change", function(){ var t = parseTC(startInput.value); if(isFinite(t)) setStart(t); else refreshRangeUI(); });
|
|
492
|
+
if(endInput) endInput.addEventListener("change", function(){ var t = parseTC(endInput.value); if(isFinite(t)) setEnd(t); else refreshRangeUI(); });
|
|
493
|
+
if(setStartBtn) setStartBtn.addEventListener("click", function(){ setStart(video.currentTime || 0); });
|
|
494
|
+
if(setEndBtn) setEndBtn.addEventListener("click", function(){ setEnd(video.currentTime || 0); });
|
|
495
|
+
if(wholeBtn) wholeBtn.addEventListener("click", function(){ startSec = 0; endSec = durationSec || 0; refreshRangeUI(); });
|
|
496
|
+
|
|
497
|
+
// ── save clip ──
|
|
498
|
+
function setSaving(on){ if(saveBtn){ saveBtn.setAttribute("data-loading", on?"true":"false"); saveBtn.disabled = on; } if(saveLabel) saveLabel.textContent = on ? "Saving\\u2026" : "Save clip to raws"; }
|
|
499
|
+
function saveClip(){
|
|
500
|
+
if(!video){ return; }
|
|
501
|
+
var len = endSec - startSec;
|
|
502
|
+
if(!(len > 0.02)){ setStatus("Set an in point and a later out point first.", "error"); return; }
|
|
503
|
+
if(!previewUrl && !sourceUrl && !uploadTempFileId){ setStatus("Load a source video first.", "error"); return; }
|
|
504
|
+
var tracer = resolveTracer(); if(tracerAutoPending || !tracer){ tracer = nameTracerFromSeed(sourceName || "clip"); }
|
|
505
|
+
var folder = currentFolder();
|
|
506
|
+
var payload = { start_sec: Number(startSec.toFixed(3)), end_sec: Number(endSec.toFixed(3)), tracer: tracer };
|
|
507
|
+
if(folder) payload.folder_path = folder;
|
|
508
|
+
if(nameInput && nameInput.value.trim()) payload.name = nameInput.value.trim();
|
|
509
|
+
// Prefer the already-resolved direct URL (no re-resolve); else the uploaded
|
|
510
|
+
// temp file; else the raw source URL (server resolves + bills).
|
|
511
|
+
if(previewUrl) payload.preview_url = previewUrl;
|
|
512
|
+
if(uploadTempFileId) payload.temp_file_id = uploadTempFileId;
|
|
513
|
+
if(sourceUrl) payload.source_url = sourceUrl;
|
|
514
|
+
setSaving(true); setStatus("Trimming and saving your clip\\u2026");
|
|
515
|
+
fetch("/raws/clip-range", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify(payload) })
|
|
516
|
+
.then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
|
|
517
|
+
.then(function(res){
|
|
518
|
+
if(!res.ok || !res.p.clip_id) throw new Error(res.p.error || "Could not save that clip.");
|
|
519
|
+
setStatus("Saved \\u2014 clip added to your raws (" + (res.p.folder_path||folder) + ").", "success");
|
|
520
|
+
prependSessionClip(res.p);
|
|
521
|
+
loadSession();
|
|
522
|
+
})
|
|
523
|
+
.catch(function(err){ setStatus(err && err.message ? err.message : "Could not save that clip.", "error"); })
|
|
524
|
+
.then(function(){ setSaving(false); });
|
|
525
|
+
}
|
|
526
|
+
if(saveBtn) saveBtn.addEventListener("click", saveClip);
|
|
527
|
+
|
|
528
|
+
// ── this-session clip list ──
|
|
529
|
+
function clipRow(clip){
|
|
530
|
+
var a = document.createElement("a");
|
|
531
|
+
a.className = "rk-clp-clip"; a.href = clip.view_url ? absoluteUrl(clip.view_url) : "/library/raws"; a.target = "_blank"; a.rel = "noopener";
|
|
532
|
+
var thumb;
|
|
533
|
+
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 = ""; }
|
|
534
|
+
else { thumb = document.createElement("span"); thumb.className = "rk-clp-clip-thumb"; }
|
|
535
|
+
var meta = document.createElement("div"); meta.className = "rk-clp-clip-meta";
|
|
536
|
+
var title = document.createElement("div"); title.className = "rk-clp-clip-title"; title.textContent = clip.description || clip.name || clip.clip_id || "clip";
|
|
537
|
+
var sub = document.createElement("div"); sub.className = "rk-clp-clip-sub";
|
|
538
|
+
var d = Number(clip.duration_sec || 0);
|
|
539
|
+
sub.textContent = (d>0 ? d.toFixed(2)+"s" : "clip");
|
|
540
|
+
meta.appendChild(title); meta.appendChild(sub); a.appendChild(thumb); a.appendChild(meta);
|
|
541
|
+
return a;
|
|
542
|
+
}
|
|
543
|
+
function prependSessionClip(clip){
|
|
544
|
+
if(!sessionList) return;
|
|
545
|
+
var empty = sessionList.querySelector(".rk-clp-session-empty"); if(empty) empty.remove();
|
|
546
|
+
sessionList.insertBefore(clipRow(clip), sessionList.firstChild);
|
|
547
|
+
}
|
|
548
|
+
function loadSession(){
|
|
549
|
+
if(!sessionList) return;
|
|
550
|
+
var folder = currentFolder();
|
|
551
|
+
if(!folder){ return; }
|
|
552
|
+
var endpoint = new URL("/raws/feed", window.location.origin);
|
|
553
|
+
endpoint.searchParams.set("folder", folder); endpoint.searchParams.set("limit", "200");
|
|
554
|
+
fetch(endpoint, { credentials:"same-origin", headers:{ Accept:"application/json" } })
|
|
555
|
+
.then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
|
|
556
|
+
.then(function(res){
|
|
557
|
+
if(!res.ok) return;
|
|
558
|
+
var clips = (res.p && res.p.clips) || [];
|
|
559
|
+
sessionList.innerHTML = "";
|
|
560
|
+
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 save your first clip."; sessionList.appendChild(e); return; }
|
|
561
|
+
clips.forEach(function(c){ sessionList.appendChild(clipRow(c)); });
|
|
562
|
+
}).catch(function(){});
|
|
563
|
+
}
|
|
564
|
+
if(sessionRefresh) sessionRefresh.addEventListener("click", loadSession);
|
|
565
|
+
|
|
566
|
+
// ── past sessions (grouped by folder/tracer) ──
|
|
567
|
+
function openSessions(){ if(sessionsPanel) sessionsPanel.classList.add("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = false; loadPastSessions(); }
|
|
568
|
+
function closeSessions(){ if(sessionsPanel) sessionsPanel.classList.remove("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = true; }
|
|
569
|
+
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(); }
|
|
570
|
+
function loadPastSessions(){
|
|
571
|
+
if(!sessionsList) return;
|
|
572
|
+
sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">Loading\\u2026</div>";
|
|
573
|
+
var endpoint = new URL("/raws/feed", window.location.origin); endpoint.searchParams.set("limit", "200");
|
|
574
|
+
fetch(endpoint, { credentials:"same-origin", headers:{ Accept:"application/json" } })
|
|
575
|
+
.then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
|
|
576
|
+
.then(function(res){
|
|
577
|
+
var clips = (res.p && res.p.clips) || [];
|
|
578
|
+
var groups = {}, order = [];
|
|
579
|
+
clips.forEach(function(c){
|
|
580
|
+
var key = (c.folder_path || c.tracer || "").trim(); if(!key) return;
|
|
581
|
+
if(!groups[key]){ groups[key] = { key:key, thumb:"", count:0, createdAt:c.created_at||"" }; order.push(key); }
|
|
582
|
+
var g = groups[key]; g.count++;
|
|
583
|
+
if(!g.thumb && c.thumbnail_url) g.thumb = absoluteUrl(c.thumbnail_url);
|
|
584
|
+
if(Date.parse(c.created_at||0) > Date.parse(g.createdAt||0)) g.createdAt = c.created_at;
|
|
585
|
+
});
|
|
586
|
+
var list = order.map(function(k){ return groups[k]; }).sort(function(a,b){ return Date.parse(b.createdAt||0) - Date.parse(a.createdAt||0); });
|
|
587
|
+
sessionsList.innerHTML = "";
|
|
588
|
+
if(!list.length){ sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">No past clipping sessions yet.</div>"; return; }
|
|
589
|
+
list.forEach(function(g){
|
|
590
|
+
var row = document.createElement("button"); row.type="button"; row.className="rk-ip-session" + (g.key === currentFolder() ? " is-active" : "");
|
|
591
|
+
if(g.thumb){ var im=document.createElement("img"); im.className="rk-ip-session-thumb"; im.src=g.thumb; im.alt=""; row.appendChild(im); }
|
|
592
|
+
else { var ph=document.createElement("span"); ph.className="rk-ip-session-thumb is-empty"; row.appendChild(ph); }
|
|
593
|
+
var meta=document.createElement("div"); meta.className="rk-ip-session-meta";
|
|
594
|
+
var title=document.createElement("div"); title.className="rk-ip-session-title"; title.textContent=g.key;
|
|
595
|
+
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):"");
|
|
596
|
+
meta.appendChild(title); meta.appendChild(sub); row.appendChild(meta);
|
|
597
|
+
row.addEventListener("click", function(){ switchToSession(g.key); });
|
|
598
|
+
sessionsList.appendChild(row);
|
|
599
|
+
});
|
|
600
|
+
}).catch(function(){ if(sessionsList) sessionsList.innerHTML = "<div class=\\"rk-ip-sessions-empty\\">Unable to load sessions.</div>"; });
|
|
601
|
+
}
|
|
602
|
+
function switchToSession(key){
|
|
603
|
+
if(!key) return;
|
|
604
|
+
sessionTracer = key; tracerAutoPending = false;
|
|
605
|
+
if(tracerInput) tracerInput.value = key;
|
|
606
|
+
if(folderInput) folderInput.value = key;
|
|
607
|
+
closeSessions(); loadSession();
|
|
608
|
+
setStatus("Opened session " + key + " \\u2014 load a source to add more clips.", "");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── settings / sessions open-close ──
|
|
612
|
+
if(settingsBtn) settingsBtn.addEventListener("click", function(){ if(settingsModal) settingsModal.hidden = false; });
|
|
613
|
+
Array.prototype.forEach.call(settingsCloses, function(el){ el.addEventListener("click", function(){ if(settingsModal) settingsModal.hidden = true; }); });
|
|
614
|
+
if(sessionsToggle) sessionsToggle.addEventListener("click", openSessions);
|
|
615
|
+
if(sessionsClose) sessionsClose.addEventListener("click", closeSessions);
|
|
616
|
+
if(sessionsBackdrop) sessionsBackdrop.addEventListener("click", closeSessions);
|
|
617
|
+
document.addEventListener("keydown", function(e){ if(e.key === "Escape"){ if(settingsModal && !settingsModal.hidden) settingsModal.hidden = true; closeSessions(); } });
|
|
618
|
+
|
|
619
|
+
// ── start new ──
|
|
620
|
+
function resetWorkspace(){
|
|
621
|
+
clearSourceObjectUrl();
|
|
622
|
+
sourceUrl=""; previewUrl=""; sourceName=""; uploadTempFileId=""; durationSec=0; startSec=0; endSec=0;
|
|
623
|
+
try { video.pause(); } catch(_){} if(video){ video.removeAttribute("src"); video.load(); }
|
|
624
|
+
if(emptyState) emptyState.hidden = false; if(player) player.hidden = true;
|
|
625
|
+
if(rangeControls) rangeControls.hidden = true; if(saveBlock) saveBlock.hidden = true;
|
|
626
|
+
if(urlInput) urlInput.value=""; if(nameInput) nameInput.value=""; if(folderInput) folderInput.value="";
|
|
627
|
+
if(sessionList) sessionList.innerHTML = "<div class=\\"rk-clp-session-empty\\">No clips yet — set an in/out point and save your first clip.</div>";
|
|
628
|
+
setStatus(""); setRangeNote("");
|
|
629
|
+
}
|
|
630
|
+
if(startNewBtn) startNewBtn.addEventListener("click", function(){
|
|
631
|
+
sessionTracer=""; tracerAutoPending=true; if(tracerInput) tracerInput.value="";
|
|
632
|
+
clearUrlParams(); resetWorkspace();
|
|
633
|
+
setStatus("Started a new session.", "");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ── URL param preload ──
|
|
637
|
+
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 ""; }
|
|
638
|
+
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()); }
|
|
639
|
+
function initFromUrl(){
|
|
640
|
+
var t = firstParam(["tracer","tracer_id","trace"]);
|
|
641
|
+
if(t){ sessionTracer=t; tracerAutoPending=false; if(tracerInput) tracerInput.value=t; if(folderInput && !folderInput.value) folderInput.value=slug(t); }
|
|
642
|
+
var src = firstParam(["source_url","source_video_url","video_url","url"]);
|
|
643
|
+
if(src){ if(urlInput) urlInput.value = src; loadSourceUrl(src, firstParam(["source_video_name","video_name","file_name"])); }
|
|
644
|
+
if(t) loadSession();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
initFromUrl();
|
|
648
|
+
})();`;
|
|
649
|
+
//# sourceMappingURL=inpaint-clipper-page.js.map
|