nuxt-media-library 0.0.64 → 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 +1 -1
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +33 -287
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +1 -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/utils/browserActions.d.ts +47 -0
- package/dist/runtime/utils/browserActions.js +36 -0
- package/package.json +1 -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
|
+
};
|
|
@@ -86,10 +86,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
86
86
|
pickFrom: readonly string[] | null;
|
|
87
87
|
required: boolean;
|
|
88
88
|
removeLabel: string;
|
|
89
|
+
onPick: () => void;
|
|
89
90
|
hint: string;
|
|
90
91
|
missingAlt: boolean;
|
|
91
92
|
picker: boolean;
|
|
92
|
-
onPick: () => void;
|
|
93
93
|
editor: boolean;
|
|
94
94
|
onOpen: (assetId: string) => void;
|
|
95
95
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -86,10 +86,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
86
86
|
pickFrom: readonly string[] | null;
|
|
87
87
|
required: boolean;
|
|
88
88
|
removeLabel: string;
|
|
89
|
+
onPick: () => void;
|
|
89
90
|
hint: string;
|
|
90
91
|
missingAlt: boolean;
|
|
91
92
|
picker: boolean;
|
|
92
|
-
onPick: () => void;
|
|
93
93
|
editor: boolean;
|
|
94
94
|
onOpen: (assetId: string) => void;
|
|
95
95
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -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;
|
|
@@ -1,13 +1,6 @@
|
|
|
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 },
|
|
@@ -20,291 +13,44 @@ const props = defineProps({
|
|
|
20
13
|
bodyClass: { type: String, required: false, default: "max-h-[min(60vh,560px)]" }
|
|
21
14
|
});
|
|
22
15
|
const emit = defineEmits(["done", "cancel"]);
|
|
23
|
-
const acceptKinds = computed(() => {
|
|
24
|
-
const raw = typeof props.accept === "string" ? props.accept.split(",") : [...props.accept];
|
|
25
|
-
return raw.map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
26
|
-
});
|
|
27
|
-
const isAccepted = (kind) => !acceptKinds.value.length || acceptKinds.value.includes((kind ?? "").toLowerCase());
|
|
28
|
-
const notAcceptedReason = computed(() => `Only ${describeKinds(acceptKinds.value)} can be chosen here`);
|
|
29
|
-
const search = ref("");
|
|
30
|
-
const view = ref("grid");
|
|
31
|
-
const kindFilter = ref(acceptKinds.value.length === 1 ? [acceptKinds.value[0]] : []);
|
|
32
|
-
const kindOptions = computed(
|
|
33
|
-
() => (acceptKinds.value.length ? acceptKinds.value : ALL_KINDS).map((k) => ({ label: assetKindMeta(k).label, value: k }))
|
|
34
|
-
);
|
|
35
|
-
const filtering = computed(() => !!search.value.trim() || kindFilter.value.length > 0);
|
|
36
|
-
const browse = useMLMediaBrowse(
|
|
37
|
-
() => ({ kind: kindFilter.value[0] ?? null, query: search.value }),
|
|
38
|
-
props.pickFrom ? { ids: () => props.pickFrom ?? [], backend: () => props.upload?.backend } : {}
|
|
39
|
-
);
|
|
40
16
|
const initial = [...props.selected];
|
|
41
17
|
const chosen = ref([...initial]);
|
|
42
|
-
const limitNote = ref(null);
|
|
43
|
-
const single = computed(() => props.max === 1);
|
|
44
18
|
const changed = computed(() => chosen.value.length !== initial.length || chosen.value.some((id, i) => id !== initial[i]));
|
|
45
|
-
function isChosen(id) {
|
|
46
|
-
return !!id && chosen.value.includes(id);
|
|
47
|
-
}
|
|
48
|
-
function choose(id) {
|
|
49
|
-
limitNote.value = null;
|
|
50
|
-
if (single.value) {
|
|
51
|
-
chosen.value = [id];
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
if (chosen.value.length >= props.max) {
|
|
55
|
-
limitNote.value = `You can choose up to ${props.max}. Unselect one first.`;
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
chosen.value = [...chosen.value, id];
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
function toggle(id) {
|
|
62
|
-
if (isChosen(id)) {
|
|
63
|
-
limitNote.value = null;
|
|
64
|
-
chosen.value = chosen.value.filter((x) => x !== id);
|
|
65
|
-
} else {
|
|
66
|
-
choose(id);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const pinned = useMLMediaAssets(() => initial, { backend: () => props.upload?.backend });
|
|
70
|
-
const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
|
|
71
|
-
const uploader = useMLMediaUpload(props.upload);
|
|
72
|
-
const rejectNote = ref(null);
|
|
73
|
-
function onFiles(files) {
|
|
74
|
-
rejectNote.value = null;
|
|
75
|
-
for (const file of files) {
|
|
76
|
-
const u = uploader.start(file);
|
|
77
|
-
void u.created.then((id) => {
|
|
78
|
-
if (!id) return;
|
|
79
|
-
u.attached = true;
|
|
80
|
-
props.onUploaded?.(id, u.sessionId);
|
|
81
|
-
if (!isChosen(id)) choose(id);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function onRejected(rejected) {
|
|
86
|
-
const first = rejected[0];
|
|
87
|
-
if (!first) return;
|
|
88
|
-
const more = rejected.length > 1 ? ` (and ${rejected.length - 1} more)` : "";
|
|
89
|
-
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"}.`;
|
|
90
|
-
}
|
|
91
|
-
const uploadIds = computed(() => new Set(uploader.uploads.value.map((u) => u.assetId).filter(Boolean)));
|
|
92
|
-
const visibleUploads = computed(() => uploader.uploads.value.filter((u) => u.phase !== "canceled"));
|
|
93
|
-
function uploadPhase(u) {
|
|
94
|
-
return u.phase === "uploading" || u.phase === "processing" || u.phase === "failed" ? u.phase : null;
|
|
95
|
-
}
|
|
96
|
-
const listed = computed(() => {
|
|
97
|
-
const pinnedIds = new Set(filtering.value ? [] : pinnedAssets.value.map((a) => a.id));
|
|
98
|
-
const rest = browse.items.value.filter((a) => !pinnedIds.has(a.id) && !uploadIds.value.has(a.id));
|
|
99
|
-
return filtering.value ? rest : [...pinnedAssets.value, ...rest];
|
|
100
|
-
});
|
|
101
|
-
const initialLoading = computed(() => browse.loading.value && !browse.items.value.length);
|
|
102
|
-
const isEmpty = computed(() => !initialLoading.value && !listed.value.length && !visibleUploads.value.length);
|
|
103
|
-
watch(kindFilter, () => {
|
|
104
|
-
limitNote.value = null;
|
|
105
|
-
});
|
|
106
|
-
const mediaEditor = useMLMediaEditor();
|
|
107
|
-
function preview(id) {
|
|
108
|
-
void mediaEditor.open(id, { readonly: true, siblings: listed.value.map((a) => a.id) });
|
|
109
|
-
}
|
|
110
|
-
const footerText = computed(() => {
|
|
111
|
-
if (limitNote.value) return limitNote.value;
|
|
112
|
-
const n = chosen.value.length;
|
|
113
|
-
if (single.value) return n ? "1 selected" : "Nothing selected";
|
|
114
|
-
return Number.isFinite(props.max) ? `${n} of ${props.max} selected` : `${n} selected`;
|
|
115
|
-
});
|
|
116
19
|
</script>
|
|
117
20
|
|
|
118
21
|
<template>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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')"
|
|
129
47
|
/>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
:variant="view === 'grid' ? 'soft' : 'outline'"
|
|
135
|
-
aria-label="Grid view"
|
|
136
|
-
:aria-pressed="view === 'grid'"
|
|
137
|
-
@click="view = 'grid'"
|
|
138
|
-
/>
|
|
139
|
-
<UButton
|
|
140
|
-
icon="i-lucide-list"
|
|
141
|
-
color="neutral"
|
|
142
|
-
:variant="view === 'list' ? 'soft' : 'outline'"
|
|
143
|
-
aria-label="List view"
|
|
144
|
-
:aria-pressed="view === 'list'"
|
|
145
|
-
@click="view = 'list'"
|
|
146
|
-
/>
|
|
147
|
-
</UFieldGroup>
|
|
148
|
-
</div>
|
|
149
|
-
<div
|
|
150
|
-
v-if="kindOptions.length > 1"
|
|
151
|
-
class="flex flex-wrap gap-1.5"
|
|
152
|
-
>
|
|
153
|
-
<MLMediaFilterPill
|
|
154
|
-
v-model="kindFilter"
|
|
155
|
-
label="File type"
|
|
156
|
-
:options="kindOptions"
|
|
157
|
-
single
|
|
48
|
+
<UButton
|
|
49
|
+
label="Done"
|
|
50
|
+
:disabled="!changed"
|
|
51
|
+
@click="emit('done', [...chosen])"
|
|
158
52
|
/>
|
|
159
53
|
</div>
|
|
160
|
-
</
|
|
161
|
-
|
|
162
|
-
<div
|
|
163
|
-
class="flex min-h-0 flex-col gap-3 overflow-y-auto p-4"
|
|
164
|
-
:class="bodyClass"
|
|
165
|
-
>
|
|
166
|
-
<MLMediaDropzone
|
|
167
|
-
:accept="acceptForKinds(acceptKinds)"
|
|
168
|
-
:multiple="!single"
|
|
169
|
-
:max-size="maxSize"
|
|
170
|
-
compact
|
|
171
|
-
:title="single ? 'Drop a file to add it to the library' : 'Drop files to add them to the library'"
|
|
172
|
-
button-label="Upload from computer"
|
|
173
|
-
@files="onFiles"
|
|
174
|
-
@rejected="onRejected"
|
|
175
|
-
/>
|
|
176
|
-
<p
|
|
177
|
-
v-if="rejectNote"
|
|
178
|
-
class="text-xs text-error"
|
|
179
|
-
>
|
|
180
|
-
{{ rejectNote }}
|
|
181
|
-
</p>
|
|
182
|
-
|
|
183
|
-
<UAlert
|
|
184
|
-
v-if="browse.error.value"
|
|
185
|
-
color="error"
|
|
186
|
-
variant="subtle"
|
|
187
|
-
icon="i-lucide-circle-alert"
|
|
188
|
-
:title="browse.error.value"
|
|
189
|
-
:actions="[{ label: 'Try again', color: 'neutral', variant: 'outline', onClick: () => browse.reload() }]"
|
|
190
|
-
/>
|
|
191
|
-
|
|
192
|
-
<!-- Loading -->
|
|
193
|
-
<div
|
|
194
|
-
v-if="initialLoading"
|
|
195
|
-
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
196
|
-
>
|
|
197
|
-
<div
|
|
198
|
-
v-for="i in 12"
|
|
199
|
-
:key="i"
|
|
200
|
-
class="flex flex-col items-center gap-2 p-2.5"
|
|
201
|
-
>
|
|
202
|
-
<USkeleton class="aspect-square w-full max-w-28 rounded-md" />
|
|
203
|
-
<USkeleton class="h-3 w-3/4" />
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<!-- Empty -->
|
|
208
|
-
<UEmpty
|
|
209
|
-
v-else-if="isEmpty && !browse.error.value"
|
|
210
|
-
variant="naked"
|
|
211
|
-
:icon="filtering ? 'i-lucide-search-x' : 'i-lucide-folder-open'"
|
|
212
|
-
:title="filtering ? 'No files match' : 'No files yet'"
|
|
213
|
-
:description="filtering ? 'Try another search or clear the file type.' : 'Drop files above to add the first one.'"
|
|
214
|
-
/>
|
|
215
|
-
|
|
216
|
-
<!-- Grid -->
|
|
217
|
-
<div
|
|
218
|
-
v-else-if="view === 'grid'"
|
|
219
|
-
class="grid grid-cols-[repeat(auto-fill,minmax(8rem,1fr))] gap-1.5"
|
|
220
|
-
>
|
|
221
|
-
<MLMediaPickerCard
|
|
222
|
-
v-for="u in visibleUploads"
|
|
223
|
-
:key="u.key"
|
|
224
|
-
:src="u.localPreview"
|
|
225
|
-
:kind="u.kind"
|
|
226
|
-
:name="u.file.name"
|
|
227
|
-
:meta="u.phase === 'failed' ? u.error ?? 'Failed' : void 0"
|
|
228
|
-
:selected="isChosen(u.assetId)"
|
|
229
|
-
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
230
|
-
@toggle="u.assetId && toggle(u.assetId)"
|
|
231
|
-
>
|
|
232
|
-
<MLMediaProgressVeil
|
|
233
|
-
v-if="uploadPhase(u)"
|
|
234
|
-
:phase="uploadPhase(u)"
|
|
235
|
-
:progress="u.progress"
|
|
236
|
-
compact
|
|
237
|
-
/>
|
|
238
|
-
</MLMediaPickerCard>
|
|
239
|
-
<MLMediaPickerCard
|
|
240
|
-
v-for="a in listed"
|
|
241
|
-
:key="a.id"
|
|
242
|
-
:asset="a"
|
|
243
|
-
:selected="isChosen(a.id)"
|
|
244
|
-
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
245
|
-
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
246
|
-
:on-preview="() => preview(a.id)"
|
|
247
|
-
@toggle="toggle(a.id)"
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
<!-- List -->
|
|
252
|
-
<div
|
|
253
|
-
v-else
|
|
254
|
-
class="flex flex-col divide-y divide-default overflow-hidden rounded-lg border border-default"
|
|
255
|
-
>
|
|
256
|
-
<MLMediaPickerRow
|
|
257
|
-
v-for="u in visibleUploads"
|
|
258
|
-
:key="u.key"
|
|
259
|
-
:src="u.localPreview"
|
|
260
|
-
:kind="u.kind"
|
|
261
|
-
:name="u.file.name"
|
|
262
|
-
:size="u.phase === 'uploading' ? `${Math.round(u.progress ?? 0)}%` : formatBytes(u.file.size)"
|
|
263
|
-
:date="u.phase === 'ready' ? 'Just now' : u.phase === 'failed' ? 'Failed' : 'Uploading'"
|
|
264
|
-
:selected="isChosen(u.assetId)"
|
|
265
|
-
:disabled="!u.assetId || !isAccepted(u.kind)"
|
|
266
|
-
@toggle="u.assetId && toggle(u.assetId)"
|
|
267
|
-
/>
|
|
268
|
-
<MLMediaPickerRow
|
|
269
|
-
v-for="a in listed"
|
|
270
|
-
:key="a.id"
|
|
271
|
-
:asset="a"
|
|
272
|
-
:selected="isChosen(a.id)"
|
|
273
|
-
:disabled="!isAccepted(a.kind) || a.status === 'failed'"
|
|
274
|
-
:disabled-reason="a.status === 'failed' ? 'Processing failed for this file' : notAcceptedReason"
|
|
275
|
-
@toggle="toggle(a.id)"
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
278
|
-
|
|
279
|
-
<UButton
|
|
280
|
-
v-if="browse.hasMore.value && !initialLoading"
|
|
281
|
-
class="self-center"
|
|
282
|
-
color="neutral"
|
|
283
|
-
variant="outline"
|
|
284
|
-
size="sm"
|
|
285
|
-
label="Load more"
|
|
286
|
-
:loading="browse.loading.value"
|
|
287
|
-
@click="browse.loadMore()"
|
|
288
|
-
/>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
<div class="flex flex-wrap items-center gap-2 border-t border-default px-4 py-3">
|
|
292
|
-
<span
|
|
293
|
-
class="me-auto text-sm"
|
|
294
|
-
:class="limitNote ? 'text-warning' : 'text-muted'"
|
|
295
|
-
aria-live="polite"
|
|
296
|
-
>{{ footerText }}</span>
|
|
297
|
-
<UButton
|
|
298
|
-
color="neutral"
|
|
299
|
-
variant="outline"
|
|
300
|
-
label="Cancel"
|
|
301
|
-
@click="emit('cancel')"
|
|
302
|
-
/>
|
|
303
|
-
<UButton
|
|
304
|
-
label="Done"
|
|
305
|
-
:disabled="!changed"
|
|
306
|
-
@click="emit('done', [...chosen])"
|
|
307
|
-
/>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
54
|
+
</template>
|
|
55
|
+
</MLMediaBrowser>
|
|
310
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;
|
|
@@ -108,9 +108,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
108
108
|
max: number | null;
|
|
109
109
|
maxSize: number | null;
|
|
110
110
|
pickFrom: readonly string[] | null;
|
|
111
|
+
onPick: () => void;
|
|
111
112
|
hint: string;
|
|
112
113
|
picker: boolean;
|
|
113
|
-
onPick: () => void;
|
|
114
114
|
editor: boolean;
|
|
115
115
|
onOpen: (assetId: string) => void;
|
|
116
116
|
featured: boolean;
|
|
@@ -108,9 +108,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
108
108
|
max: number | null;
|
|
109
109
|
maxSize: number | null;
|
|
110
110
|
pickFrom: readonly string[] | null;
|
|
111
|
+
onPick: () => void;
|
|
111
112
|
hint: string;
|
|
112
113
|
picker: boolean;
|
|
113
|
-
onPick: () => void;
|
|
114
114
|
editor: boolean;
|
|
115
115
|
onOpen: (assetId: string) => void;
|
|
116
116
|
featured: boolean;
|
|
@@ -104,9 +104,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
104
104
|
pickFrom: readonly string[] | null;
|
|
105
105
|
removeLabel: string;
|
|
106
106
|
onAdd: (assetId: string, tag: string | undefined) => void;
|
|
107
|
+
onPick: () => void;
|
|
107
108
|
hint: string;
|
|
108
109
|
picker: boolean;
|
|
109
|
-
onPick: () => void;
|
|
110
110
|
editor: boolean;
|
|
111
111
|
onOpen: (assetId: string) => void;
|
|
112
112
|
featured: boolean;
|
|
@@ -104,9 +104,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
104
104
|
pickFrom: readonly string[] | null;
|
|
105
105
|
removeLabel: string;
|
|
106
106
|
onAdd: (assetId: string, tag: string | undefined) => void;
|
|
107
|
+
onPick: () => void;
|
|
107
108
|
hint: string;
|
|
108
109
|
picker: boolean;
|
|
109
|
-
onPick: () => void;
|
|
110
110
|
editor: boolean;
|
|
111
111
|
onOpen: (assetId: string) => void;
|
|
112
112
|
featured: boolean;
|
|
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
46
46
|
disabled: boolean;
|
|
47
47
|
phase: ProgressVeilPhase | null;
|
|
48
48
|
selected: boolean;
|
|
49
|
+
selectable: boolean;
|
|
49
50
|
missingAlt: boolean;
|
|
50
51
|
onOpen: () => void;
|
|
51
|
-
selectable: boolean;
|
|
52
52
|
selecting: boolean;
|
|
53
53
|
phaseDetail: string;
|
|
54
54
|
aspect: "square" | "landscape" | "portrait" | "video";
|
|
@@ -46,9 +46,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
46
46
|
disabled: boolean;
|
|
47
47
|
phase: ProgressVeilPhase | null;
|
|
48
48
|
selected: boolean;
|
|
49
|
+
selectable: boolean;
|
|
49
50
|
missingAlt: boolean;
|
|
50
51
|
onOpen: () => void;
|
|
51
|
-
selectable: boolean;
|
|
52
52
|
selecting: boolean;
|
|
53
53
|
phaseDetail: string;
|
|
54
54
|
aspect: "square" | "landscape" | "portrait" | "video";
|
|
@@ -7,6 +7,8 @@ type __VLS_Props = {
|
|
|
7
7
|
/** Second line. Defaults to the file extension. */
|
|
8
8
|
meta?: string;
|
|
9
9
|
selected?: boolean;
|
|
10
|
+
/** Shows the checkbox. False for a list without selection. */
|
|
11
|
+
selectable?: boolean;
|
|
10
12
|
/** Dimmed and not selectable, e.g. a kind the calling field doesn't accept. */
|
|
11
13
|
disabled?: boolean;
|
|
12
14
|
disabledReason?: string;
|
|
@@ -16,6 +18,10 @@ type __VLS_Props = {
|
|
|
16
18
|
type __VLS_Slots = {
|
|
17
19
|
/** Overlays on the thumbnail, e.g. an upload veil. */
|
|
18
20
|
default?: () => unknown;
|
|
21
|
+
/** Small labels over the top of the card, e.g. "Used in 3 lessons". */
|
|
22
|
+
badge?: () => unknown;
|
|
23
|
+
/** Controls in the top-right corner, e.g. a menu. Usable when the card is disabled. */
|
|
24
|
+
actions?: () => unknown;
|
|
19
25
|
};
|
|
20
26
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
21
27
|
toggle: () => any;
|
|
@@ -29,6 +35,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
29
35
|
disabled: boolean;
|
|
30
36
|
meta: string;
|
|
31
37
|
selected: boolean;
|
|
38
|
+
selectable: boolean;
|
|
32
39
|
disabledReason: string;
|
|
33
40
|
onPreview: () => void;
|
|
34
41
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -8,6 +8,7 @@ const props = defineProps({
|
|
|
8
8
|
name: { type: String, required: false, default: void 0 },
|
|
9
9
|
meta: { type: String, required: false, default: void 0 },
|
|
10
10
|
selected: { type: Boolean, required: false, default: false },
|
|
11
|
+
selectable: { type: Boolean, required: false, default: true },
|
|
11
12
|
disabled: { type: Boolean, required: false, default: false },
|
|
12
13
|
disabledReason: { type: String, required: false, default: void 0 },
|
|
13
14
|
onPreview: { type: Function, required: false, default: void 0 }
|
|
@@ -30,7 +31,7 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
|
|
|
30
31
|
type="button"
|
|
31
32
|
class="flex w-full flex-col items-center gap-2 rounded-lg px-2.5 pb-2.5 pt-3 text-center outline-offset-2 focus-visible:outline-2 focus-visible:outline-primary"
|
|
32
33
|
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
|
|
33
|
-
:aria-pressed="selected"
|
|
34
|
+
:aria-pressed="selectable ? selected : void 0"
|
|
34
35
|
:disabled="disabled"
|
|
35
36
|
@click="emit('toggle')"
|
|
36
37
|
>
|
|
@@ -58,6 +59,7 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
|
|
|
58
59
|
</button>
|
|
59
60
|
|
|
60
61
|
<span
|
|
62
|
+
v-if="selectable"
|
|
61
63
|
class="pointer-events-none absolute left-2 top-2 grid size-4 place-items-center rounded border-[1.5px]"
|
|
62
64
|
:class="selected ? 'border-inverted bg-inverted text-inverted' : 'border-accented bg-default'"
|
|
63
65
|
aria-hidden="true"
|
|
@@ -69,6 +71,20 @@ const isTall = computed(() => (resolvedKind.value ?? "").toLowerCase() === "docu
|
|
|
69
71
|
/>
|
|
70
72
|
</span>
|
|
71
73
|
|
|
74
|
+
<div
|
|
75
|
+
v-if="$slots.badge"
|
|
76
|
+
class="pointer-events-none absolute right-9 top-2 flex flex-wrap gap-1"
|
|
77
|
+
:class="selectable ? 'left-8' : 'left-2'"
|
|
78
|
+
>
|
|
79
|
+
<slot name="badge" />
|
|
80
|
+
</div>
|
|
81
|
+
<div
|
|
82
|
+
v-if="$slots.actions"
|
|
83
|
+
class="absolute right-1.5 top-1.5 z-10 flex items-center gap-1"
|
|
84
|
+
>
|
|
85
|
+
<slot name="actions" />
|
|
86
|
+
</div>
|
|
87
|
+
|
|
72
88
|
<MLMediaPreviewEye
|
|
73
89
|
v-if="onPreview && !disabled"
|
|
74
90
|
:name="resolvedName"
|
|
@@ -7,6 +7,8 @@ type __VLS_Props = {
|
|
|
7
7
|
/** Second line. Defaults to the file extension. */
|
|
8
8
|
meta?: string;
|
|
9
9
|
selected?: boolean;
|
|
10
|
+
/** Shows the checkbox. False for a list without selection. */
|
|
11
|
+
selectable?: boolean;
|
|
10
12
|
/** Dimmed and not selectable, e.g. a kind the calling field doesn't accept. */
|
|
11
13
|
disabled?: boolean;
|
|
12
14
|
disabledReason?: string;
|
|
@@ -16,6 +18,10 @@ type __VLS_Props = {
|
|
|
16
18
|
type __VLS_Slots = {
|
|
17
19
|
/** Overlays on the thumbnail, e.g. an upload veil. */
|
|
18
20
|
default?: () => unknown;
|
|
21
|
+
/** Small labels over the top of the card, e.g. "Used in 3 lessons". */
|
|
22
|
+
badge?: () => unknown;
|
|
23
|
+
/** Controls in the top-right corner, e.g. a menu. Usable when the card is disabled. */
|
|
24
|
+
actions?: () => unknown;
|
|
19
25
|
};
|
|
20
26
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
21
27
|
toggle: () => any;
|
|
@@ -29,6 +35,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
29
35
|
disabled: boolean;
|
|
30
36
|
meta: string;
|
|
31
37
|
selected: boolean;
|
|
38
|
+
selectable: boolean;
|
|
32
39
|
disabledReason: string;
|
|
33
40
|
onPreview: () => void;
|
|
34
41
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|