@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +476 -260
- package/dist/src/cli.js +362 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21603
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -828
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3654
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,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
|