nuxt-media-library 0.0.63 → 0.0.65
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/dist/module.json +1 -1
- package/dist/runtime/components/MLMedia/Browser.d.vue.ts +98 -0
- package/dist/runtime/components/MLMedia/Browser.vue +521 -0
- package/dist/runtime/components/MLMedia/Browser.vue.d.ts +98 -0
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +7 -1
- package/dist/runtime/components/MLMedia/Field.vue +2 -0
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +7 -1
- package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +2 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue +2 -0
- package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +2 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +7 -1
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +34 -284
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +7 -1
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +18 -1
- package/dist/runtime/components/MLMedia/Gallery.vue +56 -7
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +18 -1
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +6 -1
- package/dist/runtime/components/MLMedia/GallerySection.vue +8 -3
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +6 -1
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +7 -0
- package/dist/runtime/components/MLMedia/PickerCard.vue +17 -1
- package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +7 -0
- package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +16 -1
- package/dist/runtime/components/MLMedia/PickerRow.vue +47 -30
- package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +16 -1
- package/dist/runtime/components/MLProcessing/StepLayout.d.vue.ts +1 -1
- package/dist/runtime/components/MLProcessing/StepLayout.vue.d.ts +1 -1
- package/dist/runtime/composables/useMLFilePicker.d.ts +6 -0
- package/dist/runtime/composables/useMLMediaBrowse.d.ts +25 -5
- package/dist/runtime/composables/useMLMediaBrowse.js +26 -0
- package/dist/runtime/utils/browserActions.d.ts +47 -0
- package/dist/runtime/utils/browserActions.js +36 -0
- package/dist/runtime/utils/galleryRoles.d.ts +7 -0
- package/dist/runtime/utils/galleryRoles.js +11 -0
- package/package.json +2 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AssetDetail } from '../../types/asset.js';
|
|
2
|
+
import { type MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
3
|
+
import { type MediaEditorResult } from '../../composables/useMLMediaEditor.js';
|
|
4
|
+
import { type MediaBrowserAction, type MediaBrowserRole } from '../../utils/browserActions.js';
|
|
5
|
+
export type { MediaBrowserAction, MediaBrowserRole };
|
|
6
|
+
type __VLS_Props = {
|
|
7
|
+
/**
|
|
8
|
+
* List only these asset IDs (resolved through `backend`'s `resolve`
|
|
9
|
+
* route, searched and filtered here) instead of the library. A pool the
|
|
10
|
+
* host owns, e.g. a course's files. null lists the library.
|
|
11
|
+
*/
|
|
12
|
+
pickFrom?: readonly string[] | null;
|
|
13
|
+
/** Backend whose `resolve` route reads the files. Defaults to the upload backend. */
|
|
14
|
+
backend?: string | null;
|
|
15
|
+
/** Kinds that can be selected. Others are listed but dimmed. Empty means any. */
|
|
16
|
+
accept?: readonly string[] | string;
|
|
17
|
+
/** 'none' shows no checkboxes (a click previews); 'single' replaces; 'multi' adds up to `max`. */
|
|
18
|
+
selection?: 'none' | 'single' | 'multi';
|
|
19
|
+
/** Most files that can be selected in 'multi'. */
|
|
20
|
+
max?: number;
|
|
21
|
+
/** IDs listed first (when not searching), e.g. the files a record already has. */
|
|
22
|
+
pinned?: readonly string[];
|
|
23
|
+
/** Upload session settings; shows the dropzone. false hides it. */
|
|
24
|
+
upload?: MediaUploadOptions | false;
|
|
25
|
+
/** Bytes, for uploads. */
|
|
26
|
+
maxSize?: number | null;
|
|
27
|
+
/**
|
|
28
|
+
* What can be done to a file, from its menu and (for several) the bar over
|
|
29
|
+
* the selection: 'preview', 'edit', 'remove', 'delete',
|
|
30
|
+
* `{ key: 'assign', roles }`, or the host's own `{ key, label, … }`.
|
|
31
|
+
*/
|
|
32
|
+
actions?: readonly MediaBrowserAction[];
|
|
33
|
+
/** Grid rows for a fixed-height body; the list scrolls inside it. */
|
|
34
|
+
bodyClass?: string;
|
|
35
|
+
};
|
|
36
|
+
type __VLS_Slots = {
|
|
37
|
+
/** Next to the search box, e.g. "Add from library". */
|
|
38
|
+
'toolbar'?: () => unknown;
|
|
39
|
+
/** Small labels on a card or row, e.g. "Used in 3 lessons". */
|
|
40
|
+
'card-badge'?: (p: {
|
|
41
|
+
asset: AssetDetail;
|
|
42
|
+
}) => unknown;
|
|
43
|
+
/** Extra controls on a card or row, next to its menu. */
|
|
44
|
+
'card-actions'?: (p: {
|
|
45
|
+
asset: AssetDetail;
|
|
46
|
+
}) => unknown;
|
|
47
|
+
/** Replaces the empty state. */
|
|
48
|
+
'empty'?: (p: {
|
|
49
|
+
filtering: boolean;
|
|
50
|
+
}) => unknown;
|
|
51
|
+
/** Under the list, outside the scroll, e.g. the picker's Done / Cancel. */
|
|
52
|
+
'footer'?: (p: {
|
|
53
|
+
selected: string[];
|
|
54
|
+
summary: string;
|
|
55
|
+
note: string | null;
|
|
56
|
+
}) => unknown;
|
|
57
|
+
};
|
|
58
|
+
type __VLS_ModelProps = {
|
|
59
|
+
/** Selected IDs, in the order they were chosen. Two-way and live. */
|
|
60
|
+
'selected'?: string[];
|
|
61
|
+
};
|
|
62
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
63
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
64
|
+
delete: (assetIds: string[]) => any;
|
|
65
|
+
remove: (assetIds: string[]) => any;
|
|
66
|
+
uploaded: (assetId: string, sessionId: string | null) => any;
|
|
67
|
+
action: (key: string, assetIds: string[], role?: string | undefined) => any;
|
|
68
|
+
edit: (assetId: string, result: MediaEditorResult | null) => any;
|
|
69
|
+
assign: (role: string, assetIds: string[]) => any;
|
|
70
|
+
"update:selected": (value: string[]) => any;
|
|
71
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
72
|
+
onDelete?: ((assetIds: string[]) => any) | undefined;
|
|
73
|
+
onRemove?: ((assetIds: string[]) => any) | undefined;
|
|
74
|
+
onUploaded?: ((assetId: string, sessionId: string | null) => any) | undefined;
|
|
75
|
+
onAction?: ((key: string, assetIds: string[], role?: string | undefined) => any) | undefined;
|
|
76
|
+
onEdit?: ((assetId: string, result: MediaEditorResult | null) => any) | undefined;
|
|
77
|
+
onAssign?: ((role: string, assetIds: string[]) => any) | undefined;
|
|
78
|
+
"onUpdate:selected"?: ((value: string[]) => any) | undefined;
|
|
79
|
+
}>, {
|
|
80
|
+
upload: MediaUploadOptions | false;
|
|
81
|
+
actions: readonly MediaBrowserAction[];
|
|
82
|
+
backend: string | null;
|
|
83
|
+
accept: readonly string[] | string;
|
|
84
|
+
max: number;
|
|
85
|
+
maxSize: number | null;
|
|
86
|
+
pickFrom: readonly string[] | null;
|
|
87
|
+
selection: "none" | "single" | "multi";
|
|
88
|
+
pinned: readonly string[];
|
|
89
|
+
bodyClass: string;
|
|
90
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
91
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
92
|
+
declare const _default: typeof __VLS_export;
|
|
93
|
+
export default _default;
|
|
94
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
95
|
+
new (): {
|
|
96
|
+
$slots: S;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -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,12 +83,13 @@ 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;
|
|
89
|
+
onPick: () => void;
|
|
83
90
|
hint: string;
|
|
84
91
|
missingAlt: boolean;
|
|
85
92
|
picker: boolean;
|
|
86
|
-
onPick: () => void;
|
|
87
93
|
editor: boolean;
|
|
88
94
|
onOpen: (assetId: string) => void;
|
|
89
95
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -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,12 +83,13 @@ 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;
|
|
89
|
+
onPick: () => void;
|
|
83
90
|
hint: string;
|
|
84
91
|
missingAlt: boolean;
|
|
85
92
|
picker: boolean;
|
|
86
|
-
onPick: () => void;
|
|
87
93
|
editor: boolean;
|
|
88
94
|
onOpen: (assetId: string) => void;
|
|
89
95
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
/** Kinds the caller takes. Others are listed but dimmed. Empty means any. */
|
|
4
4
|
accept?: readonly string[] | string;
|
|
@@ -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>;
|
|
@@ -1,306 +1,56 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref
|
|
3
|
-
import
|
|
4
|
-
import { useMLMediaBrowse } from "../../composables/useMLMediaBrowse";
|
|
5
|
-
import { useMLMediaUpload } from "../../composables/useMLMediaUpload";
|
|
6
|
-
import { useMLMediaEditor } from "../../composables/useMLMediaEditor";
|
|
7
|
-
import { assetKindMeta } from "../../utils/assetDisplay";
|
|
8
|
-
import { acceptForKinds, describeKinds } from "../../utils/fileAccept";
|
|
9
|
-
import { formatBytes } from "../../utils/assetFormat";
|
|
10
|
-
const ALL_KINDS = ["image", "video", "audio", "document", "model_3d", "archive", "font", "web_app", "image_batch", "other"];
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import MLMediaBrowser from "./Browser.vue";
|
|
11
4
|
const props = defineProps({
|
|
12
5
|
accept: { type: [Array, String], required: false, default: "" },
|
|
13
6
|
max: { type: Number, required: false, default: Number.POSITIVE_INFINITY },
|
|
14
7
|
selected: { type: Array, required: false, default: () => [] },
|
|
15
8
|
title: { type: String, required: false, default: void 0 },
|
|
16
9
|
maxSize: { type: [Number, null], required: false, default: null },
|
|
10
|
+
pickFrom: { type: [Array, null], required: false, default: null },
|
|
17
11
|
upload: { type: Object, required: false, default: () => ({}) },
|
|
18
12
|
onUploaded: { type: Function, required: false, default: void 0 },
|
|
19
13
|
bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
|
|
20
14
|
});
|
|
21
15
|
const emit = defineEmits(["done", "cancel"]);
|
|
22
|
-
const acceptKinds = computed(() => {
|
|
23
|
-
const raw = typeof props.accept === "string" ? props.accept.split(",") : [...props.accept];
|
|
24
|
-
return raw.map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
25
|
-
});
|
|
26
|
-
const isAccepted = (kind) => !acceptKinds.value.length || acceptKinds.value.includes((kind ?? "").toLowerCase());
|
|
27
|
-
const notAcceptedReason = computed(() => `Only ${describeKinds(acceptKinds.value)} can be chosen here`);
|
|
28
|
-
const search = ref("");
|
|
29
|
-
const view = ref("grid");
|
|
30
|
-
const kindFilter = ref(acceptKinds.value.length === 1 ? [acceptKinds.value[0]] : []);
|
|
31
|
-
const kindOptions = computed(
|
|
32
|
-
() => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
|
|
33
|
-
);
|
|
34
|
-
const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
|
|
35
|
-
const browse = useMLMediaBrowse(() => ({ kind: kindFilter.value[0] ?? null, query: search.value }));
|
|
36
16
|
const initial = [...props.selected];
|
|
37
17
|
const chosen = ref([...initial]);
|
|
38
|
-
const limitNote = ref(null);
|
|
39
|
-
const single = computed(() => props.max === 1);
|
|
40
18
|
const changed = computed(() => chosen.value.length !== initial.length || chosen.value.some((id, i) => id !== initial[i]));
|
|
41
|
-
function isChosen(id) {
|
|
42
|
-
return !!id && chosen.value.includes(id);
|
|
43
|
-
}
|
|
44
|
-
function choose(id) {
|
|
45
|
-
limitNote.value = null;
|
|
46
|
-
if (single.value) {
|
|
47
|
-
chosen.value = [id];
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
if (chosen.value.length >= props.max) {
|
|
51
|
-
limitNote.value = `You can choose up to ${props.max}. Unselect one first.`;
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
chosen.value = [...chosen.value, id];
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
function toggle(id) {
|
|
58
|
-
if (isChosen(id)) {
|
|
59
|
-
limitNote.value = null;
|
|
60
|
-
chosen.value = chosen.value.filter((x) => x !== id);
|
|
61
|
-
} else {
|
|
62
|
-
choose(id);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const pinned = useMLMediaAssets(() => initial, { backend: () => props.upload?.backend });
|
|
66
|
-
const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
|
|
67
|
-
const uploader = useMLMediaUpload(props.upload);
|
|
68
|
-
const rejectNote = ref(null);
|
|
69
|
-
function onFiles(files) {
|
|
70
|
-
rejectNote.value = null;
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const u = uploader.start(file);
|
|
73
|
-
void u.created.then((id) => {
|
|
74
|
-
if (!id) return;
|
|
75
|
-
u.attached = true;
|
|
76
|
-
props.onUploaded?.(id, u.sessionId);
|
|
77
|
-
if (!isChosen(id)) choose(id);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function onRejected(rejected) {
|
|
82
|
-
const first = rejected[0];
|
|
83
|
-
if (!first) return;
|
|
84
|
-
const more = rejected.length > 1 ? ` (and ${rejected.length - 1} more)` : "";
|
|
85
|
-
rejectNote.value = first.reason === "type" ? `${first.file.name}${more} can't be used here. Only ${describeKinds(acceptKinds.value)}.` : `${first.file.name}${more} is larger than ${props.maxSize ? formatBytes(props.maxSize) : "the limit"}.`;
|
|
86
|
-
}
|
|
87
|
-
const uploadIds = computed(() => new Set(uploader.uploads.value.map((u) => u.assetId).filter(Boolean)));
|
|
88
|
-
const visibleUploads = computed(() => uploader.uploads.value.filter((u) => u.phase !== "canceled"));
|
|
89
|
-
function uploadPhase(u) {
|
|
90
|
-
return u.phase === "uploading" || u.phase === "processing" || u.phase === "failed" ? u.phase : null;
|
|
91
|
-
}
|
|
92
|
-
const listed = computed(() => {
|
|
93
|
-
const pinnedIds = new Set(filtering.value ? [] : pinnedAssets.value.map((a) => a.id));
|
|
94
|
-
const rest = browse.items.value.filter((a) => !pinnedIds.has(a.id) && !uploadIds.value.has(a.id));
|
|
95
|
-
return filtering.value ? rest : [...pinnedAssets.value, ...rest];
|
|
96
|
-
});
|
|
97
|
-
const initialLoading = computed(() => browse.loading.value && !browse.items.value.length);
|
|
98
|
-
const isEmpty = computed(() => !initialLoading.value && !listed.value.length && !visibleUploads.value.length);
|
|
99
|
-
watch(kindFilter, () => {
|
|
100
|
-
limitNote.value = null;
|
|
101
|
-
});
|
|
102
|
-
const mediaEditor = useMLMediaEditor();
|
|
103
|
-
function preview(id) {
|
|
104
|
-
void mediaEditor.open(id, { readonly: true, siblings: listed.value.map((a) => a.id) });
|
|
105
|
-
}
|
|
106
|
-
const footerText = computed(() => {
|
|
107
|
-
if (limitNote.value) return limitNote.value;
|
|
108
|
-
const n = chosen.value.length;
|
|
109
|
-
if (single.value) return n ? "1 selected" : "Nothing selected";
|
|
110
|
-
return Number.isFinite(props.max) ? `${n} of ${props.max} selected` : `${n} selected`;
|
|
111
|
-
});
|
|
112
19
|
</script>
|
|
113
20
|
|
|
114
21
|
<template>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
22
|
+
<MLMediaBrowser
|
|
23
|
+
v-model:selected="chosen"
|
|
24
|
+
:pick-from="pickFrom"
|
|
25
|
+
:accept="accept"
|
|
26
|
+
:selection="max === 1 ? 'single' : 'multi'"
|
|
27
|
+
:max="max"
|
|
28
|
+
:pinned="initial"
|
|
29
|
+
:upload="upload"
|
|
30
|
+
:max-size="maxSize"
|
|
31
|
+
:actions="['preview']"
|
|
32
|
+
:body-class="bodyClass"
|
|
33
|
+
@uploaded="(id, sessionId) => props.onUploaded?.(id, sessionId)"
|
|
34
|
+
>
|
|
35
|
+
<template #footer="{ summary, note }">
|
|
36
|
+
<div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
|
|
37
|
+
<span
|
|
38
|
+
class="me-auto text-sm"
|
|
39
|
+
:class="note ? 'text-warning' : 'text-muted'"
|
|
40
|
+
aria-live="polite"
|
|
41
|
+
>{{ summary }}</span>
|
|
42
|
+
<UButton
|
|
43
|
+
color="neutral"
|
|
44
|
+
variant="outline"
|
|
45
|
+
label="Cancel"
|
|
46
|
+
@click="emit('cancel')"
|
|
125
47
|
/>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
:variant="view === 'grid' ? 'soft' : 'outline'"
|
|
131
|
-
aria-label="Grid view"
|
|
132
|
-
:aria-pressed="view === 'grid'"
|
|
133
|
-
@click="view = 'grid'"
|
|
134
|
-
/>
|
|
135
|
-
<UButton
|
|
136
|
-
icon="i-lucide-list"
|
|
137
|
-
color="neutral"
|
|
138
|
-
:variant="view === 'list' ? 'soft' : 'outline'"
|
|
139
|
-
aria-label="List view"
|
|
140
|
-
:aria-pressed="view === 'list'"
|
|
141
|
-
@click="view = 'list'"
|
|
142
|
-
/>
|
|
143
|
-
</UFieldGroup>
|
|
144
|
-
</div>
|
|
145
|
-
<div
|
|
146
|
-
v-if="kindOptions.length > 1"
|
|
147
|
-
class="flex flex-wrap gap-1.5"
|
|
148
|
-
>
|
|
149
|
-
<MLMediaFilterPill
|
|
150
|
-
v-model="kindFilter"
|
|
151
|
-
label="File type"
|
|
152
|
-
:options="kindOptions"
|
|
153
|
-
single
|
|
48
|
+
<UButton
|
|
49
|
+
label="Done"
|
|
50
|
+
:disabled="!changed"
|
|
51
|
+
@click="emit('done', [...chosen])"
|
|
154
52
|
/>
|
|
155
53
|
</div>
|
|
156
|
-
</
|
|
157
|
-
|
|
158
|
-
<div
|
|
159
|
-
class="flex min-h-0 flex-col gap-3 overflow-y-auto p-4"
|
|
160
|
-
:class="bodyClass"
|
|
161
|
-
>
|
|
162
|
-
<MLMediaDropzone
|
|
163
|
-
:accept="acceptForKinds(acceptKinds)"
|
|
164
|
-
:multiple="!single"
|
|
165
|
-
:max-size="maxSize"
|
|
166
|
-
compact
|
|
167
|
-
:title="single ? 'Drop a file to add it to the library' : 'Drop files to add them to the library'"
|
|
168
|
-
button-label="Upload from computer"
|
|
169
|
-
@files="onFiles"
|
|
170
|
-
@rejected="onRejected"
|
|
171
|
-
/>
|
|
172
|
-
<p
|
|
173
|
-
v-if="rejectNote"
|
|
174
|
-
class="text-xs text-error"
|
|
175
|
-
>
|
|
176
|
-
{{ rejectNote }}
|
|
177
|
-
</p>
|
|
178
|
-
|
|
179
|
-
<UAlert
|
|
180
|
-
v-if="browse.error.value"
|
|
181
|
-
color="error"
|
|
182
|
-
variant="subtle"
|
|
183
|
-
icon="i-lucide-circle-alert"
|
|
184
|
-
:title="browse.error.value"
|
|
185
|
-
:actions="[{ label: 'Try again', color: 'neutral', variant: 'outline', onClick: () => browse.reload() }]"
|
|
186
|
-
/>
|
|
187
|
-
|
|
188
|
-
<!-- Loading -->
|
|
189
|
-
<div
|
|
190
|
-
v-if="initialLoading"
|
|
191
|
-
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
192
|
-
>
|
|
193
|
-
<div
|
|
194
|
-
v-for="i in 12"
|
|
195
|
-
:key="i"
|
|
196
|
-
class="flex flex-col items-center gap-2 p-2.5"
|
|
197
|
-
>
|
|
198
|
-
<USkeleton class="aspect-square w-full max-w-28 rounded-md" />
|
|
199
|
-
<USkeleton class="h-3 w-3/4" />
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
<!-- Empty -->
|
|
204
|
-
<UEmpty
|
|
205
|
-
v-else-if="isEmpty && !browse.error.value"
|
|
206
|
-
variant="naked"
|
|
207
|
-
:icon="filtering ? 'i-lucide-search-x' : 'i-lucide-folder-open'"
|
|
208
|
-
:title="filtering ? 'No files match' : 'No files yet'"
|
|
209
|
-
:description="filtering ? 'Try another search or clear the file type.' : 'Drop files above to add the first one.'"
|
|
210
|
-
/>
|
|
211
|
-
|
|
212
|
-
<!-- Grid -->
|
|
213
|
-
<div
|
|
214
|
-
v-else-if="view === 'grid'"
|
|
215
|
-
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
216
|
-
>
|
|
217
|
-
<MLMediaPickerCard
|
|
218
|
-
v-for="u in visibleUploads"
|
|
219
|
-
:key="u.key"
|
|
220
|
-
:src="u.localPreview"
|
|
221
|
-
:kind="u.kind"
|
|
222
|
-
:name="u.file.name"
|
|
223
|
-
:meta="u.phase === 'failed' ? u.error ?? 'Failed' : void 0"
|
|
224
|
-
:selected="isChosen(u.assetId)"
|
|
225
|
-
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
226
|
-
@toggle="u.assetId && toggle(u.assetId)"
|
|
227
|
-
>
|
|
228
|
-
<MLMediaProgressVeil
|
|
229
|
-
v-if="uploadPhase(u)"
|
|
230
|
-
:phase="uploadPhase(u)"
|
|
231
|
-
:progress="u.progress"
|
|
232
|
-
compact
|
|
233
|
-
/>
|
|
234
|
-
</MLMediaPickerCard>
|
|
235
|
-
<MLMediaPickerCard
|
|
236
|
-
v-for="a in listed"
|
|
237
|
-
:key="a.id"
|
|
238
|
-
:asset="a"
|
|
239
|
-
:selected="isChosen(a.id)"
|
|
240
|
-
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
241
|
-
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
242
|
-
:on-preview="() => preview(a.id)"
|
|
243
|
-
@toggle="toggle(a.id)"
|
|
244
|
-
/>
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
<!-- List -->
|
|
248
|
-
<div
|
|
249
|
-
v-else
|
|
250
|
-
class="flex flex-col divide-y divide-default overflow-hidden rounded-lg border border-default"
|
|
251
|
-
>
|
|
252
|
-
<MLMediaPickerRow
|
|
253
|
-
v-for="u in visibleUploads"
|
|
254
|
-
:key="u.key"
|
|
255
|
-
:src="u.localPreview"
|
|
256
|
-
:kind="u.kind"
|
|
257
|
-
:name="u.file.name"
|
|
258
|
-
:size="u.phase === 'uploading' ? `${Math.round(u.progress ?? 0)}%` : formatBytes(u.file.size)"
|
|
259
|
-
:date="u.phase === 'ready' ? 'Just now' : u.phase === 'failed' ? 'Failed' : 'Uploading'"
|
|
260
|
-
:selected="isChosen(u.assetId)"
|
|
261
|
-
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
262
|
-
@toggle="u.assetId && toggle(u.assetId)"
|
|
263
|
-
/>
|
|
264
|
-
<MLMediaPickerRow
|
|
265
|
-
v-for="a in listed"
|
|
266
|
-
:key="a.id"
|
|
267
|
-
:asset="a"
|
|
268
|
-
:selected="isChosen(a.id)"
|
|
269
|
-
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
270
|
-
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
271
|
-
@toggle="toggle(a.id)"
|
|
272
|
-
/>
|
|
273
|
-
</div>
|
|
274
|
-
|
|
275
|
-
<UButton
|
|
276
|
-
v-if="browse.hasMore.value && !initialLoading"
|
|
277
|
-
class="self-center"
|
|
278
|
-
color="neutral"
|
|
279
|
-
variant="outline"
|
|
280
|
-
size="sm"
|
|
281
|
-
label="Load more"
|
|
282
|
-
:loading="browse.loading.value"
|
|
283
|
-
@click="browse.loadMore()"
|
|
284
|
-
/>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
|
|
288
|
-
<span
|
|
289
|
-
class="me-auto text-sm"
|
|
290
|
-
:class="limitNote ? 'text-warning' : 'text-muted'"
|
|
291
|
-
aria-live="polite"
|
|
292
|
-
>{{ footerText }}</span>
|
|
293
|
-
<UButton
|
|
294
|
-
color="neutral"
|
|
295
|
-
variant="outline"
|
|
296
|
-
label="Cancel"
|
|
297
|
-
@click="emit('cancel')"
|
|
298
|
-
/>
|
|
299
|
-
<UButton
|
|
300
|
-
label="Done"
|
|
301
|
-
:disabled="!changed"
|
|
302
|
-
@click="emit('done', [...chosen])"
|
|
303
|
-
/>
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
54
|
+
</template>
|
|
55
|
+
</MLMediaBrowser>
|
|
306
56
|
</template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MediaUploadOptions } from '../../composables/useMLMediaUpload.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
/** Kinds the caller takes. Others are listed but dimmed. Empty means any. */
|
|
4
4
|
accept?: readonly string[] | string;
|
|
@@ -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,9 +107,10 @@ 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;
|
|
111
|
+
onPick: () => void;
|
|
94
112
|
hint: string;
|
|
95
113
|
picker: boolean;
|
|
96
|
-
onPick: () => void;
|
|
97
114
|
editor: boolean;
|
|
98
115
|
onOpen: (assetId: string) => void;
|
|
99
116
|
featured: boolean;
|