fauna-api 0.0.0-stage → 1.0.679
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/dist/brands.d.ts +2 -0
- package/dist/brands.js +2 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +121 -0
- package/dist/src/ant/chat.d.ts +130 -0
- package/dist/src/ant/chat.js +467 -0
- package/dist/src/ant/format.d.ts +40 -0
- package/dist/src/ant/format.js +143 -0
- package/dist/src/ant/graph.d.ts +130 -0
- package/dist/src/ant/graph.js +449 -0
- package/dist/src/ant/index.d.ts +16 -0
- package/dist/src/ant/index.js +61 -0
- package/dist/src/ant/persona.d.ts +151 -0
- package/dist/src/ant/persona.js +136 -0
- package/dist/src/ant/timeline.d.ts +283 -0
- package/dist/src/ant/timeline.js +1025 -0
- package/dist/src/authentification.d.ts +45 -0
- package/dist/src/authentification.js +194 -0
- package/dist/src/billing.d.ts +135 -0
- package/dist/src/billing.js +128 -0
- package/dist/src/calendar.d.ts +83 -0
- package/dist/src/calendar.js +133 -0
- package/dist/src/chats.d.ts +113 -0
- package/dist/src/chats.js +100 -0
- package/dist/src/cicada/contacts.d.ts +31 -0
- package/dist/src/cicada/contacts.js +28 -0
- package/dist/src/cicada/conversations.d.ts +73 -0
- package/dist/src/cicada/conversations.js +236 -0
- package/dist/src/cicada/inboxes.d.ts +48 -0
- package/dist/src/cicada/inboxes.js +82 -0
- package/dist/src/cicada/index.d.ts +16 -0
- package/dist/src/cicada/index.js +61 -0
- package/dist/src/cicada/phone.d.ts +48 -0
- package/dist/src/cicada/phone.js +306 -0
- package/dist/src/drive.d.ts +80 -0
- package/dist/src/drive.js +472 -0
- package/dist/src/extensions/index.d.ts +4 -0
- package/dist/src/extensions/index.js +5 -0
- package/dist/src/extensions/web/index.d.ts +6 -0
- package/dist/src/extensions/web/index.js +7 -0
- package/dist/src/extensions/web/session.d.ts +108 -0
- package/dist/src/extensions/web/session.js +470 -0
- package/dist/src/extensions/web/tools.d.ts +13 -0
- package/dist/src/extensions/web/tools.js +72 -0
- package/dist/src/friends.d.ts +39 -0
- package/dist/src/friends.js +119 -0
- package/dist/src/groups.d.ts +82 -0
- package/dist/src/groups.js +269 -0
- package/dist/src/inbox.d.ts +11 -0
- package/dist/src/inbox.js +35 -0
- package/dist/src/integrations.d.ts +52 -0
- package/dist/src/integrations.js +205 -0
- package/dist/src/magpie/explorer.d.ts +14 -0
- package/dist/src/magpie/explorer.js +36 -0
- package/dist/src/magpie/index.d.ts +16 -0
- package/dist/src/magpie/index.js +61 -0
- package/dist/src/magpie/portfolio.d.ts +52 -0
- package/dist/src/magpie/portfolio.js +179 -0
- package/dist/src/magpie/quotes.d.ts +35 -0
- package/dist/src/magpie/quotes.js +47 -0
- package/dist/src/magpie/room.d.ts +61 -0
- package/dist/src/magpie/room.js +238 -0
- package/dist/src/maps.d.ts +159 -0
- package/dist/src/maps.js +171 -0
- package/dist/src/marmot/browser.d.ts +139 -0
- package/dist/src/marmot/browser.js +389 -0
- package/dist/src/marmot/index.d.ts +20 -0
- package/dist/src/marmot/index.js +65 -0
- package/dist/src/marmot/media.d.ts +56 -0
- package/dist/src/marmot/media.js +265 -0
- package/dist/src/marmot/research.d.ts +337 -0
- package/dist/src/marmot/research.js +1138 -0
- package/dist/src/marmot/scraper.d.ts +59 -0
- package/dist/src/marmot/scraper.js +94 -0
- package/dist/src/marmot/web.d.ts +402 -0
- package/dist/src/marmot/web.js +2586 -0
- package/dist/src/marmot/workspace.d.ts +81 -0
- package/dist/src/marmot/workspace.js +263 -0
- package/dist/src/parrot/agent.d.ts +53 -0
- package/dist/src/parrot/agent.js +93 -0
- package/dist/src/parrot/chat.d.ts +84 -0
- package/dist/src/parrot/chat.js +237 -0
- package/dist/src/parrot/format.d.ts +128 -0
- package/dist/src/parrot/format.js +639 -0
- package/dist/src/parrot/generative.d.ts +261 -0
- package/dist/src/parrot/generative.js +185 -0
- package/dist/src/parrot/index.d.ts +22 -0
- package/dist/src/parrot/index.js +67 -0
- package/dist/src/parrot/room.d.ts +225 -0
- package/dist/src/parrot/room.js +466 -0
- package/dist/src/parrot/routing.d.ts +90 -0
- package/dist/src/parrot/routing.js +78 -0
- package/dist/src/parrot/share.d.ts +126 -0
- package/dist/src/parrot/share.js +261 -0
- package/dist/src/parrot/socket.d.ts +41 -0
- package/dist/src/parrot/socket.js +478 -0
- package/dist/src/payments.d.ts +73 -0
- package/dist/src/payments.js +107 -0
- package/dist/src/private/cards.css +75 -0
- package/dist/src/rooms.d.ts +76 -0
- package/dist/src/rooms.js +168 -0
- package/dist/src/shared/schema.d.ts +24 -0
- package/dist/src/shared/schema.js +1 -0
- package/dist/src/shared/server.d.ts +65 -0
- package/dist/src/shared/server.js +302 -0
- package/dist/src/termite/environment.d.ts +110 -0
- package/dist/src/termite/environment.js +263 -0
- package/dist/src/termite/index.d.ts +14 -0
- package/dist/src/termite/index.js +59 -0
- package/dist/src/termite/os.d.ts +326 -0
- package/dist/src/termite/os.js +572 -0
- package/dist/src/termite/versioning.d.ts +94 -0
- package/dist/src/termite/versioning.js +217 -0
- package/dist/src/tickets.d.ts +43 -0
- package/dist/src/tickets.js +138 -0
- package/dist/src/travel.d.ts +85 -0
- package/dist/src/travel.js +44 -0
- package/dist/src/ui/authentification.d.ts +24 -0
- package/dist/src/ui/authentification.js +177 -0
- package/dist/src/ui/brands.css +11 -0
- package/dist/src/ui/brands.d.ts +7 -0
- package/dist/src/ui/brands.js +14 -0
- package/dist/src/ui/browser.d.ts +74 -0
- package/dist/src/ui/browser.js +470 -0
- package/dist/src/ui/chat.d.ts +183 -0
- package/dist/src/ui/chat.js +2379 -0
- package/dist/src/ui/chatPanel.d.ts +109 -0
- package/dist/src/ui/chatPanel.js +768 -0
- package/dist/src/ui/chatSequence.d.ts +36 -0
- package/dist/src/ui/chatSequence.js +42 -0
- package/dist/src/ui/crown.d.ts +19 -0
- package/dist/src/ui/crown.js +199 -0
- package/dist/src/ui/cursor.d.ts +73 -0
- package/dist/src/ui/cursor.js +175 -0
- package/dist/src/ui/device.d.ts +33 -0
- package/dist/src/ui/device.js +49 -0
- package/dist/src/ui/drive.d.ts +69 -0
- package/dist/src/ui/drive.js +1243 -0
- package/dist/src/ui/generative.d.ts +39 -0
- package/dist/src/ui/generative.js +313 -0
- package/dist/src/ui/hover.d.ts +6 -0
- package/dist/src/ui/hover.js +210 -0
- package/dist/src/ui/index.d.ts +42 -0
- package/dist/src/ui/index.js +87 -0
- package/dist/src/ui/integrations.d.ts +13 -0
- package/dist/src/ui/integrations.js +175 -0
- package/dist/src/ui/maps.d.ts +87 -0
- package/dist/src/ui/maps.js +547 -0
- package/dist/src/ui/roster.d.ts +32 -0
- package/dist/src/ui/roster.js +163 -0
- package/dist/src/ui/talk.d.ts +12 -0
- package/dist/src/ui/talk.js +560 -0
- package/dist/src/ui/tickets.d.ts +13 -0
- package/dist/src/ui/tickets.js +966 -0
- package/dist/src/ui/toast.d.ts +7 -0
- package/dist/src/ui/toast.js +258 -0
- package/dist/src/ui/tokenText.d.ts +18 -0
- package/dist/src/ui/tokenText.js +96 -0
- package/dist/src/ui/tokenized.d.ts +42 -0
- package/dist/src/ui/tokenized.js +622 -0
- package/dist/src/ui/ui.css +4763 -0
- package/dist/src/ui/view.d.ts +23 -0
- package/dist/src/ui/view.js +584 -0
- package/dist/src/view.d.ts +113 -0
- package/dist/src/view.js +386 -0
- package/dist/src/webhooks.d.ts +66 -0
- package/dist/src/webhooks.js +78 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/package.json +87 -4
- package/README.md +0 -3
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useImperativeHandle, useLayoutEffect, useRef, forwardRef } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { Fauna } from "fauna-api";
|
|
6
|
+
import gsap from "gsap";
|
|
7
|
+
import { createHoveringElement, createTooltip } from "./hover.js";
|
|
8
|
+
import { sendProgressToast } from "./toast.js";
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// Drive — the file explorer + file card, ported from the original imperative
|
|
11
|
+
// widget to React. The public surface is unchanged: `createFileElement` and
|
|
12
|
+
// `createFileExplorer` are still imperative factories that return a mountable
|
|
13
|
+
// element + control handle, so nothing at the call sites (apps/fauna Drive tab,
|
|
14
|
+
// apps/fauna /view/) has to change. Internally, `createFileExplorer` mounts a
|
|
15
|
+
// React `FileExplorer` component (via a flushSync createRoot) that owns an
|
|
16
|
+
// `ExplorerEngine` instance holding the DOM-building logic lifted verbatim from
|
|
17
|
+
// the original widget, exposed through useImperativeHandle — the same bridge
|
|
18
|
+
// pattern used by chat.tsx.
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// ─── MIME maps ─────────────────────────────────────────────────────────────
|
|
21
|
+
// Content-type → extension is sourced from Marmot's public `/media/types`
|
|
22
|
+
// route (the canonical `PARSABLE_CONTENT_TYPES` map) rather than hardcoded, so
|
|
23
|
+
// there's a single server-owned source of truth. `getTypes()` is cached, so
|
|
24
|
+
// hydration is cheap and idempotent; we flatten it into a synchronous lookup so
|
|
25
|
+
// `createFileElement` (which builds DOM synchronously) can read it inline.
|
|
26
|
+
const EXTENSION_CACHE = {
|
|
27
|
+
// Folders are a Drive-only virtual type, not a Marmot-parsable media type,
|
|
28
|
+
// so the server list never carries it — keep it seeded locally.
|
|
29
|
+
'application/x-directory': 'dir',
|
|
30
|
+
};
|
|
31
|
+
// Guard so the network fetch happens at most once regardless of how many cards
|
|
32
|
+
// or explorers trigger it. `getTypes()` is itself cached, but this also avoids
|
|
33
|
+
// spawning redundant in-flight promises.
|
|
34
|
+
let extensionsHydration = null;
|
|
35
|
+
function hydrateExtensions() {
|
|
36
|
+
if (!extensionsHydration) {
|
|
37
|
+
extensionsHydration = Fauna.Marmot.Media.getTypes().then((grouped) => {
|
|
38
|
+
for (const infos of Object.values(grouped)) {
|
|
39
|
+
for (const info of infos) {
|
|
40
|
+
if (info.extensions[0])
|
|
41
|
+
EXTENSION_CACHE[info.contentType] = info.extensions[0];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}).catch(() => {
|
|
45
|
+
// Let a later call retry; the filename-extension fallback covers the gap.
|
|
46
|
+
extensionsHydration = null;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return extensionsHydration;
|
|
50
|
+
}
|
|
51
|
+
// Resolve a display extension for a file. Prefers the server-sourced map, then
|
|
52
|
+
// falls back to the filename's own extension, then a generic 'file' — so an
|
|
53
|
+
// unmapped content type degrades gracefully instead of throwing. Also warms the
|
|
54
|
+
// cache on first use so consumers that build cards without an ExplorerEngine
|
|
55
|
+
// (e.g. previews) still converge to the canonical extension on re-render.
|
|
56
|
+
function extensionFor(contentType, name) {
|
|
57
|
+
void hydrateExtensions();
|
|
58
|
+
return EXTENSION_CACHE[contentType]
|
|
59
|
+
?? (name.includes('.') ? name.split('.').pop() : 'file');
|
|
60
|
+
}
|
|
61
|
+
// Material-symbol fallback per content type, shown when no thumbnail has been
|
|
62
|
+
// painted onto the preview canvas yet (or the type doesn't have a meaningful
|
|
63
|
+
// visual preview, e.g. folders, audio, generic text).
|
|
64
|
+
export const MIME_TO_ICON = {
|
|
65
|
+
'application/x-directory': 'folder',
|
|
66
|
+
'application/pdf': 'picture_as_pdf',
|
|
67
|
+
'text/plain': 'description',
|
|
68
|
+
'text/markdown': 'article',
|
|
69
|
+
'application/json': 'data_object',
|
|
70
|
+
'text/csv': 'table_chart',
|
|
71
|
+
'text/html': 'code',
|
|
72
|
+
'application/xml': 'code',
|
|
73
|
+
'audio/mpeg': 'graphic_eq',
|
|
74
|
+
'audio/wav': 'graphic_eq',
|
|
75
|
+
'image/jpeg': 'image',
|
|
76
|
+
'image/png': 'image',
|
|
77
|
+
'image/webp': 'image',
|
|
78
|
+
'image/heic': 'image',
|
|
79
|
+
'image/heif': 'image',
|
|
80
|
+
};
|
|
81
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
82
|
+
export function normalizePath(p) {
|
|
83
|
+
return p.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
84
|
+
}
|
|
85
|
+
// ─── File card (imperative DOM factory, ported verbatim) ───────────────────────
|
|
86
|
+
export function createFileElement(file, options = {}) {
|
|
87
|
+
function clone(element) {
|
|
88
|
+
const clone = element.cloneNode(true);
|
|
89
|
+
clone.addEventListener('click', () => {
|
|
90
|
+
element.click();
|
|
91
|
+
});
|
|
92
|
+
return clone;
|
|
93
|
+
}
|
|
94
|
+
const view = options.view || 'list';
|
|
95
|
+
var element = document.createElement('div');
|
|
96
|
+
element.className = `file-${view}`;
|
|
97
|
+
const preview = document.createElement('canvas');
|
|
98
|
+
preview.className = `file-${view}-preview`;
|
|
99
|
+
const dpr = window.devicePixelRatio || 1;
|
|
100
|
+
const width = element.offsetWidth * dpr;
|
|
101
|
+
const height = element.offsetHeight * dpr;
|
|
102
|
+
preview.width = width;
|
|
103
|
+
preview.height = height;
|
|
104
|
+
element.appendChild(preview);
|
|
105
|
+
// Centred material-symbol icon shown over the preview canvas while nothing
|
|
106
|
+
// has been painted into it. Subsequent thumbnail rendering can hide this by
|
|
107
|
+
// adding the `drawn` class to the file element.
|
|
108
|
+
const previewIcon = document.createElement('div');
|
|
109
|
+
previewIcon.className = 'file-preview-icon';
|
|
110
|
+
const previewIconGlyph = document.createElement('span');
|
|
111
|
+
previewIconGlyph.className = 'material-symbols-outlined';
|
|
112
|
+
previewIconGlyph.textContent = MIME_TO_ICON[file.contentType] ?? 'draft';
|
|
113
|
+
previewIcon.appendChild(previewIconGlyph);
|
|
114
|
+
element.appendChild(previewIcon);
|
|
115
|
+
// Lazily fetch a server-generated thumbnail and paint it into the preview
|
|
116
|
+
// canvas as a small, contained "sheet" — like a little file resting inside
|
|
117
|
+
// the card — rather than a full-bleed cover image. Once painted, the `drawn`
|
|
118
|
+
// class fades out the fallback icon. The bitmap is cached so the
|
|
119
|
+
// ResizeObserver can cheaply repaint after the canvas backing store resizes.
|
|
120
|
+
let previewBitmap = null;
|
|
121
|
+
let previewRequested = false;
|
|
122
|
+
function drawBoxyImage(ctx, bitmap, x, y, w, h, dpr) {
|
|
123
|
+
ctx.save();
|
|
124
|
+
// The server renders text sheets on white and images on their own bg;
|
|
125
|
+
// fill white behind so any letterboxing reads as paper, not a hole.
|
|
126
|
+
ctx.fillStyle = '#ffffff';
|
|
127
|
+
ctx.fillRect(x, y, w, h);
|
|
128
|
+
// contain-fit inside the box
|
|
129
|
+
ctx.beginPath();
|
|
130
|
+
ctx.rect(x, y, w, h);
|
|
131
|
+
ctx.clip();
|
|
132
|
+
const scale = Math.min(w / bitmap.width, h / bitmap.height);
|
|
133
|
+
const dw = bitmap.width * scale;
|
|
134
|
+
const dh = bitmap.height * scale;
|
|
135
|
+
ctx.drawImage(bitmap, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
|
|
136
|
+
ctx.restore();
|
|
137
|
+
// Crisp 1px border so the file reads as a boxy sheet against the card.
|
|
138
|
+
ctx.save();
|
|
139
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.28)';
|
|
140
|
+
ctx.lineWidth = Math.max(1, Math.round(dpr));
|
|
141
|
+
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
|
|
142
|
+
ctx.restore();
|
|
143
|
+
}
|
|
144
|
+
function paintPreview() {
|
|
145
|
+
if (!previewBitmap)
|
|
146
|
+
return;
|
|
147
|
+
const ctx = preview.getContext('2d');
|
|
148
|
+
if (!ctx || preview.width === 0 || preview.height === 0)
|
|
149
|
+
return;
|
|
150
|
+
const cw = preview.width;
|
|
151
|
+
const ch = preview.height;
|
|
152
|
+
const dpr = window.devicePixelRatio || 1;
|
|
153
|
+
ctx.clearRect(0, 0, cw, ch);
|
|
154
|
+
// Layout the sheet in the region above the footer, with even padding so
|
|
155
|
+
// the file "floats" inside the card. Footer is ~44px tall in CSS px.
|
|
156
|
+
const padX = 26 * dpr;
|
|
157
|
+
const padTop = 26 * dpr;
|
|
158
|
+
const footer = 52 * dpr;
|
|
159
|
+
const boxW = cw - padX * 2;
|
|
160
|
+
const boxH = ch - padTop - footer;
|
|
161
|
+
if (boxW <= 0 || boxH <= 0)
|
|
162
|
+
return;
|
|
163
|
+
// Fit the sheet to the source aspect ratio within the available box so a
|
|
164
|
+
// portrait doc stays portrait and a wide image stays wide.
|
|
165
|
+
const iw = previewBitmap.width;
|
|
166
|
+
const ih = previewBitmap.height;
|
|
167
|
+
const fit = Math.min(boxW / iw, boxH / ih);
|
|
168
|
+
const sheetW = iw * fit;
|
|
169
|
+
const sheetH = ih * fit;
|
|
170
|
+
const x = (cw - sheetW) / 2;
|
|
171
|
+
const y = padTop + (boxH - sheetH) / 2;
|
|
172
|
+
// Boxy sheet with a crisp border — no rounding, no shadow.
|
|
173
|
+
drawBoxyImage(ctx, previewBitmap, x, y, sheetW, sheetH, dpr);
|
|
174
|
+
element.classList.add('drawn');
|
|
175
|
+
}
|
|
176
|
+
async function requestPreview() {
|
|
177
|
+
if (previewRequested)
|
|
178
|
+
return;
|
|
179
|
+
// A freshly-wrapped file uploads asynchronously; requesting a preview
|
|
180
|
+
// before the bytes land server-side just 404s and would burn the
|
|
181
|
+
// one-shot flag. Skip until it's online — the upload's completion
|
|
182
|
+
// handler re-invokes this once the file exists.
|
|
183
|
+
//
|
|
184
|
+
// Exception: when `upload: false`, paint a local image preview from
|
|
185
|
+
// cached bytes (design / offline canvas use) instead of hitting the server.
|
|
186
|
+
if (!file.online && options.upload !== false)
|
|
187
|
+
return;
|
|
188
|
+
previewRequested = true;
|
|
189
|
+
try {
|
|
190
|
+
// Browsers generally can't paint HEIC via createImageBitmap — wait for
|
|
191
|
+
// the server WebP preview once the file is online.
|
|
192
|
+
const localImage = !file.online
|
|
193
|
+
&& file.contentType.startsWith('image/')
|
|
194
|
+
&& file.contentType !== 'image/heic'
|
|
195
|
+
&& file.contentType !== 'image/heif';
|
|
196
|
+
if (localImage) {
|
|
197
|
+
const bytes = await file.download();
|
|
198
|
+
previewBitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: file.contentType }));
|
|
199
|
+
paintPreview();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!file.online)
|
|
203
|
+
return;
|
|
204
|
+
const bytes = await file.preview();
|
|
205
|
+
if (!bytes)
|
|
206
|
+
return;
|
|
207
|
+
previewBitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: 'image/webp' }));
|
|
208
|
+
paintPreview();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// Leave the fallback icon in place on any failure.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const previewViewport = new IntersectionObserver((entries) => {
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (entry.isIntersecting) {
|
|
217
|
+
previewViewport.disconnect();
|
|
218
|
+
void requestPreview();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, { rootMargin: '200px' });
|
|
222
|
+
previewViewport.observe(element);
|
|
223
|
+
const details = document.createElement('div');
|
|
224
|
+
details.className = `file-${view}-details`;
|
|
225
|
+
element.appendChild(details);
|
|
226
|
+
const label = document.createElement('div');
|
|
227
|
+
label.className = `file-${view}-label`;
|
|
228
|
+
details.appendChild(label);
|
|
229
|
+
const name = document.createElement('input');
|
|
230
|
+
name.type = 'text';
|
|
231
|
+
name.placeholder = 'Untitled';
|
|
232
|
+
name.value = file.name;
|
|
233
|
+
name.readOnly = true;
|
|
234
|
+
label.appendChild(name);
|
|
235
|
+
const type = document.createElement('span');
|
|
236
|
+
type.innerHTML = extensionFor(file.contentType, file.name).toUpperCase();
|
|
237
|
+
label.appendChild(type);
|
|
238
|
+
const opts = document.createElement('div');
|
|
239
|
+
opts.className = `file-${view}-options`;
|
|
240
|
+
details.appendChild(opts);
|
|
241
|
+
if (options.remove || options.remove === undefined) {
|
|
242
|
+
const remove = document.createElement('span');
|
|
243
|
+
remove.className = 'material-symbols-outlined';
|
|
244
|
+
remove.innerHTML = 'delete';
|
|
245
|
+
opts.appendChild(remove);
|
|
246
|
+
createTooltip(remove, file.contentType === 'application/x-directory' ? 'Delete folder' : 'Delete file');
|
|
247
|
+
remove.onclick = () => {
|
|
248
|
+
if (options.onRemove) {
|
|
249
|
+
options.onRemove();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
file.remove();
|
|
253
|
+
gsap.to(element, {
|
|
254
|
+
duration: 0.3,
|
|
255
|
+
opacity: 0,
|
|
256
|
+
display: 'none',
|
|
257
|
+
ease: 'power2.inOut',
|
|
258
|
+
onComplete: () => {
|
|
259
|
+
element.remove();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (options.rename || options.rename === undefined) {
|
|
265
|
+
const rename = document.createElement('span');
|
|
266
|
+
rename.className = 'material-symbols-outlined';
|
|
267
|
+
rename.innerHTML = 'edit';
|
|
268
|
+
opts.appendChild(rename);
|
|
269
|
+
createTooltip(rename, 'Rename');
|
|
270
|
+
rename.onclick = () => {
|
|
271
|
+
if (!('path' in file.authorization)) {
|
|
272
|
+
throw new Error('401: Unauthorized request.');
|
|
273
|
+
}
|
|
274
|
+
name.readOnly = false;
|
|
275
|
+
name.focus();
|
|
276
|
+
name.select();
|
|
277
|
+
const commit = async () => {
|
|
278
|
+
if (!('path' in file.authorization)) {
|
|
279
|
+
throw new Error('401: Unauthorized request.');
|
|
280
|
+
}
|
|
281
|
+
name.readOnly = true;
|
|
282
|
+
const nextName = name.value.trim();
|
|
283
|
+
if (!nextName || nextName === file.name) {
|
|
284
|
+
name.value = file.name;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const oldPath = normalizePath(file.authorization.path);
|
|
288
|
+
const split = oldPath.split('/');
|
|
289
|
+
split.pop();
|
|
290
|
+
const parent = split.join('/');
|
|
291
|
+
const newPath = (parent ? parent + '/' : '') + nextName + (file.contentType === 'application/x-directory' ? '/' : '');
|
|
292
|
+
try {
|
|
293
|
+
const toast = sendProgressToast(`Renaming ${file.name}...`);
|
|
294
|
+
toast.setProgress(0.1);
|
|
295
|
+
element.style.pointerEvents = 'none';
|
|
296
|
+
await file.copy(newPath);
|
|
297
|
+
toast.setProgress(0.7);
|
|
298
|
+
const newFile = await Fauna.Drive.pull({
|
|
299
|
+
authorization: file.authorization.authorization,
|
|
300
|
+
path: newPath,
|
|
301
|
+
group: 'path' in file.authorization ? file.authorization.group : undefined
|
|
302
|
+
});
|
|
303
|
+
await file.remove();
|
|
304
|
+
toast.setProgress(1);
|
|
305
|
+
file = newFile;
|
|
306
|
+
name.value = newFile.name;
|
|
307
|
+
element.style.pointerEvents = 'auto';
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
name.value = file.name;
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
name.onblur = () => { void commit(); };
|
|
315
|
+
name.onkeydown = (e) => {
|
|
316
|
+
if (e.key === 'Enter') {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
name.blur();
|
|
319
|
+
}
|
|
320
|
+
else if (e.key === 'Escape') {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
name.value = file.name;
|
|
323
|
+
name.readOnly = true;
|
|
324
|
+
name.blur();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (options.download || options.download === undefined) {
|
|
330
|
+
const download = document.createElement('span');
|
|
331
|
+
download.className = 'material-symbols-outlined';
|
|
332
|
+
download.innerHTML = 'download';
|
|
333
|
+
opts.appendChild(download);
|
|
334
|
+
createTooltip(download, file.contentType === 'application/x-directory' ? 'Download as .zip' : 'Download');
|
|
335
|
+
download.onclick = async () => {
|
|
336
|
+
const toast = sendProgressToast(`Downloading ${file.name}...`);
|
|
337
|
+
const bytes = await file.download(true, (percent) => {
|
|
338
|
+
toast.setProgress(percent);
|
|
339
|
+
});
|
|
340
|
+
const blob = new Blob([new Uint8Array(bytes)], { type: file.contentType === 'application/x-directory' ? 'application/zip' : file.contentType });
|
|
341
|
+
const url = URL.createObjectURL(blob);
|
|
342
|
+
const a = document.createElement('a');
|
|
343
|
+
a.href = url;
|
|
344
|
+
a.download = file.name + (file.contentType === 'application/x-directory' ? '.zip' : '');
|
|
345
|
+
a.click();
|
|
346
|
+
URL.revokeObjectURL(url);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if ((options.open || options.open === undefined) && (file.contentType !== 'application/x-directory' || options.onFolderOpen)) {
|
|
350
|
+
const open = document.createElement('span');
|
|
351
|
+
open.className = 'material-symbols-outlined';
|
|
352
|
+
open.innerHTML = 'open_in_new';
|
|
353
|
+
opts.appendChild(open);
|
|
354
|
+
createTooltip(open, file.contentType === 'application/x-directory' ? 'Open folder' : 'Open');
|
|
355
|
+
open.onclick = () => {
|
|
356
|
+
if (file.contentType === 'application/x-directory' && options.onFolderOpen) {
|
|
357
|
+
options.onFolderOpen();
|
|
358
|
+
}
|
|
359
|
+
else if (options.onOpen) {
|
|
360
|
+
options.onOpen();
|
|
361
|
+
}
|
|
362
|
+
else if (!('repository' in file.authorization)) {
|
|
363
|
+
if (!('path' in file.authorization)) {
|
|
364
|
+
throw new Error('401: Unauthorized request.');
|
|
365
|
+
}
|
|
366
|
+
window.location.href = `/view/?path=${encodeURIComponent(file.authorization.path)}`;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (options.share || options.share === undefined) {
|
|
371
|
+
const share = document.createElement('span');
|
|
372
|
+
share.className = 'material-symbols-outlined';
|
|
373
|
+
share.innerHTML = 'public';
|
|
374
|
+
opts.appendChild(share);
|
|
375
|
+
createTooltip(share, 'Share a public link');
|
|
376
|
+
share.onclick = async () => {
|
|
377
|
+
const accessCode = await file.generateAccessCode();
|
|
378
|
+
if (options.onShare) {
|
|
379
|
+
options.onShare(accessCode);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
window.location.href = `/view/?access-code=${accessCode}`;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (options.remove || options.remove === undefined || options.rename || options.rename === undefined || options.download || options.download === undefined || options.open || options.open === undefined || options.share || options.share === undefined) {
|
|
387
|
+
const expand = document.createElement('span');
|
|
388
|
+
expand.className = 'material-symbols-outlined';
|
|
389
|
+
expand.dataset.role = 'expand';
|
|
390
|
+
expand.innerHTML = view === 'list' ? 'more_vert' : 'more_horiz';
|
|
391
|
+
opts.appendChild(expand);
|
|
392
|
+
createTooltip(expand, 'More actions');
|
|
393
|
+
createHoveringElement([...opts.children].filter((element) => element.dataset.role !== 'expand').map(clone), expand);
|
|
394
|
+
}
|
|
395
|
+
[...opts.children].forEach((element) => {
|
|
396
|
+
element.onmouseenter = () => {
|
|
397
|
+
gsap.to(element, {
|
|
398
|
+
duration: 0.3,
|
|
399
|
+
scale: 1.1,
|
|
400
|
+
rotate: -10,
|
|
401
|
+
ease: 'power2.inOut'
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
element.onmouseleave = () => {
|
|
405
|
+
gsap.to(element, {
|
|
406
|
+
duration: 0.3,
|
|
407
|
+
scale: 1,
|
|
408
|
+
rotate: 0,
|
|
409
|
+
ease: 'power2.inOut'
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
const isExpandGlyph = (el) => el.dataset.role === 'expand';
|
|
414
|
+
// Collapse the inline action rail behind a single "…" menu when the row is
|
|
415
|
+
// too narrow to comfortably fit the file name alongside every action. Cozy
|
|
416
|
+
// cards always collapse (their footer is tight); list rows collapse below a
|
|
417
|
+
// fixed width threshold. Keying purely off the row's own width (rather than
|
|
418
|
+
// the icons' measured widths) keeps this stable — hiding the icons doesn't
|
|
419
|
+
// change the row width, so the decision never oscillates.
|
|
420
|
+
const COLLAPSE_BELOW = 520;
|
|
421
|
+
let collapsed = null;
|
|
422
|
+
function resize() {
|
|
423
|
+
const rowWidth = element.getBoundingClientRect().width;
|
|
424
|
+
// A freshly-created, not-yet-laid-out row reports 0; don't commit a
|
|
425
|
+
// decision until we have a real measurement.
|
|
426
|
+
if (rowWidth === 0)
|
|
427
|
+
return;
|
|
428
|
+
const next = view === 'cozy' || rowWidth < COLLAPSE_BELOW;
|
|
429
|
+
if (next === collapsed)
|
|
430
|
+
return;
|
|
431
|
+
collapsed = next;
|
|
432
|
+
const actionIcons = [...opts.children].filter((el) => !isExpandGlyph(el));
|
|
433
|
+
const expandIcons = [...opts.children].filter(isExpandGlyph);
|
|
434
|
+
gsap.set(actionIcons, { display: next ? 'none' : 'flex' });
|
|
435
|
+
gsap.set(expandIcons, { display: next ? 'flex' : 'none' });
|
|
436
|
+
}
|
|
437
|
+
const observer = new ResizeObserver(() => {
|
|
438
|
+
const dpr = window.devicePixelRatio || 1;
|
|
439
|
+
preview.width = element.offsetWidth * dpr;
|
|
440
|
+
preview.height = element.offsetHeight * dpr;
|
|
441
|
+
resize();
|
|
442
|
+
paintPreview();
|
|
443
|
+
});
|
|
444
|
+
observer.observe(element);
|
|
445
|
+
requestAnimationFrame(resize);
|
|
446
|
+
const load = document.createElement('div');
|
|
447
|
+
load.className = 'file-load';
|
|
448
|
+
const progress = document.createElement('div');
|
|
449
|
+
progress.className = 'file-load-progress';
|
|
450
|
+
load.appendChild(progress);
|
|
451
|
+
details.appendChild(load);
|
|
452
|
+
const highlightTl = gsap.timeline({ paused: true });
|
|
453
|
+
highlightTl.to(element, {
|
|
454
|
+
duration: 0.3,
|
|
455
|
+
borderColor: getComputedStyle(document.body).getPropertyValue('--accent'),
|
|
456
|
+
ease: 'power2.inOut'
|
|
457
|
+
});
|
|
458
|
+
element.onmouseenter = () => {
|
|
459
|
+
highlightTl.play();
|
|
460
|
+
};
|
|
461
|
+
element.onmouseleave = () => {
|
|
462
|
+
highlightTl.reverse();
|
|
463
|
+
};
|
|
464
|
+
element.onclick = (ev) => {
|
|
465
|
+
const target = ev.target;
|
|
466
|
+
if (!target) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (opts.contains(target)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (element.classList.contains('active')) {
|
|
473
|
+
element.classList.remove('active');
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
element.classList.add('active');
|
|
477
|
+
}
|
|
478
|
+
if (options.onSelect) {
|
|
479
|
+
options.onSelect();
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
if (!file.online && options.upload !== false) {
|
|
483
|
+
file.upload((percent) => {
|
|
484
|
+
gsap.to(progress, {
|
|
485
|
+
duration: 0.1,
|
|
486
|
+
width: `${percent * 100}%`,
|
|
487
|
+
ease: 'linear'
|
|
488
|
+
});
|
|
489
|
+
}).then(() => {
|
|
490
|
+
gsap.to(load, {
|
|
491
|
+
duration: 0.5,
|
|
492
|
+
opacity: 0,
|
|
493
|
+
display: 'none',
|
|
494
|
+
ease: 'power2.inOut'
|
|
495
|
+
});
|
|
496
|
+
// Now that the bytes exist server-side, pull the generated preview.
|
|
497
|
+
void requestPreview();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
gsap.to(load, {
|
|
502
|
+
display: 'none'
|
|
503
|
+
});
|
|
504
|
+
// Offline + upload:false (or already online) — try preview immediately.
|
|
505
|
+
void requestPreview();
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
getElement: () => element,
|
|
509
|
+
getFile: () => file,
|
|
510
|
+
isSelected: () => element.classList.contains('active')
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// The imperative explorer logic, lifted verbatim from the original widget into
|
|
514
|
+
// a class so the React `FileExplorer` component can own its lifecycle (build in
|
|
515
|
+
// a layout effect, tear down on unmount) and expose it via useImperativeHandle.
|
|
516
|
+
class ExplorerEngine {
|
|
517
|
+
constructor(authorization, options) {
|
|
518
|
+
Object.defineProperty(this, "authorization", {
|
|
519
|
+
enumerable: true,
|
|
520
|
+
configurable: true,
|
|
521
|
+
writable: true,
|
|
522
|
+
value: void 0
|
|
523
|
+
});
|
|
524
|
+
Object.defineProperty(this, "options", {
|
|
525
|
+
enumerable: true,
|
|
526
|
+
configurable: true,
|
|
527
|
+
writable: true,
|
|
528
|
+
value: void 0
|
|
529
|
+
});
|
|
530
|
+
Object.defineProperty(this, "currentPath", {
|
|
531
|
+
enumerable: true,
|
|
532
|
+
configurable: true,
|
|
533
|
+
writable: true,
|
|
534
|
+
value: void 0
|
|
535
|
+
});
|
|
536
|
+
Object.defineProperty(this, "currentView", {
|
|
537
|
+
enumerable: true,
|
|
538
|
+
configurable: true,
|
|
539
|
+
writable: true,
|
|
540
|
+
value: void 0
|
|
541
|
+
});
|
|
542
|
+
Object.defineProperty(this, "files", {
|
|
543
|
+
enumerable: true,
|
|
544
|
+
configurable: true,
|
|
545
|
+
writable: true,
|
|
546
|
+
value: []
|
|
547
|
+
});
|
|
548
|
+
Object.defineProperty(this, "clipboard", {
|
|
549
|
+
enumerable: true,
|
|
550
|
+
configurable: true,
|
|
551
|
+
writable: true,
|
|
552
|
+
value: []
|
|
553
|
+
});
|
|
554
|
+
Object.defineProperty(this, "loadGeneration", {
|
|
555
|
+
enumerable: true,
|
|
556
|
+
configurable: true,
|
|
557
|
+
writable: true,
|
|
558
|
+
value: 0
|
|
559
|
+
});
|
|
560
|
+
Object.defineProperty(this, "loading", {
|
|
561
|
+
enumerable: true,
|
|
562
|
+
configurable: true,
|
|
563
|
+
writable: true,
|
|
564
|
+
value: false
|
|
565
|
+
});
|
|
566
|
+
Object.defineProperty(this, "wantedPath", {
|
|
567
|
+
enumerable: true,
|
|
568
|
+
configurable: true,
|
|
569
|
+
writable: true,
|
|
570
|
+
value: null
|
|
571
|
+
});
|
|
572
|
+
Object.defineProperty(this, "destroyed", {
|
|
573
|
+
enumerable: true,
|
|
574
|
+
configurable: true,
|
|
575
|
+
writable: true,
|
|
576
|
+
value: false
|
|
577
|
+
});
|
|
578
|
+
Object.defineProperty(this, "root", {
|
|
579
|
+
enumerable: true,
|
|
580
|
+
configurable: true,
|
|
581
|
+
writable: true,
|
|
582
|
+
value: void 0
|
|
583
|
+
});
|
|
584
|
+
Object.defineProperty(this, "toolbar", {
|
|
585
|
+
enumerable: true,
|
|
586
|
+
configurable: true,
|
|
587
|
+
writable: true,
|
|
588
|
+
value: void 0
|
|
589
|
+
});
|
|
590
|
+
Object.defineProperty(this, "breadcrumbs", {
|
|
591
|
+
enumerable: true,
|
|
592
|
+
configurable: true,
|
|
593
|
+
writable: true,
|
|
594
|
+
value: void 0
|
|
595
|
+
});
|
|
596
|
+
Object.defineProperty(this, "viewListBtn", {
|
|
597
|
+
enumerable: true,
|
|
598
|
+
configurable: true,
|
|
599
|
+
writable: true,
|
|
600
|
+
value: void 0
|
|
601
|
+
});
|
|
602
|
+
Object.defineProperty(this, "viewCozyBtn", {
|
|
603
|
+
enumerable: true,
|
|
604
|
+
configurable: true,
|
|
605
|
+
writable: true,
|
|
606
|
+
value: void 0
|
|
607
|
+
});
|
|
608
|
+
Object.defineProperty(this, "copyBtn", {
|
|
609
|
+
enumerable: true,
|
|
610
|
+
configurable: true,
|
|
611
|
+
writable: true,
|
|
612
|
+
value: null
|
|
613
|
+
});
|
|
614
|
+
Object.defineProperty(this, "pasteBtn", {
|
|
615
|
+
enumerable: true,
|
|
616
|
+
configurable: true,
|
|
617
|
+
writable: true,
|
|
618
|
+
value: null
|
|
619
|
+
});
|
|
620
|
+
Object.defineProperty(this, "body", {
|
|
621
|
+
enumerable: true,
|
|
622
|
+
configurable: true,
|
|
623
|
+
writable: true,
|
|
624
|
+
value: void 0
|
|
625
|
+
});
|
|
626
|
+
Object.defineProperty(this, "loadingEl", {
|
|
627
|
+
enumerable: true,
|
|
628
|
+
configurable: true,
|
|
629
|
+
writable: true,
|
|
630
|
+
value: void 0
|
|
631
|
+
});
|
|
632
|
+
Object.defineProperty(this, "emptyEl", {
|
|
633
|
+
enumerable: true,
|
|
634
|
+
configurable: true,
|
|
635
|
+
writable: true,
|
|
636
|
+
value: void 0
|
|
637
|
+
});
|
|
638
|
+
Object.defineProperty(this, "listEl", {
|
|
639
|
+
enumerable: true,
|
|
640
|
+
configurable: true,
|
|
641
|
+
writable: true,
|
|
642
|
+
value: void 0
|
|
643
|
+
});
|
|
644
|
+
Object.defineProperty(this, "dropzone", {
|
|
645
|
+
enumerable: true,
|
|
646
|
+
configurable: true,
|
|
647
|
+
writable: true,
|
|
648
|
+
value: void 0
|
|
649
|
+
});
|
|
650
|
+
Object.defineProperty(this, "onResize", {
|
|
651
|
+
enumerable: true,
|
|
652
|
+
configurable: true,
|
|
653
|
+
writable: true,
|
|
654
|
+
value: () => { }
|
|
655
|
+
});
|
|
656
|
+
this.authorization = authorization;
|
|
657
|
+
this.options = authorization.repository
|
|
658
|
+
? { ...options, rename: false, remove: false, copyPaste: false, onShare: undefined }
|
|
659
|
+
: options;
|
|
660
|
+
this.currentPath = authorization.path;
|
|
661
|
+
this.currentView = options.view ?? 'list';
|
|
662
|
+
// Warm the server-sourced content-type → extension map (cached, so this
|
|
663
|
+
// only hits the network once across the whole app). Fire-and-forget:
|
|
664
|
+
// cards rendered before it resolves fall back to the filename extension.
|
|
665
|
+
void hydrateExtensions();
|
|
666
|
+
// ── DOM ──
|
|
667
|
+
const root = document.createElement('div');
|
|
668
|
+
root.className = 'explorer-root';
|
|
669
|
+
this.root = root;
|
|
670
|
+
const toolbar = document.createElement('div');
|
|
671
|
+
toolbar.className = 'explorer-toolbar';
|
|
672
|
+
this.toolbar = toolbar;
|
|
673
|
+
const breadcrumbs = document.createElement('div');
|
|
674
|
+
breadcrumbs.className = 'explorer-breadcrumbs';
|
|
675
|
+
this.breadcrumbs = breadcrumbs;
|
|
676
|
+
if (options.navigate) {
|
|
677
|
+
toolbar.appendChild(breadcrumbs);
|
|
678
|
+
}
|
|
679
|
+
// Right column of the toolbar — stacks view switches above the file
|
|
680
|
+
// actions.
|
|
681
|
+
const actionsCol = document.createElement('div');
|
|
682
|
+
actionsCol.className = 'explorer-actions-col';
|
|
683
|
+
const viewSwitch = document.createElement('div');
|
|
684
|
+
viewSwitch.className = 'explorer-view-switch';
|
|
685
|
+
const viewListBtn = this.createActionIcon('view_list');
|
|
686
|
+
createTooltip(viewListBtn, 'List view');
|
|
687
|
+
if (this.currentView === 'list')
|
|
688
|
+
viewListBtn.classList.add('active');
|
|
689
|
+
viewSwitch.appendChild(viewListBtn);
|
|
690
|
+
this.viewListBtn = viewListBtn;
|
|
691
|
+
const viewCozyBtn = this.createActionIcon('grid_view');
|
|
692
|
+
createTooltip(viewCozyBtn, 'Grid view');
|
|
693
|
+
if (this.currentView === 'cozy')
|
|
694
|
+
viewCozyBtn.classList.add('active');
|
|
695
|
+
viewSwitch.appendChild(viewCozyBtn);
|
|
696
|
+
this.viewCozyBtn = viewCozyBtn;
|
|
697
|
+
viewListBtn.onclick = () => { this.switchView('list'); this.options.onView?.('list'); };
|
|
698
|
+
viewCozyBtn.onclick = () => { this.switchView('cozy'); this.options.onView?.('cozy'); };
|
|
699
|
+
actionsCol.appendChild(viewSwitch);
|
|
700
|
+
const actions = document.createElement('div');
|
|
701
|
+
actions.className = 'explorer-actions';
|
|
702
|
+
if (options.upload) {
|
|
703
|
+
const uploadBtn = this.createActionIcon('upload_file');
|
|
704
|
+
createTooltip(uploadBtn, 'Upload files');
|
|
705
|
+
uploadBtn.onclick = () => this.openFilePicker();
|
|
706
|
+
actions.appendChild(uploadBtn);
|
|
707
|
+
}
|
|
708
|
+
if (options.createFolder) {
|
|
709
|
+
const folderBtn = this.createActionIcon('create_new_folder');
|
|
710
|
+
createTooltip(folderBtn, 'New folder');
|
|
711
|
+
folderBtn.onclick = () => this.createNewFolder();
|
|
712
|
+
actions.appendChild(folderBtn);
|
|
713
|
+
}
|
|
714
|
+
if (options.copyPaste) {
|
|
715
|
+
const copyBtn = this.createActionIcon('content_copy');
|
|
716
|
+
createTooltip(copyBtn, 'Copy selected');
|
|
717
|
+
copyBtn.style.opacity = '0.3';
|
|
718
|
+
copyBtn.style.pointerEvents = 'none';
|
|
719
|
+
copyBtn.onclick = () => {
|
|
720
|
+
this.clipboard = this.files.filter(f => f.selected).map(f => ({ file: f.file, path: f.path }));
|
|
721
|
+
if (this.clipboard.length > 0 && this.pasteBtn) {
|
|
722
|
+
gsap.to(this.pasteBtn, { duration: 0.3, opacity: 1, ease: 'power2.out' });
|
|
723
|
+
this.pasteBtn.style.pointerEvents = 'auto';
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
actions.appendChild(copyBtn);
|
|
727
|
+
this.copyBtn = copyBtn;
|
|
728
|
+
const pasteBtn = this.createActionIcon('content_paste');
|
|
729
|
+
createTooltip(pasteBtn, 'Paste');
|
|
730
|
+
pasteBtn.style.opacity = '0.3';
|
|
731
|
+
pasteBtn.style.pointerEvents = 'none';
|
|
732
|
+
pasteBtn.onclick = () => this.pasteFiles();
|
|
733
|
+
actions.appendChild(pasteBtn);
|
|
734
|
+
this.pasteBtn = pasteBtn;
|
|
735
|
+
}
|
|
736
|
+
actionsCol.appendChild(actions);
|
|
737
|
+
toolbar.appendChild(actionsCol);
|
|
738
|
+
root.appendChild(toolbar);
|
|
739
|
+
// Body
|
|
740
|
+
const body = document.createElement('div');
|
|
741
|
+
body.className = 'explorer-body';
|
|
742
|
+
this.body = body;
|
|
743
|
+
const loadingEl = document.createElement('div');
|
|
744
|
+
loadingEl.className = 'explorer-loading';
|
|
745
|
+
for (let i = 0; i < 4; i++) {
|
|
746
|
+
const skeleton = document.createElement('div');
|
|
747
|
+
skeleton.className = 'explorer-loading-skeleton';
|
|
748
|
+
loadingEl.appendChild(skeleton);
|
|
749
|
+
}
|
|
750
|
+
body.appendChild(loadingEl);
|
|
751
|
+
this.loadingEl = loadingEl;
|
|
752
|
+
const emptyEl = document.createElement('div');
|
|
753
|
+
emptyEl.className = 'explorer-empty';
|
|
754
|
+
emptyEl.style.display = 'none';
|
|
755
|
+
const emptyIcon = document.createElement('span');
|
|
756
|
+
emptyIcon.className = 'material-symbols-outlined';
|
|
757
|
+
emptyIcon.textContent = 'folder_open';
|
|
758
|
+
emptyEl.appendChild(emptyIcon);
|
|
759
|
+
const emptyText = document.createElement('div');
|
|
760
|
+
emptyText.textContent = 'No files';
|
|
761
|
+
emptyEl.appendChild(emptyText);
|
|
762
|
+
if (options.upload) {
|
|
763
|
+
const emptyHint = document.createElement('div');
|
|
764
|
+
emptyHint.className = 'explorer-empty-hint';
|
|
765
|
+
emptyHint.textContent = 'Drop files here or click upload';
|
|
766
|
+
emptyEl.appendChild(emptyHint);
|
|
767
|
+
}
|
|
768
|
+
body.appendChild(emptyEl);
|
|
769
|
+
this.emptyEl = emptyEl;
|
|
770
|
+
const listEl = document.createElement('div');
|
|
771
|
+
listEl.className = `explorer-list ${this.currentView === 'cozy' ? 'cozy' : ''}`;
|
|
772
|
+
body.appendChild(listEl);
|
|
773
|
+
this.listEl = listEl;
|
|
774
|
+
const dropzone = document.createElement('div');
|
|
775
|
+
dropzone.className = 'explorer-dropzone';
|
|
776
|
+
const dropIcon = document.createElement('span');
|
|
777
|
+
dropIcon.className = 'material-symbols-outlined';
|
|
778
|
+
dropIcon.textContent = 'upload_file';
|
|
779
|
+
dropzone.appendChild(dropIcon);
|
|
780
|
+
const dropText = document.createElement('span');
|
|
781
|
+
dropText.textContent = 'Drop to upload';
|
|
782
|
+
dropzone.appendChild(dropText);
|
|
783
|
+
body.appendChild(dropzone);
|
|
784
|
+
this.dropzone = dropzone;
|
|
785
|
+
root.appendChild(body);
|
|
786
|
+
// ── Drag-drop upload ──
|
|
787
|
+
if (options.upload) {
|
|
788
|
+
let dragCounter = 0;
|
|
789
|
+
body.addEventListener('dragenter', (e) => {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
dragCounter++;
|
|
792
|
+
if (dragCounter === 1) {
|
|
793
|
+
dropzone.style.display = 'flex';
|
|
794
|
+
gsap.fromTo(dropzone, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: 'power2.out' });
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
body.addEventListener('dragover', (e) => {
|
|
798
|
+
e.preventDefault();
|
|
799
|
+
});
|
|
800
|
+
body.addEventListener('dragleave', () => {
|
|
801
|
+
dragCounter--;
|
|
802
|
+
if (dragCounter <= 0) {
|
|
803
|
+
dragCounter = 0;
|
|
804
|
+
gsap.to(dropzone, { duration: 0.2, opacity: 0, ease: 'power2.in', onComplete: () => { dropzone.style.display = 'none'; } });
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
body.addEventListener('drop', (e) => {
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
dragCounter = 0;
|
|
810
|
+
gsap.to(dropzone, { duration: 0.2, opacity: 0, ease: 'power2.in', onComplete: () => { dropzone.style.display = 'none'; } });
|
|
811
|
+
const droppedFiles = e.dataTransfer?.files;
|
|
812
|
+
if (droppedFiles && droppedFiles.length > 0) {
|
|
813
|
+
this.uploadNativeFiles(Array.from(droppedFiles));
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
// ── Initial load ──
|
|
818
|
+
this.load(this.currentPath);
|
|
819
|
+
}
|
|
820
|
+
createActionIcon(icon) {
|
|
821
|
+
const span = document.createElement('span');
|
|
822
|
+
span.className = 'material-symbols-outlined';
|
|
823
|
+
span.textContent = icon;
|
|
824
|
+
span.onmouseenter = () => { gsap.to(span, { duration: 0.3, scale: 1.1, rotate: -10, ease: 'power2.inOut' }); };
|
|
825
|
+
span.onmouseleave = () => { gsap.to(span, { duration: 0.3, scale: 1, rotate: 0, ease: 'power2.inOut' }); };
|
|
826
|
+
return span;
|
|
827
|
+
}
|
|
828
|
+
switchView(view) {
|
|
829
|
+
this.currentView = view;
|
|
830
|
+
this.viewListBtn.classList.toggle('active', view === 'list');
|
|
831
|
+
this.viewCozyBtn.classList.toggle('active', view === 'cozy');
|
|
832
|
+
this.listEl.classList.toggle('cozy', view === 'cozy');
|
|
833
|
+
// Rebuild file elements with new view
|
|
834
|
+
this.load(this.currentPath);
|
|
835
|
+
}
|
|
836
|
+
updateBreadcrumbs() {
|
|
837
|
+
if (!this.options.navigate)
|
|
838
|
+
return;
|
|
839
|
+
this.breadcrumbs.innerHTML = '';
|
|
840
|
+
const homeIcon = document.createElement('span');
|
|
841
|
+
homeIcon.className = 'material-symbols-outlined explorer-breadcrumb-home';
|
|
842
|
+
homeIcon.textContent = 'home';
|
|
843
|
+
homeIcon.onclick = () => this.load('');
|
|
844
|
+
this.breadcrumbs.appendChild(homeIcon);
|
|
845
|
+
const normalized = normalizePath(this.currentPath);
|
|
846
|
+
if (!normalized)
|
|
847
|
+
return;
|
|
848
|
+
const parts = normalized.split('/');
|
|
849
|
+
let accumulated = '';
|
|
850
|
+
for (let i = 0; i < parts.length; i++) {
|
|
851
|
+
const sep = document.createElement('span');
|
|
852
|
+
sep.className = 'explorer-breadcrumb-sep';
|
|
853
|
+
sep.textContent = '/';
|
|
854
|
+
this.breadcrumbs.appendChild(sep);
|
|
855
|
+
accumulated += parts[i] + '/';
|
|
856
|
+
const crumb = document.createElement('span');
|
|
857
|
+
crumb.className = 'explorer-breadcrumb';
|
|
858
|
+
crumb.textContent = parts[i];
|
|
859
|
+
const target = accumulated;
|
|
860
|
+
if (i < parts.length - 1) {
|
|
861
|
+
crumb.onclick = () => this.load(target);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
crumb.classList.add('current');
|
|
865
|
+
}
|
|
866
|
+
this.breadcrumbs.appendChild(crumb);
|
|
867
|
+
}
|
|
868
|
+
// Scroll breadcrumbs to end
|
|
869
|
+
this.breadcrumbs.scrollLeft = this.breadcrumbs.scrollWidth;
|
|
870
|
+
}
|
|
871
|
+
updateSelectionState() {
|
|
872
|
+
const selected = this.files.filter(f => f.selected).map(f => f.file);
|
|
873
|
+
if (this.copyBtn) {
|
|
874
|
+
const hasSelection = selected.length > 0;
|
|
875
|
+
gsap.to(this.copyBtn, { duration: 0.2, opacity: hasSelection ? 1 : 0.3, ease: 'power2.out' });
|
|
876
|
+
this.copyBtn.style.pointerEvents = hasSelection ? 'auto' : 'none';
|
|
877
|
+
}
|
|
878
|
+
this.options.onSelect?.(selected);
|
|
879
|
+
}
|
|
880
|
+
async load(path) {
|
|
881
|
+
if (this.loading)
|
|
882
|
+
return;
|
|
883
|
+
this.loadGeneration++;
|
|
884
|
+
const gen = this.loadGeneration;
|
|
885
|
+
this.loading = true;
|
|
886
|
+
this.currentPath = path;
|
|
887
|
+
// Show loading
|
|
888
|
+
this.listEl.innerHTML = '';
|
|
889
|
+
this.emptyEl.style.display = 'none';
|
|
890
|
+
this.loadingEl.style.display = 'flex';
|
|
891
|
+
gsap.fromTo(this.loadingEl, { opacity: 0 }, { opacity: 1, duration: 0.3 });
|
|
892
|
+
this.files = [];
|
|
893
|
+
this.updateBreadcrumbs();
|
|
894
|
+
this.options.onNavigate?.(path);
|
|
895
|
+
try {
|
|
896
|
+
const repository = this.authorization.repository;
|
|
897
|
+
const pulled = repository
|
|
898
|
+
? [await repository.open(path)].flat().map((file) => ({ file, path: 'path' in file.authorization ? file.authorization.path : path }))
|
|
899
|
+
: await Promise.all((await Fauna.Drive.list(this.authorization.authorization, path, this.authorization.group)).map(async (p) => {
|
|
900
|
+
try {
|
|
901
|
+
// list() returns full paths, not relative
|
|
902
|
+
const file = await Fauna.Drive.pull({
|
|
903
|
+
authorization: this.authorization.authorization,
|
|
904
|
+
path: p,
|
|
905
|
+
group: this.authorization.group
|
|
906
|
+
});
|
|
907
|
+
return { file, path: p };
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}));
|
|
913
|
+
if (gen !== this.loadGeneration)
|
|
914
|
+
return;
|
|
915
|
+
// Hide loading
|
|
916
|
+
this.loadingEl.style.display = 'none';
|
|
917
|
+
this.listEl.innerHTML = '';
|
|
918
|
+
this.files = [];
|
|
919
|
+
const validFiles = pulled.filter(Boolean);
|
|
920
|
+
if (validFiles.length === 0) {
|
|
921
|
+
this.emptyEl.style.display = 'flex';
|
|
922
|
+
gsap.from(this.emptyEl, { duration: 0.3, opacity: 0, ease: 'power2.out' });
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
this.emptyEl.style.display = 'none';
|
|
926
|
+
for (let i = 0; i < validFiles.length; i++) {
|
|
927
|
+
const { file, path: filePath } = validFiles[i];
|
|
928
|
+
const isFolder = file.contentType === 'application/x-directory';
|
|
929
|
+
const fileEl = createFileElement(file, {
|
|
930
|
+
view: this.currentView,
|
|
931
|
+
remove: this.options.remove,
|
|
932
|
+
rename: this.options.rename,
|
|
933
|
+
download: this.options.download,
|
|
934
|
+
open: isFolder ? !!this.options.navigate : !!this.options.onOpen,
|
|
935
|
+
share: !!this.options.onShare,
|
|
936
|
+
onShare: this.options.onShare ? (code) => this.options.onShare(fileEl.getFile(), code) : undefined,
|
|
937
|
+
onFolderOpen: isFolder && this.options.navigate ? () => {
|
|
938
|
+
const auth = fileEl.getFile().authorization;
|
|
939
|
+
const livePath = 'path' in auth ? auth.path : filePath;
|
|
940
|
+
this.load(livePath.endsWith('/') ? livePath : livePath + '/');
|
|
941
|
+
} : undefined,
|
|
942
|
+
onOpen: !isFolder && this.options.onOpen ? () => {
|
|
943
|
+
const auth = fileEl.getFile().authorization;
|
|
944
|
+
const livePath = 'path' in auth ? auth.path : filePath;
|
|
945
|
+
this.options.onOpen(fileEl.getFile(), livePath);
|
|
946
|
+
} : undefined,
|
|
947
|
+
onSelect: () => {
|
|
948
|
+
const entry = this.files.find(f => f.file === file);
|
|
949
|
+
if (entry) {
|
|
950
|
+
entry.selected = fileEl.isSelected();
|
|
951
|
+
this.updateSelectionState();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
const element = fileEl.getElement();
|
|
956
|
+
// Double-click: folders navigate, files open
|
|
957
|
+
if (isFolder && this.options.navigate) {
|
|
958
|
+
element.addEventListener('dblclick', (e) => {
|
|
959
|
+
e.stopPropagation();
|
|
960
|
+
const auth = fileEl.getFile().authorization;
|
|
961
|
+
const livePath = 'path' in auth ? auth.path : filePath;
|
|
962
|
+
this.load(livePath.endsWith('/') ? livePath : livePath + '/');
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
else if (!isFolder && this.options.onOpen) {
|
|
966
|
+
element.addEventListener('dblclick', (e) => {
|
|
967
|
+
e.stopPropagation();
|
|
968
|
+
const auth = fileEl.getFile().authorization;
|
|
969
|
+
const livePath = 'path' in auth ? auth.path : filePath;
|
|
970
|
+
this.options.onOpen(fileEl.getFile(), livePath);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
this.files.push({ file, path: filePath, element, selected: false });
|
|
974
|
+
this.listEl.appendChild(element);
|
|
975
|
+
gsap.from(element, { duration: 0.3, opacity: 0, y: 10, ease: 'power2.out', delay: i * 0.04 });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
catch (err) {
|
|
980
|
+
if (gen !== this.loadGeneration)
|
|
981
|
+
return;
|
|
982
|
+
this.loadingEl.style.display = 'none';
|
|
983
|
+
this.emptyEl.style.display = 'none';
|
|
984
|
+
const errorEl = document.createElement('div');
|
|
985
|
+
errorEl.className = 'explorer-empty';
|
|
986
|
+
errorEl.style.color = 'var(--error, #DC322F)';
|
|
987
|
+
const errorIcon = document.createElement('span');
|
|
988
|
+
errorIcon.className = 'material-symbols-outlined';
|
|
989
|
+
errorIcon.textContent = 'error';
|
|
990
|
+
errorEl.appendChild(errorIcon);
|
|
991
|
+
const errorText = document.createElement('div');
|
|
992
|
+
errorText.textContent = 'Failed to load files';
|
|
993
|
+
errorEl.appendChild(errorText);
|
|
994
|
+
const retryBtn = document.createElement('div');
|
|
995
|
+
retryBtn.className = 'explorer-retry';
|
|
996
|
+
retryBtn.textContent = 'Retry';
|
|
997
|
+
retryBtn.onclick = () => {
|
|
998
|
+
errorEl.remove();
|
|
999
|
+
this.load(this.currentPath);
|
|
1000
|
+
};
|
|
1001
|
+
errorEl.appendChild(retryBtn);
|
|
1002
|
+
this.listEl.appendChild(errorEl);
|
|
1003
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
if (gen === this.loadGeneration) {
|
|
1007
|
+
this.loading = false;
|
|
1008
|
+
const wanted = this.wantedPath;
|
|
1009
|
+
this.wantedPath = null;
|
|
1010
|
+
if (wanted !== null && wanted !== this.currentPath)
|
|
1011
|
+
void this.load(wanted);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
openFilePicker() {
|
|
1016
|
+
const input = document.createElement('input');
|
|
1017
|
+
input.type = 'file';
|
|
1018
|
+
input.multiple = true;
|
|
1019
|
+
input.style.display = 'none';
|
|
1020
|
+
input.addEventListener('change', () => {
|
|
1021
|
+
if (input.files && input.files.length > 0) {
|
|
1022
|
+
this.uploadNativeFiles(Array.from(input.files));
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
document.body.appendChild(input);
|
|
1026
|
+
input.click();
|
|
1027
|
+
input.remove();
|
|
1028
|
+
}
|
|
1029
|
+
async uploadNativeFiles(nativeFiles) {
|
|
1030
|
+
const repository = this.authorization.repository;
|
|
1031
|
+
if (repository && !this.currentPath.startsWith('/repository/')) {
|
|
1032
|
+
this.options.onError?.(new Error('Uploads only go into /repository.'));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
for (const nativeFile of nativeFiles) {
|
|
1036
|
+
try {
|
|
1037
|
+
const filePath = this.currentPath + nativeFile.name;
|
|
1038
|
+
const wrapped = await Fauna.Drive.wrap(repository
|
|
1039
|
+
? { authorization: this.authorization.authorization, repository: repository.id, path: filePath, group: this.authorization.group }
|
|
1040
|
+
: { authorization: this.authorization.authorization, path: filePath, group: this.authorization.group }, nativeFile);
|
|
1041
|
+
const fileEl = createFileElement(wrapped, {
|
|
1042
|
+
view: this.currentView,
|
|
1043
|
+
remove: this.options.remove,
|
|
1044
|
+
rename: this.options.rename,
|
|
1045
|
+
download: this.options.download,
|
|
1046
|
+
open: !!this.options.onOpen,
|
|
1047
|
+
share: !!this.options.onShare,
|
|
1048
|
+
onOpen: this.options.onOpen ? () => this.options.onOpen(wrapped, filePath) : undefined,
|
|
1049
|
+
onShare: this.options.onShare ? (code) => this.options.onShare(wrapped, code) : undefined
|
|
1050
|
+
});
|
|
1051
|
+
const element = fileEl.getElement();
|
|
1052
|
+
this.files.push({ file: wrapped, path: filePath, element, selected: false });
|
|
1053
|
+
this.listEl.prepend(element);
|
|
1054
|
+
gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
|
|
1055
|
+
// Hide empty state if showing
|
|
1056
|
+
this.emptyEl.style.display = 'none';
|
|
1057
|
+
}
|
|
1058
|
+
catch (err) {
|
|
1059
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async createNewFolder() {
|
|
1064
|
+
// Find unique name
|
|
1065
|
+
const existingNames = this.files.map(f => f.file.name);
|
|
1066
|
+
let name = 'Untitled';
|
|
1067
|
+
let counter = 0;
|
|
1068
|
+
while (existingNames.includes(name)) {
|
|
1069
|
+
counter++;
|
|
1070
|
+
name = `Untitled-${counter}`;
|
|
1071
|
+
}
|
|
1072
|
+
const repository = this.authorization.repository;
|
|
1073
|
+
if (repository && !this.currentPath.startsWith('/repository/')) {
|
|
1074
|
+
this.options.onError?.(new Error('Folders can only be created in /repository.'));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const folderPath = this.currentPath + name + '/';
|
|
1079
|
+
const folderFile = await Fauna.Drive.folder(repository
|
|
1080
|
+
? { authorization: this.authorization.authorization, repository: repository.id, path: folderPath, group: this.authorization.group }
|
|
1081
|
+
: { authorization: this.authorization.authorization, path: folderPath, group: this.authorization.group });
|
|
1082
|
+
const fileEl = createFileElement(folderFile, {
|
|
1083
|
+
view: this.currentView,
|
|
1084
|
+
remove: this.options.remove,
|
|
1085
|
+
rename: this.options.rename,
|
|
1086
|
+
download: this.options.download,
|
|
1087
|
+
open: !!this.options.navigate,
|
|
1088
|
+
share: !!this.options.onShare,
|
|
1089
|
+
onShare: this.options.onShare ? (code) => this.options.onShare(folderFile, code) : undefined,
|
|
1090
|
+
onFolderOpen: this.options.navigate ? () => {
|
|
1091
|
+
const auth = fileEl.getFile().authorization;
|
|
1092
|
+
const livePath = 'path' in auth ? auth.path : folderPath;
|
|
1093
|
+
this.load(livePath.endsWith('/') ? livePath : livePath + '/');
|
|
1094
|
+
} : undefined
|
|
1095
|
+
});
|
|
1096
|
+
const element = fileEl.getElement();
|
|
1097
|
+
this.files.push({ file: folderFile, path: folderPath, element, selected: false });
|
|
1098
|
+
this.listEl.prepend(element);
|
|
1099
|
+
gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
|
|
1100
|
+
this.emptyEl.style.display = 'none';
|
|
1101
|
+
}
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async pasteFiles() {
|
|
1107
|
+
if (this.clipboard.length === 0)
|
|
1108
|
+
return;
|
|
1109
|
+
for (const item of this.clipboard) {
|
|
1110
|
+
try {
|
|
1111
|
+
const newPath = this.currentPath + item.file.name + (item.file.contentType === 'application/x-directory' ? '/' : '');
|
|
1112
|
+
await item.file.copy(newPath);
|
|
1113
|
+
const newFile = await Fauna.Drive.pull({
|
|
1114
|
+
authorization: this.authorization.authorization,
|
|
1115
|
+
path: newPath,
|
|
1116
|
+
group: this.authorization.group
|
|
1117
|
+
});
|
|
1118
|
+
const isFolder = newFile.contentType === 'application/x-directory';
|
|
1119
|
+
const fileEl = createFileElement(newFile, {
|
|
1120
|
+
view: this.currentView,
|
|
1121
|
+
remove: this.options.remove,
|
|
1122
|
+
rename: this.options.rename,
|
|
1123
|
+
download: this.options.download,
|
|
1124
|
+
open: isFolder ? !!this.options.navigate : !!this.options.onOpen,
|
|
1125
|
+
share: !!this.options.onShare,
|
|
1126
|
+
onShare: this.options.onShare ? (code) => this.options.onShare(newFile, code) : undefined,
|
|
1127
|
+
onFolderOpen: isFolder && this.options.navigate
|
|
1128
|
+
? () => {
|
|
1129
|
+
const auth = fileEl.getFile().authorization;
|
|
1130
|
+
const livePath = 'path' in auth ? auth.path : newPath;
|
|
1131
|
+
this.load(livePath.endsWith('/') ? livePath : livePath + '/');
|
|
1132
|
+
}
|
|
1133
|
+
: undefined,
|
|
1134
|
+
onOpen: !isFolder && this.options.onOpen ? () => this.options.onOpen(newFile, newPath) : undefined
|
|
1135
|
+
});
|
|
1136
|
+
const element = fileEl.getElement();
|
|
1137
|
+
this.files.push({ file: newFile, path: newPath, element, selected: false });
|
|
1138
|
+
this.listEl.prepend(element);
|
|
1139
|
+
gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
|
|
1140
|
+
this.emptyEl.style.display = 'none';
|
|
1141
|
+
}
|
|
1142
|
+
catch (err) {
|
|
1143
|
+
this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
this.clipboard = [];
|
|
1147
|
+
if (this.pasteBtn) {
|
|
1148
|
+
gsap.to(this.pasteBtn, { duration: 0.3, opacity: 0, ease: 'power2.in' });
|
|
1149
|
+
this.pasteBtn.style.pointerEvents = 'none';
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// ── Public API ──
|
|
1153
|
+
getElement() {
|
|
1154
|
+
return this.root;
|
|
1155
|
+
}
|
|
1156
|
+
getPath() {
|
|
1157
|
+
return this.currentPath;
|
|
1158
|
+
}
|
|
1159
|
+
/** Go to a folder. One asked for while another loads goes next, instead of being dropped. */
|
|
1160
|
+
async setPath(path) {
|
|
1161
|
+
if (this.loading) {
|
|
1162
|
+
this.wantedPath = path;
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
await this.load(path);
|
|
1166
|
+
}
|
|
1167
|
+
async refresh() {
|
|
1168
|
+
await this.load(this.currentPath);
|
|
1169
|
+
}
|
|
1170
|
+
getSelected() {
|
|
1171
|
+
return this.files.filter(f => f.selected).map(f => f.file);
|
|
1172
|
+
}
|
|
1173
|
+
setView(view) {
|
|
1174
|
+
this.switchView(view);
|
|
1175
|
+
}
|
|
1176
|
+
destroy() {
|
|
1177
|
+
this.destroyed = true;
|
|
1178
|
+
this.loadGeneration++;
|
|
1179
|
+
this.root.remove();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// Owns an ExplorerEngine for its lifetime: builds it during the commit phase so
|
|
1183
|
+
// the engine's root exists synchronously by the time createFileExplorer()
|
|
1184
|
+
// returns (paired with the flushSync mount below), and tears it down on
|
|
1185
|
+
// unmount. The engine's root is appended into a host <div> ref.
|
|
1186
|
+
export const FileExplorer = forwardRef(function FileExplorer({ authorization, options }, ref) {
|
|
1187
|
+
const hostRef = useRef(null);
|
|
1188
|
+
const engineRef = useRef(null);
|
|
1189
|
+
useLayoutEffect(() => {
|
|
1190
|
+
if (!hostRef.current)
|
|
1191
|
+
return;
|
|
1192
|
+
const engine = new ExplorerEngine(authorization, options);
|
|
1193
|
+
engineRef.current = engine;
|
|
1194
|
+
hostRef.current.appendChild(engine.getElement());
|
|
1195
|
+
return () => {
|
|
1196
|
+
engine.destroy();
|
|
1197
|
+
engineRef.current = null;
|
|
1198
|
+
};
|
|
1199
|
+
// Authorization + options are read once at mount (matching the imperative
|
|
1200
|
+
// widget's one-time configuration); consumers rebuild to change them.
|
|
1201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1202
|
+
}, []);
|
|
1203
|
+
useImperativeHandle(ref, () => ({
|
|
1204
|
+
getElement: () => engineRef.current?.getElement() ?? hostRef.current,
|
|
1205
|
+
getPath: () => engineRef.current?.getPath() ?? authorization.path,
|
|
1206
|
+
setPath: (path) => engineRef.current?.setPath(path) ?? Promise.resolve(),
|
|
1207
|
+
refresh: () => engineRef.current?.refresh() ?? Promise.resolve(),
|
|
1208
|
+
getSelected: () => engineRef.current?.getSelected() ?? [],
|
|
1209
|
+
setView: (view) => engineRef.current?.setView(view),
|
|
1210
|
+
destroy: () => engineRef.current?.destroy(),
|
|
1211
|
+
}), []);
|
|
1212
|
+
return _jsx("div", { className: "explorer-mount", ref: hostRef, style: { display: 'contents' } });
|
|
1213
|
+
});
|
|
1214
|
+
export function createFileExplorer(authorization, options = {}) {
|
|
1215
|
+
// Host element the React root mounts into. Because <FileExplorer> uses
|
|
1216
|
+
// display:contents and appends the engine root as its own child, this host
|
|
1217
|
+
// is effectively transparent — getElement() returns the engine root, so
|
|
1218
|
+
// consumers that query `.explorer-toolbar` off the returned element (e.g.
|
|
1219
|
+
// the Drive tab's scope-selector injection) work unchanged.
|
|
1220
|
+
const container = document.createElement('div');
|
|
1221
|
+
container.style.display = 'contents';
|
|
1222
|
+
const root = createRoot(container);
|
|
1223
|
+
let instance = null;
|
|
1224
|
+
const ready = [];
|
|
1225
|
+
const handleRef = (inst) => {
|
|
1226
|
+
instance = inst;
|
|
1227
|
+
if (inst)
|
|
1228
|
+
ready.push(inst);
|
|
1229
|
+
};
|
|
1230
|
+
// React 19's createRoot().render() is asynchronous by default; flushSync
|
|
1231
|
+
// forces a synchronous commit + layout effects so the engine (built in a
|
|
1232
|
+
// useLayoutEffect) is live before this function returns.
|
|
1233
|
+
flushSync(() => root.render(_jsx(FileExplorer, { ref: handleRef, authorization: authorization, options: options })));
|
|
1234
|
+
const get = () => instance ?? ready[ready.length - 1] ?? null;
|
|
1235
|
+
return {
|
|
1236
|
+
getElement: () => get()?.getElement() ?? container,
|
|
1237
|
+
getPath: () => get()?.getPath() ?? authorization.path,
|
|
1238
|
+
setPath: (path) => get()?.setPath(path) ?? Promise.resolve(),
|
|
1239
|
+
refresh: () => get()?.refresh() ?? Promise.resolve(),
|
|
1240
|
+
getSelected: () => get()?.getSelected() ?? [],
|
|
1241
|
+
setView: (view) => get()?.setView(view),
|
|
1242
|
+
};
|
|
1243
|
+
}
|