@useupup/core 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +75 -0
- package/dist/browser-SXR426OQ.cjs +10 -0
- package/dist/browser-WD6T5ABJ.js +1 -0
- package/dist/chunk-2H5ICKFT.cjs +122 -0
- package/dist/chunk-6HF3I2JS.cjs +39 -0
- package/dist/chunk-6S5DQDQH.js +212 -0
- package/dist/chunk-BMQC27OE.cjs +92 -0
- package/dist/chunk-BSGQGINR.cjs +219 -0
- package/dist/chunk-CPKZ2CXD.cjs +179 -0
- package/dist/chunk-DTKDIMKT.cjs +573 -0
- package/dist/chunk-DYE5XJ6Z.cjs +2134 -0
- package/dist/chunk-E3ZCNOXG.cjs +74 -0
- package/dist/chunk-FEQY2WYY.cjs +2377 -0
- package/dist/chunk-FMURBCZ7.js +120 -0
- package/dist/chunk-FYQQRULL.js +37 -0
- package/dist/chunk-GE4LIVXP.js +133 -0
- package/dist/chunk-HFR5HZJZ.js +56 -0
- package/dist/chunk-I7ULPCA2.js +72 -0
- package/dist/chunk-MHYXDLZR.js +569 -0
- package/dist/chunk-N2SLHVAW.cjs +61 -0
- package/dist/chunk-OOPDEOTM.js +169 -0
- package/dist/chunk-PMBR3OUW.js +2359 -0
- package/dist/chunk-QK7HZIVY.cjs +144 -0
- package/dist/chunk-T2AQHY7V.cjs +59 -0
- package/dist/chunk-TRCP5FUR.js +90 -0
- package/dist/chunk-U7KL5M2H.cjs +374 -0
- package/dist/chunk-UXNPM54W.js +2108 -0
- package/dist/chunk-VV6QCQLL.js +365 -0
- package/dist/chunk-ZMS2HXLB.js +56 -0
- package/dist/context-shapes-123dd3b6.d.cts +207 -0
- package/dist/context-shapes-27b24c8f.d.ts +207 -0
- package/dist/contracts-pipeline-cf177270.d.cts +78 -0
- package/dist/contracts-pipeline-cf177270.d.ts +78 -0
- package/dist/contracts-strategies-378a16b7.d.cts +168 -0
- package/dist/contracts-strategies-378a16b7.d.ts +168 -0
- package/dist/contracts.cjs +181 -0
- package/dist/contracts.d.cts +11 -0
- package/dist/contracts.d.ts +11 -0
- package/dist/contracts.js +4 -0
- package/dist/core-141335ac.d.ts +651 -0
- package/dist/core-bea2ceca.d.cts +651 -0
- package/dist/create-worker-provider-AA5SRII6.js +58 -0
- package/dist/create-worker-provider-KJNUSSTW.cjs +60 -0
- package/dist/eligibility-ONLNJ6FB.cjs +10 -0
- package/dist/eligibility-ULFC3E4G.js +8 -0
- package/dist/error-code-map-9c44a425.d.ts +62 -0
- package/dist/error-code-map-dd5becfa.d.cts +62 -0
- package/dist/i18n/index.cjs +90 -0
- package/dist/i18n/index.d.cts +3 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +1 -0
- package/dist/index.cjs +1770 -0
- package/dist/index.d.cts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +1589 -0
- package/dist/internal.cjs +2407 -0
- package/dist/internal.d.cts +595 -0
- package/dist/internal.d.ts +595 -0
- package/dist/internal.js +2275 -0
- package/dist/pipeline/index.cjs +10 -0
- package/dist/pipeline/index.d.cts +11 -0
- package/dist/pipeline/index.d.ts +11 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline-worker.cjs +199 -0
- package/dist/pipeline-worker.d.cts +2 -0
- package/dist/pipeline-worker.d.ts +2 -0
- package/dist/pipeline-worker.js +197 -0
- package/dist/resolve-locale-81feef21.d.ts +20 -0
- package/dist/resolve-locale-9cb63600.d.cts +20 -0
- package/dist/resolve-theme-01758105.d.ts +53 -0
- package/dist/resolve-theme-66cf8d98.d.cts +53 -0
- package/dist/server-mode-drive-controller-b5f8ee12.d.cts +180 -0
- package/dist/server-mode-drive-controller-b5f8ee12.d.ts +180 -0
- package/dist/steps/compress.cjs +66 -0
- package/dist/steps/compress.d.cts +10 -0
- package/dist/steps/compress.d.ts +10 -0
- package/dist/steps/compress.js +64 -0
- package/dist/steps/exif.cjs +44 -0
- package/dist/steps/exif.d.cts +5 -0
- package/dist/steps/exif.d.ts +5 -0
- package/dist/steps/exif.js +42 -0
- package/dist/steps/hash.cjs +44 -0
- package/dist/steps/hash.d.cts +5 -0
- package/dist/steps/hash.d.ts +5 -0
- package/dist/steps/hash.js +42 -0
- package/dist/steps/heic.cjs +59 -0
- package/dist/steps/heic.d.cts +5 -0
- package/dist/steps/heic.d.ts +5 -0
- package/dist/steps/heic.js +57 -0
- package/dist/steps/thumbnail.cjs +54 -0
- package/dist/steps/thumbnail.d.cts +10 -0
- package/dist/steps/thumbnail.d.ts +10 -0
- package/dist/steps/thumbnail.js +52 -0
- package/dist/strategies/index.cjs +82 -0
- package/dist/strategies/index.d.cts +187 -0
- package/dist/strategies/index.d.ts +187 -0
- package/dist/strategies/index.js +60 -0
- package/dist/strategies/tus-upload.cjs +14 -0
- package/dist/strategies/tus-upload.d.cts +15 -0
- package/dist/strategies/tus-upload.d.ts +15 -0
- package/dist/strategies/tus-upload.js +5 -0
- package/dist/theme/index.cjs +38 -0
- package/dist/theme/index.d.cts +3 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.js +1 -0
- package/dist/theme-store-2e56a674.d.cts +39 -0
- package/dist/theme-store-f13e0315.d.ts +39 -0
- package/dist/token-endpoint-5fdb4a35.d.ts +20 -0
- package/dist/token-endpoint-a7d15ab6.d.cts +20 -0
- package/dist/ui-translations-c5d00392.d.cts +455 -0
- package/dist/ui-translations-c5d00392.d.ts +455 -0
- package/dist/uploader-props-72242002.d.ts +340 -0
- package/dist/uploader-props-7f956e60.d.cts +340 -0
- package/dist/vars-d3421f6c.d.cts +240 -0
- package/dist/vars-d3421f6c.d.ts +240 -0
- package/package.json +182 -0
package/dist/internal.js
ADDED
|
@@ -0,0 +1,2275 @@
|
|
|
1
|
+
export { BrowserRuntime } from './chunk-HFR5HZJZ.js';
|
|
2
|
+
import { resolveAccept, isUploadActive, GoogleDrivePlugin, DropboxPlugin, BoxPlugin, OneDrivePlugin } from './chunk-PMBR3OUW.js';
|
|
3
|
+
export { CrashRecoveryManager, EventEmitter, FileManager, IndexedDBStorage, PluginManager, UploadManager, isFileRemovalLocked, isUploadActive, isUploadIdle } from './chunk-PMBR3OUW.js';
|
|
4
|
+
export { PipelineEngine } from './chunk-FYQQRULL.js';
|
|
5
|
+
export { DirectUpload, TokenEndpointCredentials, clearAllSessions, fileFingerprint, loadSession, removeSession, saveSession, updateSessionProgress } from './chunk-OOPDEOTM.js';
|
|
6
|
+
import { FileSource } from './chunk-ZMS2HXLB.js';
|
|
7
|
+
import { UpupError, UpupNetworkError } from './chunk-GE4LIVXP.js';
|
|
8
|
+
import { LOCALE_META, resolveLocaleBundle, createTranslator, enUS, flattenTranslatorToUiTranslations } from './chunk-UXNPM54W.js';
|
|
9
|
+
export { buildFallbackChain, resolveMessage } from './chunk-UXNPM54W.js';
|
|
10
|
+
import { ThemeStore } from './chunk-VV6QCQLL.js';
|
|
11
|
+
export { ThemeStore, tokensToVarRefs } from './chunk-VV6QCQLL.js';
|
|
12
|
+
import { clsx } from 'clsx';
|
|
13
|
+
import { twMerge } from 'tailwind-merge';
|
|
14
|
+
|
|
15
|
+
// src/sse-processor.ts
|
|
16
|
+
function withProcessingKey(endpoint, key) {
|
|
17
|
+
const hashIndex = endpoint.indexOf("#");
|
|
18
|
+
const beforeHash = hashIndex >= 0 ? endpoint.slice(0, hashIndex) : endpoint;
|
|
19
|
+
const hash = hashIndex >= 0 ? endpoint.slice(hashIndex) : "";
|
|
20
|
+
const queryIndex = beforeHash.indexOf("?");
|
|
21
|
+
const path = queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
|
|
22
|
+
const query = queryIndex >= 0 ? beforeHash.slice(queryIndex + 1) : "";
|
|
23
|
+
const params = new URLSearchParams(query);
|
|
24
|
+
params.set("key", key);
|
|
25
|
+
return `${path}?${params.toString()}${hash}`;
|
|
26
|
+
}
|
|
27
|
+
var SSEProcessor = class {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.sources = /* @__PURE__ */ new Map();
|
|
30
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
31
|
+
}
|
|
32
|
+
subscribe(key, endpoint, onMessage, onError, timeout = 6e4) {
|
|
33
|
+
if (this.sources.has(key)) return;
|
|
34
|
+
const url = withProcessingKey(endpoint, key);
|
|
35
|
+
const source = new EventSource(url);
|
|
36
|
+
this.sources.set(key, source);
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
this.unsubscribe(key);
|
|
39
|
+
};
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
onError(new Error(`Processing timed out for ${key}`));
|
|
42
|
+
cleanup();
|
|
43
|
+
}, timeout);
|
|
44
|
+
this.timers.set(key, timer);
|
|
45
|
+
source.onmessage = (event) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
this.timers.delete(key);
|
|
48
|
+
const raw = event.data;
|
|
49
|
+
let data;
|
|
50
|
+
try {
|
|
51
|
+
data = typeof raw === "string" ? JSON.parse(raw) : { raw };
|
|
52
|
+
} catch {
|
|
53
|
+
data = { raw };
|
|
54
|
+
}
|
|
55
|
+
onMessage(data);
|
|
56
|
+
cleanup();
|
|
57
|
+
};
|
|
58
|
+
source.onerror = () => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
this.timers.delete(key);
|
|
61
|
+
onError(new Error(`Processing stream failed for ${key}`));
|
|
62
|
+
cleanup();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
unsubscribe(key) {
|
|
66
|
+
const source = this.sources.get(key);
|
|
67
|
+
if (source) {
|
|
68
|
+
source.close();
|
|
69
|
+
this.sources.delete(key);
|
|
70
|
+
}
|
|
71
|
+
const timer = this.timers.get(key);
|
|
72
|
+
if (timer) {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
this.timers.delete(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
destroy() {
|
|
78
|
+
this.sources.forEach((s) => {
|
|
79
|
+
s.close();
|
|
80
|
+
});
|
|
81
|
+
this.sources.clear();
|
|
82
|
+
this.timers.forEach((t) => {
|
|
83
|
+
clearTimeout(t);
|
|
84
|
+
});
|
|
85
|
+
this.timers.clear();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/utils/encoder.ts
|
|
90
|
+
function b64EncodeUnicode(str) {
|
|
91
|
+
return btoa(
|
|
92
|
+
encodeURIComponent(str).replace(
|
|
93
|
+
/%([0-9A-F]{2})/g,
|
|
94
|
+
(_, p1) => String.fromCharCode(parseInt(p1, 16))
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/utils/file-helpers.ts
|
|
100
|
+
var fileAppendParams = (file) => {
|
|
101
|
+
const partial = file;
|
|
102
|
+
const rel = partial.relativePath || file.webkitRelativePath || file.name;
|
|
103
|
+
Object.assign(file, {
|
|
104
|
+
id: partial.id || b64EncodeUnicode(rel),
|
|
105
|
+
url: partial.url || URL.createObjectURL(file),
|
|
106
|
+
source: partial.source || "local" /* LOCAL */,
|
|
107
|
+
status: partial.status || "READY" /* READY */,
|
|
108
|
+
metadata: partial.metadata || {}
|
|
109
|
+
});
|
|
110
|
+
return file;
|
|
111
|
+
};
|
|
112
|
+
var revokeFileUrl = (file) => {
|
|
113
|
+
if (file.url?.startsWith("blob:")) {
|
|
114
|
+
URL.revokeObjectURL(file.url);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/utils/image-helpers.ts
|
|
119
|
+
function dataURLtoBlob(dataURL) {
|
|
120
|
+
const [header = "", base64Data = ""] = dataURL.split(",");
|
|
121
|
+
const colon = header.indexOf(":");
|
|
122
|
+
const semi = colon === -1 ? -1 : header.indexOf(";", colon + 1);
|
|
123
|
+
const mime = semi === -1 ? "application/octet-stream" : header.slice(colon + 1, semi);
|
|
124
|
+
const binary = atob(base64Data);
|
|
125
|
+
const bytes = new Uint8Array(binary.length);
|
|
126
|
+
for (let i = 0; i < binary.length; i++) {
|
|
127
|
+
bytes[i] = binary.charCodeAt(i);
|
|
128
|
+
}
|
|
129
|
+
return new Blob([bytes], { type: mime });
|
|
130
|
+
}
|
|
131
|
+
function blobToUploadFile(blob, original, output) {
|
|
132
|
+
const fileName = output?.fileName ? output.fileName(original) : original.name;
|
|
133
|
+
const file = new File([blob], fileName, {
|
|
134
|
+
type: blob.type || original.type,
|
|
135
|
+
lastModified: Date.now()
|
|
136
|
+
});
|
|
137
|
+
const fileWithParams = file;
|
|
138
|
+
fileWithParams.id = original.id;
|
|
139
|
+
fileWithParams.url = URL.createObjectURL(file);
|
|
140
|
+
fileWithParams.source = original.source;
|
|
141
|
+
fileWithParams.status = original.status;
|
|
142
|
+
fileWithParams.metadata = original.metadata;
|
|
143
|
+
if (original.key !== void 0) fileWithParams.key = original.key;
|
|
144
|
+
const originalLegacy = original;
|
|
145
|
+
const targetLegacy = fileWithParams;
|
|
146
|
+
if (originalLegacy.fileHash !== void 0)
|
|
147
|
+
targetLegacy.fileHash = originalLegacy.fileHash;
|
|
148
|
+
if (originalLegacy.thumbnail !== void 0)
|
|
149
|
+
targetLegacy.thumbnail = originalLegacy.thumbnail;
|
|
150
|
+
return fileWithParams;
|
|
151
|
+
}
|
|
152
|
+
function revokeAndReplace(map, fileId, newFile) {
|
|
153
|
+
const oldFile = map.get(fileId);
|
|
154
|
+
if (oldFile) {
|
|
155
|
+
revokeFileUrl(oldFile);
|
|
156
|
+
}
|
|
157
|
+
const next = new Map(map);
|
|
158
|
+
next.set(fileId, newFile);
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/orchestrator/upload-speed.ts
|
|
163
|
+
var SPEED_SAMPLE_WINDOW_MS = 3e3;
|
|
164
|
+
function computeSpeedEta(args) {
|
|
165
|
+
const { now, uploadedBytes, totalBytes, previousSpeed, previousEta } = args;
|
|
166
|
+
const samples = [
|
|
167
|
+
...args.samples,
|
|
168
|
+
{ time: now, bytes: uploadedBytes }
|
|
169
|
+
].filter((s) => s.time >= now - SPEED_SAMPLE_WINDOW_MS);
|
|
170
|
+
let speed = previousSpeed;
|
|
171
|
+
let eta = previousEta;
|
|
172
|
+
if (samples.length >= 2) {
|
|
173
|
+
const oldest = samples[0];
|
|
174
|
+
const newest = samples[samples.length - 1];
|
|
175
|
+
if (oldest && newest) {
|
|
176
|
+
const elapsed = (newest.time - oldest.time) / 1e3;
|
|
177
|
+
if (elapsed > 0) {
|
|
178
|
+
speed = Math.max(0, (newest.bytes - oldest.bytes) / elapsed);
|
|
179
|
+
const remaining = totalBytes - uploadedBytes;
|
|
180
|
+
eta = speed > 0 ? Math.ceil(remaining / speed) : 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { samples, speed, eta };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/orchestrator/uploader-orchestrator.ts
|
|
188
|
+
var UploaderOrchestrator = class {
|
|
189
|
+
constructor(core, callbacks) {
|
|
190
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
191
|
+
this.unsubs = [];
|
|
192
|
+
this.disposed = false;
|
|
193
|
+
/** useSyncExternalStore-compatible subscribe (bound). */
|
|
194
|
+
this.subscribe = (listener) => {
|
|
195
|
+
this.listeners.add(listener);
|
|
196
|
+
return () => this.listeners.delete(listener);
|
|
197
|
+
};
|
|
198
|
+
/** useSyncExternalStore-compatible getSnapshot (bound). */
|
|
199
|
+
this.getSnapshot = () => {
|
|
200
|
+
return this.state;
|
|
201
|
+
};
|
|
202
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
203
|
+
this.speedSamples = [];
|
|
204
|
+
this.core = core;
|
|
205
|
+
this.callbacks = callbacks;
|
|
206
|
+
this.state = {
|
|
207
|
+
files: /* @__PURE__ */ new Map(),
|
|
208
|
+
uploadStatus: "IDLE" /* IDLE */,
|
|
209
|
+
uploadError: "",
|
|
210
|
+
uploadErrorCode: void 0,
|
|
211
|
+
totalProgress: 0,
|
|
212
|
+
filesProgressMap: {},
|
|
213
|
+
uploadSpeed: 0,
|
|
214
|
+
uploadEta: 0,
|
|
215
|
+
uploadedBytes: 0,
|
|
216
|
+
totalBytes: 0,
|
|
217
|
+
activeSource: void 0,
|
|
218
|
+
editingFile: null,
|
|
219
|
+
editorQueue: [],
|
|
220
|
+
isAddingMore: false,
|
|
221
|
+
viewMode: "grid",
|
|
222
|
+
isOnline: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
setState(partial) {
|
|
226
|
+
this.state = { ...this.state, ...partial };
|
|
227
|
+
this.notify();
|
|
228
|
+
}
|
|
229
|
+
notify() {
|
|
230
|
+
this.listeners.forEach((fn) => {
|
|
231
|
+
fn();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// ── File management methods ──────────────────────────────────────
|
|
235
|
+
/** Remove a file by id: revoke its blob URL, then delegate to core. */
|
|
236
|
+
removeFile(fileId) {
|
|
237
|
+
const file = this.state.files.get(fileId);
|
|
238
|
+
if (file) revokeFileUrl(file);
|
|
239
|
+
this.core.removeFile(fileId);
|
|
240
|
+
}
|
|
241
|
+
/** Set the active upload source. */
|
|
242
|
+
setActiveSource(source) {
|
|
243
|
+
this.setState({ activeSource: source });
|
|
244
|
+
}
|
|
245
|
+
/** Toggle between grid and list view. */
|
|
246
|
+
setViewMode(mode) {
|
|
247
|
+
this.setState({ viewMode: mode });
|
|
248
|
+
}
|
|
249
|
+
/** Toggle the "adding more files" state. */
|
|
250
|
+
setIsAddingMore(value) {
|
|
251
|
+
this.setState({ isAddingMore: value });
|
|
252
|
+
}
|
|
253
|
+
// ── Upload control methods ──────────────────────────────────────
|
|
254
|
+
/** Trigger upload for all current files. Calls onPrepareFiles if provided. */
|
|
255
|
+
async startUpload() {
|
|
256
|
+
const currentFiles = [...this.state.files.values()];
|
|
257
|
+
if (currentFiles.length === 0) return void 0;
|
|
258
|
+
this.setState({ uploadError: "", uploadErrorCode: void 0 });
|
|
259
|
+
const prepared = this.callbacks.onPrepareFiles ? await this.callbacks.onPrepareFiles(currentFiles) : currentFiles;
|
|
260
|
+
if (prepared !== currentFiles) {
|
|
261
|
+
this.core.removeAll();
|
|
262
|
+
await this.core.addFiles(prepared);
|
|
263
|
+
}
|
|
264
|
+
return await this.core.upload();
|
|
265
|
+
}
|
|
266
|
+
/** Retry upload for a specific file or all failed files. */
|
|
267
|
+
async retryUpload(fileId) {
|
|
268
|
+
if (this.state.files.size === 0) return void 0;
|
|
269
|
+
this.setState({ uploadError: "", uploadErrorCode: void 0 });
|
|
270
|
+
return await this.core.retry(fileId);
|
|
271
|
+
}
|
|
272
|
+
/** Cancel in-progress upload, revoke blob URLs, clear all files and progress. */
|
|
273
|
+
handleCancel() {
|
|
274
|
+
this.core.cancel();
|
|
275
|
+
this.state.files.forEach((file) => {
|
|
276
|
+
revokeFileUrl(file);
|
|
277
|
+
});
|
|
278
|
+
this.core.removeAll();
|
|
279
|
+
this.setState({
|
|
280
|
+
filesProgressMap: {},
|
|
281
|
+
uploadSpeed: 0,
|
|
282
|
+
uploadEta: 0,
|
|
283
|
+
uploadedBytes: 0,
|
|
284
|
+
totalBytes: 0
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** Pause in-progress upload. */
|
|
288
|
+
handlePause() {
|
|
289
|
+
this.core.pause();
|
|
290
|
+
}
|
|
291
|
+
/** Resume paused upload. */
|
|
292
|
+
handleResume() {
|
|
293
|
+
this.core.resume();
|
|
294
|
+
}
|
|
295
|
+
/** Mark upload as done: invoke callback, emit event, then cancel/clear. */
|
|
296
|
+
handleDone() {
|
|
297
|
+
this.callbacks.onDoneClicked?.();
|
|
298
|
+
this.core.emit("done", {});
|
|
299
|
+
this.handleCancel();
|
|
300
|
+
}
|
|
301
|
+
/** Full state reset: clear isAddingMore, emit state-reset, then handleDone. */
|
|
302
|
+
resetState() {
|
|
303
|
+
this.setState({ isAddingMore: false });
|
|
304
|
+
this.core.emit("state-reset", {});
|
|
305
|
+
this.handleDone();
|
|
306
|
+
}
|
|
307
|
+
// ── Image editor methods ────────────────────────────────────────
|
|
308
|
+
/** Open the image editor for a given file. Invokes onOpen callback and emits event. */
|
|
309
|
+
openImageEditor(file) {
|
|
310
|
+
this.setState({ editingFile: file });
|
|
311
|
+
this.callbacks.imageEditorOptions?.onOpen?.(file);
|
|
312
|
+
this.core.emit("image-editor-open", { file });
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Close the image editor without saving. Invokes onCancel callback.
|
|
316
|
+
* If the editor queue is non-empty, the next file is automatically opened.
|
|
317
|
+
*/
|
|
318
|
+
closeImageEditor() {
|
|
319
|
+
const current = this.state.editingFile;
|
|
320
|
+
this.setState({ editingFile: null });
|
|
321
|
+
if (current) {
|
|
322
|
+
this.callbacks.imageEditorOptions?.onCancel?.(current);
|
|
323
|
+
this.core.emit("image-editor-cancel", { file: current });
|
|
324
|
+
}
|
|
325
|
+
this.processEditorQueue();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Save an edited image. Converts the data URL to a blob, creates a new
|
|
329
|
+
* UploadFile preserving the original's identity, replaces it in state
|
|
330
|
+
* and core, then invokes the onSave callback.
|
|
331
|
+
*/
|
|
332
|
+
saveImageEdit(editedImageData, mimeType) {
|
|
333
|
+
const editing = this.state.editingFile;
|
|
334
|
+
if (!editing) return;
|
|
335
|
+
const opts = this.callbacks.imageEditorOptions;
|
|
336
|
+
const outputMime = mimeType || opts?.output?.mimeType || editing.type;
|
|
337
|
+
const blob = new Blob([dataURLtoBlob(editedImageData)], {
|
|
338
|
+
type: outputMime
|
|
339
|
+
});
|
|
340
|
+
const newFile = blobToUploadFile(blob, editing, opts?.output);
|
|
341
|
+
const nextFiles = revokeAndReplace(
|
|
342
|
+
this.state.files,
|
|
343
|
+
editing.id,
|
|
344
|
+
newFile
|
|
345
|
+
);
|
|
346
|
+
this.setState({ files: nextFiles, editingFile: null });
|
|
347
|
+
this.core.replaceFile(editing.id, newFile);
|
|
348
|
+
opts?.onSave?.(newFile, editing);
|
|
349
|
+
this.core.emit("image-editor-save", {
|
|
350
|
+
file: newFile,
|
|
351
|
+
original: editing
|
|
352
|
+
});
|
|
353
|
+
this.processEditorQueue();
|
|
354
|
+
}
|
|
355
|
+
/** Replace a single file in state by id (revokes old blob URL). Also replaces in core. */
|
|
356
|
+
replaceFile(fileId, newFile) {
|
|
357
|
+
const nextFiles = revokeAndReplace(this.state.files, fileId, newFile);
|
|
358
|
+
this.setState({ files: nextFiles });
|
|
359
|
+
this.core.replaceFile(fileId, newFile);
|
|
360
|
+
}
|
|
361
|
+
/** Enqueue files for auto-opening in the image editor. */
|
|
362
|
+
enqueueForEditor(files) {
|
|
363
|
+
if (files.length === 0) return;
|
|
364
|
+
this.setState({ editorQueue: [...this.state.editorQueue, ...files] });
|
|
365
|
+
if (!this.state.editingFile) {
|
|
366
|
+
this.processEditorQueue();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** Pop the next file from the editor queue and open it. */
|
|
370
|
+
processEditorQueue() {
|
|
371
|
+
if (this.state.editingFile || this.state.editorQueue.length === 0)
|
|
372
|
+
return;
|
|
373
|
+
const [next, ...rest] = this.state.editorQueue;
|
|
374
|
+
if (!next) return;
|
|
375
|
+
this.setState({ editorQueue: rest });
|
|
376
|
+
this.openImageEditor(next);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Project orchestrator state from core — the single source of truth (F-145).
|
|
380
|
+
* `uploadStatus` is derived from the `status` each status-carrying `state-change`
|
|
381
|
+
* event supplies (core sets `_status` before every such emit); the discrete
|
|
382
|
+
* upload-start/upload-all-complete/upload-error listeners no longer write it. `files`
|
|
383
|
+
* is rebuilt from `core.files`, and is a no-op when the file set is unchanged (same
|
|
384
|
+
* size + ordered ids + same UploadFile refs) so the `files` reference stays stable
|
|
385
|
+
* across the many status-only `state-change` events core emits during upload —
|
|
386
|
+
* preserving referential stability for useSyncExternalStore / computed consumers.
|
|
387
|
+
*/
|
|
388
|
+
projectFromCore(payload) {
|
|
389
|
+
const partial = {};
|
|
390
|
+
if (payload?.status !== void 0) partial.uploadStatus = payload.status;
|
|
391
|
+
const coreFiles = this.core.files;
|
|
392
|
+
if (this.filesProjectionChanged(coreFiles)) {
|
|
393
|
+
partial.files = new Map(coreFiles);
|
|
394
|
+
partial.totalBytes = [...coreFiles.values()].reduce(
|
|
395
|
+
(sum, f) => sum + f.size,
|
|
396
|
+
0
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (Object.keys(partial).length > 0) this.setState(partial);
|
|
400
|
+
}
|
|
401
|
+
filesProjectionChanged(coreFiles) {
|
|
402
|
+
const current = this.state.files;
|
|
403
|
+
if (current.size !== coreFiles.size) return true;
|
|
404
|
+
const a = current.entries();
|
|
405
|
+
const b = coreFiles.entries();
|
|
406
|
+
for (let x = a.next(), y = b.next(); !x.done && !y.done; x = a.next(), y = b.next()) {
|
|
407
|
+
if (x.value[0] !== y.value[0] || x.value[1] !== y.value[1])
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
init() {
|
|
413
|
+
this.projectFromCore();
|
|
414
|
+
this.unsubs.push(
|
|
415
|
+
this.core.on("upload-start", () => {
|
|
416
|
+
this.callbacks.onUploadStart?.();
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
this.unsubs.push(
|
|
420
|
+
this.core.on(
|
|
421
|
+
"file-upload-start",
|
|
422
|
+
(payload) => {
|
|
423
|
+
this.callbacks.onFileUploadStart?.(payload.file);
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
this.unsubs.push(
|
|
428
|
+
this.core.on(
|
|
429
|
+
"state-change",
|
|
430
|
+
(payload) => {
|
|
431
|
+
this.projectFromCore(payload);
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
this.unsubs.push(
|
|
436
|
+
this.core.on("files-added", (added) => {
|
|
437
|
+
if (!Array.isArray(added) || added.length === 0) return;
|
|
438
|
+
this.callbacks.onFilesSelected?.(added);
|
|
439
|
+
const editorOpts = this.callbacks.imageEditorOptions;
|
|
440
|
+
if (editorOpts?.enabled) {
|
|
441
|
+
const images = added.filter(
|
|
442
|
+
(file) => file.type.startsWith("image/")
|
|
443
|
+
);
|
|
444
|
+
if (editorOpts.autoOpen === "single" && images.length === 1) {
|
|
445
|
+
const [firstImage] = images;
|
|
446
|
+
if (firstImage) this.enqueueForEditor([firstImage]);
|
|
447
|
+
}
|
|
448
|
+
if (editorOpts.autoOpen === "always" && images.length > 0) {
|
|
449
|
+
this.enqueueForEditor(images);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (this.callbacks.autoUpload) {
|
|
453
|
+
this.core.emit("auto-upload", { count: added.length });
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
if (this.disposed) return;
|
|
456
|
+
void this.core.upload().catch(() => {
|
|
457
|
+
});
|
|
458
|
+
}, 0);
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
this.unsubs.push(
|
|
463
|
+
this.core.on("file-removed", (file) => {
|
|
464
|
+
this.callbacks.onFileRemoved?.(file);
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
this.unsubs.push(
|
|
468
|
+
this.core.on(
|
|
469
|
+
"upload-progress",
|
|
470
|
+
(progress) => {
|
|
471
|
+
const nextProgressMap = {
|
|
472
|
+
...this.state.filesProgressMap,
|
|
473
|
+
[progress.fileId]: {
|
|
474
|
+
id: progress.fileId,
|
|
475
|
+
loaded: progress.loaded,
|
|
476
|
+
total: progress.total
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const nextUploaded = Object.values(nextProgressMap).reduce(
|
|
480
|
+
(sum, item) => sum + item.loaded,
|
|
481
|
+
0
|
|
482
|
+
);
|
|
483
|
+
const { samples, speed, eta } = computeSpeedEta({
|
|
484
|
+
samples: this.speedSamples,
|
|
485
|
+
now: Date.now(),
|
|
486
|
+
uploadedBytes: nextUploaded,
|
|
487
|
+
totalBytes: this.state.totalBytes,
|
|
488
|
+
previousSpeed: this.state.uploadSpeed,
|
|
489
|
+
previousEta: this.state.uploadEta
|
|
490
|
+
});
|
|
491
|
+
this.speedSamples = samples;
|
|
492
|
+
this.setState({
|
|
493
|
+
filesProgressMap: nextProgressMap,
|
|
494
|
+
uploadedBytes: nextUploaded,
|
|
495
|
+
uploadSpeed: speed,
|
|
496
|
+
uploadEta: eta
|
|
497
|
+
});
|
|
498
|
+
const file = this.state.files.get(progress.fileId);
|
|
499
|
+
if (file) {
|
|
500
|
+
this.callbacks.onFileUploadProgress?.(file, {
|
|
501
|
+
loaded: progress.loaded,
|
|
502
|
+
total: progress.total,
|
|
503
|
+
percentage: progress.total > 0 ? Math.round(
|
|
504
|
+
progress.loaded / progress.total * 100
|
|
505
|
+
) : 0
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
this.callbacks.onFilesUploadProgress?.(
|
|
509
|
+
this.core.progress.completedFiles,
|
|
510
|
+
this.core.progress.totalFiles
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
this.unsubs.push(
|
|
516
|
+
this.core.on(
|
|
517
|
+
"upload-success",
|
|
518
|
+
(payload) => {
|
|
519
|
+
const resultKey = payload.result.key;
|
|
520
|
+
this.callbacks.onFileUploadComplete?.(
|
|
521
|
+
payload.file,
|
|
522
|
+
resultKey ?? payload.file.key ?? ""
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
);
|
|
527
|
+
this.unsubs.push(
|
|
528
|
+
this.core.on("upload-all-complete", (completed) => {
|
|
529
|
+
this.callbacks.onFilesUploadComplete?.(completed);
|
|
530
|
+
this.callbacks.onUploadComplete?.(completed);
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
this.unsubs.push(
|
|
534
|
+
this.core.on(
|
|
535
|
+
"upload-error",
|
|
536
|
+
(payload) => {
|
|
537
|
+
const message = payload.error.message;
|
|
538
|
+
const code = payload.error instanceof UpupError ? payload.error.code : void 0;
|
|
539
|
+
this.setState({
|
|
540
|
+
uploadError: message,
|
|
541
|
+
uploadErrorCode: code
|
|
542
|
+
});
|
|
543
|
+
this.callbacks.onError?.(message);
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
);
|
|
547
|
+
if (typeof globalThis.window !== "undefined" && typeof globalThis.window.addEventListener === "function") {
|
|
548
|
+
const handleOnline = () => {
|
|
549
|
+
this.setState({ isOnline: true });
|
|
550
|
+
this.core.emit("connection-online", {});
|
|
551
|
+
};
|
|
552
|
+
const handleOffline = () => {
|
|
553
|
+
this.setState({ isOnline: false });
|
|
554
|
+
this.core.emit("connection-offline", {});
|
|
555
|
+
};
|
|
556
|
+
globalThis.window.addEventListener("online", handleOnline);
|
|
557
|
+
globalThis.window.addEventListener("offline", handleOffline);
|
|
558
|
+
this.unsubs.push(() => {
|
|
559
|
+
globalThis.window.removeEventListener("online", handleOnline);
|
|
560
|
+
globalThis.window.removeEventListener("offline", handleOffline);
|
|
561
|
+
});
|
|
562
|
+
if (typeof navigator !== "undefined" && typeof navigator.onLine === "boolean") {
|
|
563
|
+
this.setState({ isOnline: navigator.onLine });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
destroy() {
|
|
568
|
+
this.disposed = true;
|
|
569
|
+
this.state.files.forEach((file) => {
|
|
570
|
+
revokeFileUrl(file);
|
|
571
|
+
});
|
|
572
|
+
this.unsubs.forEach((u) => {
|
|
573
|
+
u();
|
|
574
|
+
});
|
|
575
|
+
this.unsubs = [];
|
|
576
|
+
this.speedSamples = [];
|
|
577
|
+
this.listeners.clear();
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/orchestrator/helpers.ts
|
|
582
|
+
function getDir(locale) {
|
|
583
|
+
if (locale && typeof locale === "object" && "dir" in locale)
|
|
584
|
+
return locale.dir;
|
|
585
|
+
const code = typeof locale === "string" ? locale : "en-US";
|
|
586
|
+
const base = code.split("-")[0] ?? "";
|
|
587
|
+
const meta = LOCALE_META[code] ?? Object.values(LOCALE_META).find((m) => m.code.startsWith(base + "-"));
|
|
588
|
+
return meta?.dir ?? "ltr";
|
|
589
|
+
}
|
|
590
|
+
function normalizeSource(source) {
|
|
591
|
+
return Object.values(FileSource).includes(source) ? source : void 0;
|
|
592
|
+
}
|
|
593
|
+
var DEFAULT_SOURCES = [
|
|
594
|
+
"local" /* LOCAL */,
|
|
595
|
+
"url" /* URL */,
|
|
596
|
+
"camera" /* CAMERA */,
|
|
597
|
+
"microphone" /* MICROPHONE */,
|
|
598
|
+
"screen" /* SCREEN */
|
|
599
|
+
];
|
|
600
|
+
var DEFAULT_MAX_FILE_SIZE = { size: 1, unit: "GB" };
|
|
601
|
+
|
|
602
|
+
// src/uploader/normalize-options.ts
|
|
603
|
+
function normalizeUploaderOptions(options) {
|
|
604
|
+
const acceptProp = options.allowedFileTypes ?? "*";
|
|
605
|
+
const mini = options.mini ?? false;
|
|
606
|
+
const animations = options.animations ?? true;
|
|
607
|
+
const resolvedSources = options.sources ? options.sources.map((s) => normalizeSource(s)).filter(Boolean) : DEFAULT_SOURCES;
|
|
608
|
+
const resolvedLimit = options.maxFiles ?? 10;
|
|
609
|
+
const resolvedMode = options.mode ?? (options.serverUrl && !options.uploadEndpoint ? "server" : "client");
|
|
610
|
+
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
611
|
+
const accept = resolveAccept(
|
|
612
|
+
typeof acceptProp === "string" ? acceptProp : acceptProp.join(",")
|
|
613
|
+
);
|
|
614
|
+
const limit = mini ? 1 : Math.max(resolvedLimit, 1);
|
|
615
|
+
const multiple = mini ? false : limit > 1;
|
|
616
|
+
const folderUploadAllowDrop = options.folderUpload?.allowDrop ?? false;
|
|
617
|
+
const folderPickerButtonVisible = options.folderUpload?.showSelectFolderButton ?? false;
|
|
618
|
+
const imageEditor = (() => {
|
|
619
|
+
const ie = options.imageEditor;
|
|
620
|
+
if (ie === void 0 || ie === true)
|
|
621
|
+
return { enabled: true, autoOpen: "never", display: "inline" };
|
|
622
|
+
if (typeof ie === "object" && ie !== null) {
|
|
623
|
+
const o = ie;
|
|
624
|
+
return {
|
|
625
|
+
...o,
|
|
626
|
+
enabled: o.enabled ?? true,
|
|
627
|
+
autoOpen: o.autoOpen ?? "never",
|
|
628
|
+
display: o.display ?? "inline"
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return { enabled: false, autoOpen: "never", display: "inline" };
|
|
632
|
+
})();
|
|
633
|
+
const i18n = options.i18n;
|
|
634
|
+
const bundle = i18n?.bundle ?? resolveLocaleBundle(i18n?.locale);
|
|
635
|
+
const fallbackBundle = resolveLocaleBundle(i18n?.fallbackLocale);
|
|
636
|
+
const translator = createTranslator({
|
|
637
|
+
bundle: bundle ?? enUS,
|
|
638
|
+
fallback: fallbackBundle ?? enUS,
|
|
639
|
+
overrides: i18n?.overrides
|
|
640
|
+
});
|
|
641
|
+
const translations = flattenTranslatorToUiTranslations(translator);
|
|
642
|
+
const lang = bundle?.code ?? (typeof i18n?.locale === "string" ? i18n.locale : "en-US");
|
|
643
|
+
const dir = bundle?.dir ?? getDir(i18n?.locale);
|
|
644
|
+
const resolved = {
|
|
645
|
+
mini,
|
|
646
|
+
animations,
|
|
647
|
+
sources: resolvedSources,
|
|
648
|
+
allowedFileTypes: accept,
|
|
649
|
+
limit,
|
|
650
|
+
maxFileSize,
|
|
651
|
+
multiple,
|
|
652
|
+
mode: resolvedMode,
|
|
653
|
+
serverUrl: options.serverUrl,
|
|
654
|
+
folderUploadAllowDrop,
|
|
655
|
+
folderPickerButtonVisible,
|
|
656
|
+
imageEditor,
|
|
657
|
+
resumable: options.resumable,
|
|
658
|
+
translator,
|
|
659
|
+
translations,
|
|
660
|
+
lang,
|
|
661
|
+
dir,
|
|
662
|
+
cloudDrives: options.cloudDrives
|
|
663
|
+
};
|
|
664
|
+
const coreOptions = {
|
|
665
|
+
uploadEndpoint: options.uploadEndpoint || void 0,
|
|
666
|
+
serverUrl: options.serverUrl,
|
|
667
|
+
provider: options.provider,
|
|
668
|
+
mode: resolvedMode,
|
|
669
|
+
allowedFileTypes: accept,
|
|
670
|
+
limit,
|
|
671
|
+
maxFileSize,
|
|
672
|
+
minFileSize: options.minFileSize,
|
|
673
|
+
maxTotalFileSize: options.maxTotalFileSize,
|
|
674
|
+
maxRetries: options.maxRetries,
|
|
675
|
+
onBeforeFileAdded: options.onBeforeFileAdded,
|
|
676
|
+
imageCompression: options.imageCompression,
|
|
677
|
+
thumbnailGenerator: options.thumbnailGenerator,
|
|
678
|
+
checksumVerification: options.checksumVerification,
|
|
679
|
+
webWorker: options.webWorker,
|
|
680
|
+
heicConversion: options.heicConversion,
|
|
681
|
+
stripExifData: options.stripExifData,
|
|
682
|
+
contentDeduplication: options.contentDeduplication,
|
|
683
|
+
crashRecovery: options.crashRecovery,
|
|
684
|
+
// Forwarded verbatim: `undefined` must survive so core's default-on
|
|
685
|
+
// network awareness applies; only an explicit `false` opts out.
|
|
686
|
+
networkAware: options.networkAware,
|
|
687
|
+
maxConcurrentUploads: options.maxConcurrentUploads,
|
|
688
|
+
metadata: options.metadata,
|
|
689
|
+
cors: options.cors,
|
|
690
|
+
resumable: options.resumable,
|
|
691
|
+
cloudDrives: options.cloudDrives,
|
|
692
|
+
locale: bundle
|
|
693
|
+
};
|
|
694
|
+
return { coreOptions, resolved };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/uploader/motion-gate.ts
|
|
698
|
+
var REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
|
|
699
|
+
function createMotionGate({
|
|
700
|
+
animations,
|
|
701
|
+
matchMedia = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia.bind(window) : void 0
|
|
702
|
+
}) {
|
|
703
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
704
|
+
let reduced = false;
|
|
705
|
+
let mql = null;
|
|
706
|
+
const onChange = (e) => {
|
|
707
|
+
reduced = e.matches;
|
|
708
|
+
listeners.forEach((l) => {
|
|
709
|
+
l();
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
if (matchMedia) {
|
|
713
|
+
mql = matchMedia(REDUCE_QUERY);
|
|
714
|
+
reduced = mql.matches;
|
|
715
|
+
mql.addEventListener("change", onChange);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
getSnapshot: () => animations === false || reduced ? "off" : "on",
|
|
719
|
+
subscribe(listener) {
|
|
720
|
+
listeners.add(listener);
|
|
721
|
+
return () => listeners.delete(listener);
|
|
722
|
+
},
|
|
723
|
+
destroy() {
|
|
724
|
+
mql?.removeEventListener("change", onChange);
|
|
725
|
+
listeners.clear();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/uploader/transient-ui-state.ts
|
|
731
|
+
function createTransientUiState({
|
|
732
|
+
motion,
|
|
733
|
+
reallyRemove,
|
|
734
|
+
exitMs = 200,
|
|
735
|
+
overlayMs = 350,
|
|
736
|
+
toastMs = 3e3
|
|
737
|
+
}) {
|
|
738
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
739
|
+
const leaving = /* @__PURE__ */ new Set();
|
|
740
|
+
const timers = /* @__PURE__ */ new Set();
|
|
741
|
+
let overlayOpen = false;
|
|
742
|
+
let overlayClosing = false;
|
|
743
|
+
let overlayGen = 0;
|
|
744
|
+
let rejected = null;
|
|
745
|
+
let toastGen = 0;
|
|
746
|
+
let destroyed = false;
|
|
747
|
+
function compute() {
|
|
748
|
+
return {
|
|
749
|
+
leavingFileIds: new Set(leaving),
|
|
750
|
+
sourceOverlayOpen: overlayOpen,
|
|
751
|
+
sourceOverlayClosing: overlayClosing,
|
|
752
|
+
dropRejected: rejected
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
let snapshot = compute();
|
|
756
|
+
function notify() {
|
|
757
|
+
snapshot = compute();
|
|
758
|
+
listeners.forEach((l) => {
|
|
759
|
+
l();
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
function later(ms, fn) {
|
|
763
|
+
const t = setTimeout(() => {
|
|
764
|
+
timers.delete(t);
|
|
765
|
+
if (!destroyed) fn();
|
|
766
|
+
}, ms);
|
|
767
|
+
timers.add(t);
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
getSnapshot: () => snapshot,
|
|
771
|
+
subscribe(listener) {
|
|
772
|
+
listeners.add(listener);
|
|
773
|
+
return () => listeners.delete(listener);
|
|
774
|
+
},
|
|
775
|
+
removeFileAnimated(fileId) {
|
|
776
|
+
if (leaving.has(fileId)) return;
|
|
777
|
+
if (motion() === "off") {
|
|
778
|
+
reallyRemove(fileId);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
leaving.add(fileId);
|
|
782
|
+
notify();
|
|
783
|
+
later(exitMs, () => {
|
|
784
|
+
leaving.delete(fileId);
|
|
785
|
+
reallyRemove(fileId);
|
|
786
|
+
notify();
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
openSourceOverlay() {
|
|
790
|
+
if (overlayOpen) return;
|
|
791
|
+
overlayOpen = true;
|
|
792
|
+
overlayClosing = false;
|
|
793
|
+
overlayGen++;
|
|
794
|
+
notify();
|
|
795
|
+
},
|
|
796
|
+
closeSourceOverlay() {
|
|
797
|
+
if (!overlayOpen) return;
|
|
798
|
+
overlayOpen = false;
|
|
799
|
+
if (motion() === "off") {
|
|
800
|
+
overlayClosing = false;
|
|
801
|
+
notify();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
overlayClosing = true;
|
|
805
|
+
const g = ++overlayGen;
|
|
806
|
+
notify();
|
|
807
|
+
later(overlayMs, () => {
|
|
808
|
+
if (g === overlayGen) {
|
|
809
|
+
overlayClosing = false;
|
|
810
|
+
notify();
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
flagDropRejected(provider) {
|
|
815
|
+
rejected = provider;
|
|
816
|
+
const g = ++toastGen;
|
|
817
|
+
notify();
|
|
818
|
+
later(toastMs, () => {
|
|
819
|
+
if (g === toastGen) {
|
|
820
|
+
rejected = null;
|
|
821
|
+
notify();
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
},
|
|
825
|
+
destroy() {
|
|
826
|
+
destroyed = true;
|
|
827
|
+
timers.forEach((t) => {
|
|
828
|
+
clearTimeout(t);
|
|
829
|
+
});
|
|
830
|
+
timers.clear();
|
|
831
|
+
listeners.clear();
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/uploader/create-uploader-controller.ts
|
|
837
|
+
function createUploaderController({ core, options, normalized }, hostHooks = {}) {
|
|
838
|
+
const { resolved } = normalized;
|
|
839
|
+
const { connectSSE } = hostHooks;
|
|
840
|
+
const onError = (message) => options.onError?.(message);
|
|
841
|
+
let callbackRefs = { ...options };
|
|
842
|
+
const proxiedCallbacks = {
|
|
843
|
+
get onError() {
|
|
844
|
+
return callbackRefs.onError;
|
|
845
|
+
},
|
|
846
|
+
get onWarn() {
|
|
847
|
+
return callbackRefs.onWarn;
|
|
848
|
+
},
|
|
849
|
+
get onUploadStart() {
|
|
850
|
+
return callbackRefs.onUploadStart;
|
|
851
|
+
},
|
|
852
|
+
get onFileUploadStart() {
|
|
853
|
+
return callbackRefs.onFileUploadStart;
|
|
854
|
+
},
|
|
855
|
+
get onFileUploadProgress() {
|
|
856
|
+
return callbackRefs.onFileUploadProgress;
|
|
857
|
+
},
|
|
858
|
+
get onFilesUploadProgress() {
|
|
859
|
+
return callbackRefs.onFilesUploadProgress;
|
|
860
|
+
},
|
|
861
|
+
get onFileUploadComplete() {
|
|
862
|
+
return callbackRefs.onFileUploadComplete;
|
|
863
|
+
},
|
|
864
|
+
get onFilesUploadComplete() {
|
|
865
|
+
return (files) => {
|
|
866
|
+
callbackRefs.onFilesUploadComplete?.(files);
|
|
867
|
+
if (connectSSE && Array.isArray(files))
|
|
868
|
+
files.forEach((f) => {
|
|
869
|
+
connectSSE(f);
|
|
870
|
+
});
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
get onUploadComplete() {
|
|
874
|
+
return callbackRefs.onUploadComplete;
|
|
875
|
+
},
|
|
876
|
+
get onFilesSelected() {
|
|
877
|
+
return callbackRefs.onFilesSelected;
|
|
878
|
+
},
|
|
879
|
+
get onDoneClicked() {
|
|
880
|
+
return callbackRefs.onDoneClicked;
|
|
881
|
+
},
|
|
882
|
+
get onPrepareFiles() {
|
|
883
|
+
return callbackRefs.onPrepareFiles;
|
|
884
|
+
},
|
|
885
|
+
get onFileRemoved() {
|
|
886
|
+
return callbackRefs.onFileRemoved;
|
|
887
|
+
},
|
|
888
|
+
get imageEditorOptions() {
|
|
889
|
+
return resolved.imageEditor;
|
|
890
|
+
},
|
|
891
|
+
get autoUpload() {
|
|
892
|
+
return callbackRefs.autoUpload ?? false;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const orchestrator = new UploaderOrchestrator(core, proxiedCallbacks);
|
|
896
|
+
const themeConfig = options.theme ?? (options.dark !== void 0 ? { mode: options.dark ? "dark" : "light" } : void 0);
|
|
897
|
+
const theme = new ThemeStore(themeConfig);
|
|
898
|
+
const motionGate = createMotionGate({ animations: resolved.animations });
|
|
899
|
+
const transientUi = createTransientUiState({
|
|
900
|
+
motion: () => motionGate.getSnapshot(),
|
|
901
|
+
reallyRemove: (fileId) => {
|
|
902
|
+
orchestrator.removeFile(fileId);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
const drivePlugins = [];
|
|
906
|
+
function registerPlugins() {
|
|
907
|
+
const cd = resolved.cloudDrives;
|
|
908
|
+
if (cd?.googleDrive) {
|
|
909
|
+
const p = new GoogleDrivePlugin();
|
|
910
|
+
p.configure(cd.googleDrive);
|
|
911
|
+
try {
|
|
912
|
+
core.use(p);
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
drivePlugins.push(p);
|
|
916
|
+
}
|
|
917
|
+
if (cd?.dropbox) {
|
|
918
|
+
const p = new DropboxPlugin();
|
|
919
|
+
p.configure(cd.dropbox);
|
|
920
|
+
try {
|
|
921
|
+
core.use(p);
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
drivePlugins.push(p);
|
|
925
|
+
}
|
|
926
|
+
if (cd?.box) {
|
|
927
|
+
const p = new BoxPlugin();
|
|
928
|
+
p.configure(cd.box);
|
|
929
|
+
try {
|
|
930
|
+
core.use(p);
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
drivePlugins.push(p);
|
|
934
|
+
}
|
|
935
|
+
if (cd?.oneDrive) {
|
|
936
|
+
const p = new OneDrivePlugin();
|
|
937
|
+
p.configure(cd.oneDrive);
|
|
938
|
+
try {
|
|
939
|
+
core.use(p);
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
drivePlugins.push(p);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
let inputEl = null;
|
|
946
|
+
const registerFileInput = (el) => {
|
|
947
|
+
inputEl = el;
|
|
948
|
+
};
|
|
949
|
+
const getFileInput = () => inputEl;
|
|
950
|
+
const openFilePicker = () => inputEl?.click();
|
|
951
|
+
const filesArray = () => [...core.files.values()];
|
|
952
|
+
const commands = {
|
|
953
|
+
async handleSetSelectedFiles(newFiles) {
|
|
954
|
+
const before = core.files.size;
|
|
955
|
+
try {
|
|
956
|
+
await core.addFiles(newFiles);
|
|
957
|
+
if (core.files.size > before && !orchestrator.getSnapshot().activeSource) {
|
|
958
|
+
transientUi.closeSourceOverlay();
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
962
|
+
onError(message);
|
|
963
|
+
const first = newFiles[0];
|
|
964
|
+
if (first) {
|
|
965
|
+
const m = message.toLowerCase();
|
|
966
|
+
if (m.includes("type")) {
|
|
967
|
+
callbackRefs.onFileTypeMismatch?.(
|
|
968
|
+
first,
|
|
969
|
+
resolved.allowedFileTypes
|
|
970
|
+
);
|
|
971
|
+
callbackRefs.onRestrictionFailed?.(
|
|
972
|
+
first,
|
|
973
|
+
"TYPE_MISMATCH"
|
|
974
|
+
);
|
|
975
|
+
} else if (m.includes("limit")) {
|
|
976
|
+
callbackRefs.onRestrictionFailed?.(
|
|
977
|
+
first,
|
|
978
|
+
"LIMIT_EXCEEDED"
|
|
979
|
+
);
|
|
980
|
+
} else if (m.includes("below")) {
|
|
981
|
+
callbackRefs.onRestrictionFailed?.(
|
|
982
|
+
first,
|
|
983
|
+
"FILE_TOO_SMALL"
|
|
984
|
+
);
|
|
985
|
+
} else if (m.includes("size")) {
|
|
986
|
+
callbackRefs.onRestrictionFailed?.(
|
|
987
|
+
first,
|
|
988
|
+
"FILE_TOO_LARGE"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
// Upload-control commands are pure delegation: UploaderOrchestrator is
|
|
995
|
+
// the single owner of the upload-control flow (F-722) — it holds the
|
|
996
|
+
// projected state, clears uploadError/uploadErrorCode before a new
|
|
997
|
+
// run, and owns the done/state-reset emissions. Re-implementing any of
|
|
998
|
+
// these here re-creates the drifting second copy pass 2 removed.
|
|
999
|
+
handleFileRemove(fileId) {
|
|
1000
|
+
transientUi.removeFileAnimated(fileId);
|
|
1001
|
+
},
|
|
1002
|
+
handleRemoveAll() {
|
|
1003
|
+
core.removeAll();
|
|
1004
|
+
},
|
|
1005
|
+
async uploadFiles(newFiles) {
|
|
1006
|
+
await core.setFiles(newFiles);
|
|
1007
|
+
return core.upload();
|
|
1008
|
+
},
|
|
1009
|
+
replaceFiles(newFiles) {
|
|
1010
|
+
filesArray().forEach((f) => {
|
|
1011
|
+
revokeFileUrl(f);
|
|
1012
|
+
});
|
|
1013
|
+
void core.setFiles(newFiles);
|
|
1014
|
+
},
|
|
1015
|
+
startUpload() {
|
|
1016
|
+
return orchestrator.startUpload();
|
|
1017
|
+
},
|
|
1018
|
+
retryUpload(fileId) {
|
|
1019
|
+
return orchestrator.retryUpload(fileId);
|
|
1020
|
+
},
|
|
1021
|
+
handleCancel() {
|
|
1022
|
+
orchestrator.handleCancel();
|
|
1023
|
+
},
|
|
1024
|
+
handlePause() {
|
|
1025
|
+
orchestrator.handlePause();
|
|
1026
|
+
},
|
|
1027
|
+
handleResume() {
|
|
1028
|
+
orchestrator.handleResume();
|
|
1029
|
+
},
|
|
1030
|
+
handleDone() {
|
|
1031
|
+
orchestrator.handleDone();
|
|
1032
|
+
},
|
|
1033
|
+
resetState() {
|
|
1034
|
+
orchestrator.resetState();
|
|
1035
|
+
},
|
|
1036
|
+
openImageEditor(file) {
|
|
1037
|
+
orchestrator.openImageEditor(file);
|
|
1038
|
+
},
|
|
1039
|
+
closeImageEditor() {
|
|
1040
|
+
orchestrator.closeImageEditor();
|
|
1041
|
+
},
|
|
1042
|
+
saveImageEdit(editedImageData, mimeType) {
|
|
1043
|
+
orchestrator.saveImageEdit(editedImageData, mimeType);
|
|
1044
|
+
},
|
|
1045
|
+
replaceFile(fileId, newFile) {
|
|
1046
|
+
orchestrator.replaceFile(fileId, newFile);
|
|
1047
|
+
},
|
|
1048
|
+
setActiveSource(a) {
|
|
1049
|
+
orchestrator.setActiveSource(a);
|
|
1050
|
+
},
|
|
1051
|
+
setIsAddingMore(v) {
|
|
1052
|
+
orchestrator.setIsAddingMore(v);
|
|
1053
|
+
},
|
|
1054
|
+
setViewMode(m) {
|
|
1055
|
+
orchestrator.setViewMode(m);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
let lastStatus;
|
|
1059
|
+
let statusUnsub = null;
|
|
1060
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1061
|
+
let fanInUnsubs = [];
|
|
1062
|
+
function ensureFanIn() {
|
|
1063
|
+
if (fanInUnsubs.length > 0) return;
|
|
1064
|
+
const notifyAll = () => {
|
|
1065
|
+
subscribers.forEach((l) => {
|
|
1066
|
+
l();
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
1069
|
+
fanInUnsubs = [
|
|
1070
|
+
core.on("state-change", notifyAll),
|
|
1071
|
+
orchestrator.subscribe(notifyAll),
|
|
1072
|
+
theme.subscribe(notifyAll),
|
|
1073
|
+
motionGate.subscribe(notifyAll),
|
|
1074
|
+
transientUi.subscribe(notifyAll)
|
|
1075
|
+
];
|
|
1076
|
+
}
|
|
1077
|
+
function tearDownFanIn() {
|
|
1078
|
+
fanInUnsubs.forEach((u) => {
|
|
1079
|
+
u();
|
|
1080
|
+
});
|
|
1081
|
+
fanInUnsubs = [];
|
|
1082
|
+
}
|
|
1083
|
+
let initialized = false;
|
|
1084
|
+
let destroyed = false;
|
|
1085
|
+
function init() {
|
|
1086
|
+
if (initialized) return;
|
|
1087
|
+
initialized = true;
|
|
1088
|
+
destroyed = false;
|
|
1089
|
+
orchestrator.init();
|
|
1090
|
+
theme.init();
|
|
1091
|
+
registerPlugins();
|
|
1092
|
+
statusUnsub = orchestrator.subscribe(() => {
|
|
1093
|
+
const s = orchestrator.getSnapshot().uploadStatus;
|
|
1094
|
+
const str = s.toLowerCase();
|
|
1095
|
+
if (str !== lastStatus) {
|
|
1096
|
+
lastStatus = str;
|
|
1097
|
+
callbackRefs.onStatusChange?.(str);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
if (options.crashRecovery)
|
|
1101
|
+
void core.restoreFromCrashRecovery().catch((err) => {
|
|
1102
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
1103
|
+
console.warn("[upup] crash-recovery restore failed", err);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function destroy() {
|
|
1108
|
+
if (destroyed) return;
|
|
1109
|
+
destroyed = true;
|
|
1110
|
+
initialized = false;
|
|
1111
|
+
statusUnsub?.();
|
|
1112
|
+
statusUnsub = null;
|
|
1113
|
+
lastStatus = void 0;
|
|
1114
|
+
drivePlugins.splice(0).forEach((p) => {
|
|
1115
|
+
p.destroy();
|
|
1116
|
+
});
|
|
1117
|
+
tearDownFanIn();
|
|
1118
|
+
orchestrator.destroy();
|
|
1119
|
+
transientUi.destroy();
|
|
1120
|
+
motionGate.destroy();
|
|
1121
|
+
theme.destroy();
|
|
1122
|
+
}
|
|
1123
|
+
function subscribe(listener) {
|
|
1124
|
+
subscribers.add(listener);
|
|
1125
|
+
ensureFanIn();
|
|
1126
|
+
return () => {
|
|
1127
|
+
subscribers.delete(listener);
|
|
1128
|
+
if (subscribers.size === 0) {
|
|
1129
|
+
tearDownFanIn();
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function updateCallbacks(next) {
|
|
1134
|
+
callbackRefs = { ...callbackRefs, ...next };
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
core,
|
|
1138
|
+
orchestrator,
|
|
1139
|
+
theme,
|
|
1140
|
+
motionGate,
|
|
1141
|
+
transientUi,
|
|
1142
|
+
resolved,
|
|
1143
|
+
commands,
|
|
1144
|
+
registerFileInput,
|
|
1145
|
+
getFileInput,
|
|
1146
|
+
openFilePicker,
|
|
1147
|
+
updateCallbacks,
|
|
1148
|
+
subscribe,
|
|
1149
|
+
init,
|
|
1150
|
+
destroy
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/uploader/create-child-controller.ts
|
|
1155
|
+
function createChildController(factory, options = {}) {
|
|
1156
|
+
const controller = factory();
|
|
1157
|
+
let started = false;
|
|
1158
|
+
let stopped = false;
|
|
1159
|
+
let unsub = null;
|
|
1160
|
+
return {
|
|
1161
|
+
controller,
|
|
1162
|
+
init() {
|
|
1163
|
+
if (started) return;
|
|
1164
|
+
started = true;
|
|
1165
|
+
controller.init?.();
|
|
1166
|
+
if (options.onChange && controller.subscribe)
|
|
1167
|
+
unsub = controller.subscribe(options.onChange);
|
|
1168
|
+
},
|
|
1169
|
+
destroy() {
|
|
1170
|
+
if (stopped) return;
|
|
1171
|
+
stopped = true;
|
|
1172
|
+
unsub?.();
|
|
1173
|
+
unsub = null;
|
|
1174
|
+
controller.destroy?.();
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/drives/bind-drive-events.ts
|
|
1180
|
+
function bindDriveEvents(core, provider, callbacks) {
|
|
1181
|
+
const unsubs = [
|
|
1182
|
+
core.on(`${provider}:authenticated`, callbacks.onAuthenticated),
|
|
1183
|
+
core.on(`${provider}:signed-out`, callbacks.onSignedOut),
|
|
1184
|
+
core.on(`${provider}:session-expired`, callbacks.onSessionExpired),
|
|
1185
|
+
core.on(`${provider}:files-loaded`, callbacks.onFilesLoaded),
|
|
1186
|
+
core.on(`${provider}:state-change`, callbacks.onStateChange),
|
|
1187
|
+
core.on(`${provider}:error`, callbacks.onError)
|
|
1188
|
+
];
|
|
1189
|
+
return () => {
|
|
1190
|
+
unsubs.forEach((u) => {
|
|
1191
|
+
u();
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/utils/load-gapi.ts
|
|
1197
|
+
var GIS_SCRIPT_SRC = "https://accounts.google.com/gsi/client";
|
|
1198
|
+
var gisPromise = null;
|
|
1199
|
+
function loadGoogleIdentityServices() {
|
|
1200
|
+
if (gisPromise) return gisPromise;
|
|
1201
|
+
gisPromise = new Promise((resolve, reject) => {
|
|
1202
|
+
const existing = document.querySelector(
|
|
1203
|
+
`script[src="${GIS_SCRIPT_SRC}"]`
|
|
1204
|
+
);
|
|
1205
|
+
if (existing) {
|
|
1206
|
+
existing.addEventListener("load", () => {
|
|
1207
|
+
resolve();
|
|
1208
|
+
});
|
|
1209
|
+
existing.addEventListener("error", () => {
|
|
1210
|
+
reject(new Error(`Failed to load script: ${GIS_SCRIPT_SRC}`));
|
|
1211
|
+
});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const script = document.createElement("script");
|
|
1215
|
+
script.src = GIS_SCRIPT_SRC;
|
|
1216
|
+
script.async = true;
|
|
1217
|
+
script.onload = () => {
|
|
1218
|
+
resolve();
|
|
1219
|
+
};
|
|
1220
|
+
script.onerror = () => {
|
|
1221
|
+
reject(new Error(`Failed to load script: ${GIS_SCRIPT_SRC}`));
|
|
1222
|
+
};
|
|
1223
|
+
document.head.appendChild(script);
|
|
1224
|
+
});
|
|
1225
|
+
return gisPromise;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/drives/drive-browser-controller.ts
|
|
1229
|
+
var GIS_SCOPE = "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/userinfo.profile";
|
|
1230
|
+
var DriveBrowserController = class {
|
|
1231
|
+
constructor(core, descriptor, callbacks) {
|
|
1232
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1233
|
+
this.plugin = null;
|
|
1234
|
+
this.tokenClient = null;
|
|
1235
|
+
this.unsubs = [];
|
|
1236
|
+
this.subscribe = (listener) => {
|
|
1237
|
+
this.listeners.add(listener);
|
|
1238
|
+
return () => this.listeners.delete(listener);
|
|
1239
|
+
};
|
|
1240
|
+
this.getSnapshot = () => this.state;
|
|
1241
|
+
this.core = core;
|
|
1242
|
+
this.descriptor = descriptor;
|
|
1243
|
+
this.callbacks = callbacks;
|
|
1244
|
+
this.state = {
|
|
1245
|
+
user: void 0,
|
|
1246
|
+
folder: void 0,
|
|
1247
|
+
path: [],
|
|
1248
|
+
selectedFiles: [],
|
|
1249
|
+
isClickLoading: false,
|
|
1250
|
+
showLoader: false,
|
|
1251
|
+
isAuthReady: false,
|
|
1252
|
+
isAuthenticated: false,
|
|
1253
|
+
isLoading: true,
|
|
1254
|
+
authCancelled: false,
|
|
1255
|
+
token: void 0,
|
|
1256
|
+
error: void 0,
|
|
1257
|
+
hasMore: false,
|
|
1258
|
+
isLoadingMore: false
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
setState(partial) {
|
|
1262
|
+
this.state = { ...this.state, ...partial };
|
|
1263
|
+
this.notify();
|
|
1264
|
+
}
|
|
1265
|
+
notify() {
|
|
1266
|
+
this.listeners.forEach((fn) => {
|
|
1267
|
+
fn();
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
// ── Lifecycle ────────────────────────────────────────────────
|
|
1271
|
+
init() {
|
|
1272
|
+
const plugin = this.core.getPlugin(this.descriptor.pluginId);
|
|
1273
|
+
if (!plugin) return;
|
|
1274
|
+
this.plugin = plugin;
|
|
1275
|
+
this.restore(plugin);
|
|
1276
|
+
this.unsubs.push(this.bindEvents());
|
|
1277
|
+
if (this.descriptor.auth === "gis") {
|
|
1278
|
+
void this.initGis(plugin);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
destroy() {
|
|
1282
|
+
this.unsubs.forEach((u) => {
|
|
1283
|
+
u();
|
|
1284
|
+
});
|
|
1285
|
+
this.unsubs = [];
|
|
1286
|
+
this.listeners.clear();
|
|
1287
|
+
this.plugin = null;
|
|
1288
|
+
this.tokenClient = null;
|
|
1289
|
+
}
|
|
1290
|
+
// ── Restore session on mount ─────────────────────────────────
|
|
1291
|
+
restore(plugin) {
|
|
1292
|
+
const restored = plugin.restoreSession();
|
|
1293
|
+
if (this.descriptor.auth === "gis") {
|
|
1294
|
+
if (!restored) return;
|
|
1295
|
+
this.setState({
|
|
1296
|
+
token: {
|
|
1297
|
+
access_token: plugin.getAccessToken() || "",
|
|
1298
|
+
expires_in: 3600
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
void (async () => {
|
|
1302
|
+
try {
|
|
1303
|
+
const userInfo = await plugin.getUserInfo();
|
|
1304
|
+
if (userInfo) this.setState({ user: userInfo });
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
await plugin.loadFiles(this.descriptor.loadFilesRootArg);
|
|
1308
|
+
})();
|
|
1309
|
+
} else {
|
|
1310
|
+
this.setState({ isAuthenticated: restored, isLoading: false });
|
|
1311
|
+
if (!restored) return;
|
|
1312
|
+
void (async () => {
|
|
1313
|
+
try {
|
|
1314
|
+
const userInfo = await plugin.getUserInfo();
|
|
1315
|
+
if (userInfo) this.setState({ user: userInfo });
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
await plugin.loadFiles(this.descriptor.loadFilesRootArg);
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
})();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
// ── Event subscriptions (the 6 canonical drive events) ─────
|
|
1326
|
+
bindEvents() {
|
|
1327
|
+
const isGis = this.descriptor.auth === "gis";
|
|
1328
|
+
return bindDriveEvents(this.core, this.descriptor.eventPrefix, {
|
|
1329
|
+
onAuthenticated: (payload) => {
|
|
1330
|
+
const data = payload;
|
|
1331
|
+
if (isGis) {
|
|
1332
|
+
if (data.user) this.setState({ user: data.user });
|
|
1333
|
+
this.setState({ isAuthReady: true });
|
|
1334
|
+
} else {
|
|
1335
|
+
this.setState({
|
|
1336
|
+
user: data.user ?? this.state.user,
|
|
1337
|
+
isAuthenticated: true,
|
|
1338
|
+
isLoading: false
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
onSignedOut: () => {
|
|
1343
|
+
this.resetSession();
|
|
1344
|
+
},
|
|
1345
|
+
onSessionExpired: () => {
|
|
1346
|
+
this.resetSession();
|
|
1347
|
+
},
|
|
1348
|
+
onFilesLoaded: (payload) => {
|
|
1349
|
+
const p = payload;
|
|
1350
|
+
const folder = this.buildRootFolder(p);
|
|
1351
|
+
this.cursor = p.cursor;
|
|
1352
|
+
this.setState({
|
|
1353
|
+
folder,
|
|
1354
|
+
path: this.nextPath(folder),
|
|
1355
|
+
isClickLoading: false,
|
|
1356
|
+
error: void 0,
|
|
1357
|
+
hasMore: !!p.hasMore
|
|
1358
|
+
});
|
|
1359
|
+
},
|
|
1360
|
+
onStateChange: (payload) => {
|
|
1361
|
+
const data = payload;
|
|
1362
|
+
if (isGis) {
|
|
1363
|
+
if (data.state === "browsing")
|
|
1364
|
+
this.setState({ isClickLoading: true });
|
|
1365
|
+
} else {
|
|
1366
|
+
this.setState({
|
|
1367
|
+
isLoading: data.state === "authenticating" || data.state === "browsing"
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
onError: (payload) => {
|
|
1372
|
+
const p = payload;
|
|
1373
|
+
this.setState({
|
|
1374
|
+
isClickLoading: false,
|
|
1375
|
+
showLoader: false,
|
|
1376
|
+
isLoadingMore: false,
|
|
1377
|
+
error: {
|
|
1378
|
+
message: p?.error?.message || "Unknown error",
|
|
1379
|
+
action: p?.action
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* The single session-teardown home (F-127). onSignedOut and onSessionExpired
|
|
1387
|
+
* previously drifted independently — onSessionExpired forgot to clear
|
|
1388
|
+
* selectedFiles/pendingFolder, leaking a stale selection into the next
|
|
1389
|
+
* sign-in. Both now delegate here.
|
|
1390
|
+
*/
|
|
1391
|
+
resetSession() {
|
|
1392
|
+
this.pendingFolder = void 0;
|
|
1393
|
+
this.cursor = void 0;
|
|
1394
|
+
this.setState({
|
|
1395
|
+
user: void 0,
|
|
1396
|
+
folder: void 0,
|
|
1397
|
+
token: void 0,
|
|
1398
|
+
path: [],
|
|
1399
|
+
selectedFiles: [],
|
|
1400
|
+
isAuthenticated: false,
|
|
1401
|
+
hasMore: false,
|
|
1402
|
+
error: void 0
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
buildRootFolder(payload) {
|
|
1406
|
+
const d = this.descriptor;
|
|
1407
|
+
const rawId = d.folderIdField === "path" ? payload.path : payload.folderId;
|
|
1408
|
+
const id = rawId || d.rootFolderId;
|
|
1409
|
+
const isRoot = !rawId || id === d.rootFolderId;
|
|
1410
|
+
let name;
|
|
1411
|
+
if (isRoot) name = d.rootFolderName;
|
|
1412
|
+
else if (this.pendingFolder?.id === id) name = this.pendingFolder.name;
|
|
1413
|
+
else if (d.folderKey === "path")
|
|
1414
|
+
name = id.split("/").pop() || d.rootFolderName;
|
|
1415
|
+
else name = id;
|
|
1416
|
+
return {
|
|
1417
|
+
id,
|
|
1418
|
+
name,
|
|
1419
|
+
path: d.folderKey === "path" ? payload.path || "" : "",
|
|
1420
|
+
size: 0,
|
|
1421
|
+
mimeType: "",
|
|
1422
|
+
isFolder: true,
|
|
1423
|
+
children: payload.files
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
// ── Auth ─────────────────────────────────────────────────────
|
|
1427
|
+
async initGis(plugin) {
|
|
1428
|
+
const cfg = plugin.getConfig?.() ?? {};
|
|
1429
|
+
if (!cfg.clientId || !cfg.apiKey) {
|
|
1430
|
+
this.setState({
|
|
1431
|
+
isAuthReady: true,
|
|
1432
|
+
error: {
|
|
1433
|
+
message: "Google Drive is not configured (missing clientId/apiKey)",
|
|
1434
|
+
action: "init"
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (plugin.isAuthenticated()) {
|
|
1440
|
+
this.setState({ isAuthReady: true });
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
await loadGoogleIdentityServices();
|
|
1444
|
+
const google = window.google;
|
|
1445
|
+
if (!google) {
|
|
1446
|
+
this.setState({
|
|
1447
|
+
isAuthReady: true,
|
|
1448
|
+
error: {
|
|
1449
|
+
message: "Google Identity Services failed to load",
|
|
1450
|
+
action: "init"
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
this.tokenClient = google.accounts.oauth2.initTokenClient({
|
|
1456
|
+
client_id: cfg.clientId,
|
|
1457
|
+
scope: GIS_SCOPE,
|
|
1458
|
+
ux_mode: "popup",
|
|
1459
|
+
callback: (tokenResponse) => {
|
|
1460
|
+
if (tokenResponse.error) return;
|
|
1461
|
+
this.setState({
|
|
1462
|
+
authCancelled: false,
|
|
1463
|
+
token: {
|
|
1464
|
+
access_token: tokenResponse.access_token,
|
|
1465
|
+
expires_in: Date.now() + (tokenResponse.expires_in - 20) * 1e3
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
void plugin.authenticate?.(
|
|
1469
|
+
tokenResponse.access_token,
|
|
1470
|
+
tokenResponse.expires_in
|
|
1471
|
+
).then(
|
|
1472
|
+
() => plugin.loadFiles(this.descriptor.loadFilesRootArg)
|
|
1473
|
+
);
|
|
1474
|
+
},
|
|
1475
|
+
error_callback: () => {
|
|
1476
|
+
this.setState({ authCancelled: true });
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
this.setState({ isAuthReady: true });
|
|
1480
|
+
}
|
|
1481
|
+
signIn() {
|
|
1482
|
+
if (this.descriptor.auth === "gis") {
|
|
1483
|
+
this.tokenClient?.requestAccessToken({});
|
|
1484
|
+
} else {
|
|
1485
|
+
void this.runPopupAuth();
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
retryAuth() {
|
|
1489
|
+
if (this.descriptor.auth === "gis") {
|
|
1490
|
+
this.setState({ authCancelled: false });
|
|
1491
|
+
this.tokenClient?.requestAccessToken({});
|
|
1492
|
+
} else {
|
|
1493
|
+
void this.runPopupAuth();
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
async runPopupAuth() {
|
|
1497
|
+
const plugin = this.plugin;
|
|
1498
|
+
if (!plugin?.authenticateViaPopup) return;
|
|
1499
|
+
this.setState({ isLoading: true });
|
|
1500
|
+
await plugin.authenticateViaPopup();
|
|
1501
|
+
if (plugin.isAuthenticated()) {
|
|
1502
|
+
await plugin.loadFiles(this.descriptor.loadFilesRootArg);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
signOut() {
|
|
1506
|
+
this.plugin?.signOut();
|
|
1507
|
+
this.setState({ token: void 0 });
|
|
1508
|
+
}
|
|
1509
|
+
// ── Browse / select actions ──────────────────────────────────
|
|
1510
|
+
/**
|
|
1511
|
+
* Folder trail for the breadcrumb, owned here so every framework stays consistent
|
|
1512
|
+
* (react/vue/svelte previously seeded it in a component effect that collapsed the
|
|
1513
|
+
* trail; vanilla/angular had no seed at all). Root load resets to [root]; descending
|
|
1514
|
+
* appends; re-loading a folder already in the trail truncates to it. Ids must stay
|
|
1515
|
+
* unique — the breadcrumb is keyed by folder id.
|
|
1516
|
+
*/
|
|
1517
|
+
nextPath(folder) {
|
|
1518
|
+
if (folder.id === this.descriptor.rootFolderId) return [folder];
|
|
1519
|
+
const cur = this.state.path;
|
|
1520
|
+
const idx = cur.findIndex((p) => p.id === folder.id);
|
|
1521
|
+
if (idx >= 0) return [...cur.slice(0, idx), folder];
|
|
1522
|
+
return [...cur, folder];
|
|
1523
|
+
}
|
|
1524
|
+
setPath(path) {
|
|
1525
|
+
this.cursor = void 0;
|
|
1526
|
+
this.setState({ path, hasMore: false });
|
|
1527
|
+
}
|
|
1528
|
+
handleClick(file) {
|
|
1529
|
+
const plugin = this.plugin;
|
|
1530
|
+
if (!plugin) return;
|
|
1531
|
+
if (file.isFolder) {
|
|
1532
|
+
this.setState({ isClickLoading: true });
|
|
1533
|
+
const arg = this.descriptor.folderKey === "path" ? file.path : file.id;
|
|
1534
|
+
this.pendingFolder = { id: arg, name: file.name };
|
|
1535
|
+
void plugin.loadFiles(arg);
|
|
1536
|
+
} else {
|
|
1537
|
+
const prev = this.state.selectedFiles;
|
|
1538
|
+
const next = prev.some((f) => f.id === file.id) ? prev.filter((f) => f.id !== file.id) : [...prev, file];
|
|
1539
|
+
this.setState({ selectedFiles: next });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async handleSubmit() {
|
|
1543
|
+
const plugin = this.plugin;
|
|
1544
|
+
if (!plugin || this.state.selectedFiles.length === 0) return;
|
|
1545
|
+
this.setState({ showLoader: true });
|
|
1546
|
+
try {
|
|
1547
|
+
await this.downloadAndClose(this.state.selectedFiles);
|
|
1548
|
+
} catch {
|
|
1549
|
+
} finally {
|
|
1550
|
+
this.setState({ showLoader: false });
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
handleCancelDownload() {
|
|
1554
|
+
this.setState({ selectedFiles: [] });
|
|
1555
|
+
}
|
|
1556
|
+
async onSelectCurrentFolder() {
|
|
1557
|
+
const plugin = this.plugin;
|
|
1558
|
+
if (!plugin) return;
|
|
1559
|
+
if (this.descriptor.selectFolder === "cached-children") {
|
|
1560
|
+
const current = this.state.path[this.state.path.length - 1];
|
|
1561
|
+
if (!current) return;
|
|
1562
|
+
this.setState({ showLoader: true });
|
|
1563
|
+
try {
|
|
1564
|
+
await this.downloadAndClose(current.children);
|
|
1565
|
+
} catch {
|
|
1566
|
+
} finally {
|
|
1567
|
+
this.setState({ showLoader: false });
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (!plugin.loadAllFilesInFolder || !this.state.folder) return;
|
|
1572
|
+
this.setState({ showLoader: true });
|
|
1573
|
+
try {
|
|
1574
|
+
const folderArg = this.state.folder[this.descriptor.folderKey] || this.descriptor.loadAllRootArg || "";
|
|
1575
|
+
const all = await plugin.loadAllFilesInFolder(folderArg);
|
|
1576
|
+
await this.downloadAndClose(all);
|
|
1577
|
+
} catch {
|
|
1578
|
+
} finally {
|
|
1579
|
+
this.setState({ showLoader: false });
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Append-driven pagination (F-125), mirroring onSelectCurrentFolder's
|
|
1584
|
+
* await-then-use-the-return-value style rather than reacting to another
|
|
1585
|
+
* files-loaded event — the same convention, not a second one. A no-op
|
|
1586
|
+
* unless the plugin supports it, the current view has more to fetch, and
|
|
1587
|
+
* a continuation token exists.
|
|
1588
|
+
*/
|
|
1589
|
+
async loadMore() {
|
|
1590
|
+
const plugin = this.plugin;
|
|
1591
|
+
if (!plugin?.loadMoreFiles || !this.state.hasMore || !this.cursor || this.state.isLoadingMore) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
this.setState({ isLoadingMore: true, error: void 0 });
|
|
1595
|
+
try {
|
|
1596
|
+
const page = await plugin.loadMoreFiles(this.cursor);
|
|
1597
|
+
const folder = this.state.folder;
|
|
1598
|
+
if (!folder) {
|
|
1599
|
+
this.setState({ isLoadingMore: false });
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const merged = {
|
|
1603
|
+
...folder,
|
|
1604
|
+
children: [...folder.children, ...page.files]
|
|
1605
|
+
};
|
|
1606
|
+
const path = this.state.path.slice();
|
|
1607
|
+
const tail = path[path.length - 1];
|
|
1608
|
+
if (tail && tail.id === folder.id) {
|
|
1609
|
+
path[path.length - 1] = merged;
|
|
1610
|
+
}
|
|
1611
|
+
this.cursor = page.cursor;
|
|
1612
|
+
this.setState({
|
|
1613
|
+
folder: merged,
|
|
1614
|
+
path,
|
|
1615
|
+
hasMore: page.hasMore,
|
|
1616
|
+
isLoadingMore: false
|
|
1617
|
+
});
|
|
1618
|
+
} catch {
|
|
1619
|
+
this.setState({ isLoadingMore: false });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
async downloadAndClose(files) {
|
|
1623
|
+
const plugin = this.plugin;
|
|
1624
|
+
if (!plugin) return;
|
|
1625
|
+
const fileOnly = files.filter((f) => !f.isFolder);
|
|
1626
|
+
if (fileOnly.length > 0) {
|
|
1627
|
+
const downloaded = await plugin.downloadFiles(fileOnly);
|
|
1628
|
+
if (downloaded.length > 0)
|
|
1629
|
+
this.callbacks.onFilesSelected(downloaded);
|
|
1630
|
+
}
|
|
1631
|
+
this.setState({ selectedFiles: [] });
|
|
1632
|
+
this.callbacks.onClose();
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
// src/folder-drop.ts
|
|
1637
|
+
function setRelativePath(file, path) {
|
|
1638
|
+
try {
|
|
1639
|
+
Object.defineProperty(file, "relativePath", {
|
|
1640
|
+
value: path.replace(/^\//, ""),
|
|
1641
|
+
configurable: true,
|
|
1642
|
+
enumerable: false,
|
|
1643
|
+
writable: true
|
|
1644
|
+
});
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function fileFromWebkitEntry(entry) {
|
|
1649
|
+
return new Promise((resolve, reject) => {
|
|
1650
|
+
entry.file?.((file) => {
|
|
1651
|
+
setRelativePath(file, entry.fullPath || `/${file.name}`);
|
|
1652
|
+
resolve([file]);
|
|
1653
|
+
}, reject);
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
async function traverseWebkitEntry(entry, allowFolderDrop, state) {
|
|
1657
|
+
if (entry.isFile) return fileFromWebkitEntry(entry);
|
|
1658
|
+
if (!entry.isDirectory) return [];
|
|
1659
|
+
if (!allowFolderDrop) {
|
|
1660
|
+
state.skippedDirectory = true;
|
|
1661
|
+
return [];
|
|
1662
|
+
}
|
|
1663
|
+
const reader = entry.createReader?.();
|
|
1664
|
+
if (!reader) return [];
|
|
1665
|
+
const entries = await new Promise((resolve, reject) => {
|
|
1666
|
+
reader.readEntries(resolve, reject);
|
|
1667
|
+
});
|
|
1668
|
+
const nested = await Promise.all(
|
|
1669
|
+
entries.map(
|
|
1670
|
+
(child) => traverseWebkitEntry(child, allowFolderDrop, state)
|
|
1671
|
+
)
|
|
1672
|
+
);
|
|
1673
|
+
return nested.flat();
|
|
1674
|
+
}
|
|
1675
|
+
async function traverseHandle(handle, allowFolderDrop, state, path = "") {
|
|
1676
|
+
if (handle.kind === "file" && handle.getFile) {
|
|
1677
|
+
const file = await handle.getFile();
|
|
1678
|
+
setRelativePath(file, path + file.name);
|
|
1679
|
+
return [file];
|
|
1680
|
+
}
|
|
1681
|
+
if (handle.kind !== "directory") return [];
|
|
1682
|
+
if (!allowFolderDrop) {
|
|
1683
|
+
state.skippedDirectory = true;
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
if (!handle.entries) return [];
|
|
1687
|
+
const out = [];
|
|
1688
|
+
for await (const [name, child] of handle.entries()) {
|
|
1689
|
+
out.push(
|
|
1690
|
+
...await traverseHandle(
|
|
1691
|
+
child,
|
|
1692
|
+
allowFolderDrop,
|
|
1693
|
+
state,
|
|
1694
|
+
path + name + "/"
|
|
1695
|
+
)
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
return out;
|
|
1699
|
+
}
|
|
1700
|
+
async function collectDroppedFiles(dataTransfer, allowFolderDrop) {
|
|
1701
|
+
const items = Array.from(dataTransfer.items);
|
|
1702
|
+
const state = { skippedDirectory: false };
|
|
1703
|
+
const firstItem = items[0];
|
|
1704
|
+
const supportsWebkitEntries = typeof firstItem?.webkitGetAsEntry === "function";
|
|
1705
|
+
if (supportsWebkitEntries) {
|
|
1706
|
+
const entries = items.map((item) => item.webkitGetAsEntry()).filter(Boolean);
|
|
1707
|
+
const nested = await Promise.all(
|
|
1708
|
+
entries.map(
|
|
1709
|
+
(entry) => traverseWebkitEntry(entry, allowFolderDrop, state)
|
|
1710
|
+
)
|
|
1711
|
+
);
|
|
1712
|
+
const files = nested.flat();
|
|
1713
|
+
if (files.length > 0 || state.skippedDirectory) {
|
|
1714
|
+
return { files, skippedDirectory: state.skippedDirectory };
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const supportsHandles = "getAsFileSystemHandle" in (firstItem || {});
|
|
1718
|
+
if (supportsHandles) {
|
|
1719
|
+
const handles = await Promise.all(
|
|
1720
|
+
items.map(
|
|
1721
|
+
async (item) => await item.getAsFileSystemHandle?.()
|
|
1722
|
+
)
|
|
1723
|
+
);
|
|
1724
|
+
const nested = await Promise.all(
|
|
1725
|
+
handles.filter(
|
|
1726
|
+
(handle) => Boolean(handle)
|
|
1727
|
+
).map((handle) => traverseHandle(handle, allowFolderDrop, state))
|
|
1728
|
+
);
|
|
1729
|
+
const files = nested.flat();
|
|
1730
|
+
if (files.length > 0 || state.skippedDirectory) {
|
|
1731
|
+
return { files, skippedDirectory: state.skippedDirectory };
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
files: Array.from(dataTransfer.files),
|
|
1736
|
+
skippedDirectory: false
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/controllers/drag-drop-controller.ts
|
|
1741
|
+
var READONLY_DRIVE_SOURCE_MAP = {
|
|
1742
|
+
googleDrive: true,
|
|
1743
|
+
oneDrive: true,
|
|
1744
|
+
dropbox: true,
|
|
1745
|
+
box: true
|
|
1746
|
+
};
|
|
1747
|
+
var READONLY_DRIVE_SOURCES = new Set(
|
|
1748
|
+
Object.keys(READONLY_DRIVE_SOURCE_MAP)
|
|
1749
|
+
);
|
|
1750
|
+
var DragDropController = class {
|
|
1751
|
+
constructor(deps) {
|
|
1752
|
+
this.deps = deps;
|
|
1753
|
+
this.isDragging = false;
|
|
1754
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1755
|
+
this.unsubOrchestrator = null;
|
|
1756
|
+
this.subscribe = (listener) => {
|
|
1757
|
+
this.listeners.add(listener);
|
|
1758
|
+
return () => this.listeners.delete(listener);
|
|
1759
|
+
};
|
|
1760
|
+
this.getSnapshot = () => this.snapshot;
|
|
1761
|
+
this.handleDragOver = this.handleDragOver.bind(this);
|
|
1762
|
+
this.handleDragLeave = this.handleDragLeave.bind(this);
|
|
1763
|
+
this.handleDrop = this.handleDrop.bind(this);
|
|
1764
|
+
this.handlePaste = this.handlePaste.bind(this);
|
|
1765
|
+
this.snapshot = this.compute();
|
|
1766
|
+
}
|
|
1767
|
+
/** Subscribe to the orchestrator so border/dragging derive on change. Call on mount. Idempotent. */
|
|
1768
|
+
init() {
|
|
1769
|
+
if (this.unsubOrchestrator) return;
|
|
1770
|
+
this.unsubOrchestrator = this.deps.orchestrator.subscribe(() => {
|
|
1771
|
+
this.recompute();
|
|
1772
|
+
});
|
|
1773
|
+
this.recompute();
|
|
1774
|
+
}
|
|
1775
|
+
get disabled() {
|
|
1776
|
+
const p = this.deps.props();
|
|
1777
|
+
const o = this.deps.orchestrator.getSnapshot();
|
|
1778
|
+
return p.disableDragDrop || !!o.activeSource || isUploadActive(o.uploadStatus);
|
|
1779
|
+
}
|
|
1780
|
+
compute() {
|
|
1781
|
+
const o = this.deps.orchestrator.getSnapshot();
|
|
1782
|
+
const filesSize = this.deps.filesSize();
|
|
1783
|
+
const active = o.activeSource;
|
|
1784
|
+
const isAddingMore = o.isAddingMore;
|
|
1785
|
+
return {
|
|
1786
|
+
isDragging: this.isDragging,
|
|
1787
|
+
absoluteIsDragging: this.isDragging && !active,
|
|
1788
|
+
absoluteHasBorder: (!filesSize || isAddingMore || this.isDragging) && !active
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Recompute the cached snapshot from current inputs and notify if it changed.
|
|
1793
|
+
* Runs automatically on drag events and orchestrator changes. Hosts may also call
|
|
1794
|
+
* it to refresh after external state the controller cannot observe via the
|
|
1795
|
+
* orchestrator — e.g. a framework that removes files through `core.removeFile`
|
|
1796
|
+
* directly (which emits no orchestrator notify), leaving the file-count-derived
|
|
1797
|
+
* `absoluteHasBorder` stale.
|
|
1798
|
+
*/
|
|
1799
|
+
recompute() {
|
|
1800
|
+
const next = this.compute();
|
|
1801
|
+
const prev = this.snapshot;
|
|
1802
|
+
if (next.isDragging === prev.isDragging && next.absoluteIsDragging === prev.absoluteIsDragging && next.absoluteHasBorder === prev.absoluteHasBorder)
|
|
1803
|
+
return;
|
|
1804
|
+
this.snapshot = next;
|
|
1805
|
+
this.notify();
|
|
1806
|
+
}
|
|
1807
|
+
notify() {
|
|
1808
|
+
this.listeners.forEach((fn) => {
|
|
1809
|
+
fn();
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
handleDragOver(e) {
|
|
1813
|
+
if (this.disabled || this.deps.props().isProcessing) return;
|
|
1814
|
+
e.preventDefault();
|
|
1815
|
+
this.isDragging = true;
|
|
1816
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
1817
|
+
const dropped = Array.from(e.dataTransfer?.files || []);
|
|
1818
|
+
this.deps.options().onFilesDragOver?.(dropped);
|
|
1819
|
+
this.deps.core.emit("drag-over", {});
|
|
1820
|
+
this.recompute();
|
|
1821
|
+
}
|
|
1822
|
+
handleDragLeave(e) {
|
|
1823
|
+
if (this.disabled || this.deps.props().isProcessing) return;
|
|
1824
|
+
e.preventDefault();
|
|
1825
|
+
const container = e.currentTarget;
|
|
1826
|
+
const destination = e.relatedTarget;
|
|
1827
|
+
if (container && destination && typeof container.contains === "function" && container.contains(destination))
|
|
1828
|
+
return;
|
|
1829
|
+
this.isDragging = false;
|
|
1830
|
+
const dropped = Array.from(e.dataTransfer?.files || []);
|
|
1831
|
+
this.deps.options().onFilesDragLeave?.(dropped);
|
|
1832
|
+
this.deps.core.emit("drag-leave", {});
|
|
1833
|
+
this.recompute();
|
|
1834
|
+
}
|
|
1835
|
+
/** True when the drag payload carries at least one file. */
|
|
1836
|
+
dropHasFiles(e) {
|
|
1837
|
+
const dt = e.dataTransfer;
|
|
1838
|
+
if (!dt) return false;
|
|
1839
|
+
if (dt.files && dt.files.length > 0) return true;
|
|
1840
|
+
const types = dt.types ? Array.from(dt.types) : [];
|
|
1841
|
+
if (types.includes("Files")) return true;
|
|
1842
|
+
return dt.items ? Array.from(dt.items).some((i) => i.kind === "file") : false;
|
|
1843
|
+
}
|
|
1844
|
+
async handleDrop(e) {
|
|
1845
|
+
const p = this.deps.props();
|
|
1846
|
+
if (p.isProcessing || p.disableDragDrop) return;
|
|
1847
|
+
const active = this.deps.orchestrator.getSnapshot().activeSource;
|
|
1848
|
+
if (active && READONLY_DRIVE_SOURCES.has(active) && this.dropHasFiles(e) && this.deps.onReadonlyDropRejected) {
|
|
1849
|
+
e.preventDefault();
|
|
1850
|
+
this.deps.onReadonlyDropRejected(active);
|
|
1851
|
+
this.isDragging = false;
|
|
1852
|
+
this.recompute();
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (this.disabled) return;
|
|
1856
|
+
e.preventDefault();
|
|
1857
|
+
if (!e.dataTransfer) {
|
|
1858
|
+
this.isDragging = false;
|
|
1859
|
+
this.recompute();
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const { files: dropped, skippedDirectory } = await collectDroppedFiles(
|
|
1863
|
+
e.dataTransfer,
|
|
1864
|
+
this.deps.props().folderUploadAllowDrop
|
|
1865
|
+
);
|
|
1866
|
+
if (skippedDirectory) {
|
|
1867
|
+
this.deps.options().onWarn?.(
|
|
1868
|
+
dropped.length > 0 ? "Dropped folders were ignored because folderUpload.allowDrop is disabled." : "Folder drop is disabled. Enable folderUpload.allowDrop to accept dropped folders."
|
|
1869
|
+
);
|
|
1870
|
+
this.deps.core.emit("folder-drop-blocked", {
|
|
1871
|
+
acceptedFiles: dropped.length
|
|
1872
|
+
});
|
|
1873
|
+
if (dropped.length === 0) {
|
|
1874
|
+
this.isDragging = false;
|
|
1875
|
+
this.recompute();
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
this.deps.options().onFilesDrop?.(dropped);
|
|
1880
|
+
void this.deps.setFiles(dropped);
|
|
1881
|
+
this.deps.core.emit("drop", { files: dropped });
|
|
1882
|
+
this.isDragging = false;
|
|
1883
|
+
this.recompute();
|
|
1884
|
+
}
|
|
1885
|
+
handlePaste(e) {
|
|
1886
|
+
if (!this.deps.options().enablePaste || this.deps.props().isProcessing)
|
|
1887
|
+
return;
|
|
1888
|
+
const items = e.clipboardData?.items;
|
|
1889
|
+
if (!items) return;
|
|
1890
|
+
const files = [];
|
|
1891
|
+
for (const item of Array.from(items)) {
|
|
1892
|
+
if (item.kind === "file") {
|
|
1893
|
+
const f = item.getAsFile();
|
|
1894
|
+
if (f) {
|
|
1895
|
+
const name = f.name === "image.png" || !f.name ? `pasted-${Date.now()}.${f.type.split("/")[1] || "png"}` : f.name;
|
|
1896
|
+
files.push(new File([f], name, { type: f.type }));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (files.length) {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
void this.deps.setFiles(files);
|
|
1903
|
+
this.deps.core.emit("paste", { files });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
destroy() {
|
|
1907
|
+
this.unsubOrchestrator?.();
|
|
1908
|
+
this.unsubOrchestrator = null;
|
|
1909
|
+
this.listeners.clear();
|
|
1910
|
+
this.isDragging = false;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
// src/controllers/server-mode-drive-controller.ts
|
|
1915
|
+
var ServerModeDriveController = class {
|
|
1916
|
+
constructor(deps) {
|
|
1917
|
+
this.deps = deps;
|
|
1918
|
+
this.snapshot = {
|
|
1919
|
+
state: { status: "idle" },
|
|
1920
|
+
folderId: void 0,
|
|
1921
|
+
search: ""
|
|
1922
|
+
};
|
|
1923
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1924
|
+
this.abort = null;
|
|
1925
|
+
this.authListener = null;
|
|
1926
|
+
this.subscribe = (listener) => {
|
|
1927
|
+
this.listeners.add(listener);
|
|
1928
|
+
return () => this.listeners.delete(listener);
|
|
1929
|
+
};
|
|
1930
|
+
this.getSnapshot = () => this.snapshot;
|
|
1931
|
+
this.list = this.list.bind(this);
|
|
1932
|
+
this.refresh = this.refresh.bind(this);
|
|
1933
|
+
this.transfer = this.transfer.bind(this);
|
|
1934
|
+
this.startAuth = this.startAuth.bind(this);
|
|
1935
|
+
}
|
|
1936
|
+
setState(p) {
|
|
1937
|
+
this.snapshot = { ...this.snapshot, ...p };
|
|
1938
|
+
this.listeners.forEach((fn) => {
|
|
1939
|
+
fn();
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
/** Initial list. Call on mount. */
|
|
1943
|
+
init() {
|
|
1944
|
+
void this.list();
|
|
1945
|
+
}
|
|
1946
|
+
async list(opts) {
|
|
1947
|
+
const serverUrl = this.deps.serverUrl();
|
|
1948
|
+
if (!serverUrl) {
|
|
1949
|
+
this.setState({
|
|
1950
|
+
state: {
|
|
1951
|
+
status: "error",
|
|
1952
|
+
message: "Server Mode requires `serverUrl` prop"
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
this.abort?.abort();
|
|
1958
|
+
const ac = new AbortController();
|
|
1959
|
+
this.abort = ac;
|
|
1960
|
+
this.setState({ state: { status: "loading" } });
|
|
1961
|
+
const params = new URLSearchParams();
|
|
1962
|
+
const nextFolder = opts?.folderId ?? this.snapshot.folderId;
|
|
1963
|
+
const nextSearch = opts?.search ?? this.snapshot.search;
|
|
1964
|
+
if (nextFolder) params.set("folderId", nextFolder);
|
|
1965
|
+
if (nextSearch) params.set("search", nextSearch);
|
|
1966
|
+
try {
|
|
1967
|
+
const res = await fetch(
|
|
1968
|
+
`${serverUrl}/files/${this.deps.provider}${params.toString() ? `?${params}` : ""}`,
|
|
1969
|
+
{ credentials: "include", signal: ac.signal }
|
|
1970
|
+
);
|
|
1971
|
+
if (res.status === 401) {
|
|
1972
|
+
const body = await res.clone().json().catch(() => ({}));
|
|
1973
|
+
if (body.reauth) {
|
|
1974
|
+
this.setState({ state: { status: "reauth" } });
|
|
1975
|
+
} else {
|
|
1976
|
+
this.setState({
|
|
1977
|
+
state: {
|
|
1978
|
+
status: "error",
|
|
1979
|
+
message: body.error ?? "Unauthorized",
|
|
1980
|
+
code: "UNAUTHENTICATED"
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (!res.ok) {
|
|
1987
|
+
const text = await res.text().catch(() => "");
|
|
1988
|
+
throw new UpupNetworkError(text || `${res.status}`, res.status);
|
|
1989
|
+
}
|
|
1990
|
+
const data = await res.json();
|
|
1991
|
+
this.setState({ state: { status: "ready", files: data.files } });
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
if (err.name === "AbortError") return;
|
|
1994
|
+
this.setState({
|
|
1995
|
+
state: { status: "error", message: err.message }
|
|
1996
|
+
});
|
|
1997
|
+
} finally {
|
|
1998
|
+
if (this.abort === ac) this.abort = null;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
refresh(opts) {
|
|
2002
|
+
return this.list(opts);
|
|
2003
|
+
}
|
|
2004
|
+
async transfer(file) {
|
|
2005
|
+
const serverUrl = this.deps.serverUrl();
|
|
2006
|
+
if (!serverUrl)
|
|
2007
|
+
return {
|
|
2008
|
+
status: "error",
|
|
2009
|
+
message: "Server Mode requires `serverUrl` prop"
|
|
2010
|
+
};
|
|
2011
|
+
try {
|
|
2012
|
+
const res = await fetch(
|
|
2013
|
+
`${serverUrl}/files/${this.deps.provider}/transfer`,
|
|
2014
|
+
{
|
|
2015
|
+
method: "POST",
|
|
2016
|
+
credentials: "include",
|
|
2017
|
+
headers: { "Content-Type": "application/json" },
|
|
2018
|
+
body: JSON.stringify({
|
|
2019
|
+
fileId: file.id,
|
|
2020
|
+
fileName: file.name,
|
|
2021
|
+
size: file.size,
|
|
2022
|
+
mimeType: file.mimeType
|
|
2023
|
+
})
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
if (res.status === 401) {
|
|
2027
|
+
const body = await res.clone().json().catch(() => ({}));
|
|
2028
|
+
if (body.reauth) return { status: "reauth" };
|
|
2029
|
+
return {
|
|
2030
|
+
status: "error",
|
|
2031
|
+
message: body.error ?? "Unauthorized",
|
|
2032
|
+
code: "UNAUTHENTICATED"
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
if (!res.ok) {
|
|
2036
|
+
const text = await res.text().catch(() => "");
|
|
2037
|
+
return { status: "error", message: text || `${res.status}` };
|
|
2038
|
+
}
|
|
2039
|
+
const result = await res.json();
|
|
2040
|
+
return { status: "ok", result };
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
return { status: "error", message: err.message };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
startAuth() {
|
|
2046
|
+
const serverUrl = this.deps.serverUrl();
|
|
2047
|
+
if (!serverUrl) return;
|
|
2048
|
+
if (this.authListener) {
|
|
2049
|
+
window.removeEventListener("message", this.authListener);
|
|
2050
|
+
this.authListener = null;
|
|
2051
|
+
}
|
|
2052
|
+
const popup = window.open(
|
|
2053
|
+
`${serverUrl}/auth/${this.deps.provider}`,
|
|
2054
|
+
"upup-oauth",
|
|
2055
|
+
"width=600,height=700"
|
|
2056
|
+
);
|
|
2057
|
+
if (!popup) {
|
|
2058
|
+
this.setState({
|
|
2059
|
+
state: {
|
|
2060
|
+
status: "error",
|
|
2061
|
+
message: "Popup blocked. Allow popups and try again."
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const onMessage = (ev) => {
|
|
2067
|
+
const data = ev.data;
|
|
2068
|
+
if (data?.type === "upup:oauth-success" && data.provider === this.deps.provider) {
|
|
2069
|
+
window.removeEventListener("message", onMessage);
|
|
2070
|
+
if (this.authListener === onMessage) this.authListener = null;
|
|
2071
|
+
void this.list();
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
this.authListener = onMessage;
|
|
2075
|
+
window.addEventListener("message", onMessage);
|
|
2076
|
+
}
|
|
2077
|
+
setFolderId(id) {
|
|
2078
|
+
this.setState({ folderId: id });
|
|
2079
|
+
}
|
|
2080
|
+
setSearch(s) {
|
|
2081
|
+
this.setState({ search: s });
|
|
2082
|
+
}
|
|
2083
|
+
/** Force the reauth view (used by batch consumers on a transfer 401). */
|
|
2084
|
+
requestReauth() {
|
|
2085
|
+
this.setState({ state: { status: "reauth" } });
|
|
2086
|
+
}
|
|
2087
|
+
/** Force the error view (used by batch consumers on a transfer failure). */
|
|
2088
|
+
setError(message) {
|
|
2089
|
+
this.setState({ state: { status: "error", message } });
|
|
2090
|
+
}
|
|
2091
|
+
destroy() {
|
|
2092
|
+
this.abort?.abort();
|
|
2093
|
+
this.abort = null;
|
|
2094
|
+
if (this.authListener) {
|
|
2095
|
+
window.removeEventListener("message", this.authListener);
|
|
2096
|
+
this.authListener = null;
|
|
2097
|
+
}
|
|
2098
|
+
this.listeners.clear();
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
function cn(...inputs) {
|
|
2102
|
+
return twMerge(clsx(inputs));
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/utils/source-metadata.ts
|
|
2106
|
+
var sourceNameKeys = {
|
|
2107
|
+
["local" /* LOCAL */]: "myDevice",
|
|
2108
|
+
["googleDrive" /* GOOGLE_DRIVE */]: "googleDrive",
|
|
2109
|
+
["oneDrive" /* ONE_DRIVE */]: "oneDrive",
|
|
2110
|
+
["dropbox" /* DROPBOX */]: "dropbox",
|
|
2111
|
+
["box" /* BOX */]: "box",
|
|
2112
|
+
["url" /* URL */]: "link",
|
|
2113
|
+
["camera" /* CAMERA */]: "camera",
|
|
2114
|
+
["microphone" /* MICROPHONE */]: "audio",
|
|
2115
|
+
["screen" /* SCREEN */]: "screenCapture"
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// src/file-utils.ts
|
|
2119
|
+
var bytesToSize = (bytes) => {
|
|
2120
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
2121
|
+
if (bytes === 0) return "0 Byte";
|
|
2122
|
+
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
|
2123
|
+
return `${Math.round(bytes / Math.pow(1024, i))} ${sizes[i] ?? ""}`;
|
|
2124
|
+
};
|
|
2125
|
+
var sizeToBytes = (size, unit = "B") => {
|
|
2126
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
2127
|
+
const i = sizes.indexOf(unit);
|
|
2128
|
+
return size * Math.pow(1024, i);
|
|
2129
|
+
};
|
|
2130
|
+
function checkFileSize(file, sizeLimit, mode = "max") {
|
|
2131
|
+
if (!sizeLimit) return true;
|
|
2132
|
+
const limitBytes = sizeToBytes(sizeLimit.size, sizeLimit.unit);
|
|
2133
|
+
if (mode === "min") return file.size >= limitBytes;
|
|
2134
|
+
return file.size <= limitBytes;
|
|
2135
|
+
}
|
|
2136
|
+
var PREVIEW_MAX_TEXT_SIZE = 512 * 1024;
|
|
2137
|
+
var PREVIEW_TEXT_TRUNCATE_LENGTH = 1e5;
|
|
2138
|
+
function fileGetIsText(fileType, fileName) {
|
|
2139
|
+
if (!fileType) return false;
|
|
2140
|
+
if (fileType.startsWith("text/")) return true;
|
|
2141
|
+
const lower = (fileName ?? "").toLowerCase();
|
|
2142
|
+
return lower.endsWith(".txt") || lower.endsWith(".md") || lower.endsWith(".json") || lower.endsWith(".csv") || lower.endsWith(".log") || lower.endsWith(".js") || lower.endsWith(".ts") || lower.endsWith(".css") || lower.endsWith(".html");
|
|
2143
|
+
}
|
|
2144
|
+
function fileCanPreviewText(fileType, fileName, fileSize) {
|
|
2145
|
+
if (!fileGetIsText(fileType, fileName)) return false;
|
|
2146
|
+
if (fileSize === void 0) return true;
|
|
2147
|
+
return fileSize <= PREVIEW_MAX_TEXT_SIZE;
|
|
2148
|
+
}
|
|
2149
|
+
function fileGetIsImage(fileType) {
|
|
2150
|
+
return typeof fileType === "string" && fileType.startsWith("image/");
|
|
2151
|
+
}
|
|
2152
|
+
function fileGetIsPdf(fileType, fileName) {
|
|
2153
|
+
if (fileType === "application/pdf") return true;
|
|
2154
|
+
return (fileName ?? "").toLowerCase().endsWith(".pdf");
|
|
2155
|
+
}
|
|
2156
|
+
function fileGetExtension(fileType, fileName) {
|
|
2157
|
+
const safeName = fileName ?? "";
|
|
2158
|
+
if (!fileType) {
|
|
2159
|
+
return safeName.split(".").pop()?.toLowerCase() || "";
|
|
2160
|
+
}
|
|
2161
|
+
const typeSplit = fileType.split("/");
|
|
2162
|
+
const nameSplit = safeName.split(".");
|
|
2163
|
+
const lastNamePart = nameSplit[nameSplit.length - 1]?.toLowerCase() || "";
|
|
2164
|
+
if (!typeSplit[1]) {
|
|
2165
|
+
return lastNamePart;
|
|
2166
|
+
}
|
|
2167
|
+
if (typeSplit[1].includes(".")) {
|
|
2168
|
+
return lastNamePart;
|
|
2169
|
+
}
|
|
2170
|
+
return typeSplit[1].toLowerCase();
|
|
2171
|
+
}
|
|
2172
|
+
function fileIs3D(ext) {
|
|
2173
|
+
const threeDExtensions = [
|
|
2174
|
+
"3ds",
|
|
2175
|
+
"3dm",
|
|
2176
|
+
"blend",
|
|
2177
|
+
"dxf",
|
|
2178
|
+
"dwg",
|
|
2179
|
+
"c4d",
|
|
2180
|
+
"ma",
|
|
2181
|
+
"mb",
|
|
2182
|
+
"ply",
|
|
2183
|
+
"stl",
|
|
2184
|
+
"fbx",
|
|
2185
|
+
"obj",
|
|
2186
|
+
"dae",
|
|
2187
|
+
"gltf",
|
|
2188
|
+
"glb",
|
|
2189
|
+
"m3"
|
|
2190
|
+
];
|
|
2191
|
+
return threeDExtensions.includes(ext.toLowerCase());
|
|
2192
|
+
}
|
|
2193
|
+
function searchDriveFiles(files, searchTerm, options = {}) {
|
|
2194
|
+
const {
|
|
2195
|
+
caseSensitive = false,
|
|
2196
|
+
exactMatch = false,
|
|
2197
|
+
maxResults = 100
|
|
2198
|
+
} = options;
|
|
2199
|
+
if (!searchTerm) return files.slice(0, maxResults);
|
|
2200
|
+
let searchString = searchTerm;
|
|
2201
|
+
let fileNames;
|
|
2202
|
+
if (!caseSensitive) {
|
|
2203
|
+
searchString = searchTerm.toLowerCase();
|
|
2204
|
+
fileNames = files.map((file) => file.name.toLowerCase());
|
|
2205
|
+
} else {
|
|
2206
|
+
fileNames = files.map((file) => file.name);
|
|
2207
|
+
}
|
|
2208
|
+
return files.filter((_file, index) => {
|
|
2209
|
+
const name = fileNames[index];
|
|
2210
|
+
if (name === void 0) return false;
|
|
2211
|
+
return exactMatch ? name === searchString : name.includes(searchString);
|
|
2212
|
+
}).slice(0, maxResults);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/utils/fetch-helpers.ts
|
|
2216
|
+
var MIME_EXTENSION_MAP = {
|
|
2217
|
+
"application/json": "json",
|
|
2218
|
+
"application/pdf": "pdf",
|
|
2219
|
+
"application/zip": "zip",
|
|
2220
|
+
"image/jpeg": "jpg",
|
|
2221
|
+
"text/csv": "csv",
|
|
2222
|
+
"text/html": "html",
|
|
2223
|
+
"text/markdown": "md",
|
|
2224
|
+
"text/plain": "txt"
|
|
2225
|
+
};
|
|
2226
|
+
function sanitizeFileName(value) {
|
|
2227
|
+
let decoded;
|
|
2228
|
+
try {
|
|
2229
|
+
decoded = decodeURIComponent(value);
|
|
2230
|
+
} catch {
|
|
2231
|
+
decoded = value;
|
|
2232
|
+
}
|
|
2233
|
+
decoded = decoded.trim();
|
|
2234
|
+
const lastSegment = decoded.split(/[\\/]/).filter(Boolean).pop() ?? "";
|
|
2235
|
+
return lastSegment.replace(/[\x00-\x1f\x7f<>:"/\\|?*]+/g, "_");
|
|
2236
|
+
}
|
|
2237
|
+
function extensionFromMime(type) {
|
|
2238
|
+
const mime = type.split(";")[0]?.trim().toLowerCase();
|
|
2239
|
+
if (!mime) return "bin";
|
|
2240
|
+
if (MIME_EXTENSION_MAP[mime]) return MIME_EXTENSION_MAP[mime];
|
|
2241
|
+
const subtype = mime.split("/")[1];
|
|
2242
|
+
if (!subtype) return "bin";
|
|
2243
|
+
return subtype.split("+").pop() || subtype;
|
|
2244
|
+
}
|
|
2245
|
+
function fileNameFromContentDisposition(header) {
|
|
2246
|
+
if (!header) return void 0;
|
|
2247
|
+
const encodedMatch = header.match(/filename\*=([^;]+)/i);
|
|
2248
|
+
if (encodedMatch?.[1]) {
|
|
2249
|
+
const value2 = encodedMatch[1].trim().replace(/^UTF-8''/i, "");
|
|
2250
|
+
const sanitized2 = sanitizeFileName(value2);
|
|
2251
|
+
if (sanitized2) return sanitized2;
|
|
2252
|
+
}
|
|
2253
|
+
const quotedMatch = header.match(/filename=(?:"([^"]+)"|([^;]+))/i);
|
|
2254
|
+
const value = quotedMatch?.[1] ?? quotedMatch?.[2];
|
|
2255
|
+
if (!value) return void 0;
|
|
2256
|
+
const sanitized = sanitizeFileName(value);
|
|
2257
|
+
return sanitized || void 0;
|
|
2258
|
+
}
|
|
2259
|
+
function deriveFetchedFileName(url, response, blob) {
|
|
2260
|
+
const dispositionName = fileNameFromContentDisposition(
|
|
2261
|
+
response.headers.get("content-disposition")
|
|
2262
|
+
);
|
|
2263
|
+
if (dispositionName) return dispositionName;
|
|
2264
|
+
try {
|
|
2265
|
+
const parsedUrl = new URL(url, globalThis.location.href);
|
|
2266
|
+
if (["http:", "https:", "file:"].includes(parsedUrl.protocol)) {
|
|
2267
|
+
const urlName = sanitizeFileName(parsedUrl.pathname);
|
|
2268
|
+
if (urlName) return urlName;
|
|
2269
|
+
}
|
|
2270
|
+
} catch {
|
|
2271
|
+
}
|
|
2272
|
+
return `${crypto.randomUUID()}.${extensionFromMime(blob.type || response.headers.get("content-type") || "")}`;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export { DEFAULT_MAX_FILE_SIZE, DEFAULT_SOURCES, DragDropController, DriveBrowserController, MIME_EXTENSION_MAP, PREVIEW_MAX_TEXT_SIZE, PREVIEW_TEXT_TRUNCATE_LENGTH, SSEProcessor, ServerModeDriveController, UploaderOrchestrator, b64EncodeUnicode, bindDriveEvents, blobToUploadFile, bytesToSize, checkFileSize, cn, collectDroppedFiles, createChildController, createMotionGate, createTransientUiState, createUploaderController, dataURLtoBlob, deriveFetchedFileName, extensionFromMime, fileAppendParams, fileCanPreviewText, fileGetExtension, fileGetIsImage, fileGetIsPdf, fileGetIsText, fileIs3D, fileNameFromContentDisposition, getDir, loadGoogleIdentityServices, normalizeSource, normalizeUploaderOptions, revokeAndReplace, revokeFileUrl, sanitizeFileName, searchDriveFiles, sizeToBytes, sourceNameKeys };
|