@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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +8 -0
  3. package/README.md +75 -0
  4. package/dist/browser-SXR426OQ.cjs +10 -0
  5. package/dist/browser-WD6T5ABJ.js +1 -0
  6. package/dist/chunk-2H5ICKFT.cjs +122 -0
  7. package/dist/chunk-6HF3I2JS.cjs +39 -0
  8. package/dist/chunk-6S5DQDQH.js +212 -0
  9. package/dist/chunk-BMQC27OE.cjs +92 -0
  10. package/dist/chunk-BSGQGINR.cjs +219 -0
  11. package/dist/chunk-CPKZ2CXD.cjs +179 -0
  12. package/dist/chunk-DTKDIMKT.cjs +573 -0
  13. package/dist/chunk-DYE5XJ6Z.cjs +2134 -0
  14. package/dist/chunk-E3ZCNOXG.cjs +74 -0
  15. package/dist/chunk-FEQY2WYY.cjs +2377 -0
  16. package/dist/chunk-FMURBCZ7.js +120 -0
  17. package/dist/chunk-FYQQRULL.js +37 -0
  18. package/dist/chunk-GE4LIVXP.js +133 -0
  19. package/dist/chunk-HFR5HZJZ.js +56 -0
  20. package/dist/chunk-I7ULPCA2.js +72 -0
  21. package/dist/chunk-MHYXDLZR.js +569 -0
  22. package/dist/chunk-N2SLHVAW.cjs +61 -0
  23. package/dist/chunk-OOPDEOTM.js +169 -0
  24. package/dist/chunk-PMBR3OUW.js +2359 -0
  25. package/dist/chunk-QK7HZIVY.cjs +144 -0
  26. package/dist/chunk-T2AQHY7V.cjs +59 -0
  27. package/dist/chunk-TRCP5FUR.js +90 -0
  28. package/dist/chunk-U7KL5M2H.cjs +374 -0
  29. package/dist/chunk-UXNPM54W.js +2108 -0
  30. package/dist/chunk-VV6QCQLL.js +365 -0
  31. package/dist/chunk-ZMS2HXLB.js +56 -0
  32. package/dist/context-shapes-123dd3b6.d.cts +207 -0
  33. package/dist/context-shapes-27b24c8f.d.ts +207 -0
  34. package/dist/contracts-pipeline-cf177270.d.cts +78 -0
  35. package/dist/contracts-pipeline-cf177270.d.ts +78 -0
  36. package/dist/contracts-strategies-378a16b7.d.cts +168 -0
  37. package/dist/contracts-strategies-378a16b7.d.ts +168 -0
  38. package/dist/contracts.cjs +181 -0
  39. package/dist/contracts.d.cts +11 -0
  40. package/dist/contracts.d.ts +11 -0
  41. package/dist/contracts.js +4 -0
  42. package/dist/core-141335ac.d.ts +651 -0
  43. package/dist/core-bea2ceca.d.cts +651 -0
  44. package/dist/create-worker-provider-AA5SRII6.js +58 -0
  45. package/dist/create-worker-provider-KJNUSSTW.cjs +60 -0
  46. package/dist/eligibility-ONLNJ6FB.cjs +10 -0
  47. package/dist/eligibility-ULFC3E4G.js +8 -0
  48. package/dist/error-code-map-9c44a425.d.ts +62 -0
  49. package/dist/error-code-map-dd5becfa.d.cts +62 -0
  50. package/dist/i18n/index.cjs +90 -0
  51. package/dist/i18n/index.d.cts +3 -0
  52. package/dist/i18n/index.d.ts +3 -0
  53. package/dist/i18n/index.js +1 -0
  54. package/dist/index.cjs +1770 -0
  55. package/dist/index.d.cts +280 -0
  56. package/dist/index.d.ts +280 -0
  57. package/dist/index.js +1589 -0
  58. package/dist/internal.cjs +2407 -0
  59. package/dist/internal.d.cts +595 -0
  60. package/dist/internal.d.ts +595 -0
  61. package/dist/internal.js +2275 -0
  62. package/dist/pipeline/index.cjs +10 -0
  63. package/dist/pipeline/index.d.cts +11 -0
  64. package/dist/pipeline/index.d.ts +11 -0
  65. package/dist/pipeline/index.js +1 -0
  66. package/dist/pipeline-worker.cjs +199 -0
  67. package/dist/pipeline-worker.d.cts +2 -0
  68. package/dist/pipeline-worker.d.ts +2 -0
  69. package/dist/pipeline-worker.js +197 -0
  70. package/dist/resolve-locale-81feef21.d.ts +20 -0
  71. package/dist/resolve-locale-9cb63600.d.cts +20 -0
  72. package/dist/resolve-theme-01758105.d.ts +53 -0
  73. package/dist/resolve-theme-66cf8d98.d.cts +53 -0
  74. package/dist/server-mode-drive-controller-b5f8ee12.d.cts +180 -0
  75. package/dist/server-mode-drive-controller-b5f8ee12.d.ts +180 -0
  76. package/dist/steps/compress.cjs +66 -0
  77. package/dist/steps/compress.d.cts +10 -0
  78. package/dist/steps/compress.d.ts +10 -0
  79. package/dist/steps/compress.js +64 -0
  80. package/dist/steps/exif.cjs +44 -0
  81. package/dist/steps/exif.d.cts +5 -0
  82. package/dist/steps/exif.d.ts +5 -0
  83. package/dist/steps/exif.js +42 -0
  84. package/dist/steps/hash.cjs +44 -0
  85. package/dist/steps/hash.d.cts +5 -0
  86. package/dist/steps/hash.d.ts +5 -0
  87. package/dist/steps/hash.js +42 -0
  88. package/dist/steps/heic.cjs +59 -0
  89. package/dist/steps/heic.d.cts +5 -0
  90. package/dist/steps/heic.d.ts +5 -0
  91. package/dist/steps/heic.js +57 -0
  92. package/dist/steps/thumbnail.cjs +54 -0
  93. package/dist/steps/thumbnail.d.cts +10 -0
  94. package/dist/steps/thumbnail.d.ts +10 -0
  95. package/dist/steps/thumbnail.js +52 -0
  96. package/dist/strategies/index.cjs +82 -0
  97. package/dist/strategies/index.d.cts +187 -0
  98. package/dist/strategies/index.d.ts +187 -0
  99. package/dist/strategies/index.js +60 -0
  100. package/dist/strategies/tus-upload.cjs +14 -0
  101. package/dist/strategies/tus-upload.d.cts +15 -0
  102. package/dist/strategies/tus-upload.d.ts +15 -0
  103. package/dist/strategies/tus-upload.js +5 -0
  104. package/dist/theme/index.cjs +38 -0
  105. package/dist/theme/index.d.cts +3 -0
  106. package/dist/theme/index.d.ts +3 -0
  107. package/dist/theme/index.js +1 -0
  108. package/dist/theme-store-2e56a674.d.cts +39 -0
  109. package/dist/theme-store-f13e0315.d.ts +39 -0
  110. package/dist/token-endpoint-5fdb4a35.d.ts +20 -0
  111. package/dist/token-endpoint-a7d15ab6.d.cts +20 -0
  112. package/dist/ui-translations-c5d00392.d.cts +455 -0
  113. package/dist/ui-translations-c5d00392.d.ts +455 -0
  114. package/dist/uploader-props-72242002.d.ts +340 -0
  115. package/dist/uploader-props-7f956e60.d.cts +340 -0
  116. package/dist/vars-d3421f6c.d.cts +240 -0
  117. package/dist/vars-d3421f6c.d.ts +240 -0
  118. package/package.json +182 -0
@@ -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 };