nuxt-media-library 0.0.62 → 0.0.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -217,7 +217,7 @@ The module auto-imports the helpers that read it (a structural type,
217
217
  | `mlMediaSrcset(media)` | `srcset` from `media.sources`, or `undefined` |
218
218
  | `mlPlayableSource(media, { preferFile? })` | `{ src, type: 'hls' \| 'progressive' }`: the HLS ladder, else the file; `null` when locked or not ready |
219
219
  | `mlIsPlayableVideo(media)` | a video the viewer can play now |
220
- | `useMLMediaRenewal(media, renew, { marginSeconds? })` | calls `renew` 5 minutes (default) before tokened links expire; browser only |
220
+ | `useMLMediaRenewal(media, renew, { marginSeconds?, retrySeconds? })` | calls `renew` 5 minutes (default) before tokened links expire, and again every 30 s (default) while it fails; browser only |
221
221
 
222
222
  ```vue
223
223
  <script setup lang="ts">
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.62",
4
+ "version": "0.0.64",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -32,6 +32,11 @@ type __VLS_Props = {
32
32
  picker?: boolean;
33
33
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
34
34
  onPick?: () => void;
35
+ /**
36
+ * The picker lists only these asset IDs (resolved through the backend's
37
+ * `resolve` route) instead of the library. For backends without a library listing.
38
+ */
39
+ pickFrom?: readonly string[] | null;
35
40
  /** Show the eye button on the thumbnail, which opens the media editor. */
36
41
  editor?: boolean;
37
42
  /** Label for the editor's remove button. Empty hides it. */
@@ -78,6 +83,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
78
83
  serviceBody: Record<string, unknown> | null;
79
84
  accept: readonly string[] | string;
80
85
  maxSize: number | null;
86
+ pickFrom: readonly string[] | null;
81
87
  required: boolean;
82
88
  removeLabel: string;
83
89
  hint: string;
@@ -22,6 +22,7 @@ const props = defineProps({
22
22
  serviceBody: { type: [Object, null], required: false, default: null },
23
23
  picker: { type: Boolean, required: false, default: true },
24
24
  onPick: { type: Function, required: false, default: void 0 },
25
+ pickFrom: { type: [Array, null], required: false, default: null },
25
26
  editor: { type: Boolean, required: false, default: true },
26
27
  removeLabel: { type: String, required: false, default: "Remove from this field" },
27
28
  onOpen: { type: Function, required: false, default: void 0 }
@@ -101,6 +102,7 @@ async function pickExisting() {
101
102
  selected: [model.value],
102
103
  title: props.label ? `Select ${props.label.toLowerCase()}` : "Select file",
103
104
  maxSize: props.maxSize,
105
+ pickFrom: props.pickFrom,
104
106
  upload: uploadOptions.value,
105
107
  onUploaded: (id, sessionId) => sessions.set(id, sessionId)
106
108
  });
@@ -32,6 +32,11 @@ type __VLS_Props = {
32
32
  picker?: boolean;
33
33
  /** Replaces the built-in picker, e.g. with the host's own dialog. */
34
34
  onPick?: () => void;
35
+ /**
36
+ * The picker lists only these asset IDs (resolved through the backend's
37
+ * `resolve` route) instead of the library. For backends without a library listing.
38
+ */
39
+ pickFrom?: readonly string[] | null;
35
40
  /** Show the eye button on the thumbnail, which opens the media editor. */
36
41
  editor?: boolean;
37
42
  /** Label for the editor's remove button. Empty hides it. */
@@ -78,6 +83,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
78
83
  serviceBody: Record<string, unknown> | null;
79
84
  accept: readonly string[] | string;
80
85
  maxSize: number | null;
86
+ pickFrom: readonly string[] | null;
81
87
  required: boolean;
82
88
  removeLabel: string;
83
89
  hint: string;
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  selected?: readonly string[];
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
+ pickFrom?: readonly string[] | null;
8
9
  upload?: MediaUploadOptions;
9
10
  onUploaded?: (assetId: string, sessionId: string | null) => void;
10
11
  };
@@ -19,6 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
20
  max: number;
20
21
  selected: readonly string[];
21
22
  maxSize: number | null;
23
+ pickFrom: readonly string[] | null;
22
24
  onUploaded: (assetId: string, sessionId: string | null) => void;
23
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
26
  declare const _default: typeof __VLS_export;
@@ -5,6 +5,7 @@ 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
+ pickFrom: { type: [Array, null], required: false, default: null },
8
9
  upload: { type: Object, required: false, default: () => ({}) },
9
10
  onUploaded: { type: Function, required: false, default: void 0 }
10
11
  });
@@ -25,6 +26,7 @@ const emit = defineEmits(["close"]);
25
26
  :max="max"
26
27
  :selected="selected"
27
28
  :max-size="maxSize"
29
+ :pick-from="pickFrom"
28
30
  :upload="upload"
29
31
  :on-uploaded="onUploaded"
30
32
  @done="(ids) => emit('close', ids)"
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  selected?: readonly string[];
6
6
  title?: string;
7
7
  maxSize?: number | null;
8
+ pickFrom?: readonly string[] | null;
8
9
  upload?: MediaUploadOptions;
9
10
  onUploaded?: (assetId: string, sessionId: string | null) => void;
10
11
  };
@@ -19,6 +20,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
19
20
  max: number;
20
21
  selected: readonly string[];
21
22
  maxSize: number | null;
23
+ pickFrom: readonly string[] | null;
22
24
  onUploaded: (assetId: string, sessionId: string | null) => void;
23
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
26
  declare const _default: typeof __VLS_export;
@@ -9,6 +9,11 @@ type __VLS_Props = {
9
9
  title?: string;
10
10
  /** Bytes, for uploads from the picker. */
11
11
  maxSize?: number | null;
12
+ /**
13
+ * List only these asset IDs (resolved through `upload.backend`'s `resolve`
14
+ * route) instead of the library. Search and the kind filter run here.
15
+ */
16
+ pickFrom?: readonly string[] | null;
12
17
  /** Upload session settings for files dropped into the picker. */
13
18
  upload?: MediaUploadOptions;
14
19
  /** A file uploaded from the picker exists in the library (with its upload session). */
@@ -29,6 +34,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
34
  max: number;
30
35
  selected: readonly string[];
31
36
  maxSize: number | null;
37
+ pickFrom: readonly string[] | null;
32
38
  onUploaded: (assetId: string, sessionId: string | null) => void;
33
39
  bodyClass: string;
34
40
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -14,6 +14,7 @@ const props = defineProps({
14
14
  selected: { type: Array, required: false, default: () => [] },
15
15
  title: { type: String, required: false, default: void 0 },
16
16
  maxSize: { type: [Number, null], required: false, default: null },
17
+ pickFrom: { type: [Array, null], required: false, default: null },
17
18
  upload: { type: Object, required: false, default: () => ({}) },
18
19
  onUploaded: { type: Function, required: false, default: void 0 },
19
20
  bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
@@ -32,7 +33,10 @@ const kindOptions = computed(
32
33
  () => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
33
34
  );
34
35
  const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
35
- const browse = useMLMediaBrowse(() => ({ kind: kindFilter.value[0] ?? null, query: search.value }));
36
+ const browse = useMLMediaBrowse(
37
+ () => ({ kind: kindFilter.value[0] ?? null, query: search.value }),
38
+ props.pickFrom ? { ids: () => props.pickFrom ?? [], backend: () => props.upload?.backend } : {}
39
+ );
36
40
  const initial = [...props.selected];
37
41
  const chosen = ref([...initial]);
38
42
  const limitNote = ref(null);
@@ -9,6 +9,11 @@ type __VLS_Props = {
9
9
  title?: string;
10
10
  /** Bytes, for uploads from the picker. */
11
11
  maxSize?: number | null;
12
+ /**
13
+ * List only these asset IDs (resolved through `upload.backend`'s `resolve`
14
+ * route) instead of the library. Search and the kind filter run here.
15
+ */
16
+ pickFrom?: readonly string[] | null;
12
17
  /** Upload session settings for files dropped into the picker. */
13
18
  upload?: MediaUploadOptions;
14
19
  /** A file uploaded from the picker exists in the library (with its upload session). */
@@ -29,6 +34,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
34
  max: number;
30
35
  selected: readonly string[];
31
36
  maxSize: number | null;
37
+ pickFrom: readonly string[] | null;
32
38
  onUploaded: (assetId: string, sessionId: string | null) => void;
33
39
  bodyClass: string;
34
40
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -47,6 +47,12 @@ type __VLS_Props = {
47
47
  serviceBody?: Record<string, unknown> | null;
48
48
  picker?: boolean;
49
49
  onPick?: () => void;
50
+ /**
51
+ * The picker lists only these asset IDs (resolved through the backend's
52
+ * `resolve` route, searched and filtered here) instead of the library.
53
+ * For backends without a library listing.
54
+ */
55
+ pickFrom?: readonly string[] | null;
50
56
  editor?: boolean;
51
57
  onOpen?: (assetId: string) => void;
52
58
  };
@@ -58,6 +64,12 @@ type __VLS_ModelProps = {
58
64
  * extension, MIME type, kind, size, duration, dimensions. Output only.
59
65
  */
60
66
  'meta'?: Record<string, MediaFileMeta>;
67
+ /**
68
+ * With `profiles`: files in the grid that no role holds (replaced out of
69
+ * their last role, or unchecked). They are not in `update:profiles`, so the
70
+ * page's save drops them; warn before saving. Set [] to clear.
71
+ */
72
+ 'unplaced'?: string[];
61
73
  };
62
74
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
63
75
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -66,14 +78,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
66
78
  "update:modelValue": (value: string[]) => any;
67
79
  "update:alt": (value: Record<string, string> | undefined) => any;
68
80
  "update:meta": (value: Record<string, MediaFileMeta>) => any;
81
+ picked: (assetIds: string[], role: string) => any;
69
82
  "update:profiles": (profiles: MediaGalleryProfiles) => any;
83
+ "update:unplaced": (value: string[]) => any;
70
84
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
71
85
  onUploaded?: ((assetId: string, role: string, sessionId: string | null) => any) | undefined;
72
86
  onMeta?: ((assetId: string, meta: MediaFileMeta | null) => any) | undefined;
73
87
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
74
88
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
75
89
  "onUpdate:meta"?: ((value: Record<string, MediaFileMeta>) => any) | undefined;
90
+ onPicked?: ((assetIds: string[], role: string) => any) | undefined;
76
91
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
92
+ "onUpdate:unplaced"?: ((value: string[]) => any) | undefined;
77
93
  }>, {
78
94
  title: string;
79
95
  visibility: "private" | "restricted" | "public";
@@ -91,6 +107,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
91
107
  accept: readonly string[] | string;
92
108
  max: number | null;
93
109
  maxSize: number | null;
110
+ pickFrom: readonly string[] | null;
94
111
  hint: string;
95
112
  picker: boolean;
96
113
  onPick: () => void;
@@ -6,6 +6,7 @@ import { watchMediaMeta } from "../../composables/useMLMediaMeta";
6
6
  import { kindForFile } from "../../utils/fileAccept";
7
7
  import { backendsFromConfig, resolveAssetsUrl } from "../../utils/backends";
8
8
  import { GALLERY_TILE_TYPE } from "../../utils/galleryDrag";
9
+ import { nextUnplaced } from "../../utils/galleryRoles";
9
10
  import GallerySection from "./GallerySection.vue";
10
11
  const props = defineProps({
11
12
  profiles: { type: [Object, null], required: false, default: null },
@@ -27,13 +28,23 @@ const props = defineProps({
27
28
  serviceBody: { type: [Object, null], required: false, default: null },
28
29
  picker: { type: Boolean, required: false, default: true },
29
30
  onPick: { type: Function, required: false, default: void 0 },
31
+ pickFrom: { type: [Array, null], required: false, default: null },
30
32
  editor: { type: Boolean, required: false, default: true },
31
33
  onOpen: { type: Function, required: false, default: void 0 }
32
34
  });
33
35
  const model = defineModel({ type: Array, ...{ default: () => [] } });
34
36
  const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
35
37
  const meta = defineModel("meta", { type: Object, ...{ default: () => ({}) } });
36
- const emit = defineEmits(["uploaded", "update:profiles", "meta"]);
38
+ const unplacedModel = defineModel("unplaced", { type: Array, ...{ default: () => [] } });
39
+ const unplaced = ref([...unplacedModel.value]);
40
+ const sameIds = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]);
41
+ watch(unplacedModel, (v) => {
42
+ if (!sameIds(v, unplaced.value)) unplaced.value = [...v];
43
+ });
44
+ watch(unplaced, (v) => {
45
+ if (!sameIds(v, unplacedModel.value)) unplacedModel.value = [...v];
46
+ });
47
+ const emit = defineEmits(["uploaded", "picked", "update:profiles", "meta"]);
37
48
  const sectionMeta = ref({});
38
49
  watchMediaMeta(computed(() => sectionMeta.value), (all) => {
39
50
  meta.value = all;
@@ -52,16 +63,22 @@ watch(() => props.profiles, (p) => {
52
63
  const next = {};
53
64
  for (const [key, r] of Object.entries(p ?? {})) next[key] = [...r.selected ?? []];
54
65
  selections.value = next;
66
+ const placed = new Set(Object.values(next).flat());
67
+ if (unplaced.value.some((id) => placed.has(id))) unplaced.value = unplaced.value.filter((id) => !placed.has(id));
55
68
  }, { immediate: true, deep: true });
56
69
  const sel = (key) => selections.value[key] ?? [];
57
70
  const union = computed(() => {
58
71
  const seen = /* @__PURE__ */ new Set();
59
72
  const ordered = [...roles.value.filter(single), ...roles.value.filter((r) => !single(r))];
60
73
  for (const r of ordered) for (const id of sel(r.key)) seen.add(id);
74
+ for (const id of unplaced.value) seen.add(id);
61
75
  return [...seen];
62
76
  });
63
- function commit(changes) {
77
+ function commit(changes, removed = /* @__PURE__ */ new Set()) {
78
+ const before = selections.value;
64
79
  selections.value = { ...selections.value, ...changes };
80
+ const left = nextUnplaced(before, selections.value, unplaced.value, removed);
81
+ if (!sameIds(left, unplaced.value)) unplaced.value = left;
65
82
  const out = {};
66
83
  for (const [k, r] of Object.entries(props.profiles ?? {})) out[k] = { ...r, selected: [...sel(k)] };
67
84
  emit("update:profiles", out);
@@ -174,21 +191,44 @@ async function placeByKind(ids) {
174
191
  }
175
192
  }
176
193
  if (Object.keys(changes).length) commit(changes);
194
+ for (const id of ids) awaitingKind.delete(id);
195
+ const picked = ids.filter((id) => pendingPicked.delete(id));
196
+ if (picked.length) emitPicked(picked);
177
197
  }
178
198
  const pendingEmit = /* @__PURE__ */ new Map();
199
+ const awaitingKind = /* @__PURE__ */ new Set();
200
+ const pendingPicked = /* @__PURE__ */ new Set();
201
+ function emitPicked(ids) {
202
+ const byRole = /* @__PURE__ */ new Map();
203
+ for (const id of ids) {
204
+ const role = roles.value.find((r) => sel(r.key).includes(id));
205
+ if (role) byRole.set(role.key, [...byRole.get(role.key) ?? [], id]);
206
+ }
207
+ for (const [key, list] of byRole) emit("picked", list, key);
208
+ }
209
+ function onPicked(ids) {
210
+ const now = [];
211
+ for (const id of ids) {
212
+ if (awaitingKind.has(id)) pendingPicked.add(id);
213
+ else now.push(id);
214
+ }
215
+ if (now.length) emitPicked(now);
216
+ }
179
217
  function onUnionUpdate(next) {
180
218
  notice.value = null;
181
219
  const keep = new Set(next);
182
220
  const before = new Set(union.value);
221
+ const removed = new Set(union.value.filter((id) => !keep.has(id)));
183
222
  const changes = {};
184
223
  for (const r of roles.value) changes[r.key] = sel(r.key).filter((id) => keep.has(id));
185
- const unplaced = [];
224
+ const needKind = [];
186
225
  for (const id of next) {
187
226
  if (before.has(id)) continue;
188
227
  const tagged = pendingRole.get(id);
189
228
  pendingRole.delete(id);
190
229
  if (!tagged && kindMatters.value) {
191
- unplaced.push(id);
230
+ needKind.push(id);
231
+ awaitingKind.add(id);
192
232
  continue;
193
233
  }
194
234
  const key = tagged ?? defaultKey.value;
@@ -199,8 +239,10 @@ function onUnionUpdate(next) {
199
239
  for (const r of roles.value) {
200
240
  if (!single(r)) changes[r.key] = [...changes[r.key]].sort((a, b) => (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
201
241
  }
202
- commit(changes);
203
- if (unplaced.length) void placeByKind(unplaced);
242
+ const order = unplaced.value.filter((id) => keep.has(id)).sort((a, b) => (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
243
+ if (!sameIds(order, unplaced.value)) unplaced.value = order;
244
+ commit(changes, removed);
245
+ if (needKind.length) void placeByKind(needKind);
204
246
  }
205
247
  function onAdd(id, tag) {
206
248
  const key = tag ?? defaultKey.value;
@@ -217,7 +259,10 @@ function onUploaded(id, sessionId) {
217
259
  }
218
260
  emit("uploaded", id, key ?? defaultKey.value, sessionId);
219
261
  }
220
- const badges = (id) => roles.value.filter((r) => sel(r.key).includes(id)).map((r) => r.label);
262
+ const badges = (id) => {
263
+ const labels = roles.value.filter((r) => sel(r.key).includes(id)).map((r) => r.label);
264
+ return labels.length || !unplaced.value.includes(id) ? labels : ["Not used"];
265
+ };
221
266
  function tileMenu(id, kind) {
222
267
  return [
223
268
  [
@@ -297,6 +342,7 @@ function chipCount(key, max) {
297
342
  :collection-name="collectionName"
298
343
  :picker="picker"
299
344
  :on-pick="onPick"
345
+ :pick-from="pickFrom"
300
346
  :editor="editor"
301
347
  :on-open="onOpen"
302
348
  :badges="badges"
@@ -305,6 +351,7 @@ function chipCount(key, max) {
305
351
  :on-add="onAdd"
306
352
  @update:model-value="onUnionUpdate"
307
353
  @uploaded="onUploaded"
354
+ @picked="onPicked"
308
355
  @meta="(m) => sectionMeta = m"
309
356
  >
310
357
  <template #toolbar>
@@ -370,9 +417,11 @@ function chipCount(key, max) {
370
417
  :service-body="serviceBody"
371
418
  :picker="picker"
372
419
  :on-pick="onPick"
420
+ :pick-from="pickFrom"
373
421
  :editor="editor"
374
422
  :on-open="onOpen"
375
423
  @uploaded="(id, sessionId) => emit('uploaded', id, '', sessionId)"
424
+ @picked="(ids) => emit('picked', ids, '')"
376
425
  @meta="(m) => sectionMeta = m"
377
426
  />
378
427
  </template>
@@ -47,6 +47,12 @@ type __VLS_Props = {
47
47
  serviceBody?: Record<string, unknown> | null;
48
48
  picker?: boolean;
49
49
  onPick?: () => void;
50
+ /**
51
+ * The picker lists only these asset IDs (resolved through the backend's
52
+ * `resolve` route, searched and filtered here) instead of the library.
53
+ * For backends without a library listing.
54
+ */
55
+ pickFrom?: readonly string[] | null;
50
56
  editor?: boolean;
51
57
  onOpen?: (assetId: string) => void;
52
58
  };
@@ -58,6 +64,12 @@ type __VLS_ModelProps = {
58
64
  * extension, MIME type, kind, size, duration, dimensions. Output only.
59
65
  */
60
66
  'meta'?: Record<string, MediaFileMeta>;
67
+ /**
68
+ * With `profiles`: files in the grid that no role holds (replaced out of
69
+ * their last role, or unchecked). They are not in `update:profiles`, so the
70
+ * page's save drops them; warn before saving. Set [] to clear.
71
+ */
72
+ 'unplaced'?: string[];
61
73
  };
62
74
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
63
75
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -66,14 +78,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
66
78
  "update:modelValue": (value: string[]) => any;
67
79
  "update:alt": (value: Record<string, string> | undefined) => any;
68
80
  "update:meta": (value: Record<string, MediaFileMeta>) => any;
81
+ picked: (assetIds: string[], role: string) => any;
69
82
  "update:profiles": (profiles: MediaGalleryProfiles) => any;
83
+ "update:unplaced": (value: string[]) => any;
70
84
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
71
85
  onUploaded?: ((assetId: string, role: string, sessionId: string | null) => any) | undefined;
72
86
  onMeta?: ((assetId: string, meta: MediaFileMeta | null) => any) | undefined;
73
87
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
74
88
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
75
89
  "onUpdate:meta"?: ((value: Record<string, MediaFileMeta>) => any) | undefined;
90
+ onPicked?: ((assetIds: string[], role: string) => any) | undefined;
76
91
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
92
+ "onUpdate:unplaced"?: ((value: string[]) => any) | undefined;
77
93
  }>, {
78
94
  title: string;
79
95
  visibility: "private" | "restricted" | "public";
@@ -91,6 +107,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
91
107
  accept: readonly string[] | string;
92
108
  max: number | null;
93
109
  maxSize: number | null;
110
+ pickFrom: readonly string[] | null;
94
111
  hint: string;
95
112
  picker: boolean;
96
113
  onPick: () => void;
@@ -22,6 +22,8 @@ type __VLS_Props = {
22
22
  /** Show "Select existing", which opens the library picker. */
23
23
  picker?: boolean;
24
24
  onPick?: () => void;
25
+ /** The picker lists only these asset IDs instead of the library. */
26
+ pickFrom?: readonly string[] | null;
25
27
  /** Show the eye on each tile, which opens the media editor. */
26
28
  editor?: boolean;
27
29
  onOpen?: (assetId: string) => void;
@@ -73,11 +75,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
73
75
  meta: (meta: Record<string, MediaFileMeta>) => any;
74
76
  "update:modelValue": (value: string[]) => any;
75
77
  "update:alt": (value: Record<string, string> | undefined) => any;
78
+ picked: (assetIds: string[]) => any;
76
79
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
77
80
  onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
78
81
  onMeta?: ((meta: Record<string, MediaFileMeta>) => any) | undefined;
79
82
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
80
83
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
84
+ onPicked?: ((assetIds: string[]) => any) | undefined;
81
85
  }>, {
82
86
  title: string;
83
87
  visibility: "private" | "restricted" | "public";
@@ -97,6 +101,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
97
101
  accept: readonly string[] | string;
98
102
  max: number | null;
99
103
  maxSize: number | null;
104
+ pickFrom: readonly string[] | null;
100
105
  removeLabel: string;
101
106
  onAdd: (assetId: string, tag: string | undefined) => void;
102
107
  hint: string;
@@ -22,6 +22,7 @@ const props = defineProps({
22
22
  collectionName: { type: [String, null], required: false, default: null },
23
23
  picker: { type: Boolean, required: false, default: true },
24
24
  onPick: { type: Function, required: false, default: void 0 },
25
+ pickFrom: { type: [Array, null], required: false, default: null },
25
26
  editor: { type: Boolean, required: false, default: true },
26
27
  onOpen: { type: Function, required: false, default: void 0 },
27
28
  profile: { type: [String, null], required: false, default: null },
@@ -38,7 +39,7 @@ const props = defineProps({
38
39
  });
39
40
  const model = defineModel({ type: Array, ...{ default: () => [] } });
40
41
  const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
41
- const emit = defineEmits(["uploaded", "meta"]);
42
+ const emit = defineEmits(["uploaded", "picked", "meta"]);
42
43
  const gallery = useMLMediaGallery(model, {
43
44
  accept: () => props.accept,
44
45
  max: () => props.max,
@@ -132,6 +133,7 @@ async function pickExisting() {
132
133
  selected: model.value,
133
134
  title: `Select ${props.title.toLowerCase()}`,
134
135
  maxSize: props.maxSize,
136
+ pickFrom: props.pickFrom,
135
137
  upload: {
136
138
  visibility: props.visibility,
137
139
  processingConfig: props.processingConfig,
@@ -146,9 +148,12 @@ async function pickExisting() {
146
148
  });
147
149
  if (!ids) return;
148
150
  gallery.setFromPicker(ids);
149
- for (const id of ids) {
150
- if (!before.has(id) && sessions.has(id)) emit("uploaded", id, sessions.get(id) ?? null);
151
+ const added = ids.filter((id) => !before.has(id));
152
+ for (const id of added) {
153
+ if (sessions.has(id)) emit("uploaded", id, sessions.get(id) ?? null);
151
154
  }
155
+ const picked = added.filter((id) => !sessions.has(id));
156
+ if (picked.length) emit("picked", picked);
152
157
  }
153
158
  function openFor(item) {
154
159
  const id = item.id;
@@ -22,6 +22,8 @@ type __VLS_Props = {
22
22
  /** Show "Select existing", which opens the library picker. */
23
23
  picker?: boolean;
24
24
  onPick?: () => void;
25
+ /** The picker lists only these asset IDs instead of the library. */
26
+ pickFrom?: readonly string[] | null;
25
27
  /** Show the eye on each tile, which opens the media editor. */
26
28
  editor?: boolean;
27
29
  onOpen?: (assetId: string) => void;
@@ -73,11 +75,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
73
75
  meta: (meta: Record<string, MediaFileMeta>) => any;
74
76
  "update:modelValue": (value: string[]) => any;
75
77
  "update:alt": (value: Record<string, string> | undefined) => any;
78
+ picked: (assetIds: string[]) => any;
76
79
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
77
80
  onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
78
81
  onMeta?: ((meta: Record<string, MediaFileMeta>) => any) | undefined;
79
82
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
80
83
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
84
+ onPicked?: ((assetIds: string[]) => any) | undefined;
81
85
  }>, {
82
86
  title: string;
83
87
  visibility: "private" | "restricted" | "public";
@@ -97,6 +101,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
97
101
  accept: readonly string[] | string;
98
102
  max: number | null;
99
103
  maxSize: number | null;
104
+ pickFrom: readonly string[] | null;
100
105
  removeLabel: string;
101
106
  onAdd: (assetId: string, tag: string | undefined) => void;
102
107
  hint: string;
@@ -7,6 +7,12 @@ export interface FilePickerOpenOptions {
7
7
  /** IDs already attached. Listed first and checked; the result keeps their order. */
8
8
  selected?: readonly (string | null | undefined)[];
9
9
  title?: string;
10
+ /**
11
+ * List only these asset IDs (resolved through `upload.backend`'s `resolve`
12
+ * route, searched and filtered in the browser) instead of the library.
13
+ * For backends without a library listing. null or omitted lists the library.
14
+ */
15
+ pickFrom?: readonly string[] | null;
10
16
  /** Bytes, for uploads from inside the picker. */
11
17
  maxSize?: number | null;
12
18
  upload?: MediaUploadOptions;
@@ -6,14 +6,34 @@ export interface MediaBrowseFilters {
6
6
  status?: string | null;
7
7
  query?: string | null;
8
8
  }
9
+ export interface MediaBrowseOptions {
10
+ pageSize?: number;
11
+ debounceMs?: number;
12
+ /**
13
+ * Browse only these asset IDs instead of the library: they are resolved
14
+ * through `backend`'s `resolve` route and filtered here, with no call to
15
+ * the library's list. For backends that have no listing.
16
+ */
17
+ ids?: MaybeRefOrGetter<readonly string[]>;
18
+ /** Backend whose `resolve` route answers for `ids` (see useMLMediaAssets). */
19
+ backend?: MaybeRefOrGetter<string | null | undefined>;
20
+ }
21
+ /** The assets that match `filters`, in the given order (kind and status exact, query in the name). */
22
+ export declare function filterBrowseAssets(assets: readonly AssetDetail[], filters: MediaBrowseFilters): AssetDetail[];
9
23
  /**
10
24
  * The library as a growing list: page 1 on filter change, more on loadMore().
11
- * Search text is debounced. Browser only, like useMLMediaAssets.
25
+ * Search text is debounced. Browser only, like useMLMediaAssets. With
26
+ * `ids`, a fixed set of files instead (see MediaBrowseOptions).
12
27
  */
13
- export declare function useMLMediaBrowse(filters: MaybeRefOrGetter<MediaBrowseFilters>, opts?: {
14
- pageSize?: number;
15
- debounceMs?: number;
16
- }): {
28
+ export declare function useMLMediaBrowse(filters: MaybeRefOrGetter<MediaBrowseFilters>, opts?: MediaBrowseOptions): {
29
+ items: import("vue").ComputedRef<AssetDetail[]>;
30
+ total: import("vue").ComputedRef<number>;
31
+ error: import("vue").Ref<string | null, string | null>;
32
+ loading: import("vue").ComputedRef<boolean>;
33
+ hasMore: import("vue").ComputedRef<boolean>;
34
+ loadMore: () => void;
35
+ reload: () => Promise<void>;
36
+ } | {
17
37
  items: import("vue").Ref<{
18
38
  id: string;
19
39
  kind: string;
@@ -1,7 +1,18 @@
1
1
  import { computed, onBeforeUnmount, onMounted, ref, toValue, watch } from "vue";
2
2
  import { $fetch } from "ofetch";
3
+ import { assetName } from "../utils/assetFormat.js";
3
4
  import { mlErrorMessage } from "../utils/errorMessage.js";
5
+ import { useMLMediaAssets } from "./useMLMediaAssets.js";
6
+ export function filterBrowseAssets(assets, filters) {
7
+ const kind = filters.kind?.toLowerCase();
8
+ const status = filters.status?.toLowerCase();
9
+ const query = filters.query?.trim().toLowerCase();
10
+ return assets.filter(
11
+ (a) => (!kind || (a.kind ?? "").toLowerCase() === kind) && (!status || (a.status ?? "").toLowerCase() === status) && (!query || assetName(a).toLowerCase().includes(query))
12
+ );
13
+ }
4
14
  export function useMLMediaBrowse(filters, opts = {}) {
15
+ if (opts.ids !== void 0) return browseIds(filters, opts.ids, opts.backend);
5
16
  const pageSize = opts.pageSize ?? 30;
6
17
  const debounceMs = opts.debounceMs ?? 300;
7
18
  const items = ref([]);
@@ -71,3 +82,18 @@ export function useMLMediaBrowse(filters, opts = {}) {
71
82
  reload
72
83
  };
73
84
  }
85
+ function browseIds(filters, ids, backend) {
86
+ const pool = useMLMediaAssets(ids, { backend });
87
+ const all = computed(() => toValue(ids).map((id) => pool.get(id)).filter((a) => !!a));
88
+ const items = computed(() => filterBrowseAssets(all.value, toValue(filters)));
89
+ return {
90
+ items,
91
+ total: computed(() => items.value.length),
92
+ error: pool.error,
93
+ loading: pool.loading,
94
+ hasMore: computed(() => false),
95
+ loadMore: () => {
96
+ },
97
+ reload: () => pool.refresh()
98
+ };
99
+ }
@@ -3,13 +3,18 @@ import type { MLEmbeddedMedia } from '../types/embeddedMedia.js';
3
3
  export interface UseMLMediaRenewalOptions {
4
4
  /** Seconds before expiry to renew. Default 300. */
5
5
  marginSeconds?: number;
6
+ /**
7
+ * Seconds to wait before trying again when `renew` rejects (a transient
8
+ * network error must not let the links expire mid-playback). Default 30.
9
+ */
10
+ retrySeconds?: number;
6
11
  }
7
12
  /**
8
13
  * The renewal loop without the client guard: watches `media` and calls
9
14
  * `renew` shortly before its links expire. Returns a stop function. Prefer
10
15
  * `useMLMediaRenewal`, which runs this in the browser only.
11
16
  */
12
- export declare function watchMLMediaRenewal(media: MaybeRefOrGetter<MLEmbeddedMedia | null | undefined>, renew: () => Promise<unknown>, { marginSeconds }?: UseMLMediaRenewalOptions): () => void;
17
+ export declare function watchMLMediaRenewal(media: MaybeRefOrGetter<MLEmbeddedMedia | null | undefined>, renew: () => Promise<unknown>, { marginSeconds, retrySeconds }?: UseMLMediaRenewalOptions): () => void;
13
18
  /**
14
19
  * Keeps embedded media links alive: tokened links expire (`expiresIn`), so
15
20
  * shortly before they do this calls `renew`, which re-fetches the record and
@@ -1,20 +1,31 @@
1
1
  import { onScopeDispose, toValue, watch } from "vue";
2
2
  import { refreshDelay } from "../utils/streamUrls.js";
3
3
  const DEFAULT_MARGIN_SECONDS = 300;
4
- export function watchMLMediaRenewal(media, renew, { marginSeconds = DEFAULT_MARGIN_SECONDS } = {}) {
4
+ const DEFAULT_RETRY_SECONDS = 30;
5
+ export function watchMLMediaRenewal(media, renew, { marginSeconds = DEFAULT_MARGIN_SECONDS, retrySeconds = DEFAULT_RETRY_SECONDS } = {}) {
5
6
  let timer = null;
7
+ let generation = 0;
6
8
  const clear = () => {
9
+ generation++;
7
10
  if (timer) clearTimeout(timer);
8
11
  timer = null;
9
12
  };
13
+ const arm = (delay) => {
14
+ const current = generation;
15
+ timer = setTimeout(() => {
16
+ timer = null;
17
+ renew().catch((err) => {
18
+ console.warn("[nuxt-media-library] media renewal failed, retrying:", err);
19
+ if (current === generation) arm(retrySeconds * 1e3);
20
+ });
21
+ }, delay);
22
+ };
10
23
  const stopWatch = watch(() => toValue(media), (current) => {
11
24
  clear();
12
25
  const now = Date.now();
13
26
  const delay = refreshDelay(current?.expiresIn, now, now, marginSeconds);
14
27
  if (delay == null) return;
15
- timer = setTimeout(() => {
16
- renew().catch((err) => console.warn("[nuxt-media-library] media renewal failed:", err));
17
- }, delay);
28
+ arm(delay);
18
29
  }, { immediate: true });
19
30
  return () => {
20
31
  stopWatch();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The unplaced files after a role change. A file that lost its last role
3
+ * (replaced in a one-file role, or unchecked) stays on the record, unplaced,
4
+ * until it gets a role again or is removed. Order: those already unplaced,
5
+ * then newly unplaced ones in the order they were held.
6
+ */
7
+ export declare function nextUnplaced(before: Record<string, readonly string[]>, after: Record<string, readonly string[]>, unplaced: readonly string[], removed?: ReadonlySet<string>): string[];
@@ -0,0 +1,11 @@
1
+ export function nextUnplaced(before, after, unplaced, removed = /* @__PURE__ */ new Set()) {
2
+ const placed = new Set(Object.values(after).flat());
3
+ const out = [];
4
+ const seen = /* @__PURE__ */ new Set();
5
+ for (const id of [...unplaced, ...Object.values(before).flat()]) {
6
+ if (seen.has(id) || placed.has(id) || removed.has(id)) continue;
7
+ seen.add(id);
8
+ out.push(id);
9
+ }
10
+ return out;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-media-library",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
5
5
  "repository": {
6
6
  "type": "git",
@@ -73,6 +73,7 @@
73
73
  "@nuxt/test-utils": "^3.21.0",
74
74
  "@nuxt/ui": "4.3.0",
75
75
  "@types/node": "latest",
76
+ "@vitejs/plugin-vue": "^6.0.6",
76
77
  "dashjs": "^5.1.1",
77
78
  "eslint": "^9.39.2",
78
79
  "hls.js": "^1.6.16",