@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,1018 +0,0 @@
|
|
|
1
|
-
// Reusable file-directory explorer component (browser client).
|
|
2
|
-
//
|
|
3
|
-
// One framework-free component that browses the unified virtual directory —
|
|
4
|
-
// /files, /temp, /raws — as a real folder tree, with a per-row "…" menu
|
|
5
|
-
// (Copy Path / Attach / View / Preview), default-click = Copy Path, folder
|
|
6
|
-
// Open, multi-file + folder select, and unified search (semantic + substring +
|
|
7
|
-
// absolute-path). It talks to the /me/directory[/search] endpoints and returns
|
|
8
|
-
// the one canonical DirectoryItem shape from every backend.
|
|
9
|
-
//
|
|
10
|
-
// It's a COMPONENT, not a one-off: mount it anywhere with
|
|
11
|
-
// `window.__vidfarmDirectory.mount(el, opts)`, or drop a `<div data-rk-directory>`
|
|
12
|
-
// and it auto-mounts. The chat dock hosts it in "attach" mode; /library/raws
|
|
13
|
-
// hosts the same code in "standalone" mode scoped to /raws — no duplication.
|
|
14
|
-
//
|
|
15
|
-
// Host coupling (chat dock chips / preview modal) is passed IN via opts
|
|
16
|
-
// callbacks, or, for the auto-mounted dock instance, read from a
|
|
17
|
-
// `window.__rkDirectoryHost` hook the dock's inline script sets synchronously
|
|
18
|
-
// before this deferred module runs.
|
|
19
|
-
import { buildDirectoryPath, DIRECTORY_ROOT_KEYS, DIRECTORY_ROOTS, normalizeFolderPath, parseDirectoryPath } from "../services/file-directory.js";
|
|
20
|
-
const DEFAULT_API_BASE = "/api/v1/user/me";
|
|
21
|
-
// ── small DOM helpers ────────────────────────────────────────────────────────
|
|
22
|
-
function el(tag, cls, text) {
|
|
23
|
-
const node = document.createElement(tag);
|
|
24
|
-
if (cls)
|
|
25
|
-
node.className = cls;
|
|
26
|
-
if (text != null)
|
|
27
|
-
node.textContent = text;
|
|
28
|
-
return node;
|
|
29
|
-
}
|
|
30
|
-
function fmtBytes(b) {
|
|
31
|
-
if (!b || b <= 0)
|
|
32
|
-
return "";
|
|
33
|
-
const u = ["B", "KB", "MB", "GB"];
|
|
34
|
-
let v = b, i = 0;
|
|
35
|
-
while (v >= 1024 && i < u.length - 1) {
|
|
36
|
-
v /= 1024;
|
|
37
|
-
i++;
|
|
38
|
-
}
|
|
39
|
-
return (v >= 10 || i === 0 ? Math.round(v) : v.toFixed(1)) + " " + u[i];
|
|
40
|
-
}
|
|
41
|
-
function fmtDur(s) {
|
|
42
|
-
if (typeof s !== "number" || !(s > 0))
|
|
43
|
-
return "";
|
|
44
|
-
s = Math.round(s);
|
|
45
|
-
const m = Math.floor(s / 60), r = s % 60;
|
|
46
|
-
return m + ":" + (r < 10 ? "0" + r : r);
|
|
47
|
-
}
|
|
48
|
-
function iconFor(it) {
|
|
49
|
-
const ic = el("span", "rk-aichat-fic " + (it.kind === "folder" ? "rk-aichat-fic-folder" : "rk-aichat-fic-file"));
|
|
50
|
-
if (it.kind === "folder") {
|
|
51
|
-
ic.textContent = "/";
|
|
52
|
-
return ic;
|
|
53
|
-
}
|
|
54
|
-
if (it.thumbUrl) {
|
|
55
|
-
const t = el("img");
|
|
56
|
-
t.src = it.thumbUrl;
|
|
57
|
-
t.alt = "";
|
|
58
|
-
t.loading = "lazy";
|
|
59
|
-
ic.appendChild(t);
|
|
60
|
-
return ic;
|
|
61
|
-
}
|
|
62
|
-
const ct = it.contentType || "";
|
|
63
|
-
if (it.viewUrl && ct.indexOf("image/") === 0) {
|
|
64
|
-
const im = el("img");
|
|
65
|
-
im.src = it.viewUrl;
|
|
66
|
-
im.alt = "";
|
|
67
|
-
im.loading = "lazy";
|
|
68
|
-
ic.appendChild(im);
|
|
69
|
-
return ic;
|
|
70
|
-
}
|
|
71
|
-
if (it.viewUrl && ct.indexOf("video/") === 0) {
|
|
72
|
-
const v = el("video");
|
|
73
|
-
v.src = it.viewUrl;
|
|
74
|
-
v.muted = true;
|
|
75
|
-
v.playsInline = true;
|
|
76
|
-
ic.appendChild(v);
|
|
77
|
-
return ic;
|
|
78
|
-
}
|
|
79
|
-
ic.textContent = ((it.name.split(".").pop() || it.name.slice(0, 1)) || "?").slice(0, 3).toUpperCase();
|
|
80
|
-
return ic;
|
|
81
|
-
}
|
|
82
|
-
// ── shared floating layers (toast + open kebab menu) ─────────────────────────
|
|
83
|
-
let toastTimer;
|
|
84
|
-
function showToast(msg) {
|
|
85
|
-
let t = document.getElementById("rk-dir-toast");
|
|
86
|
-
if (!t) {
|
|
87
|
-
t = el("div");
|
|
88
|
-
t.id = "rk-dir-toast";
|
|
89
|
-
t.className = "rk-dir-toast";
|
|
90
|
-
document.body.appendChild(t);
|
|
91
|
-
}
|
|
92
|
-
t.textContent = msg;
|
|
93
|
-
t.classList.add("is-on");
|
|
94
|
-
if (toastTimer)
|
|
95
|
-
window.clearTimeout(toastTimer);
|
|
96
|
-
toastTimer = window.setTimeout(() => t && t.classList.remove("is-on"), 1800);
|
|
97
|
-
}
|
|
98
|
-
function copyToClipboard(text) {
|
|
99
|
-
if (navigator.clipboard && navigator.clipboard.writeText)
|
|
100
|
-
return navigator.clipboard.writeText(text);
|
|
101
|
-
return new Promise((resolve, reject) => {
|
|
102
|
-
try {
|
|
103
|
-
const ta = el("textarea");
|
|
104
|
-
ta.value = text;
|
|
105
|
-
ta.style.position = "fixed";
|
|
106
|
-
ta.style.opacity = "0";
|
|
107
|
-
document.body.appendChild(ta);
|
|
108
|
-
ta.select();
|
|
109
|
-
document.execCommand("copy");
|
|
110
|
-
document.body.removeChild(ta);
|
|
111
|
-
resolve();
|
|
112
|
-
}
|
|
113
|
-
catch (e) {
|
|
114
|
-
reject(e);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
let closeOpenMenu = null;
|
|
119
|
-
function openMenuAt(anchor, actions) {
|
|
120
|
-
if (closeOpenMenu)
|
|
121
|
-
closeOpenMenu();
|
|
122
|
-
const menu = el("div", "rk-dir-menu");
|
|
123
|
-
actions.forEach((a) => {
|
|
124
|
-
const b = el("button", "rk-dir-menuitem" + (a.danger ? " is-danger" : ""), a.label);
|
|
125
|
-
b.type = "button";
|
|
126
|
-
b.addEventListener("click", (e) => { e.stopPropagation(); close(); a.run(); });
|
|
127
|
-
menu.appendChild(b);
|
|
128
|
-
});
|
|
129
|
-
document.body.appendChild(menu);
|
|
130
|
-
const r = anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
|
|
131
|
-
// Prefer below-right of the anchor; flip up/left if it would overflow.
|
|
132
|
-
const mw = menu.offsetWidth, mh = menu.offsetHeight;
|
|
133
|
-
let top = r.bottom + 4, left = r.right - mw;
|
|
134
|
-
if (left < 8)
|
|
135
|
-
left = r.left;
|
|
136
|
-
if (top + mh > window.innerHeight - 8)
|
|
137
|
-
top = Math.max(8, r.top - mh - 4);
|
|
138
|
-
menu.style.top = top + "px";
|
|
139
|
-
menu.style.left = Math.max(8, left) + "px";
|
|
140
|
-
function close() {
|
|
141
|
-
menu.remove();
|
|
142
|
-
document.removeEventListener("click", onDoc, true);
|
|
143
|
-
document.removeEventListener("keydown", onKey, true);
|
|
144
|
-
window.removeEventListener("scroll", close, true);
|
|
145
|
-
if (closeOpenMenu === close)
|
|
146
|
-
closeOpenMenu = null;
|
|
147
|
-
}
|
|
148
|
-
function onDoc(e) { if (!menu.contains(e.target))
|
|
149
|
-
close(); }
|
|
150
|
-
function onKey(e) { if (e.key === "Escape")
|
|
151
|
-
close(); }
|
|
152
|
-
setTimeout(() => {
|
|
153
|
-
document.addEventListener("click", onDoc, true);
|
|
154
|
-
document.addEventListener("keydown", onKey, true);
|
|
155
|
-
window.addEventListener("scroll", close, true);
|
|
156
|
-
}, 0);
|
|
157
|
-
closeOpenMenu = close;
|
|
158
|
-
}
|
|
159
|
-
// ── the explorer ─────────────────────────────────────────────────────────────
|
|
160
|
-
export function createDirectoryExplorer(mount, opts = {}) {
|
|
161
|
-
const apiBase = opts.apiBase || DEFAULT_API_BASE;
|
|
162
|
-
const roots = (opts.roots && opts.roots.length ? opts.roots : DIRECTORY_ROOT_KEYS.slice());
|
|
163
|
-
const singleRoot = roots.length === 1 ? roots[0] : null;
|
|
164
|
-
const mode = opts.mode || "attach";
|
|
165
|
-
const showSearch = opts.showSearch !== false;
|
|
166
|
-
const showMultiSelect = opts.showMultiSelect !== false;
|
|
167
|
-
const showUpload = opts.showUpload != null ? opts.showUpload : mode === "attach";
|
|
168
|
-
const primaryClick = opts.primaryClick || "copyPath";
|
|
169
|
-
const cloudAvailable = opts.cloudAvailable != null
|
|
170
|
-
? opts.cloudAvailable
|
|
171
|
-
: typeof window !== "undefined" && window.__vidfarmCloudSpace === true;
|
|
172
|
-
const host = window.__rkDirectoryHost || {};
|
|
173
|
-
const emitAttach = (items) => (opts.onAttach || host.onAttach || (() => showToast("Attached " + items.length + " item(s)")))(items);
|
|
174
|
-
const emitPreview = (items, i) => {
|
|
175
|
-
const cb = opts.onPreview || host.onPreview;
|
|
176
|
-
if (cb)
|
|
177
|
-
cb(items, i);
|
|
178
|
-
else if (items[i] && items[i].viewUrl)
|
|
179
|
-
window.open(items[i].viewUrl, "_blank", "noopener");
|
|
180
|
-
};
|
|
181
|
-
const toast = (msg) => (opts.onToast || host.onToast || showToast)(msg);
|
|
182
|
-
const copyPath = (p) => copyToClipboard(p).then(() => toast("Copied " + p)).catch(() => toast("Couldn't copy path"));
|
|
183
|
-
// ── state ──
|
|
184
|
-
let currentPath = normalizeInitial(opts.initialPath, singleRoot);
|
|
185
|
-
let status = "idle";
|
|
186
|
-
let folders = [];
|
|
187
|
-
let files = [];
|
|
188
|
-
let nextOffset = null;
|
|
189
|
-
let loadingMore = false;
|
|
190
|
-
const selection = new Map();
|
|
191
|
-
let searchMode = false;
|
|
192
|
-
let searchResults = [];
|
|
193
|
-
let searchStatus = "idle";
|
|
194
|
-
let destroyed = false;
|
|
195
|
-
// Which backend the explorer reads: "local" (this box's disk backend) or
|
|
196
|
-
// "cloud" (vidfarm.cc via the serve-box passthrough). Only meaningful when
|
|
197
|
-
// cloudAvailable; the /cloud fetches hit /me/cloud/directory*.
|
|
198
|
-
let space = "local";
|
|
199
|
-
function spacePrefix() { return space === "cloud" ? "/cloud" : ""; }
|
|
200
|
-
function normalizeInitial(p, single) {
|
|
201
|
-
const parsed = parseDirectoryPath(p || "");
|
|
202
|
-
if (parsed.root)
|
|
203
|
-
return buildDirectoryPath(parsed.root, parsed.folderPath);
|
|
204
|
-
return single ? "/" + single : "/";
|
|
205
|
-
}
|
|
206
|
-
// ── scaffold ──
|
|
207
|
-
mount.classList.add("rk-dir");
|
|
208
|
-
mount.innerHTML = "";
|
|
209
|
-
const searchWrap = el("div", "rk-dir-search");
|
|
210
|
-
const searchInput = el("input", "rk-dir-search-input");
|
|
211
|
-
searchInput.type = "search";
|
|
212
|
-
searchInput.placeholder = "Search files — meaning, name, or path…";
|
|
213
|
-
searchWrap.appendChild(searchInput);
|
|
214
|
-
const crumbsEl = el("div", "rk-aichat-crumbs rk-dir-crumbs");
|
|
215
|
-
const selbar = el("div", "rk-dir-selbar");
|
|
216
|
-
selbar.hidden = true;
|
|
217
|
-
const bodyEl = el("div", "rk-aichat-files-body rk-dir-body");
|
|
218
|
-
const dropEl = el("label", "rk-aichat-drop rk-dir-drop");
|
|
219
|
-
dropEl.hidden = true;
|
|
220
|
-
const dropText = el("span", "rk-aichat-drop-text", "Drop files here");
|
|
221
|
-
const dropInput = el("input");
|
|
222
|
-
dropInput.type = "file";
|
|
223
|
-
dropInput.multiple = true;
|
|
224
|
-
dropInput.hidden = true;
|
|
225
|
-
dropEl.appendChild(dropText);
|
|
226
|
-
dropEl.appendChild(dropInput);
|
|
227
|
-
// Local ⇄ Cloud space switch (serve boxes with an upstream only).
|
|
228
|
-
const spaceBar = el("div", "rk-dir-space");
|
|
229
|
-
const localBtn = el("button", "rk-dir-space-btn is-on", "Local");
|
|
230
|
-
const cloudBtn = el("button", "rk-dir-space-btn", "Cloud");
|
|
231
|
-
localBtn.type = "button";
|
|
232
|
-
cloudBtn.type = "button";
|
|
233
|
-
spaceBar.appendChild(localBtn);
|
|
234
|
-
spaceBar.appendChild(cloudBtn);
|
|
235
|
-
function setSpace(next) {
|
|
236
|
-
if (space === next)
|
|
237
|
-
return;
|
|
238
|
-
space = next;
|
|
239
|
-
localBtn.classList.toggle("is-on", next === "local");
|
|
240
|
-
cloudBtn.classList.toggle("is-on", next === "cloud");
|
|
241
|
-
// Reset to the space's root view and clear any active search.
|
|
242
|
-
searchMode = false;
|
|
243
|
-
searchResults = [];
|
|
244
|
-
searchInput.value = "";
|
|
245
|
-
selection.clear();
|
|
246
|
-
currentPath = singleRoot ? "/" + singleRoot : "/";
|
|
247
|
-
status = "idle";
|
|
248
|
-
load(true);
|
|
249
|
-
}
|
|
250
|
-
localBtn.addEventListener("click", () => setSpace("local"));
|
|
251
|
-
cloudBtn.addEventListener("click", () => setSpace("cloud"));
|
|
252
|
-
if (cloudAvailable)
|
|
253
|
-
mount.appendChild(spaceBar);
|
|
254
|
-
if (showSearch)
|
|
255
|
-
mount.appendChild(searchWrap);
|
|
256
|
-
mount.appendChild(crumbsEl);
|
|
257
|
-
if (showMultiSelect)
|
|
258
|
-
mount.appendChild(selbar);
|
|
259
|
-
mount.appendChild(bodyEl);
|
|
260
|
-
if (showUpload)
|
|
261
|
-
mount.appendChild(dropEl);
|
|
262
|
-
// ── fetch ──
|
|
263
|
-
function dirUrl(path, offset) {
|
|
264
|
-
const u = new URL(apiBase + spacePrefix() + "/directory", window.location.href);
|
|
265
|
-
u.searchParams.set("path", path);
|
|
266
|
-
if (offset)
|
|
267
|
-
u.searchParams.set("offset", String(offset));
|
|
268
|
-
return u.toString();
|
|
269
|
-
}
|
|
270
|
-
function load(force) {
|
|
271
|
-
if (destroyed)
|
|
272
|
-
return;
|
|
273
|
-
if (!force && status === "ready") {
|
|
274
|
-
render();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
status = "loading";
|
|
278
|
-
render();
|
|
279
|
-
fetch(dirUrl(currentPath), { credentials: "same-origin", headers: { accept: "application/json" } })
|
|
280
|
-
.then((r) => {
|
|
281
|
-
if (r.status === 401 || r.status === 403) {
|
|
282
|
-
status = "auth";
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
if (!r.ok)
|
|
286
|
-
throw new Error("http " + r.status);
|
|
287
|
-
return r.json();
|
|
288
|
-
})
|
|
289
|
-
.then((j) => {
|
|
290
|
-
if (destroyed)
|
|
291
|
-
return;
|
|
292
|
-
if (!j) {
|
|
293
|
-
render();
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
folders = filterRoots(j.folders || []);
|
|
297
|
-
files = j.files || [];
|
|
298
|
-
nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
|
|
299
|
-
status = "ready";
|
|
300
|
-
render();
|
|
301
|
-
})
|
|
302
|
-
.catch(() => { if (!destroyed) {
|
|
303
|
-
status = "error";
|
|
304
|
-
render();
|
|
305
|
-
} });
|
|
306
|
-
}
|
|
307
|
-
// At the virtual root with a roots subset, hide roots we weren't asked to show.
|
|
308
|
-
function filterRoots(list) {
|
|
309
|
-
if (currentPath !== "/")
|
|
310
|
-
return list;
|
|
311
|
-
return list.filter((f) => roots.indexOf(f.root) !== -1);
|
|
312
|
-
}
|
|
313
|
-
function loadMore() {
|
|
314
|
-
if (nextOffset == null || loadingMore)
|
|
315
|
-
return;
|
|
316
|
-
loadingMore = true;
|
|
317
|
-
render();
|
|
318
|
-
fetch(dirUrl(currentPath, nextOffset), { credentials: "same-origin", headers: { accept: "application/json" } })
|
|
319
|
-
.then((r) => (r.ok ? r.json() : null))
|
|
320
|
-
.then((j) => {
|
|
321
|
-
loadingMore = false;
|
|
322
|
-
if (destroyed || !j) {
|
|
323
|
-
render();
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
files = files.concat(j.files || []);
|
|
327
|
-
nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
|
|
328
|
-
render();
|
|
329
|
-
})
|
|
330
|
-
.catch(() => { loadingMore = false; render(); });
|
|
331
|
-
}
|
|
332
|
-
// ── search ──
|
|
333
|
-
let searchDebounce;
|
|
334
|
-
function onSearchInput() {
|
|
335
|
-
const q = searchInput.value.trim();
|
|
336
|
-
if (searchDebounce)
|
|
337
|
-
window.clearTimeout(searchDebounce);
|
|
338
|
-
if (!q) {
|
|
339
|
-
searchMode = false;
|
|
340
|
-
searchResults = [];
|
|
341
|
-
render();
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
searchDebounce = window.setTimeout(() => runSearch(q), 260);
|
|
345
|
-
}
|
|
346
|
-
function runSearch(q) {
|
|
347
|
-
searchMode = true;
|
|
348
|
-
searchStatus = "loading";
|
|
349
|
-
render();
|
|
350
|
-
const scope = singleRoot ? "/" + singleRoot : (currentPath !== "/" ? currentPath : "");
|
|
351
|
-
fetch(apiBase + spacePrefix() + "/directory/search", {
|
|
352
|
-
method: "POST",
|
|
353
|
-
credentials: "same-origin",
|
|
354
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
355
|
-
body: JSON.stringify({ query: q, path: scope, mode: "auto", limit: 40 })
|
|
356
|
-
})
|
|
357
|
-
.then((r) => (r.ok ? r.json() : null))
|
|
358
|
-
.then((j) => {
|
|
359
|
-
if (destroyed)
|
|
360
|
-
return;
|
|
361
|
-
searchResults = (j && j.results) || [];
|
|
362
|
-
searchStatus = "ready";
|
|
363
|
-
render();
|
|
364
|
-
})
|
|
365
|
-
.catch(() => { if (!destroyed) {
|
|
366
|
-
searchStatus = "error";
|
|
367
|
-
render();
|
|
368
|
-
} });
|
|
369
|
-
}
|
|
370
|
-
// ── navigation ──
|
|
371
|
-
function navigate(path) {
|
|
372
|
-
const parsed = parseDirectoryPath(path);
|
|
373
|
-
currentPath = parsed.root ? buildDirectoryPath(parsed.root, parsed.folderPath) : "/";
|
|
374
|
-
status = "idle";
|
|
375
|
-
searchMode = false;
|
|
376
|
-
searchInput.value = "";
|
|
377
|
-
(opts.onNavigate || (() => { }))(currentPath);
|
|
378
|
-
load(true);
|
|
379
|
-
}
|
|
380
|
-
function currentRoot() { return parseDirectoryPath(currentPath).root; }
|
|
381
|
-
function currentFolder() { return parseDirectoryPath(currentPath).folderPath; }
|
|
382
|
-
// /projects mirrors the user's composition forks — its folders and files are
|
|
383
|
-
// backed by fork storage, not user-managed, so no create/rename/upload there.
|
|
384
|
-
function canWriteRoot(r) { return !!r && r !== "projects"; }
|
|
385
|
-
// Create an empty folder under `parentFolder` (defaults to the current folder).
|
|
386
|
-
// Only valid inside a root — at the virtual root there's nowhere to put it.
|
|
387
|
-
function createFolder(parentFolder) {
|
|
388
|
-
const root = currentRoot();
|
|
389
|
-
if (!root) {
|
|
390
|
-
toast("Open a root (/files, /temp, /raws) first");
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (!canWriteRoot(root)) {
|
|
394
|
-
toast("/projects is read-only");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const name = (window.prompt("New folder name") || "").trim();
|
|
398
|
-
if (!name)
|
|
399
|
-
return;
|
|
400
|
-
const parent = normalizeFolderPath(parentFolder != null ? parentFolder : currentFolder());
|
|
401
|
-
const target = buildDirectoryPath(root, parent ? parent + "/" + name : name);
|
|
402
|
-
fetch(apiBase + "/directory/folders", {
|
|
403
|
-
method: "POST",
|
|
404
|
-
credentials: "same-origin",
|
|
405
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
406
|
-
body: JSON.stringify({ path: target })
|
|
407
|
-
})
|
|
408
|
-
.then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
|
|
409
|
-
.then((res) => { navigate(res.path || (root === currentRoot() ? currentPath : "/" + root)); toast("Created " + (res.path || name)); })
|
|
410
|
-
.catch((e) => toast(e.message || "Couldn't create folder"));
|
|
411
|
-
}
|
|
412
|
-
// Rename a file or folder in place via POST /me/directory/rename. Files carry
|
|
413
|
-
// their backend id (display-name-only rename); folders rename their trailing
|
|
414
|
-
// path segment (files + nested subfolders move with them server-side).
|
|
415
|
-
function renameItem(it) {
|
|
416
|
-
const isFolder = it.kind === "folder";
|
|
417
|
-
const next = (window.prompt(isFolder ? "Rename folder" : "Rename file", it.name) || "").trim();
|
|
418
|
-
if (!next || next === it.name)
|
|
419
|
-
return;
|
|
420
|
-
fetch(apiBase + "/directory/rename", {
|
|
421
|
-
method: "POST",
|
|
422
|
-
credentials: "same-origin",
|
|
423
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
424
|
-
body: JSON.stringify({ path: it.path, new_name: next, ...(isFolder ? {} : { file_id: it.id }) })
|
|
425
|
-
})
|
|
426
|
-
.then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
|
|
427
|
-
.then(() => {
|
|
428
|
-
// Renaming shifts the item's path, so drop any stale selection and
|
|
429
|
-
// reload the current folder (the renamed child re-renders in place).
|
|
430
|
-
if (selection.has(it.path)) {
|
|
431
|
-
selection.delete(it.path);
|
|
432
|
-
(opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
|
|
433
|
-
}
|
|
434
|
-
load(true);
|
|
435
|
-
toast("Renamed to " + next);
|
|
436
|
-
})
|
|
437
|
-
.catch((e) => toast(e.message || "Couldn't rename"));
|
|
438
|
-
}
|
|
439
|
-
// ── selection ──
|
|
440
|
-
function toggleSelect(it, on) {
|
|
441
|
-
if (on)
|
|
442
|
-
selection.set(it.path, it);
|
|
443
|
-
else
|
|
444
|
-
selection.delete(it.path);
|
|
445
|
-
(opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
|
|
446
|
-
renderSelbar();
|
|
447
|
-
// reflect checkbox state without full re-render churn
|
|
448
|
-
const row = bodyEl.querySelector('[data-path="' + cssEscape(it.path) + '"]');
|
|
449
|
-
if (row)
|
|
450
|
-
row.classList.toggle("is-selected", on);
|
|
451
|
-
}
|
|
452
|
-
function clearSelection() {
|
|
453
|
-
selection.clear();
|
|
454
|
-
(opts.onSelectionChange || (() => { }))([]);
|
|
455
|
-
render();
|
|
456
|
-
}
|
|
457
|
-
// ── rendering ──
|
|
458
|
-
function crumb(label, current, onClick) {
|
|
459
|
-
const b = el("button", "rk-aichat-crumb" + (current ? " is-current" : ""), label);
|
|
460
|
-
b.type = "button";
|
|
461
|
-
b.addEventListener("click", onClick);
|
|
462
|
-
return b;
|
|
463
|
-
}
|
|
464
|
-
function renderCrumbs() {
|
|
465
|
-
crumbsEl.innerHTML = "";
|
|
466
|
-
const root = currentRoot();
|
|
467
|
-
const folder = currentFolder();
|
|
468
|
-
if (!singleRoot) {
|
|
469
|
-
crumbsEl.appendChild(crumb("Files", currentPath === "/", () => navigate("/")));
|
|
470
|
-
}
|
|
471
|
-
if (!root)
|
|
472
|
-
return;
|
|
473
|
-
const rootLabel = (DIRECTORY_ROOTS.find((r) => r.key === root) || { label: root }).label;
|
|
474
|
-
if (!singleRoot)
|
|
475
|
-
crumbsEl.appendChild(sep());
|
|
476
|
-
crumbsEl.appendChild(crumb(rootLabel, !folder, () => navigate("/" + root)));
|
|
477
|
-
let acc = "";
|
|
478
|
-
folder.split("/").filter(Boolean).forEach((part, idx, arr) => {
|
|
479
|
-
crumbsEl.appendChild(sep());
|
|
480
|
-
acc = acc ? acc + "/" + part : part;
|
|
481
|
-
const target = "/" + root + "/" + acc;
|
|
482
|
-
crumbsEl.appendChild(crumb(part, idx === arr.length - 1, () => navigate(target)));
|
|
483
|
-
});
|
|
484
|
-
// Discoverable "New folder" affordance (right-click also works anywhere).
|
|
485
|
-
// Read-only roots (/projects) don't get it.
|
|
486
|
-
if (canWriteRoot(root)) {
|
|
487
|
-
const nf = el("button", "rk-dir-newfolder", "+ Folder");
|
|
488
|
-
nf.type = "button";
|
|
489
|
-
nf.title = "New folder here (or right-click)";
|
|
490
|
-
nf.addEventListener("click", () => createFolder());
|
|
491
|
-
crumbsEl.appendChild(nf);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
function sep() { return el("span", "rk-aichat-crumb-sep", "/"); }
|
|
495
|
-
function renderSelbar() {
|
|
496
|
-
if (!showMultiSelect)
|
|
497
|
-
return;
|
|
498
|
-
const n = selection.size;
|
|
499
|
-
selbar.hidden = n === 0;
|
|
500
|
-
if (n === 0)
|
|
501
|
-
return;
|
|
502
|
-
selbar.innerHTML = "";
|
|
503
|
-
selbar.appendChild(el("span", "rk-dir-selcount", n + " selected"));
|
|
504
|
-
const picked = () => Array.from(selection.values());
|
|
505
|
-
// Primary action stays on the bar; everything else tucks into a "⋯" menu so
|
|
506
|
-
// the toolbar never overflows in the narrow editor dock.
|
|
507
|
-
const cp = el("button", "rk-dir-selbtn is-primary", "Copy paths");
|
|
508
|
-
cp.type = "button";
|
|
509
|
-
cp.addEventListener("click", () => copyPath(picked().map((s) => s.path).join("\n")));
|
|
510
|
-
selbar.appendChild(cp);
|
|
511
|
-
const more = [];
|
|
512
|
-
if (mode === "attach")
|
|
513
|
-
more.push({ label: "Attach", run: () => { emitAttach(picked()); clearSelection(); } });
|
|
514
|
-
more.push({ label: "Copy paths", run: () => copyPath(picked().map((s) => s.path).join("\n")) });
|
|
515
|
-
more.push({ label: "Clear selection", run: clearSelection });
|
|
516
|
-
const menuBtn = el("button", "rk-dir-selbtn is-ghost rk-dir-selmore", "⋯");
|
|
517
|
-
menuBtn.type = "button";
|
|
518
|
-
menuBtn.setAttribute("aria-label", "More actions");
|
|
519
|
-
menuBtn.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(menuBtn, more); });
|
|
520
|
-
selbar.appendChild(menuBtn);
|
|
521
|
-
}
|
|
522
|
-
function checkbox(it) {
|
|
523
|
-
const box = el("input", "rk-dir-check");
|
|
524
|
-
box.type = "checkbox";
|
|
525
|
-
box.checked = selection.has(it.path);
|
|
526
|
-
box.setAttribute("aria-label", "Select " + it.name);
|
|
527
|
-
box.addEventListener("click", (e) => e.stopPropagation());
|
|
528
|
-
box.addEventListener("change", () => toggleSelect(it, box.checked));
|
|
529
|
-
return box;
|
|
530
|
-
}
|
|
531
|
-
function folderRowEl(it) {
|
|
532
|
-
const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
|
|
533
|
-
row.setAttribute("data-path", it.path);
|
|
534
|
-
if (showMultiSelect)
|
|
535
|
-
row.appendChild(checkbox(it));
|
|
536
|
-
// Name button: default click COPIES the (relative) folder path.
|
|
537
|
-
const b = el("button", "rk-aichat-fmainbtn");
|
|
538
|
-
b.type = "button";
|
|
539
|
-
b.title = "Copy path " + it.path;
|
|
540
|
-
b.appendChild(iconFor(it));
|
|
541
|
-
const main = el("span", "rk-aichat-fmain");
|
|
542
|
-
main.appendChild(el("span", "rk-aichat-fname", it.name));
|
|
543
|
-
const desc = it.meta && typeof it.meta.description === "string" ? it.meta.description : "";
|
|
544
|
-
main.appendChild(el("span", "rk-aichat-fmeta", desc || "folder"));
|
|
545
|
-
b.appendChild(main);
|
|
546
|
-
b.addEventListener("click", () => copyPath(it.path));
|
|
547
|
-
row.appendChild(b);
|
|
548
|
-
// Open button (the primary navigation affordance).
|
|
549
|
-
const open = el("button", "rk-aichat-fadd rk-dir-open", "Open ›");
|
|
550
|
-
open.type = "button";
|
|
551
|
-
open.title = "Open folder";
|
|
552
|
-
open.addEventListener("click", (e) => { e.stopPropagation(); navigate(it.path); });
|
|
553
|
-
row.appendChild(open);
|
|
554
|
-
// Kebab. Create/rename only for writable roots (not /projects forks).
|
|
555
|
-
const folderActions = [{ label: "Open folder", run: () => navigate(it.path) }];
|
|
556
|
-
if (canWriteRoot(it.root)) {
|
|
557
|
-
folderActions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
|
|
558
|
-
folderActions.push({ label: "Rename", run: () => renameItem(it) });
|
|
559
|
-
}
|
|
560
|
-
folderActions.push({ label: "Copy path", run: () => copyPath(it.path) });
|
|
561
|
-
row.appendChild(kebab(it, folderActions));
|
|
562
|
-
return row;
|
|
563
|
-
}
|
|
564
|
-
function fileRowEl(it, siblings, index, showPath) {
|
|
565
|
-
const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
|
|
566
|
-
row.setAttribute("data-path", it.path);
|
|
567
|
-
if (showMultiSelect)
|
|
568
|
-
row.appendChild(checkbox(it));
|
|
569
|
-
const b = el("button", "rk-aichat-fmainbtn");
|
|
570
|
-
b.type = "button";
|
|
571
|
-
const primaryLabel = primaryClick === "copyPath" ? "Copy path " : primaryClick === "attach" ? "Attach " : "Preview ";
|
|
572
|
-
b.title = primaryLabel + it.name;
|
|
573
|
-
b.appendChild(iconFor(it));
|
|
574
|
-
const main = el("span", "rk-aichat-fmain");
|
|
575
|
-
main.appendChild(el("span", "rk-aichat-fname", it.name));
|
|
576
|
-
const metaText = showPath ? it.path : (fmtDur(it.durationSec) || fmtBytes(it.sizeBytes) || it.contentType || "file");
|
|
577
|
-
main.appendChild(el("span", "rk-aichat-fmeta", metaText));
|
|
578
|
-
b.appendChild(main);
|
|
579
|
-
b.addEventListener("click", () => {
|
|
580
|
-
if (primaryClick === "attach")
|
|
581
|
-
emitAttach([it]);
|
|
582
|
-
else if (primaryClick === "preview")
|
|
583
|
-
emitPreview(siblings, index);
|
|
584
|
-
else
|
|
585
|
-
copyPath(it.path);
|
|
586
|
-
});
|
|
587
|
-
row.appendChild(b);
|
|
588
|
-
// When the row's primary click previews (chat dock), keep attaching one click
|
|
589
|
-
// away with a dedicated inline "+" button so files stay easy to add to the
|
|
590
|
-
// composer without going through the kebab.
|
|
591
|
-
if (mode === "attach" && primaryClick === "preview") {
|
|
592
|
-
const add = el("button", "rk-aichat-fadd rk-dir-attach", "+");
|
|
593
|
-
add.type = "button";
|
|
594
|
-
add.title = "Attach " + it.name;
|
|
595
|
-
add.setAttribute("aria-label", "Attach " + it.name);
|
|
596
|
-
add.addEventListener("click", (e) => { e.stopPropagation(); emitAttach([it]); });
|
|
597
|
-
row.appendChild(add);
|
|
598
|
-
}
|
|
599
|
-
// Kebab: Attach / View / Preview / Copy path.
|
|
600
|
-
const actions = [];
|
|
601
|
-
if (mode === "attach")
|
|
602
|
-
actions.push({ label: "Attach", run: () => emitAttach([it]) });
|
|
603
|
-
if (it.viewUrl)
|
|
604
|
-
actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
|
|
605
|
-
if (it.viewUrl)
|
|
606
|
-
actions.push({ label: "Preview", run: () => emitPreview(siblings, index) });
|
|
607
|
-
// Raws (source-named) and approved posts (title-named) can't be file-renamed;
|
|
608
|
-
// only /files + /temp files rename in place. Folders rename in every root.
|
|
609
|
-
if ((it.root === "files" || it.root === "temp") && it.id)
|
|
610
|
-
actions.push({ label: "Rename", run: () => renameItem(it) });
|
|
611
|
-
actions.push({ label: "Copy path", run: () => copyPath(it.path) });
|
|
612
|
-
row.appendChild(kebab(it, actions));
|
|
613
|
-
return row;
|
|
614
|
-
}
|
|
615
|
-
function kebab(it, actions) {
|
|
616
|
-
void it;
|
|
617
|
-
const k = el("button", "rk-dir-kebab", "⋯");
|
|
618
|
-
k.type = "button";
|
|
619
|
-
k.setAttribute("aria-label", "More actions");
|
|
620
|
-
k.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(k, actions); });
|
|
621
|
-
return k;
|
|
622
|
-
}
|
|
623
|
-
function note(text, link, retry) {
|
|
624
|
-
const wrap = el("div", "rk-aichat-fstate");
|
|
625
|
-
wrap.appendChild(el("div", undefined, text));
|
|
626
|
-
if (link) {
|
|
627
|
-
const a = el("a");
|
|
628
|
-
a.href = link.href;
|
|
629
|
-
a.textContent = link.label;
|
|
630
|
-
wrap.appendChild(a);
|
|
631
|
-
}
|
|
632
|
-
if (retry) {
|
|
633
|
-
const btn = el("button", undefined, "Try again");
|
|
634
|
-
btn.type = "button";
|
|
635
|
-
btn.addEventListener("click", () => load(true));
|
|
636
|
-
wrap.appendChild(btn);
|
|
637
|
-
}
|
|
638
|
-
return wrap;
|
|
639
|
-
}
|
|
640
|
-
function render() {
|
|
641
|
-
if (destroyed)
|
|
642
|
-
return;
|
|
643
|
-
renderCrumbs();
|
|
644
|
-
renderSelbar();
|
|
645
|
-
updateDrop();
|
|
646
|
-
bodyEl.innerHTML = "";
|
|
647
|
-
if (searchMode) {
|
|
648
|
-
renderSearch();
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
if (status === "loading" && !files.length && !folders.length) {
|
|
652
|
-
bodyEl.appendChild(note("Loading your files…"));
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (status === "auth") {
|
|
656
|
-
bodyEl.appendChild(note("Sign in to browse your files.", { href: "/login", label: "Sign in" }));
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
if (status === "error") {
|
|
660
|
-
bodyEl.appendChild(note("Couldn't reach your files.", undefined, true));
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (!folders.length && !files.length) {
|
|
664
|
-
bodyEl.appendChild(note(currentPath === "/" ? "No files yet." : "This folder is empty."));
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
folders.forEach((f) => bodyEl.appendChild(folderRowEl(f)));
|
|
668
|
-
files.forEach((f, i) => bodyEl.appendChild(fileRowEl(f, files, i)));
|
|
669
|
-
if (nextOffset != null) {
|
|
670
|
-
const more = el("button", "rk-aichat-loadmore", loadingMore ? "Loading…" : "Load more");
|
|
671
|
-
more.type = "button";
|
|
672
|
-
more.disabled = loadingMore;
|
|
673
|
-
more.addEventListener("click", loadMore);
|
|
674
|
-
bodyEl.appendChild(more);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
function renderSearch() {
|
|
678
|
-
if (searchStatus === "loading" && !searchResults.length) {
|
|
679
|
-
bodyEl.appendChild(note("Searching…"));
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
if (searchStatus === "error") {
|
|
683
|
-
bodyEl.appendChild(note("Search failed."));
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
if (!searchResults.length) {
|
|
687
|
-
bodyEl.appendChild(note("No matches."));
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
const fileHits = searchResults.filter((r) => r.kind !== "folder");
|
|
691
|
-
searchResults.forEach((r, i) => {
|
|
692
|
-
if (r.kind === "folder")
|
|
693
|
-
bodyEl.appendChild(folderRowEl(r));
|
|
694
|
-
else
|
|
695
|
-
bodyEl.appendChild(fileRowEl(r, fileHits, fileHits.indexOf(r), true));
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
// ── upload (attach hosts only; not for raws) ──
|
|
699
|
-
let uploading = false;
|
|
700
|
-
function uploadEndpoint() {
|
|
701
|
-
const root = currentRoot();
|
|
702
|
-
if (root === "files")
|
|
703
|
-
return apiBase + "/attachments/upload";
|
|
704
|
-
if (root === "temp")
|
|
705
|
-
return apiBase + "/temporary-files/upload";
|
|
706
|
-
return null; // raws are scanned/imported, not plain-uploaded
|
|
707
|
-
}
|
|
708
|
-
function updateDrop() {
|
|
709
|
-
if (!showUpload)
|
|
710
|
-
return;
|
|
711
|
-
const endpoint = uploadEndpoint();
|
|
712
|
-
dropEl.hidden = !endpoint || status === "auth";
|
|
713
|
-
if (!dropEl.hidden && !uploading) {
|
|
714
|
-
const folder = currentFolder();
|
|
715
|
-
const rootLabel = currentRoot() ? (DIRECTORY_ROOTS.find((r) => r.key === currentRoot()) || { label: "" }).label : "";
|
|
716
|
-
dropText.textContent = "Drop files into " + rootLabel + (folder ? " / " + folder : "");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
function uploadFiles(list) {
|
|
720
|
-
const endpoint = uploadEndpoint();
|
|
721
|
-
if (!endpoint || uploading || !list || !list.length)
|
|
722
|
-
return;
|
|
723
|
-
uploading = true;
|
|
724
|
-
const folder = currentFolder();
|
|
725
|
-
const root = currentRoot();
|
|
726
|
-
const arr = Array.from(list);
|
|
727
|
-
let done = 0, failed = 0;
|
|
728
|
-
// The server may re-home an upload (e.g. /temp files land in a dated
|
|
729
|
-
// subfolder). Track where the bytes actually landed so we can reveal them —
|
|
730
|
-
// otherwise the refresh shows the folder we uploaded FROM, which looks empty.
|
|
731
|
-
let landedFolder = null;
|
|
732
|
-
dropText.textContent = "Uploading " + arr.length + " file(s)…";
|
|
733
|
-
const finish = () => {
|
|
734
|
-
uploading = false;
|
|
735
|
-
if (failed)
|
|
736
|
-
toast(failed + " of " + arr.length + " failed");
|
|
737
|
-
const target = root && landedFolder != null ? buildDirectoryPath(root, landedFolder) : null;
|
|
738
|
-
if (target && target !== currentPath)
|
|
739
|
-
navigate(target); // reveal the uploads
|
|
740
|
-
else
|
|
741
|
-
load(true);
|
|
742
|
-
};
|
|
743
|
-
const next = (i) => {
|
|
744
|
-
if (i >= arr.length) {
|
|
745
|
-
finish();
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const fd = new FormData();
|
|
749
|
-
fd.append("file", arr[i]);
|
|
750
|
-
fd.append("folder_path", folder);
|
|
751
|
-
fetch(endpoint, { method: "POST", credentials: "same-origin", body: fd })
|
|
752
|
-
.then((r) => { if (!r.ok)
|
|
753
|
-
throw new Error("http " + r.status); return r.json().catch(() => null); })
|
|
754
|
-
.then((j) => {
|
|
755
|
-
done++;
|
|
756
|
-
const fp = j && ((j.file && j.file.folderPath) ?? (j.attachment && j.attachment.folderPath));
|
|
757
|
-
if (typeof fp === "string" && landedFolder == null)
|
|
758
|
-
landedFolder = fp;
|
|
759
|
-
})
|
|
760
|
-
.catch(() => { failed++; })
|
|
761
|
-
.then(() => next(i + 1));
|
|
762
|
-
};
|
|
763
|
-
next(0);
|
|
764
|
-
}
|
|
765
|
-
// ── right-click context menu: New folder anywhere, plus row actions ──
|
|
766
|
-
function findItem(path) {
|
|
767
|
-
return folders.concat(files, searchResults).find((it) => it.path === path);
|
|
768
|
-
}
|
|
769
|
-
bodyEl.addEventListener("contextmenu", (e) => {
|
|
770
|
-
const rootSet = !!currentRoot();
|
|
771
|
-
const rowEl = e.target.closest(".rk-dir-row");
|
|
772
|
-
const at = { top: e.clientY, bottom: e.clientY, left: e.clientX, right: e.clientX };
|
|
773
|
-
const actions = [];
|
|
774
|
-
const it = rowEl ? findItem(rowEl.getAttribute("data-path") || "") : undefined;
|
|
775
|
-
if (it && it.kind === "folder") {
|
|
776
|
-
actions.push({ label: "Open folder", run: () => navigate(it.path) });
|
|
777
|
-
if (canWriteRoot(it.root))
|
|
778
|
-
actions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
|
|
779
|
-
if (canWriteRoot(it.root))
|
|
780
|
-
actions.push({ label: "Rename", run: () => renameItem(it) });
|
|
781
|
-
actions.push({ label: "Copy path", run: () => copyPath(it.path) });
|
|
782
|
-
}
|
|
783
|
-
else if (it) {
|
|
784
|
-
if (mode === "attach")
|
|
785
|
-
actions.push({ label: "Attach", run: () => emitAttach([it]) });
|
|
786
|
-
if (it.viewUrl)
|
|
787
|
-
actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
|
|
788
|
-
if (it.viewUrl)
|
|
789
|
-
actions.push({ label: "Preview", run: () => emitPreview(files, files.indexOf(it)) });
|
|
790
|
-
if (it.root !== "raws" && canWriteRoot(it.root) && it.id)
|
|
791
|
-
actions.push({ label: "Rename", run: () => renameItem(it) });
|
|
792
|
-
actions.push({ label: "Copy path", run: () => copyPath(it.path) });
|
|
793
|
-
}
|
|
794
|
-
// Always offer "New folder" in the current folder when inside a writable root.
|
|
795
|
-
if (rootSet && canWriteRoot(currentRoot()))
|
|
796
|
-
actions.push({ label: "New folder here", run: () => createFolder() });
|
|
797
|
-
if (!actions.length)
|
|
798
|
-
return; // virtual root, empty space → let the browser menu show
|
|
799
|
-
e.preventDefault();
|
|
800
|
-
openMenuAt(at, actions);
|
|
801
|
-
});
|
|
802
|
-
// ── wire events ──
|
|
803
|
-
searchInput.addEventListener("input", onSearchInput);
|
|
804
|
-
dropEl.addEventListener("click", (e) => { if (e.target === dropEl || e.target === dropText)
|
|
805
|
-
dropInput.click(); });
|
|
806
|
-
dropInput.addEventListener("change", () => { uploadFiles(dropInput.files); dropInput.value = ""; });
|
|
807
|
-
dropEl.addEventListener("dragover", (e) => { e.preventDefault(); dropEl.classList.add("is-drag"); });
|
|
808
|
-
dropEl.addEventListener("dragleave", (e) => { if (e.target === dropEl)
|
|
809
|
-
dropEl.classList.remove("is-drag"); });
|
|
810
|
-
dropEl.addEventListener("drop", (e) => {
|
|
811
|
-
e.preventDefault();
|
|
812
|
-
dropEl.classList.remove("is-drag");
|
|
813
|
-
if (e.dataTransfer && e.dataTransfer.files)
|
|
814
|
-
uploadFiles(e.dataTransfer.files);
|
|
815
|
-
});
|
|
816
|
-
(opts.onNavigate || (() => { }))(currentPath);
|
|
817
|
-
load(true);
|
|
818
|
-
return {
|
|
819
|
-
refresh: () => load(true),
|
|
820
|
-
navigate,
|
|
821
|
-
getSelection: () => Array.from(selection.values()),
|
|
822
|
-
getCurrentPath: () => currentPath,
|
|
823
|
-
destroy: () => { destroyed = true; if (closeOpenMenu)
|
|
824
|
-
closeOpenMenu(); mount.innerHTML = ""; }
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
function cssEscape(s) {
|
|
828
|
-
if (window.CSS && window.CSS.escape)
|
|
829
|
-
return window.CSS.escape(s);
|
|
830
|
-
return s.replace(/["\\]/g, "\\$&");
|
|
831
|
-
}
|
|
832
|
-
// ── pop-panel picker (modal) ──────────────────────────────────────────────────
|
|
833
|
-
// A self-contained modal wrapper around createDirectoryExplorer so any surface
|
|
834
|
-
// (editor "Replace media", inpaint "Save to Files", …) can pick a file or a
|
|
835
|
-
// destination folder without reimplementing the overlay + CSS each time.
|
|
836
|
-
const PICKER_STYLE_ID = "rk-dirpick-style";
|
|
837
|
-
function ensurePickerStyle() {
|
|
838
|
-
if (document.getElementById(PICKER_STYLE_ID))
|
|
839
|
-
return;
|
|
840
|
-
const s = document.createElement("style");
|
|
841
|
-
s.id = PICKER_STYLE_ID;
|
|
842
|
-
// Uses reskin --rk-* tokens where present, with plain fallbacks so the modal
|
|
843
|
-
// is legible even on a page that hasn't loaded the design-system variables.
|
|
844
|
-
s.textContent = [
|
|
845
|
-
".rk-dirpick{position:fixed;inset:0;z-index:1200;display:grid;place-items:center;padding:20px;",
|
|
846
|
-
"background:rgba(28,25,23,.44);backdrop-filter:blur(2px);animation:rk-dirpick-fade .16s ease}",
|
|
847
|
-
"@keyframes rk-dirpick-fade{from{opacity:0}to{opacity:1}}",
|
|
848
|
-
".rk-dirpick-panel{width:min(560px,100%);max-height:min(82vh,700px);display:flex;flex-direction:column;",
|
|
849
|
-
"background:var(--rk-surface,#fff);border:1px solid var(--rk-border,#e6e0d6);border-radius:var(--rk-r-3xl,20px);",
|
|
850
|
-
"box-shadow:var(--rk-shadow-lg,0 24px 60px rgba(28,25,23,.28));overflow:hidden}",
|
|
851
|
-
".rk-dirpick-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;",
|
|
852
|
-
"border-bottom:1px solid var(--rk-border,#e6e0d6)}",
|
|
853
|
-
".rk-dirpick-title{font-family:var(--rk-font-display,inherit);font-weight:800;font-size:15px;color:var(--rk-ink,#1c1917)}",
|
|
854
|
-
".rk-dirpick-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted,#78716c);cursor:pointer;padding:0 4px}",
|
|
855
|
-
".rk-dirpick-close:hover{color:var(--rk-ink,#1c1917)}",
|
|
856
|
-
".rk-dirpick-hint{padding:10px 18px 0;font-size:12.5px;line-height:1.4;color:var(--rk-text-muted,#78716c)}",
|
|
857
|
-
".rk-dirpick-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}",
|
|
858
|
-
".rk-dirpick-foot{display:flex;align-items:center;gap:10px;padding:12px 18px;border-top:1px solid var(--rk-border,#e6e0d6)}",
|
|
859
|
-
".rk-dirpick-dest{flex:1;min-width:0;font-size:12.5px;color:var(--rk-text-muted,#78716c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
860
|
-
".rk-dirpick-dest b{color:var(--rk-ink,#1c1917);font-weight:700}",
|
|
861
|
-
".rk-dirpick-btn{flex:none;min-height:36px;padding:0 16px;border-radius:999px;border:0;cursor:pointer;",
|
|
862
|
-
"font:inherit;font-size:13px;font-weight:800;background:var(--rk-gold-500,var(--rk-gold-600,#fcb900));color:var(--rk-ink,#1c1917)}",
|
|
863
|
-
".rk-dirpick-btn:hover:not(:disabled){background:var(--rk-gold-600,#e0a500)}",
|
|
864
|
-
".rk-dirpick-btn:disabled{opacity:.5;cursor:default}",
|
|
865
|
-
".rk-dirpick-btn.is-ghost{background:transparent;border:1px solid var(--rk-border,#e6e0d6);color:var(--rk-ink,#1c1917)}"
|
|
866
|
-
].join("");
|
|
867
|
-
document.head.appendChild(s);
|
|
868
|
-
}
|
|
869
|
-
export function openDirectoryPicker(opts = {}) {
|
|
870
|
-
ensurePickerStyle();
|
|
871
|
-
const select = opts.select === "folder" ? "folder" : "file";
|
|
872
|
-
let done = false;
|
|
873
|
-
let explorer = null;
|
|
874
|
-
const overlay = el("div", "rk-dirpick");
|
|
875
|
-
const panel = el("div", "rk-dirpick-panel");
|
|
876
|
-
const head = el("div", "rk-dirpick-head");
|
|
877
|
-
head.appendChild(el("span", "rk-dirpick-title", opts.title || (select === "folder" ? "Choose a folder" : "Choose a file")));
|
|
878
|
-
const closeBtn = el("button", "rk-dirpick-close");
|
|
879
|
-
closeBtn.type = "button";
|
|
880
|
-
closeBtn.setAttribute("aria-label", "Close");
|
|
881
|
-
closeBtn.innerHTML = "×";
|
|
882
|
-
head.appendChild(closeBtn);
|
|
883
|
-
panel.appendChild(head);
|
|
884
|
-
if (opts.hint)
|
|
885
|
-
panel.appendChild(el("div", "rk-dirpick-hint", opts.hint));
|
|
886
|
-
const mountEl = el("div", "rk-dirpick-mount");
|
|
887
|
-
panel.appendChild(mountEl);
|
|
888
|
-
// Folder mode gets a footer that commits the currently-open folder.
|
|
889
|
-
let destEl = null;
|
|
890
|
-
let confirmBtn = null;
|
|
891
|
-
function finish(result) {
|
|
892
|
-
if (done)
|
|
893
|
-
return;
|
|
894
|
-
done = true;
|
|
895
|
-
close();
|
|
896
|
-
if (result)
|
|
897
|
-
(opts.onPick || (() => { }))(result);
|
|
898
|
-
else
|
|
899
|
-
(opts.onCancel || (() => { }))();
|
|
900
|
-
}
|
|
901
|
-
function folderItemFor(path) {
|
|
902
|
-
const parsed = parseDirectoryPath(path);
|
|
903
|
-
if (!parsed.root)
|
|
904
|
-
return null; // the virtual root "/" isn't a real folder
|
|
905
|
-
const folder = parsed.folderPath || "";
|
|
906
|
-
const name = folder ? folder.split("/").filter(Boolean).pop() || folder : parsed.root;
|
|
907
|
-
return { path: buildDirectoryPath(parsed.root, folder), root: parsed.root, folderPath: folder, name, kind: "folder" };
|
|
908
|
-
}
|
|
909
|
-
function syncFooter(path) {
|
|
910
|
-
if (!destEl || !confirmBtn)
|
|
911
|
-
return;
|
|
912
|
-
const item = folderItemFor(path);
|
|
913
|
-
if (item) {
|
|
914
|
-
destEl.innerHTML = "";
|
|
915
|
-
destEl.appendChild(document.createTextNode("Save to "));
|
|
916
|
-
const b = el("b", undefined, item.path);
|
|
917
|
-
destEl.appendChild(b);
|
|
918
|
-
confirmBtn.disabled = false;
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
destEl.textContent = "Open a folder (Files, Temp, …) to choose a destination.";
|
|
922
|
-
confirmBtn.disabled = true;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
if (select === "folder") {
|
|
926
|
-
const foot = el("div", "rk-dirpick-foot");
|
|
927
|
-
destEl = el("div", "rk-dirpick-dest");
|
|
928
|
-
const cancel = el("button", "rk-dirpick-btn is-ghost", "Cancel");
|
|
929
|
-
cancel.type = "button";
|
|
930
|
-
cancel.addEventListener("click", () => finish(null));
|
|
931
|
-
confirmBtn = el("button", "rk-dirpick-btn", opts.confirmLabel || "Use this folder");
|
|
932
|
-
confirmBtn.type = "button";
|
|
933
|
-
confirmBtn.addEventListener("click", () => {
|
|
934
|
-
const item = explorer ? folderItemFor(explorer.getCurrentPath()) : null;
|
|
935
|
-
if (item)
|
|
936
|
-
finish(item);
|
|
937
|
-
});
|
|
938
|
-
foot.appendChild(destEl);
|
|
939
|
-
foot.appendChild(cancel);
|
|
940
|
-
foot.appendChild(confirmBtn);
|
|
941
|
-
panel.appendChild(foot);
|
|
942
|
-
}
|
|
943
|
-
overlay.appendChild(panel);
|
|
944
|
-
document.body.appendChild(overlay);
|
|
945
|
-
function onKey(e) { if (e.key === "Escape")
|
|
946
|
-
finish(null); }
|
|
947
|
-
function close() {
|
|
948
|
-
document.removeEventListener("keydown", onKey, true);
|
|
949
|
-
try {
|
|
950
|
-
explorer && explorer.destroy();
|
|
951
|
-
}
|
|
952
|
-
catch { /* noop */ }
|
|
953
|
-
overlay.remove();
|
|
954
|
-
}
|
|
955
|
-
closeBtn.addEventListener("click", () => finish(null));
|
|
956
|
-
overlay.addEventListener("click", (e) => { if (e.target === overlay)
|
|
957
|
-
finish(null); });
|
|
958
|
-
document.addEventListener("keydown", onKey, true);
|
|
959
|
-
explorer = createDirectoryExplorer(mountEl, {
|
|
960
|
-
roots: opts.roots,
|
|
961
|
-
initialPath: opts.initialPath,
|
|
962
|
-
apiBase: opts.apiBase,
|
|
963
|
-
// Standalone hides Attach chrome; picking is our own affordance.
|
|
964
|
-
mode: "standalone",
|
|
965
|
-
showSearch: true,
|
|
966
|
-
showMultiSelect: false,
|
|
967
|
-
// Opt-in upload: when allowed, the explorer shows its drop zone so users can
|
|
968
|
-
// add a file to the currently-open folder (Files/Temp), then click it to pick.
|
|
969
|
-
showUpload: opts.allowUpload === true,
|
|
970
|
-
// In file mode a name-click resolves the pick; folders still navigate.
|
|
971
|
-
primaryClick: select === "file" ? "attach" : "copyPath",
|
|
972
|
-
onNavigate: (p) => syncFooter(p),
|
|
973
|
-
onAttach: (items) => {
|
|
974
|
-
if (select !== "file")
|
|
975
|
-
return;
|
|
976
|
-
const it = items && items[0];
|
|
977
|
-
if (!it)
|
|
978
|
-
return;
|
|
979
|
-
if (opts.accept && !opts.accept(it)) {
|
|
980
|
-
showToast(opts.rejectMessage || "That file can't be used here.");
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
finish(it);
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
return { close: () => finish(null) };
|
|
987
|
-
}
|
|
988
|
-
// ── auto-mount + global bridge ───────────────────────────────────────────────
|
|
989
|
-
function readDataOpts(node) {
|
|
990
|
-
const d = node.dataset;
|
|
991
|
-
const rootsAttr = (d.roots || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
992
|
-
return {
|
|
993
|
-
roots: rootsAttr.length ? rootsAttr : undefined,
|
|
994
|
-
initialPath: d.initialPath || undefined,
|
|
995
|
-
mode: d.mode === "standalone" ? "standalone" : "attach",
|
|
996
|
-
showSearch: d.showSearch !== "false",
|
|
997
|
-
showMultiSelect: d.showMultiselect !== "false",
|
|
998
|
-
showUpload: d.showUpload != null ? d.showUpload === "true" : undefined,
|
|
999
|
-
primaryClick: d.primaryClick || "copyPath",
|
|
1000
|
-
cloudAvailable: d.cloudAvailable != null ? d.cloudAvailable === "true" : undefined
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
function autoMount() {
|
|
1004
|
-
const nodes = Array.prototype.slice.call(document.querySelectorAll("[data-rk-directory]"));
|
|
1005
|
-
nodes.forEach((node) => {
|
|
1006
|
-
if (node.getAttribute("data-rk-dir-mounted") === "1")
|
|
1007
|
-
return;
|
|
1008
|
-
node.setAttribute("data-rk-dir-mounted", "1");
|
|
1009
|
-
window.__rkDirectoryInstance = createDirectoryExplorer(node, readDataOpts(node));
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
window.__vidfarmDirectory = { mount: createDirectoryExplorer, pick: openDirectoryPicker };
|
|
1013
|
-
if (document.readyState === "loading")
|
|
1014
|
-
document.addEventListener("DOMContentLoaded", autoMount);
|
|
1015
|
-
else
|
|
1016
|
-
autoMount();
|
|
1017
|
-
window.dispatchEvent(new CustomEvent("vidfarm:directory-ready"));
|
|
1018
|
-
//# sourceMappingURL=file-directory.js.map
|