nuxt-media-library 0.0.51 → 0.0.53

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 (41) hide show
  1. package/README.md +57 -0
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.d.ts +6 -0
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +20 -1
  6. package/dist/runtime/components/MLAssetManager/Processing.vue +3 -34
  7. package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +1 -1
  8. package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +1 -1
  9. package/dist/runtime/components/MLMedia/Field.d.vue.ts +17 -4
  10. package/dist/runtime/components/MLMedia/Field.vue +19 -2
  11. package/dist/runtime/components/MLMedia/Field.vue.d.ts +17 -4
  12. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +2 -0
  13. package/dist/runtime/components/MLMedia/FilePicker.vue +3 -1
  14. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +2 -0
  15. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +3 -0
  16. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +6 -1
  17. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +3 -0
  18. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +15 -5
  19. package/dist/runtime/components/MLMedia/Gallery.vue +9 -3
  20. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +15 -5
  21. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +11 -5
  22. package/dist/runtime/components/MLMedia/GallerySection.vue +21 -3
  23. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +11 -5
  24. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +3 -3
  25. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +3 -3
  26. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +2 -2
  27. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +2 -2
  28. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +2 -2
  29. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +2 -2
  30. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +1 -1
  31. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +1 -1
  32. package/dist/runtime/composables/useMLFilePicker.d.ts +2 -0
  33. package/dist/runtime/composables/useMLMediaField.d.ts +6 -2
  34. package/dist/runtime/composables/useMLMediaField.js +1 -1
  35. package/dist/runtime/composables/useMLMediaGallery.d.ts +5 -1
  36. package/dist/runtime/composables/useMLMediaGallery.js +2 -2
  37. package/dist/runtime/composables/useMLMediaUpload.d.ts +21 -0
  38. package/dist/runtime/composables/useMLMediaUpload.js +41 -12
  39. package/dist/runtime/utils/uploader/serviceBackends.d.ts +26 -0
  40. package/dist/runtime/utils/uploader/serviceBackends.js +18 -0
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -208,6 +208,63 @@ await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { s
208
208
  grant mode replaces the browser's grant with its own, and the service's
209
209
  `claim_upload` rejects the session.
210
210
 
211
+ #### One layer per service, widgets by backend name
212
+
213
+ Each service can ship a Nuxt layer that registers its upload backend and
214
+ serves its own routes (start, refresh, attach, data) to its service. Nuxt
215
+ merges the layers, so every widget on the site can use any backend by name.
216
+
217
+ ```ts
218
+ // itq-videos-layer/nuxt.config.ts
219
+ export default defineNuxtConfig({
220
+ modules: ['nuxt-media-library'],
221
+ mediaLibrary: {
222
+ serviceUploads: {
223
+ backends: { videos: { start: '/api/videos/uploads' } },
224
+ // refresh defaults to `${start}/:sessionId/grant`
225
+ },
226
+ },
227
+ })
228
+ ```
229
+
230
+ ```ts
231
+ // the site
232
+ export default defineNuxtConfig({
233
+ extends: ['itq-videos-layer', 'itq-courses-layer'],
234
+ modules: ['nuxt-media-library'],
235
+ mediaLibrary: { serviceUploads: { proxy: true } }, // where parts go, for every backend
236
+ })
237
+ ```
238
+
239
+ ```vue
240
+ <MLMediaGallery
241
+ v-model="lesson.mediaIds"
242
+ service="videos"
243
+ profile="lesson-video"
244
+ :service-body="{ context: { lessonId } }"
245
+ @uploaded="(assetId, role, sessionId) => attach(assetId, sessionId)"
246
+ />
247
+ ```
248
+
249
+ - `service` on `MLMediaGallery`, `MLMediaField` and the file picker (and
250
+ `useMLMediaUpload`): a backend name, or `{ start }` for a one-off. Without
251
+ it, widgets upload through Nuxt's own route as before; an unknown name
252
+ fails that upload with a message naming the missing backend.
253
+ - The start request carries the file (`mimeType`, `sizeBytes`, `filename`),
254
+ `profile` (each role's, in a gallery with roles) and `service-body`. The
255
+ service's profile decides kind, visibility and processing, so
256
+ `visibility`, `processing-config` and `collection-name` are not used with
257
+ `service`.
258
+ - `uploaded` also gives the upload's `sessionId` (gallery:
259
+ `(assetId, role, sessionId)`, field: `(assetId, sessionId)`), including
260
+ files uploaded from the picker and chosen. Send both to the service's
261
+ attach endpoint: its `claim_upload` needs them.
262
+ - Processing progress uses `/api/media-library/manage/assets/resolve` and
263
+ the processing stream, so the app's proxy must allow `read` on a file the
264
+ user just uploaded.
265
+ - There is no default backend: each layer's widgets name theirs. The module
266
+ only stores the paths; each layer's own routes reach its service.
267
+
211
268
  ### Vue installation (non-Nuxt)
212
269
 
213
270
  ```bash
package/dist/module.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
2
3
  export * from '../dist/runtime/types/resourcePolicy.js';
3
4
  export * from '../dist/runtime/types/uploader.js';
4
5
  export * from '../dist/runtime/types/preview.js';
@@ -41,10 +42,15 @@ interface ModuleOptions {
41
42
  * `baseURL` is where the browser sends those uploads (the media library's
42
43
  * public API root with `/v1`, or a proxy route); with `proxy` it defaults
43
44
  * to the pass-through route.
45
+ *
46
+ * `backends` names the services that start uploads, so widgets can use
47
+ * `service="videos"`. Each service's Nuxt layer registers its own; Nuxt
48
+ * merges them across layers. Paths only: each layer serves its routes.
44
49
  */
45
50
  serviceUploads?: {
46
51
  proxy?: boolean;
47
52
  baseURL?: string;
53
+ backends?: Record<string, ServiceUploadBackend>;
48
54
  };
49
55
  }
50
56
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
package/dist/module.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
+ import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
2
3
  export * from '../dist/runtime/types/resourcePolicy.js';
3
4
  export * from '../dist/runtime/types/uploader.js';
4
5
  export * from '../dist/runtime/types/preview.js';
@@ -41,10 +42,15 @@ interface ModuleOptions {
41
42
  * `baseURL` is where the browser sends those uploads (the media library's
42
43
  * public API root with `/v1`, or a proxy route); with `proxy` it defaults
43
44
  * to the pass-through route.
45
+ *
46
+ * `backends` names the services that start uploads, so widgets can use
47
+ * `service="videos"`. Each service's Nuxt layer registers its own; Nuxt
48
+ * merges them across layers. Paths only: each layer serves its routes.
44
49
  */
45
50
  serviceUploads?: {
46
51
  proxy?: boolean;
47
52
  baseURL?: string;
53
+ backends?: Record<string, ServiceUploadBackend>;
48
54
  };
49
55
  }
50
56
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.51",
4
+ "version": "0.0.53",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -20,6 +20,23 @@ function browserBaseURL(existingPublic, publicBaseURL) {
20
20
  }
21
21
 
22
22
  const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
23
+ function serviceBackends(backends, logger) {
24
+ const out = {};
25
+ for (const [name, backend] of Object.entries(backends ?? {})) {
26
+ if (!backend || typeof backend.start !== "string" || !backend.start) {
27
+ logger.warn(`mediaLibrary.serviceUploads.backends.${name} has no start path; ignored.`);
28
+ continue;
29
+ }
30
+ if (backend.refresh && !backend.refresh.includes(":sessionId")) {
31
+ logger.warn(`mediaLibrary.serviceUploads.backends.${name}.refresh needs a :sessionId placeholder; using the default.`);
32
+ }
33
+ out[name] = {
34
+ start: backend.start,
35
+ ...backend.refresh?.includes(":sessionId") ? { refresh: backend.refresh } : {}
36
+ };
37
+ }
38
+ return out;
39
+ }
23
40
  const module$1 = defineNuxtModule({
24
41
  meta: {
25
42
  name: "media-library",
@@ -145,7 +162,9 @@ const module$1 = defineNuxtModule({
145
162
  },
146
163
  headers: publicHeaders.headers,
147
164
  // Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
148
- serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : "")
165
+ serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : ""),
166
+ // Names widgets can pass as `service` (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BACKENDS).
167
+ serviceUploadBackends: { ...serviceBackends(options.serviceUploads?.backends, logger), ...existingPublic.serviceUploadBackends }
149
168
  };
150
169
  addPlugin({
151
170
  src: resolver.resolve("./runtime/plugins/config")
@@ -1,33 +1,12 @@
1
1
  <script setup>
2
2
  import { computed, ref, onMounted, watch } from "vue";
3
- import { useMLResourcePolicy } from "../../composables/resourcePolicy/useMLResourcePolicy";
4
3
  import { mediaGrantHeaders } from "../../composables/useMLMediaGrants";
5
4
  const props = defineProps({
6
5
  assetId: { type: String, required: true },
7
6
  manager: { type: Object, required: true }
8
7
  });
9
- const resourcePolicy = useMLResourcePolicy();
10
- const policy = ref(null);
11
- const policyLoading = ref(false);
12
8
  const videoUrl = ref(null);
13
9
  const asset = computed(() => props.manager.selectedAsset.value);
14
- const policyId = computed(
15
- () => asset.value?.resourcePolicyId ?? asset.value?.resource_policy_id ?? null
16
- );
17
- async function loadPolicy() {
18
- if (!policyId.value) {
19
- policy.value = null;
20
- return;
21
- }
22
- policyLoading.value = true;
23
- try {
24
- policy.value = await resourcePolicy.get(policyId.value);
25
- } catch {
26
- policy.value = null;
27
- } finally {
28
- policyLoading.value = false;
29
- }
30
- }
31
10
  async function resolveVideoUrl() {
32
11
  if (!asset.value || asset.value.kind !== "video") {
33
12
  videoUrl.value = null;
@@ -43,17 +22,9 @@ async function resolveVideoUrl() {
43
22
  videoUrl.value = null;
44
23
  }
45
24
  }
46
- onMounted(() => {
47
- loadPolicy();
48
- resolveVideoUrl();
49
- });
50
- watch(policyId, loadPolicy);
25
+ onMounted(resolveVideoUrl);
51
26
  watch(() => props.assetId, resolveVideoUrl);
52
- const showStepper = computed(() => {
53
- if (policyLoading.value) return false;
54
- if (!policy.value) return !policyId.value;
55
- return policy.value.processing_config?.auto_process === false;
56
- });
27
+ const showStepper = computed(() => asset.value?.status === "uploaded");
57
28
  function onComplete() {
58
29
  props.manager.refresh(props.assetId);
59
30
  }
@@ -64,10 +35,8 @@ function onFailed() {
64
35
 
65
36
  <template>
66
37
  <div>
67
- <USkeleton v-if="policyLoading" class="h-40 w-full" />
68
-
69
38
  <MLProcessingStepper
70
- v-else-if="showStepper"
39
+ v-if="showStepper"
71
40
  :asset-id="assetId"
72
41
  :asset-kind="asset?.kind"
73
42
  :download-url="videoUrl"
@@ -39,11 +39,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
39
39
  onFiles?: ((files: File[]) => any) | undefined;
40
40
  onRejected?: ((files: RejectedFile[]) => any) | undefined;
41
41
  }>, {
42
+ title: string;
42
43
  variant: "area" | "overlay";
43
44
  compact: boolean;
44
45
  disabled: boolean;
45
46
  multiple: boolean;
46
- title: string;
47
47
  accept: string;
48
48
  maxSize: number | null;
49
49
  maxFiles: number | null;
@@ -39,11 +39,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
39
39
  onFiles?: ((files: File[]) => any) | undefined;
40
40
  onRejected?: ((files: RejectedFile[]) => any) | undefined;
41
41
  }>, {
42
+ title: string;
42
43
  variant: "area" | "overlay";
43
44
  compact: boolean;
44
45
  disabled: boolean;
45
46
  multiple: boolean;
46
- title: string;
47
47
  accept: string;
48
48
  maxSize: number | null;
49
49
  maxFiles: number | null;
@@ -15,6 +15,16 @@ type __VLS_Props = {
15
15
  visibility?: MediaUploadOptions['visibility'];
16
16
  processingConfig?: MediaUploadOptions['processingConfig'];
17
17
  collectionName?: string | null;
18
+ /** Upload profile name for new files. */
19
+ profile?: string | null;
20
+ /**
21
+ * Start uploads at a backend service instead of Nuxt's own route: a name
22
+ * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
23
+ * or `{ start }`. See useMLMediaUpload.
24
+ */
25
+ service?: MediaUploadOptions['service'];
26
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
27
+ serviceBody?: Record<string, unknown> | null;
18
28
  /** Show "Select existing", which opens the library picker. */
19
29
  picker?: boolean;
20
30
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
@@ -33,17 +43,17 @@ type __VLS_ModelProps = {
33
43
  };
34
44
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
35
45
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
36
- uploaded: (assetId: string) => any;
46
+ uploaded: (assetId: string, sessionId: string | null) => any;
37
47
  "update:modelValue": (value: string | null) => any;
38
48
  "update:alt": (value: string | undefined) => any;
39
49
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
40
- onUploaded?: ((assetId: string) => any) | undefined;
50
+ onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
41
51
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
52
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
53
  }>, {
44
- disabled: boolean;
45
- visibility: "private" | "public" | "restricted";
54
+ visibility: "private" | "restricted" | "public";
46
55
  label: string;
56
+ disabled: boolean;
47
57
  accept: readonly string[] | string;
48
58
  maxSize: number | null;
49
59
  required: boolean;
@@ -54,6 +64,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
54
64
  enableTranscription?: boolean;
55
65
  };
56
66
  collectionName: string | null;
67
+ profile: string | null;
68
+ service: string | import("../../index.js").MediaServiceUpload | null;
69
+ serviceBody: Record<string, unknown> | null;
57
70
  hint: string;
58
71
  missingAlt: boolean;
59
72
  picker: boolean;
@@ -15,6 +15,9 @@ const props = defineProps({
15
15
  visibility: { type: String, required: false, default: void 0 },
16
16
  processingConfig: { type: Object, required: false, default: void 0 },
17
17
  collectionName: { type: [String, null], required: false, default: null },
18
+ profile: { type: [String, null], required: false, default: null },
19
+ service: { type: null, required: false, default: void 0 },
20
+ serviceBody: { type: [Object, null], required: false, default: null },
18
21
  picker: { type: Boolean, required: false, default: true },
19
22
  onPick: { type: Function, required: false, default: void 0 },
20
23
  editor: { type: Boolean, required: false, default: true },
@@ -30,7 +33,10 @@ const field = useMLMediaField(model, {
30
33
  visibility: props.visibility,
31
34
  processingConfig: props.processingConfig,
32
35
  collectionName: props.collectionName,
33
- onUploaded: (id) => emit("uploaded", id)
36
+ profile: props.profile,
37
+ service: props.service,
38
+ serviceBody: props.serviceBody,
39
+ onUploaded: (id, sessionId) => emit("uploaded", id, sessionId)
34
40
  });
35
41
  const { state, asset, upload, message } = field;
36
42
  const helpText = computed(() => {
@@ -65,19 +71,30 @@ const veilPhase = computed(() => {
65
71
  });
66
72
  const filePicker = useMLFilePicker();
67
73
  const canPick = computed(() => !!props.onPick || props.picker);
74
+ const uploadOptions = computed(() => ({
75
+ visibility: props.visibility,
76
+ processingConfig: props.processingConfig,
77
+ collectionName: props.collectionName,
78
+ profile: props.profile,
79
+ service: props.service,
80
+ serviceBody: props.serviceBody
81
+ }));
68
82
  async function pickExisting() {
69
83
  if (props.onPick) return props.onPick();
84
+ const sessions = /* @__PURE__ */ new Map();
70
85
  const ids = await filePicker.open({
71
86
  accept: props.accept,
72
87
  max: 1,
73
88
  selected: [model.value],
74
89
  title: props.label ? `Select ${props.label.toLowerCase()}` : "Select file",
75
90
  maxSize: props.maxSize,
76
- upload: { visibility: props.visibility, processingConfig: props.processingConfig, collectionName: props.collectionName }
91
+ upload: uploadOptions.value,
92
+ onUploaded: (id, sessionId) => sessions.set(id, sessionId)
77
93
  });
78
94
  if (ids) {
79
95
  field.clearMessage();
80
96
  model.value = ids[0] ?? null;
97
+ if (model.value && sessions.has(model.value)) emit("uploaded", model.value, sessions.get(model.value) ?? null);
81
98
  }
82
99
  }
83
100
  const mediaEditor = useMLMediaEditor();
@@ -15,6 +15,16 @@ type __VLS_Props = {
15
15
  visibility?: MediaUploadOptions['visibility'];
16
16
  processingConfig?: MediaUploadOptions['processingConfig'];
17
17
  collectionName?: string | null;
18
+ /** Upload profile name for new files. */
19
+ profile?: string | null;
20
+ /**
21
+ * Start uploads at a backend service instead of Nuxt's own route: a name
22
+ * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
23
+ * or `{ start }`. See useMLMediaUpload.
24
+ */
25
+ service?: MediaUploadOptions['service'];
26
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
27
+ serviceBody?: Record<string, unknown> | null;
18
28
  /** Show "Select existing", which opens the library picker. */
19
29
  picker?: boolean;
20
30
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
@@ -33,17 +43,17 @@ type __VLS_ModelProps = {
33
43
  };
34
44
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
35
45
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
36
- uploaded: (assetId: string) => any;
46
+ uploaded: (assetId: string, sessionId: string | null) => any;
37
47
  "update:modelValue": (value: string | null) => any;
38
48
  "update:alt": (value: string | undefined) => any;
39
49
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
40
- onUploaded?: ((assetId: string) => any) | undefined;
50
+ onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
41
51
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
52
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
53
  }>, {
44
- disabled: boolean;
45
- visibility: "private" | "public" | "restricted";
54
+ visibility: "private" | "restricted" | "public";
46
55
  label: string;
56
+ disabled: boolean;
47
57
  accept: readonly string[] | string;
48
58
  maxSize: number | null;
49
59
  required: boolean;
@@ -54,6 +64,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
54
64
  enableTranscription?: boolean;
55
65
  };
56
66
  collectionName: string | null;
67
+ profile: string | null;
68
+ service: string | import("../../index.js").MediaServiceUpload | null;
69
+ serviceBody: Record<string, unknown> | null;
57
70
  hint: string;
58
71
  missingAlt: boolean;
59
72
  picker: boolean;
@@ -6,6 +6,7 @@ type __VLS_Props = {
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
8
  upload?: MediaUploadOptions;
9
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
9
10
  };
10
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
12
  close: (ids: string[] | null) => any;
@@ -18,6 +19,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
19
  selected: readonly string[];
19
20
  maxSize: number | null;
20
21
  upload: MediaUploadOptions;
22
+ onUploaded: (assetId: string, sessionId: string | null) => void;
21
23
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
24
  declare const _default: typeof __VLS_export;
23
25
  export default _default;
@@ -5,7 +5,8 @@ defineProps({
5
5
  selected: { type: Array, required: false, default: () => [] },
6
6
  title: { type: String, required: false, default: "Select file" },
7
7
  maxSize: { type: [Number, null], required: false, default: null },
8
- upload: { type: Object, required: false, default: () => ({}) }
8
+ upload: { type: Object, required: false, default: () => ({}) },
9
+ onUploaded: { type: Function, required: false, default: void 0 }
9
10
  });
10
11
  const emit = defineEmits(["close"]);
11
12
  </script>
@@ -25,6 +26,7 @@ const emit = defineEmits(["close"]);
25
26
  :selected="selected"
26
27
  :max-size="maxSize"
27
28
  :upload="upload"
29
+ :on-uploaded="onUploaded"
28
30
  @done="(ids) => emit('close', ids)"
29
31
  @cancel="emit('close', null)"
30
32
  />
@@ -6,6 +6,7 @@ type __VLS_Props = {
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
8
  upload?: MediaUploadOptions;
9
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
9
10
  };
10
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
12
  close: (ids: string[] | null) => any;
@@ -18,6 +19,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
19
  selected: readonly string[];
19
20
  maxSize: number | null;
20
21
  upload: MediaUploadOptions;
22
+ onUploaded: (assetId: string, sessionId: string | null) => void;
21
23
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
24
  declare const _default: typeof __VLS_export;
23
25
  export default _default;
@@ -11,6 +11,8 @@ type __VLS_Props = {
11
11
  maxSize?: number | null;
12
12
  /** Upload session settings for files dropped into the picker. */
13
13
  upload?: MediaUploadOptions;
14
+ /** A file uploaded from the picker exists in the library (with its upload session). */
15
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
14
16
  /** Grid rows for a fixed-height body; the list scrolls inside it. */
15
17
  bodyClass?: string;
16
18
  };
@@ -27,6 +29,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
27
29
  selected: readonly string[];
28
30
  maxSize: number | null;
29
31
  upload: MediaUploadOptions;
32
+ onUploaded: (assetId: string, sessionId: string | null) => void;
30
33
  bodyClass: string;
31
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
35
  declare const _default: typeof __VLS_export;
@@ -15,6 +15,7 @@ const props = defineProps({
15
15
  title: { type: String, required: false, default: void 0 },
16
16
  maxSize: { type: [Number, null], required: false, default: null },
17
17
  upload: { type: Object, required: false, default: () => ({}) },
18
+ onUploaded: { type: Function, required: false, default: void 0 },
18
19
  bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
19
20
  });
20
21
  const emit = defineEmits(["done", "cancel"]);
@@ -69,7 +70,11 @@ function onFiles(files) {
69
70
  rejectNote.value = null;
70
71
  for (const file of files) {
71
72
  const u = uploader.start(file);
72
- void u.created.then((id) => id && !isChosen(id) && choose(id));
73
+ void u.created.then((id) => {
74
+ if (!id) return;
75
+ props.onUploaded?.(id, u.sessionId);
76
+ if (!isChosen(id)) choose(id);
77
+ });
73
78
  }
74
79
  }
75
80
  function onRejected(rejected) {
@@ -11,6 +11,8 @@ type __VLS_Props = {
11
11
  maxSize?: number | null;
12
12
  /** Upload session settings for files dropped into the picker. */
13
13
  upload?: MediaUploadOptions;
14
+ /** A file uploaded from the picker exists in the library (with its upload session). */
15
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
14
16
  /** Grid rows for a fixed-height body; the list scrolls inside it. */
15
17
  bodyClass?: string;
16
18
  };
@@ -27,6 +29,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
27
29
  selected: readonly string[];
28
30
  maxSize: number | null;
29
31
  upload: MediaUploadOptions;
32
+ onUploaded: (assetId: string, sessionId: string | null) => void;
30
33
  bodyClass: string;
31
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
32
35
  declare const _default: typeof __VLS_export;
@@ -30,6 +30,14 @@ type __VLS_Props = {
30
30
  collectionName?: string | null;
31
31
  /** Upload profile for the single list (with `profiles`, each role has its own). */
32
32
  profile?: string | null;
33
+ /**
34
+ * Start uploads at a backend service instead of Nuxt's own route: a name
35
+ * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
36
+ * or `{ start }`. Each role's profile is sent with the start request.
37
+ */
38
+ service?: MediaUploadOptions['service'];
39
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
40
+ serviceBody?: Record<string, unknown> | null;
33
41
  picker?: boolean;
34
42
  onPick?: () => void;
35
43
  editor?: boolean;
@@ -41,20 +49,19 @@ type __VLS_ModelProps = {
41
49
  };
42
50
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
43
51
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
44
- uploaded: (assetId: string, role: string) => any;
52
+ uploaded: (assetId: string, role: string, sessionId: string | null) => any;
45
53
  "update:modelValue": (value: string[]) => any;
46
54
  "update:alt": (value: Record<string, string> | undefined) => any;
47
55
  "update:profiles": (profiles: MediaGalleryProfiles) => any;
48
56
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
49
- onUploaded?: ((assetId: string, role: string) => any) | undefined;
57
+ onUploaded?: ((assetId: string, role: string, sessionId: string | null) => any) | undefined;
50
58
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
51
59
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
52
60
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
53
61
  }>, {
54
- disabled: boolean;
62
+ visibility: "private" | "restricted" | "public";
55
63
  title: string;
56
- visibility: "private" | "public" | "restricted";
57
- profile: string | null;
64
+ disabled: boolean;
58
65
  accept: readonly string[] | string;
59
66
  max: number | null;
60
67
  maxSize: number | null;
@@ -64,6 +71,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
64
71
  enableTranscription?: boolean;
65
72
  };
66
73
  collectionName: string | null;
74
+ profile: string | null;
75
+ service: string | import("../../index.js").MediaServiceUpload | null;
76
+ serviceBody: Record<string, unknown> | null;
67
77
  hint: string;
68
78
  picker: boolean;
69
79
  onPick: () => void;
@@ -17,6 +17,8 @@ const props = defineProps({
17
17
  processingConfig: { type: Object, required: false, default: void 0 },
18
18
  collectionName: { type: [String, null], required: false, default: null },
19
19
  profile: { type: [String, null], required: false, default: null },
20
+ service: { type: null, required: false, default: void 0 },
21
+ serviceBody: { type: [Object, null], required: false, default: null },
20
22
  picker: { type: Boolean, required: false, default: true },
21
23
  onPick: { type: Function, required: false, default: void 0 },
22
24
  editor: { type: Boolean, required: false, default: true },
@@ -101,8 +103,8 @@ function onAdd(id, tag) {
101
103
  if (union.value.includes(id)) setRole(id, key, true);
102
104
  else pendingRole.set(id, key);
103
105
  }
104
- function onUploaded(id) {
105
- emit("uploaded", id, uploadedRole.get(id) ?? defaultKey.value);
106
+ function onUploaded(id, sessionId) {
107
+ emit("uploaded", id, uploadedRole.get(id) ?? defaultKey.value, sessionId);
106
108
  uploadedRole.delete(id);
107
109
  }
108
110
  const badges = (id) => roles.value.filter((r) => sel(r.key).includes(id)).map((r) => r.label);
@@ -171,6 +173,8 @@ function chipCount(key, max) {
171
173
  :accept="defaultRole?.accept ?? accept"
172
174
  :max-size="defaultRole?.maxSize ?? maxSize"
173
175
  :profile="defaultRole?.profile ?? null"
176
+ :service="service"
177
+ :service-body="serviceBody"
174
178
  :disabled="disabled"
175
179
  :featured="featured"
176
180
  :record-name="recordName"
@@ -247,10 +251,12 @@ function chipCount(key, max) {
247
251
  :processing-config="processingConfig"
248
252
  :collection-name="collectionName"
249
253
  :profile="profile"
254
+ :service="service"
255
+ :service-body="serviceBody"
250
256
  :picker="picker"
251
257
  :on-pick="onPick"
252
258
  :editor="editor"
253
259
  :on-open="onOpen"
254
- @uploaded="(id) => emit('uploaded', id, '')"
260
+ @uploaded="(id, sessionId) => emit('uploaded', id, '', sessionId)"
255
261
  />
256
262
  </template>
@@ -30,6 +30,14 @@ type __VLS_Props = {
30
30
  collectionName?: string | null;
31
31
  /** Upload profile for the single list (with `profiles`, each role has its own). */
32
32
  profile?: string | null;
33
+ /**
34
+ * Start uploads at a backend service instead of Nuxt's own route: a name
35
+ * registered under `mediaLibrary.serviceUploads.backends` (e.g. "videos"),
36
+ * or `{ start }`. Each role's profile is sent with the start request.
37
+ */
38
+ service?: MediaUploadOptions['service'];
39
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
40
+ serviceBody?: Record<string, unknown> | null;
33
41
  picker?: boolean;
34
42
  onPick?: () => void;
35
43
  editor?: boolean;
@@ -41,20 +49,19 @@ type __VLS_ModelProps = {
41
49
  };
42
50
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
43
51
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
44
- uploaded: (assetId: string, role: string) => any;
52
+ uploaded: (assetId: string, role: string, sessionId: string | null) => any;
45
53
  "update:modelValue": (value: string[]) => any;
46
54
  "update:alt": (value: Record<string, string> | undefined) => any;
47
55
  "update:profiles": (profiles: MediaGalleryProfiles) => any;
48
56
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
49
- onUploaded?: ((assetId: string, role: string) => any) | undefined;
57
+ onUploaded?: ((assetId: string, role: string, sessionId: string | null) => any) | undefined;
50
58
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
51
59
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
52
60
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
53
61
  }>, {
54
- disabled: boolean;
62
+ visibility: "private" | "restricted" | "public";
55
63
  title: string;
56
- visibility: "private" | "public" | "restricted";
57
- profile: string | null;
64
+ disabled: boolean;
58
65
  accept: readonly string[] | string;
59
66
  max: number | null;
60
67
  maxSize: number | null;
@@ -64,6 +71,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
64
71
  enableTranscription?: boolean;
65
72
  };
66
73
  collectionName: string | null;
74
+ profile: string | null;
75
+ service: string | import("../../index.js").MediaServiceUpload | null;
76
+ serviceBody: Record<string, unknown> | null;
67
77
  hint: string;
68
78
  picker: boolean;
69
79
  onPick: () => void;
@@ -26,6 +26,10 @@ type __VLS_Props = {
26
26
  onOpen?: (assetId: string) => void;
27
27
  /** Upload profile name for new files in this list. */
28
28
  profile?: string | null;
29
+ /** Start uploads at a backend service (name or `{ start }`). See useMLMediaUpload. */
30
+ service?: MediaUploadOptions['service'];
31
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
32
+ serviceBody?: Record<string, unknown> | null;
29
33
  /** Overrides the "Remove from …" label. */
30
34
  removeLabel?: string;
31
35
  /** Labels shown on a tile, e.g. its roles. */
@@ -58,19 +62,18 @@ type __VLS_Slots = {} & {
58
62
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
63
  uploadFiles: typeof uploadFiles;
60
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
61
- uploaded: (assetId: string) => any;
65
+ uploaded: (assetId: string, sessionId: string | null) => any;
62
66
  "update:modelValue": (value: string[]) => any;
63
67
  "update:alt": (value: Record<string, string> | undefined) => any;
64
68
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
65
- onUploaded?: ((assetId: string) => any) | undefined;
69
+ onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
66
70
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
67
71
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
68
72
  }>, {
73
+ visibility: "private" | "restricted" | "public";
74
+ title: string;
69
75
  disabled: boolean;
70
76
  highlight: ReadonlySet<string> | null;
71
- title: string;
72
- visibility: "private" | "public" | "restricted";
73
- profile: string | null;
74
77
  accept: readonly string[] | string;
75
78
  max: number | null;
76
79
  maxSize: number | null;
@@ -81,6 +84,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
81
84
  enableTranscription?: boolean;
82
85
  };
83
86
  collectionName: string | null;
87
+ profile: string | null;
88
+ service: string | import("../../index.js").MediaServiceUpload | null;
89
+ serviceBody: Record<string, unknown> | null;
84
90
  onAdd: (assetId: string, tag: string | undefined) => void;
85
91
  hint: string;
86
92
  picker: boolean;
@@ -24,6 +24,8 @@ const props = defineProps({
24
24
  editor: { type: Boolean, required: false, default: true },
25
25
  onOpen: { type: Function, required: false, default: void 0 },
26
26
  profile: { type: [String, null], required: false, default: null },
27
+ service: { type: null, required: false, default: void 0 },
28
+ serviceBody: { type: [Object, null], required: false, default: null },
27
29
  removeLabel: { type: String, required: false, default: void 0 },
28
30
  badges: { type: Function, required: false, default: void 0 },
29
31
  tileMenu: { type: Function, required: false, default: void 0 },
@@ -41,7 +43,9 @@ const gallery = useMLMediaGallery(model, {
41
43
  processingConfig: props.processingConfig,
42
44
  collectionName: props.collectionName,
43
45
  profile: props.profile,
44
- onUploaded: (id) => emit("uploaded", id),
46
+ service: props.service,
47
+ serviceBody: props.serviceBody,
48
+ onUploaded: (id, sessionId) => emit("uploaded", id, sessionId),
45
49
  onAdd: (id, tag) => props.onAdd?.(id, tag)
46
50
  });
47
51
  const { items, message, full, replaces } = gallery;
@@ -109,15 +113,29 @@ const mediaEditor = useMLMediaEditor();
109
113
  const canPick = computed(() => !props.disabled && (!!props.onPick || props.picker));
110
114
  async function pickExisting() {
111
115
  if (props.onPick) return props.onPick();
116
+ const sessions = /* @__PURE__ */ new Map();
117
+ const before = new Set(model.value);
112
118
  const ids = await filePicker.open({
113
119
  accept: props.accept,
114
120
  max: gallery.limit.value,
115
121
  selected: model.value,
116
122
  title: `Select ${props.title.toLowerCase()}`,
117
123
  maxSize: props.maxSize,
118
- upload: { visibility: props.visibility, processingConfig: props.processingConfig, collectionName: props.collectionName, profile: props.profile }
124
+ upload: {
125
+ visibility: props.visibility,
126
+ processingConfig: props.processingConfig,
127
+ collectionName: props.collectionName,
128
+ profile: props.profile,
129
+ service: props.service,
130
+ serviceBody: props.serviceBody
131
+ },
132
+ onUploaded: (id, sessionId) => sessions.set(id, sessionId)
119
133
  });
120
- if (ids) gallery.setFromPicker(ids);
134
+ if (!ids) return;
135
+ gallery.setFromPicker(ids);
136
+ for (const id of ids) {
137
+ if (!before.has(id) && sessions.has(id)) emit("uploaded", id, sessions.get(id) ?? null);
138
+ }
121
139
  }
122
140
  function openFor(item) {
123
141
  const id = item.id;
@@ -26,6 +26,10 @@ type __VLS_Props = {
26
26
  onOpen?: (assetId: string) => void;
27
27
  /** Upload profile name for new files in this list. */
28
28
  profile?: string | null;
29
+ /** Start uploads at a backend service (name or `{ start }`). See useMLMediaUpload. */
30
+ service?: MediaUploadOptions['service'];
31
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
32
+ serviceBody?: Record<string, unknown> | null;
29
33
  /** Overrides the "Remove from …" label. */
30
34
  removeLabel?: string;
31
35
  /** Labels shown on a tile, e.g. its roles. */
@@ -58,19 +62,18 @@ type __VLS_Slots = {} & {
58
62
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
59
63
  uploadFiles: typeof uploadFiles;
60
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
61
- uploaded: (assetId: string) => any;
65
+ uploaded: (assetId: string, sessionId: string | null) => any;
62
66
  "update:modelValue": (value: string[]) => any;
63
67
  "update:alt": (value: Record<string, string> | undefined) => any;
64
68
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
65
- onUploaded?: ((assetId: string) => any) | undefined;
69
+ onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
66
70
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
67
71
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
68
72
  }>, {
73
+ visibility: "private" | "restricted" | "public";
74
+ title: string;
69
75
  disabled: boolean;
70
76
  highlight: ReadonlySet<string> | null;
71
- title: string;
72
- visibility: "private" | "public" | "restricted";
73
- profile: string | null;
74
77
  accept: readonly string[] | string;
75
78
  max: number | null;
76
79
  maxSize: number | null;
@@ -81,6 +84,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
81
84
  enableTranscription?: boolean;
82
85
  };
83
86
  collectionName: string | null;
87
+ profile: string | null;
88
+ service: string | import("../../index.js").MediaServiceUpload | null;
89
+ serviceBody: Record<string, unknown> | null;
84
90
  onAdd: (assetId: string, tag: string | undefined) => void;
85
91
  hint: string;
86
92
  picker: boolean;
@@ -40,11 +40,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
40
40
  asset: AssetDetail | DeliveryAsset | null;
41
41
  src: string | null;
42
42
  progress: number | null;
43
- disabled: boolean;
44
- kind: string | null;
45
43
  name: string;
46
- phase: ProgressVeilPhase | null;
44
+ kind: string | null;
45
+ disabled: boolean;
47
46
  selected: boolean;
47
+ phase: ProgressVeilPhase | null;
48
48
  missingAlt: boolean;
49
49
  onOpen: () => void;
50
50
  duration: number | null;
@@ -40,11 +40,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
40
40
  asset: AssetDetail | DeliveryAsset | null;
41
41
  src: string | null;
42
42
  progress: number | null;
43
- disabled: boolean;
44
- kind: string | null;
45
43
  name: string;
46
- phase: ProgressVeilPhase | null;
44
+ kind: string | null;
45
+ disabled: boolean;
47
46
  selected: boolean;
47
+ phase: ProgressVeilPhase | null;
48
48
  missingAlt: boolean;
49
49
  onOpen: () => void;
50
50
  duration: number | null;
@@ -24,9 +24,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }>, {
25
25
  asset: AssetDetail | DeliveryAsset | null;
26
26
  src: string | null;
27
- disabled: boolean;
28
- kind: string | null;
29
27
  name: string;
28
+ kind: string | null;
29
+ disabled: boolean;
30
30
  selected: boolean;
31
31
  meta: string;
32
32
  disabledReason: string;
@@ -24,9 +24,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }>, {
25
25
  asset: AssetDetail | DeliveryAsset | null;
26
26
  src: string | null;
27
- disabled: boolean;
28
- kind: string | null;
29
27
  name: string;
28
+ kind: string | null;
29
+ disabled: boolean;
30
30
  selected: boolean;
31
31
  meta: string;
32
32
  disabledReason: string;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
20
20
  size: string;
21
21
  asset: AssetDetail | DeliveryAsset | null;
22
22
  src: string | null;
23
- disabled: boolean;
24
- kind: string | null;
25
23
  name: string;
24
+ kind: string | null;
25
+ disabled: boolean;
26
26
  selected: boolean;
27
27
  date: string;
28
28
  disabledReason: string;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
20
20
  size: string;
21
21
  asset: AssetDetail | DeliveryAsset | null;
22
22
  src: string | null;
23
- disabled: boolean;
24
- kind: string | null;
25
23
  name: string;
24
+ kind: string | null;
25
+ disabled: boolean;
26
26
  selected: boolean;
27
27
  date: string;
28
28
  disabledReason: string;
@@ -15,8 +15,8 @@ type __VLS_Props = {
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
17
  progress: number | null;
18
- compact: boolean;
19
18
  label: string;
19
+ compact: boolean;
20
20
  onCancel: () => void;
21
21
  onRetry: () => void;
22
22
  detail: string;
@@ -15,8 +15,8 @@ type __VLS_Props = {
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
17
  progress: number | null;
18
- compact: boolean;
19
18
  label: string;
19
+ compact: boolean;
20
20
  onCancel: () => void;
21
21
  onRetry: () => void;
22
22
  detail: string;
@@ -10,6 +10,8 @@ export interface FilePickerOpenOptions {
10
10
  /** Bytes, for uploads from inside the picker. */
11
11
  maxSize?: number | null;
12
12
  upload?: MediaUploadOptions;
13
+ /** A file uploaded from inside the picker exists in the library (its upload session too, for `service`). */
14
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
13
15
  }
14
16
  /**
15
17
  * Opens the library dialog. `open()` resolves with the chosen asset IDs in
@@ -7,8 +7,12 @@ export interface UseMLMediaFieldOptions extends MediaUploadOptions {
7
7
  accept?: MaybeRefOrGetter<readonly string[] | string | null | undefined>;
8
8
  /** Bytes. */
9
9
  maxSize?: MaybeRefOrGetter<number | null | undefined>;
10
- /** Called once the uploaded file exists in the library (before processing finishes). */
11
- onUploaded?: (assetId: string) => void;
10
+ /**
11
+ * Called once the uploaded file exists in the library (before processing
12
+ * finishes). `sessionId` is its upload session: with `service`, send both
13
+ * to the service's attach endpoint.
14
+ */
15
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
12
16
  }
13
17
  /**
14
18
  * Logic behind <MLMediaField>: one asset ID, uploads that replace it, and the
@@ -34,7 +34,7 @@ export function useMLMediaField(model, options = {}) {
34
34
  void u.created.then((id) => {
35
35
  if (!id || upload.value !== u) return;
36
36
  model.value = id;
37
- onUploaded?.(id);
37
+ onUploaded?.(id, u.sessionId);
38
38
  });
39
39
  }
40
40
  watch(() => upload.value?.phase, async (phase) => {
@@ -18,7 +18,11 @@ export interface UseMLMediaGalleryOptions extends MediaUploadOptions {
18
18
  max?: MaybeRefOrGetter<number | null | undefined>;
19
19
  /** Bytes. */
20
20
  maxSize?: MaybeRefOrGetter<number | null | undefined>;
21
- onUploaded?: (assetId: string) => void;
21
+ /**
22
+ * A new upload joined the list. `sessionId` is its upload session: with
23
+ * `service`, send both to the service's attach endpoint.
24
+ */
25
+ onUploaded?: (assetId: string, sessionId: string | null) => void;
22
26
  /**
23
27
  * Called with a new upload's asset ID right before it joins the list, with
24
28
  * the tag passed to onFiles. Lets a gallery with roles place it.
@@ -43,7 +43,7 @@ export function useMLMediaGallery(model, options = {}) {
43
43
  if (replaces.value) {
44
44
  onAdd?.(id, opts.tag);
45
45
  model.value = [id];
46
- onUploaded?.(id);
46
+ onUploaded?.(id, u.sessionId);
47
47
  return;
48
48
  }
49
49
  if (ids.value.includes(id)) {
@@ -56,7 +56,7 @@ export function useMLMediaGallery(model, options = {}) {
56
56
  }
57
57
  onAdd?.(id, opts.tag);
58
58
  model.value = [...ids.value, id];
59
- onUploaded?.(id);
59
+ onUploaded?.(id, u.sessionId);
60
60
  });
61
61
  }
62
62
  }
@@ -1,4 +1,6 @@
1
1
  import type { ChunkUploaderConfig } from '../types/uploader.js';
2
+ import { type ServiceUploadChoice } from '../utils/uploader/serviceBackends.js';
3
+ export type { MediaServiceUpload, ServiceUploadBackend, ServiceUploadChoice } from '../utils/uploader/serviceBackends.js';
2
4
  export type MediaUploadPhase = 'uploading' | 'processing' | 'ready' | 'failed' | 'canceled';
3
5
  /** What a policy-less upload session takes. See docs/resource-policy/retirement.md. */
4
6
  export interface MediaUploadOptions {
@@ -20,6 +22,18 @@ export interface MediaUploadOptions {
20
22
  profile?: string | null;
21
23
  /** Overrides the uploader config the module provides. */
22
24
  chunkUploader?: ChunkUploaderConfig;
25
+ /**
26
+ * Start uploads at a backend service (py_client `create_upload_router`)
27
+ * instead of Nuxt's own upload route: the name of a backend registered
28
+ * under `mediaLibrary.serviceUploads.backends` (e.g. `'videos'`), or
29
+ * `{ start, … }`. The service creates the session and its profile decides
30
+ * kind, visibility and processing, so those options are not used here;
31
+ * `profile` is sent as the start request's `profile`. The file's parts go
32
+ * to `mediaLibrary.serviceUploads` (pass-through route or public URL).
33
+ */
34
+ service?: ServiceUploadChoice;
35
+ /** Extra fields for the service's start request, e.g. `{ context: { lessonId } }`. */
36
+ serviceBody?: Record<string, unknown> | null;
23
37
  }
24
38
  export interface MediaUpload {
25
39
  readonly key: string;
@@ -31,6 +45,11 @@ export interface MediaUpload {
31
45
  /** 0–100, or null while a step reports no progress. */
32
46
  progress: number | null;
33
47
  assetId: string | null;
48
+ /**
49
+ * The upload session, once known. With `service`, send it with `assetId`
50
+ * to the service's attach endpoint (its `claim_upload` needs both).
51
+ */
52
+ sessionId: string | null;
34
53
  error: string | null;
35
54
  cancel: () => void;
36
55
  retry: () => void;
@@ -76,6 +95,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
76
95
  phase: MediaUploadPhase;
77
96
  progress: number | null;
78
97
  assetId: string | null;
98
+ sessionId: string | null;
79
99
  error: string | null;
80
100
  cancel: () => void;
81
101
  retry: () => void;
@@ -119,6 +139,7 @@ export declare function useMLMediaUpload(defaults?: MediaUploadOptions): {
119
139
  phase: MediaUploadPhase;
120
140
  progress: number | null;
121
141
  assetId: string | null;
142
+ sessionId: string | null;
122
143
  error: string | null;
123
144
  cancel: () => void;
124
145
  retry: () => void;
@@ -1,6 +1,11 @@
1
1
  import { onBeforeUnmount, reactive, ref, watch } from "vue";
2
2
  import { $fetch } from "ofetch";
3
+ import { useRuntimeConfig } from "#imports";
3
4
  import { ChunkUploader } from "../utils/uploader/uploader.js";
5
+ import { createServiceUpload } from "../utils/uploader/serviceUpload.js";
6
+ import {
7
+ resolveServiceUpload
8
+ } from "../utils/uploader/serviceBackends.js";
4
9
  import { kindForFile } from "../utils/fileAccept.js";
5
10
  import { useMLChunkUploader } from "./useMLChunkUploader.js";
6
11
  import { useMLProcessingStatus } from "./useMLProcessingStatus.js";
@@ -8,6 +13,9 @@ let seq = 0;
8
13
  export function useMLMediaUpload(defaults = {}) {
9
14
  const base = useMLChunkUploader(defaults.chunkUploader).config;
10
15
  const cfg = base.proxyUploads ? { ...base, baseURL: "" } : base;
16
+ const publicConfig = useRuntimeConfig().public?.mediaLibrary;
17
+ const serviceBaseURL = publicConfig?.serviceUploadBaseURL;
18
+ const serviceBackends = publicConfig?.serviceUploadBackends;
11
19
  const { track } = useMLProcessingStatus();
12
20
  const uploads = ref([]);
13
21
  const cleanups = /* @__PURE__ */ new Map();
@@ -17,6 +25,7 @@ export function useMLMediaUpload(defaults = {}) {
17
25
  const isVisual = file.type.startsWith("image/") || file.type.startsWith("video/");
18
26
  const localPreview = isVisual && typeof URL !== "undefined" ? URL.createObjectURL(file) : null;
19
27
  let uploader = null;
28
+ let serviceRun = null;
20
29
  let stream = null;
21
30
  let stopWatch = null;
22
31
  let resolveCreated;
@@ -28,6 +37,7 @@ export function useMLMediaUpload(defaults = {}) {
28
37
  phase: "uploading",
29
38
  progress: 0,
30
39
  assetId: null,
40
+ sessionId: null,
31
41
  error: null,
32
42
  cancel,
33
43
  retry,
@@ -81,11 +91,11 @@ export function useMLMediaUpload(defaults = {}) {
81
91
  if (status === "ready") finish("ready");
82
92
  else if (status === "failed") finish("failed", "Processing failed");
83
93
  }
84
- async function run() {
94
+ const onProgress = (p) => {
95
+ upload.progress = p.percent;
96
+ };
97
+ async function uploadViaNuxt() {
85
98
  uploader = new ChunkUploader(cfg);
86
- upload.phase = "uploading";
87
- upload.progress = 0;
88
- upload.error = null;
89
99
  const create = {
90
100
  kind,
91
101
  visibility: opts.visibility,
@@ -93,15 +103,32 @@ export function useMLMediaUpload(defaults = {}) {
93
103
  collection_name: opts.collectionName ?? null,
94
104
  ...opts.profile ? { profile: opts.profile } : {}
95
105
  };
106
+ const res = await uploader.uploadFile(file, { create, onProgress });
107
+ const ids = Array.isArray(res?.finalize?.asset_ids) ? res.finalize.asset_ids : res?.finalize?.asset_id ? [res.finalize.asset_id] : [];
108
+ return { assetId: ids[0] ?? null, sessionId: res?.sessionId ?? null };
109
+ }
110
+ async function uploadViaService(service) {
111
+ serviceRun = createServiceUpload({
112
+ fetch: (url, o) => $fetch(url, o),
113
+ createUploader: (c) => new ChunkUploader(c),
114
+ baseURL: serviceBaseURL || void 0
115
+ });
116
+ const res = await serviceRun.upload(file, {
117
+ ...service,
118
+ body: { ...opts.profile ? { profile: opts.profile } : {}, ...service.body },
119
+ onProgress
120
+ });
121
+ return { assetId: res.assetId, sessionId: res.sessionId };
122
+ }
123
+ async function run() {
124
+ upload.phase = "uploading";
125
+ upload.progress = 0;
126
+ upload.error = null;
127
+ upload.sessionId = null;
96
128
  try {
97
- const res = await uploader.uploadFile(file, {
98
- create,
99
- onProgress: (p) => {
100
- upload.progress = p.percent;
101
- }
102
- });
103
- const ids = Array.isArray(res?.finalize?.asset_ids) ? res.finalize.asset_ids : res?.finalize?.asset_id ? [res.finalize.asset_id] : [];
104
- const assetId = ids[0] ?? null;
129
+ const service = resolveServiceUpload(opts.service, serviceBackends, opts.serviceBody);
130
+ const { assetId, sessionId } = service ? await uploadViaService(service) : await uploadViaNuxt();
131
+ upload.sessionId = sessionId;
105
132
  if (!assetId) {
106
133
  finish("failed", "The upload finished but no file was created");
107
134
  resolveCreated(null);
@@ -119,6 +146,7 @@ export function useMLMediaUpload(defaults = {}) {
119
146
  function cancel() {
120
147
  if (upload.phase !== "uploading" && upload.phase !== "processing") return;
121
148
  uploader?.abort();
149
+ serviceRun?.abort();
122
150
  finish("canceled");
123
151
  if (!upload.assetId) resolveCreated(null);
124
152
  }
@@ -132,6 +160,7 @@ export function useMLMediaUpload(defaults = {}) {
132
160
  }
133
161
  cleanups.set(upload.key, () => {
134
162
  uploader?.abort();
163
+ serviceRun?.abort();
135
164
  stopWatch?.();
136
165
  stream?.close();
137
166
  if (localPreview) URL.revokeObjectURL(localPreview);
@@ -0,0 +1,26 @@
1
+ import type { ServiceUploadOptions } from './serviceUpload.js';
2
+ /**
3
+ * A backend service that starts uploads (py_client `create_upload_router`),
4
+ * as registered under `mediaLibrary.serviceUploads.backends`. Each service's
5
+ * Nuxt layer registers its own; Nuxt merges them across layers.
6
+ */
7
+ export interface ServiceUploadBackend {
8
+ /** The start endpoint the browser calls, e.g. `/api/videos/uploads`. */
9
+ start: string;
10
+ /**
11
+ * The grant refresh endpoint, with a `:sessionId` placeholder, e.g.
12
+ * `/api/videos/uploads/:sessionId/grant` (that is also the default).
13
+ */
14
+ refresh?: string;
15
+ }
16
+ /** How a widget names where its uploads start. */
17
+ export type MediaServiceUpload = Pick<ServiceUploadOptions, 'start' | 'body' | 'refresh' | 'headers' | 'baseURL'>;
18
+ export type ServiceUploadChoice = string | MediaServiceUpload | null | undefined;
19
+ /**
20
+ * The service upload for a widget's `service` option, or null to upload
21
+ * through Nuxt's own route. A name is looked up in the registered backends;
22
+ * `body` (e.g. `{ context }`) is merged into the start request.
23
+ *
24
+ * @throws Error for a name no layer registered.
25
+ */
26
+ export declare function resolveServiceUpload(service: ServiceUploadChoice, backends: Record<string, ServiceUploadBackend | undefined> | null | undefined, body?: Record<string, unknown> | null): MediaServiceUpload | null;
@@ -0,0 +1,18 @@
1
+ export function resolveServiceUpload(service, backends, body) {
2
+ if (!service) return null;
3
+ let target;
4
+ if (typeof service === "string") {
5
+ const backend = backends?.[service];
6
+ if (!backend?.start) {
7
+ throw new Error(`No upload backend '${service}': add it under mediaLibrary.serviceUploads.backends.`);
8
+ }
9
+ const refresh = backend.refresh;
10
+ target = {
11
+ start: backend.start,
12
+ ...refresh ? { refresh: (sessionId) => refresh.replace(/:sessionId\b|\{sessionId\}/g, encodeURIComponent(sessionId)) } : {}
13
+ };
14
+ } else {
15
+ target = service;
16
+ }
17
+ return body ? { ...target, body: { ...target.body, ...body } } : target;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-media-library",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
5
5
  "repository": {
6
6
  "type": "git",