nuxt-media-library 0.0.44 → 0.0.49

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 (111) hide show
  1. package/README.md +71 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +4 -1
  4. package/dist/runtime/components/MLAssetManager/Processing.vue +3 -1
  5. package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +60 -0
  6. package/dist/runtime/components/MLMedia/Dropzone.vue +137 -0
  7. package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +60 -0
  8. package/dist/runtime/components/MLMedia/Editor.d.vue.ts +23 -0
  9. package/dist/runtime/components/MLMedia/Editor.vue +34 -0
  10. package/dist/runtime/components/MLMedia/Editor.vue.d.ts +23 -0
  11. package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +38 -0
  12. package/dist/runtime/components/MLMedia/EditorPanel.vue +220 -0
  13. package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +38 -0
  14. package/dist/runtime/components/MLMedia/Field.d.vue.ts +65 -0
  15. package/dist/runtime/components/MLMedia/Field.vue +282 -0
  16. package/dist/runtime/components/MLMedia/Field.vue.d.ts +65 -0
  17. package/dist/runtime/components/MLMedia/FileFacts.d.vue.ts +19 -0
  18. package/dist/runtime/components/MLMedia/FileFacts.vue +38 -0
  19. package/dist/runtime/components/MLMedia/FileFacts.vue.d.ts +19 -0
  20. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +23 -0
  21. package/dist/runtime/components/MLMedia/FilePicker.vue +33 -0
  22. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +23 -0
  23. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +33 -0
  24. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +300 -0
  25. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +33 -0
  26. package/dist/runtime/components/MLMedia/FilterPill.d.vue.ts +30 -0
  27. package/dist/runtime/components/MLMedia/FilterPill.vue +84 -0
  28. package/dist/runtime/components/MLMedia/FilterPill.vue.d.ts +30 -0
  29. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +78 -0
  30. package/dist/runtime/components/MLMedia/Gallery.vue +256 -0
  31. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +78 -0
  32. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +102 -0
  33. package/dist/runtime/components/MLMedia/GallerySection.vue +422 -0
  34. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +102 -0
  35. package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +17 -0
  36. package/dist/runtime/components/MLMedia/KindBadge.vue +22 -0
  37. package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +17 -0
  38. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +63 -0
  39. package/dist/runtime/components/MLMedia/MediaTile.vue +117 -0
  40. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +63 -0
  41. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +42 -0
  42. package/dist/runtime/components/MLMedia/PickerCard.vue +79 -0
  43. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +42 -0
  44. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +31 -0
  45. package/dist/runtime/components/MLMedia/PickerRow.vue +65 -0
  46. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +31 -0
  47. package/dist/runtime/components/MLMedia/PreviewEye.d.vue.ts +16 -0
  48. package/dist/runtime/components/MLMedia/PreviewEye.vue +21 -0
  49. package/dist/runtime/components/MLMedia/PreviewEye.vue.d.ts +16 -0
  50. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +25 -0
  51. package/dist/runtime/components/MLMedia/ProgressVeil.vue +98 -0
  52. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +25 -0
  53. package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +17 -0
  54. package/dist/runtime/components/MLMedia/StatusBadge.vue +31 -0
  55. package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +17 -0
  56. package/dist/runtime/components/MLMedia/Thumbnail.d.vue.ts +36 -0
  57. package/dist/runtime/components/MLMedia/Thumbnail.vue +84 -0
  58. package/dist/runtime/components/MLMedia/Thumbnail.vue.d.ts +36 -0
  59. package/dist/runtime/components/MLMedia/UsageList.d.vue.ts +26 -0
  60. package/dist/runtime/components/MLMedia/UsageList.vue +66 -0
  61. package/dist/runtime/components/MLMedia/UsageList.vue.d.ts +26 -0
  62. package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
  63. package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
  64. package/dist/runtime/components/MLUploader/Item.d.vue.ts +2 -2
  65. package/dist/runtime/components/MLUploader/Item.vue.d.ts +2 -2
  66. package/dist/runtime/composables/useMLAssetPreview.js +17 -1
  67. package/dist/runtime/composables/useMLFilePicker.d.ts +20 -0
  68. package/dist/runtime/composables/useMLFilePicker.js +17 -0
  69. package/dist/runtime/composables/useMLMediaAssets.d.ts +19 -0
  70. package/dist/runtime/composables/useMLMediaAssets.js +56 -0
  71. package/dist/runtime/composables/useMLMediaBrowse.d.ts +147 -0
  72. package/dist/runtime/composables/useMLMediaBrowse.js +72 -0
  73. package/dist/runtime/composables/useMLMediaEditor.d.ts +22 -0
  74. package/dist/runtime/composables/useMLMediaEditor.js +20 -0
  75. package/dist/runtime/composables/useMLMediaField.d.ts +29 -0
  76. package/dist/runtime/composables/useMLMediaField.js +77 -0
  77. package/dist/runtime/composables/useMLMediaGallery.d.ts +55 -0
  78. package/dist/runtime/composables/useMLMediaGallery.js +134 -0
  79. package/dist/runtime/composables/useMLMediaGrants.d.ts +23 -0
  80. package/dist/runtime/composables/useMLMediaGrants.js +22 -0
  81. package/dist/runtime/composables/useMLMediaUpload.d.ts +134 -0
  82. package/dist/runtime/composables/useMLMediaUpload.js +153 -0
  83. package/dist/runtime/composables/useMLServiceUpload.d.ts +36 -0
  84. package/dist/runtime/composables/useMLServiceUpload.js +32 -0
  85. package/dist/runtime/index.d.ts +11 -0
  86. package/dist/runtime/index.js +11 -0
  87. package/dist/runtime/server/create-ml-proxy.d.ts +16 -0
  88. package/dist/runtime/server/create-ml-proxy.js +138 -40
  89. package/dist/runtime/server/grants.d.ts +124 -0
  90. package/dist/runtime/server/grants.js +161 -0
  91. package/dist/runtime/server/index.d.ts +2 -0
  92. package/dist/runtime/server/index.js +7 -0
  93. package/dist/runtime/server/uploads-proxy.js +5 -2
  94. package/dist/runtime/types/asset.d.ts +3 -1
  95. package/dist/runtime/utils/assetDisplay.d.ts +8 -0
  96. package/dist/runtime/utils/assetDisplay.js +29 -0
  97. package/dist/runtime/utils/assetFormat.d.ts +30 -0
  98. package/dist/runtime/utils/assetFormat.js +69 -0
  99. package/dist/runtime/utils/assetThumbnail.d.ts +11 -0
  100. package/dist/runtime/utils/assetThumbnail.js +14 -0
  101. package/dist/runtime/utils/fileAccept.d.ts +26 -0
  102. package/dist/runtime/utils/fileAccept.js +104 -0
  103. package/dist/runtime/utils/galleryDrag.d.ts +2 -0
  104. package/dist/runtime/utils/galleryDrag.js +1 -0
  105. package/dist/runtime/utils/grants/listeners.d.ts +2 -0
  106. package/dist/runtime/utils/grants/listeners.js +8 -0
  107. package/dist/runtime/utils/grants/store.d.ts +26 -0
  108. package/dist/runtime/utils/grants/store.js +37 -0
  109. package/dist/runtime/utils/uploader/serviceUpload.d.ts +84 -0
  110. package/dist/runtime/utils/uploader/serviceUpload.js +109 -0
  111. package/package.json +5 -2
@@ -0,0 +1,147 @@
1
+ import { type MaybeRefOrGetter } from 'vue';
2
+ import type { AssetDetail } from '../types/asset.js';
3
+ export interface MediaBrowseFilters {
4
+ /** One kind. The list API filters by a single kind. */
5
+ kind?: string | null;
6
+ status?: string | null;
7
+ query?: string | null;
8
+ }
9
+ /**
10
+ * The library as a growing list: page 1 on filter change, more on loadMore().
11
+ * Search text is debounced. Browser only, like useMLMediaAssets.
12
+ */
13
+ export declare function useMLMediaBrowse(filters: MaybeRefOrGetter<MediaBrowseFilters>, opts?: {
14
+ pageSize?: number;
15
+ debounceMs?: number;
16
+ }): {
17
+ items: import("vue").Ref<{
18
+ id: string;
19
+ kind: string;
20
+ title: string | null;
21
+ original_filename: string;
22
+ originalFilename?: string | undefined;
23
+ mime_type: string;
24
+ mimeType?: string | undefined;
25
+ size_bytes: number;
26
+ sizeBytes?: number | undefined;
27
+ bytes?: number | undefined;
28
+ hash_sha256: string | null;
29
+ hashSha256?: string | null | undefined;
30
+ content_hash_sha256?: string | null | undefined;
31
+ status: string;
32
+ visibility: string;
33
+ created_at: string;
34
+ createdAt?: string | undefined;
35
+ blob_id: string | null;
36
+ blobId?: string | null | undefined;
37
+ resource_policy_id: string | null;
38
+ resourcePolicyId?: string | null | undefined;
39
+ renditions: {
40
+ id: string;
41
+ renditionType: string;
42
+ entrypoint: string | null;
43
+ storagePrefix: string | null;
44
+ blobIds: string[];
45
+ variants: {
46
+ name: string;
47
+ entrypoint: string;
48
+ locale: string | null;
49
+ width: number | null;
50
+ height: number | null;
51
+ bitrateKbps: number | null;
52
+ blobIds: string[];
53
+ extras: Record<string, any> | null;
54
+ previewUrl: string | null;
55
+ previewUrlType: import("..").PreviewUrlType | null;
56
+ previewUrlExpiresIn: number | null;
57
+ }[] | null;
58
+ status: string;
59
+ durationSeconds: number | null;
60
+ pages: number | null;
61
+ channels: number | null;
62
+ extras: Record<string, any> | null;
63
+ createdAt: string;
64
+ updatedAt: string;
65
+ previewUrl: string | null;
66
+ previewUrlType: import("..").PreviewUrlType | null;
67
+ previewUrlExpiresIn: number | null;
68
+ }[];
69
+ slug?: string | null | undefined;
70
+ created_by?: string | null | undefined;
71
+ updated_at?: string | null | undefined;
72
+ deleted_at?: string | null | undefined;
73
+ upload_session_id?: string | null | undefined;
74
+ primary_blob_id?: string | null | undefined;
75
+ previewUrl: string | null;
76
+ previewUrlType: import("..").PreviewUrlType | null;
77
+ previewUrlExpiresIn: number | null;
78
+ }[], AssetDetail[] | {
79
+ id: string;
80
+ kind: string;
81
+ title: string | null;
82
+ original_filename: string;
83
+ originalFilename?: string | undefined;
84
+ mime_type: string;
85
+ mimeType?: string | undefined;
86
+ size_bytes: number;
87
+ sizeBytes?: number | undefined;
88
+ bytes?: number | undefined;
89
+ hash_sha256: string | null;
90
+ hashSha256?: string | null | undefined;
91
+ content_hash_sha256?: string | null | undefined;
92
+ status: string;
93
+ visibility: string;
94
+ created_at: string;
95
+ createdAt?: string | undefined;
96
+ blob_id: string | null;
97
+ blobId?: string | null | undefined;
98
+ resource_policy_id: string | null;
99
+ resourcePolicyId?: string | null | undefined;
100
+ renditions: {
101
+ id: string;
102
+ renditionType: string;
103
+ entrypoint: string | null;
104
+ storagePrefix: string | null;
105
+ blobIds: string[];
106
+ variants: {
107
+ name: string;
108
+ entrypoint: string;
109
+ locale: string | null;
110
+ width: number | null;
111
+ height: number | null;
112
+ bitrateKbps: number | null;
113
+ blobIds: string[];
114
+ extras: Record<string, any> | null;
115
+ previewUrl: string | null;
116
+ previewUrlType: import("..").PreviewUrlType | null;
117
+ previewUrlExpiresIn: number | null;
118
+ }[] | null;
119
+ status: string;
120
+ durationSeconds: number | null;
121
+ pages: number | null;
122
+ channels: number | null;
123
+ extras: Record<string, any> | null;
124
+ createdAt: string;
125
+ updatedAt: string;
126
+ previewUrl: string | null;
127
+ previewUrlType: import("..").PreviewUrlType | null;
128
+ previewUrlExpiresIn: number | null;
129
+ }[];
130
+ slug?: string | null | undefined;
131
+ created_by?: string | null | undefined;
132
+ updated_at?: string | null | undefined;
133
+ deleted_at?: string | null | undefined;
134
+ upload_session_id?: string | null | undefined;
135
+ primary_blob_id?: string | null | undefined;
136
+ previewUrl: string | null;
137
+ previewUrlType: import("..").PreviewUrlType | null;
138
+ previewUrlExpiresIn: number | null;
139
+ }[]>;
140
+ total: import("vue").Ref<number, number>;
141
+ error: import("vue").Ref<string | null, string | null>;
142
+ /** True for the first page too, including SSR and hydration. */
143
+ loading: import("vue").ComputedRef<boolean>;
144
+ hasMore: import("vue").ComputedRef<boolean>;
145
+ loadMore: () => void;
146
+ reload: () => Promise<void>;
147
+ };
@@ -0,0 +1,72 @@
1
+ import { computed, onBeforeUnmount, onMounted, ref, toValue, watch } from "vue";
2
+ import { $fetch } from "ofetch";
3
+ export function useMLMediaBrowse(filters, opts = {}) {
4
+ const pageSize = opts.pageSize ?? 30;
5
+ const debounceMs = opts.debounceMs ?? 300;
6
+ const items = ref([]);
7
+ const total = ref(0);
8
+ const page = ref(0);
9
+ const loading = ref(false);
10
+ const error = ref(null);
11
+ const mounted = ref(false);
12
+ let request = 0;
13
+ async function fetchPage(n) {
14
+ const id = ++request;
15
+ const f = toValue(filters);
16
+ loading.value = true;
17
+ error.value = null;
18
+ try {
19
+ const query = { page: String(n), pageSize: String(pageSize) };
20
+ if (f.kind) query.kind = f.kind;
21
+ if (f.status) query.status = f.status;
22
+ if (f.query?.trim()) query.query = f.query.trim();
23
+ const res = await $fetch("/api/media-library/manage/assets", { query });
24
+ if (id !== request) return;
25
+ const seen = new Set(n === 1 ? [] : items.value.map((a) => a.id));
26
+ items.value = n === 1 ? res.items ?? [] : [...items.value, ...(res.items ?? []).filter((a) => !seen.has(a.id))];
27
+ total.value = res.total ?? 0;
28
+ page.value = n;
29
+ } catch (err) {
30
+ if (id !== request) return;
31
+ error.value = err?.data?.message ?? err?.message ?? "Could not load the library";
32
+ } finally {
33
+ if (id === request) loading.value = false;
34
+ }
35
+ }
36
+ const hasMore = computed(() => items.value.length < total.value);
37
+ function loadMore() {
38
+ if (!loading.value && hasMore.value) void fetchPage(page.value + 1);
39
+ }
40
+ function reload() {
41
+ return fetchPage(1);
42
+ }
43
+ let timer;
44
+ onMounted(() => {
45
+ mounted.value = true;
46
+ });
47
+ watch(
48
+ [() => toValue(filters).kind, () => toValue(filters).status, mounted],
49
+ () => {
50
+ if (!mounted.value) return;
51
+ clearTimeout(timer);
52
+ void fetchPage(1);
53
+ },
54
+ { immediate: true }
55
+ );
56
+ watch(() => toValue(filters).query, () => {
57
+ if (!mounted.value) return;
58
+ clearTimeout(timer);
59
+ timer = setTimeout(() => void fetchPage(1), debounceMs);
60
+ });
61
+ onBeforeUnmount(() => clearTimeout(timer));
62
+ return {
63
+ items,
64
+ total,
65
+ error,
66
+ /** True for the first page too, including SSR and hydration. */
67
+ loading: computed(() => loading.value || !mounted.value),
68
+ hasMore,
69
+ loadMore,
70
+ reload
71
+ };
72
+ }
@@ -0,0 +1,22 @@
1
+ import type { MediaUsage } from '../components/MLMedia/UsageList.vue.js';
2
+ import type { MediaEditorResult } from '../components/MLMedia/Editor.vue.js';
3
+ export type { MediaEditorResult };
4
+ export interface MediaEditorOpenOptions {
5
+ /** The record's files, in order. Enables previous / next. */
6
+ siblings?: readonly (string | null | undefined)[];
7
+ /** Current alt text, per asset ID, or one string for the opened file. */
8
+ alt?: Readonly<Record<string, string>> | string | null;
9
+ /** Preview only: no alt editing, no Save. */
10
+ readonly?: boolean;
11
+ /** Shows a remove button with this label, e.g. "Remove from product". */
12
+ removeLabel?: string;
13
+ /** Where each file is used, per asset ID. */
14
+ usages?: Readonly<Record<string, MediaUsage[]>>;
15
+ }
16
+ /**
17
+ * Opens the media editor. Resolves with `{ action: 'save', alt }`,
18
+ * `{ action: 'remove', assetId }`, or null when cancelled. Needs <UApp>.
19
+ */
20
+ export declare function useMLMediaEditor(): {
21
+ open: (assetId: string, options?: MediaEditorOpenOptions) => Promise<MediaEditorResult | null>;
22
+ };
@@ -0,0 +1,20 @@
1
+ import { defineAsyncComponent } from "vue";
2
+ import { useOverlay } from "#imports";
3
+ const LazyEditor = defineAsyncComponent(() => import("../components/MLMedia/Editor.vue"));
4
+ export function useMLMediaEditor() {
5
+ const overlay = useOverlay();
6
+ async function open(assetId, options = {}) {
7
+ const { siblings, alt, ...rest } = options;
8
+ const altMap = typeof alt === "string" ? { [assetId]: alt } : alt ?? {};
9
+ const editor = overlay.create(LazyEditor, { destroyOnClose: true });
10
+ const instance = editor.open({
11
+ ...rest,
12
+ assetId,
13
+ siblings: (siblings ?? []).filter((id) => !!id),
14
+ alt: altMap
15
+ });
16
+ const result = await instance.result;
17
+ return result ?? null;
18
+ }
19
+ return { open };
20
+ }
@@ -0,0 +1,29 @@
1
+ import { type MaybeRefOrGetter, type Ref } from 'vue';
2
+ import { type RejectedFile } from '../utils/fileAccept.js';
3
+ import { type MediaUpload, type MediaUploadOptions } from './useMLMediaUpload.js';
4
+ export type MediaFieldState = 'empty' | 'loading' | 'uploading' | 'filled' | 'missing';
5
+ export interface UseMLMediaFieldOptions extends MediaUploadOptions {
6
+ /** Asset kinds the field takes, e.g. ['image'] or "image,video". Empty means any. */
7
+ accept?: MaybeRefOrGetter<readonly string[] | string | null | undefined>;
8
+ /** Bytes. */
9
+ maxSize?: MaybeRefOrGetter<number | null | undefined>;
10
+ /** Called once the uploaded file exists in the library (before processing finishes). */
11
+ onUploaded?: (assetId: string) => void;
12
+ }
13
+ /**
14
+ * Logic behind <MLMediaField>: one asset ID, uploads that replace it, and the
15
+ * messages for files that don't fit. Usable on its own for custom markup.
16
+ */
17
+ export declare function useMLMediaField(model: Ref<string | null | undefined>, options?: UseMLMediaFieldOptions): {
18
+ state: import("vue").ComputedRef<MediaFieldState>;
19
+ asset: import("vue").ComputedRef<import("..").AssetDetail | null>;
20
+ upload: import("vue").ShallowRef<MediaUpload | null, MediaUpload | null>;
21
+ message: Ref<string | null, string | null>;
22
+ acceptAttr: import("vue").ComputedRef<string>;
23
+ acceptLabel: import("vue").ComputedRef<string>;
24
+ onFiles: (files: File[]) => void;
25
+ onRejected: (rejected: RejectedFile[]) => void;
26
+ clear: () => void;
27
+ clearMessage: () => void;
28
+ refresh: () => Promise<void>;
29
+ };
@@ -0,0 +1,77 @@
1
+ import { computed, ref, shallowRef, toValue, watch } from "vue";
2
+ import { acceptForKinds, describeKinds } from "../utils/fileAccept.js";
3
+ import { formatBytes } from "../utils/assetFormat.js";
4
+ import { useMLMediaAssets } from "./useMLMediaAssets.js";
5
+ import { useMLMediaUpload } from "./useMLMediaUpload.js";
6
+ export function useMLMediaField(model, options = {}) {
7
+ const { accept, maxSize, onUploaded, ...uploadOptions } = options;
8
+ const library = useMLMediaAssets(() => [model.value]);
9
+ const uploader = useMLMediaUpload(uploadOptions);
10
+ const upload = shallowRef(null);
11
+ const message = ref(null);
12
+ const acceptAttr = computed(() => acceptForKinds(toValue(accept)));
13
+ const acceptLabel = computed(() => describeKinds(toValue(accept)));
14
+ const asset = computed(() => library.get(model.value));
15
+ const state = computed(() => {
16
+ const u = upload.value;
17
+ if (u && (u.phase === "uploading" || u.phase === "processing" || u.phase === "failed")) return "uploading";
18
+ if (!model.value) return "empty";
19
+ const found = library.lookup(model.value);
20
+ if (found === "loading") return "loading";
21
+ return found === "ok" ? "filled" : "missing";
22
+ });
23
+ function drop(u) {
24
+ if (u) uploader.remove(u);
25
+ if (upload.value === u) upload.value = null;
26
+ }
27
+ function onFiles(files) {
28
+ const file = files[0];
29
+ if (!file) return;
30
+ message.value = null;
31
+ drop(upload.value);
32
+ const u = uploader.start(file);
33
+ upload.value = u;
34
+ void u.created.then((id) => {
35
+ if (!id || upload.value !== u) return;
36
+ model.value = id;
37
+ onUploaded?.(id);
38
+ });
39
+ }
40
+ watch(() => upload.value?.phase, async (phase) => {
41
+ const u = upload.value;
42
+ if (!u) return;
43
+ if (phase === "ready" && u.assetId) {
44
+ await library.refresh([u.assetId]);
45
+ drop(u);
46
+ } else if (phase === "canceled") {
47
+ drop(u);
48
+ }
49
+ });
50
+ function onRejected(rejected) {
51
+ const first = rejected[0];
52
+ if (!first) return;
53
+ const name = first.file.name;
54
+ const max = toValue(maxSize);
55
+ message.value = first.reason === "type" ? `${name} can't be used here. This field takes ${acceptLabel.value}.` : first.reason === "size" ? `${name} is larger than ${max ? formatBytes(max) : "the limit"}.` : `This field holds one file. ${name} was left out.`;
56
+ }
57
+ function clear() {
58
+ drop(upload.value);
59
+ message.value = null;
60
+ model.value = null;
61
+ }
62
+ return {
63
+ state,
64
+ asset,
65
+ upload,
66
+ message,
67
+ acceptAttr,
68
+ acceptLabel,
69
+ onFiles,
70
+ onRejected,
71
+ clear,
72
+ clearMessage: () => {
73
+ message.value = null;
74
+ },
75
+ refresh: () => library.refresh()
76
+ };
77
+ }
@@ -0,0 +1,55 @@
1
+ import { type MaybeRefOrGetter, type Ref } from 'vue';
2
+ import type { AssetDetail } from '../types/asset.js';
3
+ import { type RejectedFile } from '../utils/fileAccept.js';
4
+ import { type MediaAssetLookup } from './useMLMediaAssets.js';
5
+ import { type MediaUpload, type MediaUploadOptions } from './useMLMediaUpload.js';
6
+ export interface GalleryItem {
7
+ /** Stable across the upload → asset handover, for v-for keys. */
8
+ key: string;
9
+ /** Null while the file is still uploading and has no asset yet. */
10
+ id: string | null;
11
+ asset: AssetDetail | null;
12
+ lookup: MediaAssetLookup | 'loading' | null;
13
+ upload: MediaUpload | null;
14
+ }
15
+ export interface UseMLMediaGalleryOptions extends MediaUploadOptions {
16
+ accept?: MaybeRefOrGetter<readonly string[] | string | null | undefined>;
17
+ /** Most files on the record. */
18
+ max?: MaybeRefOrGetter<number | null | undefined>;
19
+ /** Bytes. */
20
+ maxSize?: MaybeRefOrGetter<number | null | undefined>;
21
+ onUploaded?: (assetId: string) => void;
22
+ /**
23
+ * Called with a new upload's asset ID right before it joins the list, with
24
+ * the tag passed to onFiles. Lets a gallery with roles place it.
25
+ */
26
+ onAdd?: (assetId: string, tag: string | undefined) => void;
27
+ }
28
+ export interface GalleryFilesOptions {
29
+ /** Upload profile for these files instead of the default one. */
30
+ profile?: string | null;
31
+ /** Handed back to `onAdd`. */
32
+ tag?: string;
33
+ }
34
+ /**
35
+ * Logic behind <MLMediaGallery>: an ordered list of asset IDs, uploads that
36
+ * append to it, reordering and removal. Removing never deletes the file.
37
+ */
38
+ export declare function useMLMediaGallery(model: Ref<string[]>, options?: UseMLMediaGalleryOptions): {
39
+ items: import("vue").ComputedRef<GalleryItem[]>;
40
+ message: Ref<string | null, string | null>;
41
+ remaining: import("vue").ComputedRef<number>;
42
+ full: import("vue").ComputedRef<boolean>;
43
+ replaces: import("vue").ComputedRef<boolean>;
44
+ insert: (id: string, index?: number) => boolean;
45
+ acceptAttr: import("vue").ComputedRef<string>;
46
+ acceptLabel: import("vue").ComputedRef<string>;
47
+ limit: import("vue").ComputedRef<number>;
48
+ onFiles: (files: File[], opts?: GalleryFilesOptions) => void;
49
+ onRejected: (rejected: RejectedFile[]) => void;
50
+ remove: (toRemove: readonly string[]) => void;
51
+ discard: (upload: MediaUpload) => void;
52
+ move: (from: number, to: number) => void;
53
+ setFromPicker: (chosen: string[]) => void;
54
+ clearMessage: () => void;
55
+ };
@@ -0,0 +1,134 @@
1
+ import { computed, ref, toValue, watch } from "vue";
2
+ import { acceptForKinds, describeKinds } from "../utils/fileAccept.js";
3
+ import { formatBytes } from "../utils/assetFormat.js";
4
+ import { useMLMediaAssets } from "./useMLMediaAssets.js";
5
+ import { useMLMediaUpload } from "./useMLMediaUpload.js";
6
+ export function useMLMediaGallery(model, options = {}) {
7
+ const { accept, max, maxSize, onUploaded, onAdd, ...uploadOptions } = options;
8
+ const ids = computed(() => model.value ?? []);
9
+ const library = useMLMediaAssets(ids);
10
+ const uploader = useMLMediaUpload(uploadOptions);
11
+ const message = ref(null);
12
+ const limit = computed(() => toValue(max) ?? Number.POSITIVE_INFINITY);
13
+ const acceptAttr = computed(() => acceptForKinds(toValue(accept)));
14
+ const acceptLabel = computed(() => describeKinds(toValue(accept)));
15
+ const pendingUploads = computed(() => uploader.uploads.value.filter(
16
+ (u) => (u.phase === "uploading" || u.phase === "failed") && !u.assetId
17
+ ));
18
+ const replaces = computed(() => limit.value === 1);
19
+ const remaining = computed(() => replaces.value ? 1 : Math.max(0, limit.value - ids.value.length - pendingUploads.value.length));
20
+ const full = computed(() => remaining.value <= 0);
21
+ const uploadByAsset = computed(() => {
22
+ const map = /* @__PURE__ */ new Map();
23
+ for (const u of uploader.uploads.value) if (u.assetId) map.set(u.assetId, u);
24
+ return map;
25
+ });
26
+ const items = computed(() => [
27
+ ...ids.value.map((id) => {
28
+ const upload = uploadByAsset.value.get(id) ?? null;
29
+ return { key: upload?.key ?? id, id, asset: library.get(id), lookup: library.lookup(id), upload };
30
+ }),
31
+ ...pendingUploads.value.map((u) => ({ key: u.key, id: null, asset: null, lookup: null, upload: u }))
32
+ ]);
33
+ function onFiles(files, opts = {}) {
34
+ message.value = null;
35
+ if (replaces.value) {
36
+ for (const u of pendingUploads.value) discard(u);
37
+ files = files.slice(0, 1);
38
+ }
39
+ for (const file of files) {
40
+ const u = uploader.start(file, opts.profile !== void 0 ? { profile: opts.profile } : {});
41
+ void u.created.then((id) => {
42
+ if (!id) return;
43
+ if (replaces.value) {
44
+ onAdd?.(id, opts.tag);
45
+ model.value = [id];
46
+ onUploaded?.(id);
47
+ return;
48
+ }
49
+ if (ids.value.includes(id)) {
50
+ onAdd?.(id, opts.tag);
51
+ return;
52
+ }
53
+ if (ids.value.length >= limit.value) {
54
+ message.value = `${file.name} was uploaded to the library but not added: this list is full.`;
55
+ return;
56
+ }
57
+ onAdd?.(id, opts.tag);
58
+ model.value = [...ids.value, id];
59
+ onUploaded?.(id);
60
+ });
61
+ }
62
+ }
63
+ watch(() => uploader.uploads.value.map((u) => u.phase), async () => {
64
+ for (const u of uploader.uploads.value) {
65
+ if (u.phase === "ready" && u.assetId) {
66
+ await library.refresh([u.assetId]);
67
+ uploader.remove(u);
68
+ } else if (u.phase === "canceled") {
69
+ uploader.remove(u);
70
+ }
71
+ }
72
+ });
73
+ function onRejected(rejected) {
74
+ const first = rejected[0];
75
+ if (!first) return;
76
+ const more = rejected.length > 1 ? ` and ${rejected.length - 1} more` : "";
77
+ const size = toValue(maxSize);
78
+ message.value = first.reason === "type" ? `${first.file.name}${more} can't be added. This list takes ${acceptLabel.value}.` : first.reason === "size" ? `${first.file.name}${more} is larger than ${size ? formatBytes(size) : "the limit"}.` : `This list holds up to ${limit.value} files. ${first.file.name}${more} was left out.`;
79
+ }
80
+ function remove(toRemove) {
81
+ const drop = new Set(toRemove);
82
+ for (const u of uploader.uploads.value) if (u.assetId && drop.has(u.assetId)) uploader.remove(u);
83
+ model.value = ids.value.filter((id) => !drop.has(id));
84
+ }
85
+ function discard(upload) {
86
+ upload.cancel();
87
+ uploader.remove(upload);
88
+ }
89
+ function move(from, to) {
90
+ const list = [...ids.value];
91
+ if (from < 0 || from >= list.length) return;
92
+ const target = Math.max(0, Math.min(list.length - 1, to));
93
+ if (target === from) return;
94
+ const [id] = list.splice(from, 1);
95
+ list.splice(target, 0, id);
96
+ model.value = list;
97
+ }
98
+ function insert(id, index) {
99
+ if (ids.value.includes(id)) return true;
100
+ if (replaces.value) {
101
+ model.value = [id];
102
+ return true;
103
+ }
104
+ if (ids.value.length >= limit.value) return false;
105
+ const list = [...ids.value];
106
+ list.splice(index ?? list.length, 0, id);
107
+ model.value = list;
108
+ return true;
109
+ }
110
+ function setFromPicker(chosen) {
111
+ message.value = null;
112
+ model.value = chosen.slice(0, Number.isFinite(limit.value) ? limit.value : void 0);
113
+ }
114
+ return {
115
+ items,
116
+ message,
117
+ remaining,
118
+ full,
119
+ replaces,
120
+ insert,
121
+ acceptAttr,
122
+ acceptLabel,
123
+ limit,
124
+ onFiles,
125
+ onRejected,
126
+ remove,
127
+ discard,
128
+ move,
129
+ setFromPicker,
130
+ clearMessage: () => {
131
+ message.value = null;
132
+ }
133
+ };
134
+ }
@@ -0,0 +1,23 @@
1
+ import type { GrantInput, HeldGrant } from '../utils/grants/store.js';
2
+ /**
3
+ * Add read grants your services returned (`"mlg_…"` or `{ grant, expiresAt }`).
4
+ * Assets the server answered "forbidden" for are resolved again, since a
5
+ * new grant may list them. Every delivery resolve sends the live grants in `X-Media-Grants`.
6
+ */
7
+ declare function addMediaGrants(...grants: GrantInput[]): void;
8
+ declare function clearMediaGrants(): void;
9
+ /** Headers to send on a `/delivery` request. */
10
+ declare function mediaGrantHeaders(): Record<string, string>;
11
+ export { addMediaGrants, clearMediaGrants, mediaGrantHeaders };
12
+ export declare const useMLMediaGrants: () => {
13
+ grants: import("vue").Ref<{
14
+ grant: string;
15
+ expiresAt: number;
16
+ }[], HeldGrant[] | {
17
+ grant: string;
18
+ expiresAt: number;
19
+ }[]>;
20
+ addMediaGrants: typeof addMediaGrants;
21
+ clearMediaGrants: typeof clearMediaGrants;
22
+ mediaGrantHeaders: typeof mediaGrantHeaders;
23
+ };
@@ -0,0 +1,22 @@
1
+ import { ref } from "vue";
2
+ import { grantHeaders, mergeGrants } from "../utils/grants/store.js";
3
+ import { notifyMediaGrantsAdded } from "../utils/grants/listeners.js";
4
+ const held = ref([]);
5
+ function addMediaGrants(...grants) {
6
+ if (!import.meta.client) return;
7
+ held.value = mergeGrants(held.value, grants);
8
+ notifyMediaGrantsAdded();
9
+ }
10
+ function clearMediaGrants() {
11
+ held.value = [];
12
+ }
13
+ function mediaGrantHeaders() {
14
+ return import.meta.client ? grantHeaders(held.value) : {};
15
+ }
16
+ export { addMediaGrants, clearMediaGrants, mediaGrantHeaders };
17
+ export const useMLMediaGrants = () => ({
18
+ grants: held,
19
+ addMediaGrants,
20
+ clearMediaGrants,
21
+ mediaGrantHeaders
22
+ });