nuxt-media-library 0.0.56 → 0.0.57
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/Field.vue +1 -0
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -0
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +5 -1
- package/dist/runtime/components/MLMedia/Gallery.vue +106 -4
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +5 -1
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +6 -0
- package/dist/runtime/components/MLMedia/GallerySection.vue +10 -2
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +6 -0
- package/dist/runtime/components/MLMedia/UploadProgress.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/UploadProgress.vue +33 -0
- package/dist/runtime/components/MLMedia/UploadProgress.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/UploadRow.d.vue.ts +12 -0
- package/dist/runtime/components/MLMedia/UploadRow.vue +242 -0
- package/dist/runtime/components/MLMedia/UploadRow.vue.d.ts +12 -0
- package/dist/runtime/components/MLMedia/UploadStep.d.vue.ts +17 -0
- package/dist/runtime/components/MLMedia/UploadStep.vue +168 -0
- package/dist/runtime/components/MLMedia/UploadStep.vue.d.ts +17 -0
- package/dist/runtime/components/MLMedia/UploadSteps.d.vue.ts +14 -0
- package/dist/runtime/components/MLMedia/UploadSteps.vue +129 -0
- package/dist/runtime/components/MLMedia/UploadSteps.vue.d.ts +14 -0
- package/dist/runtime/components/MLMedia/UploadTray.d.vue.ts +12 -0
- package/dist/runtime/components/MLMedia/UploadTray.vue +222 -0
- package/dist/runtime/components/MLMedia/UploadTray.vue.d.ts +12 -0
- package/dist/runtime/composables/useMLMediaField.js +5 -3
- package/dist/runtime/composables/useMLMediaGallery.js +4 -1
- package/dist/runtime/composables/useMLMediaUpload.d.ts +96 -4
- package/dist/runtime/composables/useMLMediaUpload.js +26 -169
- package/dist/runtime/composables/useMLUploadQueue.d.ts +31 -0
- package/dist/runtime/composables/useMLUploadQueue.js +354 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/utils/processingSteps.d.ts +5 -0
- package/dist/runtime/utils/processingSteps.js +36 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -36,6 +36,7 @@ const field = useMLMediaField(model, {
|
|
|
36
36
|
profile: props.profile,
|
|
37
37
|
service: props.service,
|
|
38
38
|
serviceBody: props.serviceBody,
|
|
39
|
+
label: props.label || void 0,
|
|
39
40
|
onUploaded: (id, sessionId) => emit("uploaded", id, sessionId)
|
|
40
41
|
});
|
|
41
42
|
const { state, asset, upload, message } = field;
|
|
@@ -15,7 +15,11 @@ export type MediaGalleryProfiles = Record<string, MediaGalleryRole>;
|
|
|
15
15
|
type __VLS_Props = {
|
|
16
16
|
/** Roles of this usage. Omit for a single list driven by v-model. */
|
|
17
17
|
profiles?: MediaGalleryProfiles | null;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Role tried first for new uploads and picked files, when it takes their
|
|
20
|
+
* kind. Defaults to the first role that holds more than one file. Files of
|
|
21
|
+
* other kinds go to the first role that takes them.
|
|
22
|
+
*/
|
|
19
23
|
defaultRole?: string;
|
|
20
24
|
accept?: readonly string[] | string;
|
|
21
25
|
max?: number | null;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { $fetch } from "ofetch";
|
|
4
|
+
import { kindForFile } from "../../utils/fileAccept";
|
|
3
5
|
import { GALLERY_TILE_TYPE } from "../../utils/galleryDrag";
|
|
4
6
|
import GallerySection from "./GallerySection.vue";
|
|
5
7
|
const props = defineProps({
|
|
@@ -78,16 +80,108 @@ function setRole(id, key, on) {
|
|
|
78
80
|
}
|
|
79
81
|
const pendingRole = /* @__PURE__ */ new Map();
|
|
80
82
|
const uploadedRole = /* @__PURE__ */ new Map();
|
|
83
|
+
const roleAccept = (r) => r.accept ?? props.accept;
|
|
84
|
+
const anyAccept = computed(() => {
|
|
85
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const r of roles.value) {
|
|
87
|
+
const a = roleAccept(r);
|
|
88
|
+
const list = (typeof a === "string" ? a.split(",") : [...a ?? []]).map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
89
|
+
if (!list.length) return "";
|
|
90
|
+
list.forEach((k) => kinds.add(k));
|
|
91
|
+
}
|
|
92
|
+
return [...kinds];
|
|
93
|
+
});
|
|
94
|
+
const anyMaxSize = computed(() => {
|
|
95
|
+
let max = 0;
|
|
96
|
+
for (const r of roles.value) {
|
|
97
|
+
const m = r.maxSize ?? props.maxSize;
|
|
98
|
+
if (m == null) return null;
|
|
99
|
+
max = Math.max(max, m);
|
|
100
|
+
}
|
|
101
|
+
return max || null;
|
|
102
|
+
});
|
|
103
|
+
const kindMatters = computed(() => {
|
|
104
|
+
const seen = new Set(roles.value.map((r) => JSON.stringify(roleAccept(r) ?? "")));
|
|
105
|
+
return seen.size > 1;
|
|
106
|
+
});
|
|
107
|
+
function roleForKind(kind, taken = /* @__PURE__ */ new Set()) {
|
|
108
|
+
const ordered = [...roles.value.filter((r) => r.key === defaultKey.value), ...roles.value.filter((r) => r.key !== defaultKey.value)];
|
|
109
|
+
const takes = ordered.filter((r) => kindAllowed(kind, roleAccept(r)));
|
|
110
|
+
const room = takes.find((r) => single(r) ? !taken.has(r.key) && !sel(r.key).length : r.max == null || sel(r.key).length < r.max);
|
|
111
|
+
return (room ?? takes[0])?.key ?? null;
|
|
112
|
+
}
|
|
113
|
+
const profileFor = (file) => {
|
|
114
|
+
const key = roleForKind(kindForFile(file));
|
|
115
|
+
return key ? props.profiles?.[key]?.profile ?? null : null;
|
|
116
|
+
};
|
|
117
|
+
function routeFiles(files) {
|
|
118
|
+
notice.value = null;
|
|
119
|
+
const taken = /* @__PURE__ */ new Set();
|
|
120
|
+
const groups = /* @__PURE__ */ new Map();
|
|
121
|
+
const left = [];
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const key = roleForKind(kindForFile(file), taken);
|
|
124
|
+
if (!key) {
|
|
125
|
+
left.push(file);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
taken.add(key);
|
|
129
|
+
groups.set(key, [...groups.get(key) ?? [], file]);
|
|
130
|
+
}
|
|
131
|
+
for (const [key, list] of groups) {
|
|
132
|
+
const role = props.profiles[key];
|
|
133
|
+
grid.value?.uploadFiles(list, {
|
|
134
|
+
profile: role.profile ?? null,
|
|
135
|
+
tag: key,
|
|
136
|
+
accept: roleAccept(role),
|
|
137
|
+
maxSize: role.maxSize ?? props.maxSize,
|
|
138
|
+
maxFiles: single(role) ? 1 : role.max != null ? Math.max(0, role.max - sel(key).length) : void 0
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (left.length) notice.value = `${left[0].name}${left.length > 1 ? ` and ${left.length - 1} more` : ""} can't be used in any role here.`;
|
|
142
|
+
}
|
|
143
|
+
async function placeByKind(ids) {
|
|
144
|
+
let kinds = /* @__PURE__ */ new Map();
|
|
145
|
+
try {
|
|
146
|
+
const res = await $fetch(
|
|
147
|
+
"/api/media-library/manage/assets/resolve",
|
|
148
|
+
{ method: "POST", body: { assetIds: ids } }
|
|
149
|
+
);
|
|
150
|
+
kinds = new Map(res.items.map((it, i) => [it.asset?.id ?? it.assetId ?? ids[i], it.asset?.kind?.toLowerCase() ?? null]));
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
const taken = /* @__PURE__ */ new Set();
|
|
154
|
+
const changes = {};
|
|
155
|
+
for (const id of ids) {
|
|
156
|
+
const key = roleForKind(kinds.get(id) ?? null, taken) ?? defaultKey.value;
|
|
157
|
+
taken.add(key);
|
|
158
|
+
const placed = withFile(key, id, changes[key] ?? sel(key));
|
|
159
|
+
if (placed) changes[key] = placed;
|
|
160
|
+
const session = pendingEmit.get(id);
|
|
161
|
+
if (session !== void 0) {
|
|
162
|
+
pendingEmit.delete(id);
|
|
163
|
+
emit("uploaded", id, key, session);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (Object.keys(changes).length) commit(changes);
|
|
167
|
+
}
|
|
168
|
+
const pendingEmit = /* @__PURE__ */ new Map();
|
|
81
169
|
function onUnionUpdate(next) {
|
|
82
170
|
notice.value = null;
|
|
83
171
|
const keep = new Set(next);
|
|
84
172
|
const before = new Set(union.value);
|
|
85
173
|
const changes = {};
|
|
86
174
|
for (const r of roles.value) changes[r.key] = sel(r.key).filter((id) => keep.has(id));
|
|
175
|
+
const unplaced = [];
|
|
87
176
|
for (const id of next) {
|
|
88
177
|
if (before.has(id)) continue;
|
|
89
|
-
const
|
|
178
|
+
const tagged = pendingRole.get(id);
|
|
90
179
|
pendingRole.delete(id);
|
|
180
|
+
if (!tagged && kindMatters.value) {
|
|
181
|
+
unplaced.push(id);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const key = tagged ?? defaultKey.value;
|
|
91
185
|
const placed = withFile(key, id, changes[key]);
|
|
92
186
|
if (placed) changes[key] = placed;
|
|
93
187
|
}
|
|
@@ -96,6 +190,7 @@ function onUnionUpdate(next) {
|
|
|
96
190
|
if (!single(r)) changes[r.key] = [...changes[r.key]].sort((a, b) => (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
|
|
97
191
|
}
|
|
98
192
|
commit(changes);
|
|
193
|
+
if (unplaced.length) void placeByKind(unplaced);
|
|
99
194
|
}
|
|
100
195
|
function onAdd(id, tag) {
|
|
101
196
|
const key = tag ?? defaultKey.value;
|
|
@@ -104,8 +199,13 @@ function onAdd(id, tag) {
|
|
|
104
199
|
else pendingRole.set(id, key);
|
|
105
200
|
}
|
|
106
201
|
function onUploaded(id, sessionId) {
|
|
107
|
-
|
|
202
|
+
const key = uploadedRole.get(id);
|
|
108
203
|
uploadedRole.delete(id);
|
|
204
|
+
if (!key && kindMatters.value && !union.value.includes(id)) {
|
|
205
|
+
pendingEmit.set(id, sessionId);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
emit("uploaded", id, key ?? defaultKey.value, sessionId);
|
|
109
209
|
}
|
|
110
210
|
const badges = (id) => roles.value.filter((r) => sel(r.key).includes(id)).map((r) => r.label);
|
|
111
211
|
function tileMenu(id, kind) {
|
|
@@ -170,9 +270,11 @@ function chipCount(key, max) {
|
|
|
170
270
|
:model-value="union"
|
|
171
271
|
:title="title"
|
|
172
272
|
:hint="hint"
|
|
173
|
-
:accept="
|
|
174
|
-
:max-size="
|
|
273
|
+
:accept="anyAccept"
|
|
274
|
+
:max-size="anyMaxSize"
|
|
175
275
|
:profile="defaultRole?.profile ?? null"
|
|
276
|
+
:profile-for="profileFor"
|
|
277
|
+
:on-route-files="routeFiles"
|
|
176
278
|
:service="service"
|
|
177
279
|
:service-body="serviceBody"
|
|
178
280
|
:disabled="disabled"
|
|
@@ -15,7 +15,11 @@ export type MediaGalleryProfiles = Record<string, MediaGalleryRole>;
|
|
|
15
15
|
type __VLS_Props = {
|
|
16
16
|
/** Roles of this usage. Omit for a single list driven by v-model. */
|
|
17
17
|
profiles?: MediaGalleryProfiles | null;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Role tried first for new uploads and picked files, when it takes their
|
|
20
|
+
* kind. Defaults to the first role that holds more than one file. Files of
|
|
21
|
+
* other kinds go to the first role that takes them.
|
|
22
|
+
*/
|
|
19
23
|
defaultRole?: string;
|
|
20
24
|
accept?: readonly string[] | string;
|
|
21
25
|
max?: number | null;
|
|
@@ -40,6 +40,10 @@ type __VLS_Props = {
|
|
|
40
40
|
highlight?: ReadonlySet<string> | null;
|
|
41
41
|
/** See useMLMediaGallery's onAdd. */
|
|
42
42
|
onAdd?: (assetId: string, tag: string | undefined) => void;
|
|
43
|
+
/** Take new files instead of uploading them here (a gallery with roles routes them by kind). */
|
|
44
|
+
onRouteFiles?: (files: File[]) => void;
|
|
45
|
+
/** Upload profile per file, for uploads inside the picker. */
|
|
46
|
+
profileFor?: (file: File) => string | null | undefined;
|
|
43
47
|
};
|
|
44
48
|
/** Upload files with another profile than the list's own, e.g. from a role chip. */
|
|
45
49
|
declare function uploadFiles(files: File[], opts?: {
|
|
@@ -87,6 +91,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
87
91
|
profile: string | null;
|
|
88
92
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
89
93
|
serviceBody: Record<string, unknown> | null;
|
|
94
|
+
profileFor: (file: File) => string | null | undefined;
|
|
90
95
|
onAdd: (assetId: string, tag: string | undefined) => void;
|
|
91
96
|
hint: string;
|
|
92
97
|
picker: boolean;
|
|
@@ -97,6 +102,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
97
102
|
recordName: string;
|
|
98
103
|
badges: (assetId: string) => string[];
|
|
99
104
|
tileMenu: (assetId: string, kind: string | null) => DropdownMenuItem[][];
|
|
105
|
+
onRouteFiles: (files: File[]) => void;
|
|
100
106
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
101
107
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
102
108
|
declare const _default: typeof __VLS_export;
|
|
@@ -30,7 +30,9 @@ const props = defineProps({
|
|
|
30
30
|
badges: { type: Function, required: false, default: void 0 },
|
|
31
31
|
tileMenu: { type: Function, required: false, default: void 0 },
|
|
32
32
|
highlight: { type: [Set, null], required: false, default: null },
|
|
33
|
-
onAdd: { type: Function, required: false, default: void 0 }
|
|
33
|
+
onAdd: { type: Function, required: false, default: void 0 },
|
|
34
|
+
onRouteFiles: { type: Function, required: false, default: void 0 },
|
|
35
|
+
profileFor: { type: Function, required: false, default: void 0 }
|
|
34
36
|
});
|
|
35
37
|
const model = defineModel({ type: Array, ...{ default: () => [] } });
|
|
36
38
|
const alt = defineModel("alt", { type: null, ...{ default: void 0 } });
|
|
@@ -45,6 +47,7 @@ const gallery = useMLMediaGallery(model, {
|
|
|
45
47
|
profile: props.profile,
|
|
46
48
|
service: props.service,
|
|
47
49
|
serviceBody: props.serviceBody,
|
|
50
|
+
label: props.recordName ? `${props.title} of ${props.recordName}` : props.title,
|
|
48
51
|
onUploaded: (id, sessionId) => emit("uploaded", id, sessionId),
|
|
49
52
|
onAdd: (id, tag) => props.onAdd?.(id, tag)
|
|
50
53
|
});
|
|
@@ -126,6 +129,7 @@ async function pickExisting() {
|
|
|
126
129
|
processingConfig: props.processingConfig,
|
|
127
130
|
collectionName: props.collectionName,
|
|
128
131
|
profile: props.profile,
|
|
132
|
+
profileFor: props.profileFor,
|
|
129
133
|
service: props.service,
|
|
130
134
|
serviceBody: props.serviceBody
|
|
131
135
|
},
|
|
@@ -199,6 +203,10 @@ function uploadFiles(files, opts = {}) {
|
|
|
199
203
|
if (rejected.length) gallery.onRejected(rejected);
|
|
200
204
|
if (accepted.length) gallery.onFiles(accepted, { profile: opts.profile, tag: opts.tag });
|
|
201
205
|
}
|
|
206
|
+
function onDropFiles(files) {
|
|
207
|
+
if (props.onRouteFiles) props.onRouteFiles(files);
|
|
208
|
+
else gallery.onFiles(files);
|
|
209
|
+
}
|
|
202
210
|
defineExpose({ uploadFiles });
|
|
203
211
|
function moveAndAnnounce(from, to) {
|
|
204
212
|
const id = model.value[from];
|
|
@@ -279,7 +287,7 @@ async function onTileKey(e, index) {
|
|
|
279
287
|
:disabled="disabled || full"
|
|
280
288
|
title="Drop to add"
|
|
281
289
|
class="rounded-lg"
|
|
282
|
-
@files="
|
|
290
|
+
@files="onDropFiles"
|
|
283
291
|
@rejected="gallery.onRejected"
|
|
284
292
|
>
|
|
285
293
|
<template #default="{ open }">
|
|
@@ -40,6 +40,10 @@ type __VLS_Props = {
|
|
|
40
40
|
highlight?: ReadonlySet<string> | null;
|
|
41
41
|
/** See useMLMediaGallery's onAdd. */
|
|
42
42
|
onAdd?: (assetId: string, tag: string | undefined) => void;
|
|
43
|
+
/** Take new files instead of uploading them here (a gallery with roles routes them by kind). */
|
|
44
|
+
onRouteFiles?: (files: File[]) => void;
|
|
45
|
+
/** Upload profile per file, for uploads inside the picker. */
|
|
46
|
+
profileFor?: (file: File) => string | null | undefined;
|
|
43
47
|
};
|
|
44
48
|
/** Upload files with another profile than the list's own, e.g. from a role chip. */
|
|
45
49
|
declare function uploadFiles(files: File[], opts?: {
|
|
@@ -87,6 +91,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
87
91
|
profile: string | null;
|
|
88
92
|
service: string | import("../../index.js").MediaServiceUpload | null;
|
|
89
93
|
serviceBody: Record<string, unknown> | null;
|
|
94
|
+
profileFor: (file: File) => string | null | undefined;
|
|
90
95
|
onAdd: (assetId: string, tag: string | undefined) => void;
|
|
91
96
|
hint: string;
|
|
92
97
|
picker: boolean;
|
|
@@ -97,6 +102,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
97
102
|
recordName: string;
|
|
98
103
|
badges: (assetId: string) => string[];
|
|
99
104
|
tileMenu: (assetId: string, kind: string | null) => DropdownMenuItem[][];
|
|
105
|
+
onRouteFiles: (files: File[]) => void;
|
|
100
106
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
101
107
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
102
108
|
declare const _default: typeof __VLS_export;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** 0–1, bytes sent. */
|
|
3
|
+
upload: number;
|
|
4
|
+
/** 0–1 done, or null while processing reports no progress (shown pulsing). */
|
|
5
|
+
processing?: number | null;
|
|
6
|
+
/** Processing has started (draws the second part). */
|
|
7
|
+
processingActive?: boolean;
|
|
8
|
+
/** warning: retrying or waiting for input; error: failed. */
|
|
9
|
+
tone?: 'normal' | 'warning' | 'error';
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
|
+
processing: number | null;
|
|
13
|
+
processingActive: boolean;
|
|
14
|
+
tone: "normal" | "warning" | "error";
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
upload: { type: Number, required: true },
|
|
5
|
+
processing: { type: [Number, null], required: false, default: null },
|
|
6
|
+
processingActive: { type: Boolean, required: false, default: false },
|
|
7
|
+
tone: { type: String, required: false, default: "normal" }
|
|
8
|
+
});
|
|
9
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
10
|
+
const processColor = computed(() => props.tone === "error" ? "bg-error" : props.tone === "warning" ? "bg-warning" : "bg-secondary");
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div
|
|
15
|
+
class="flex gap-0.5"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
>
|
|
18
|
+
<div class="h-1 flex-[3] overflow-hidden rounded-full bg-accented">
|
|
19
|
+
<div
|
|
20
|
+
class="h-full rounded-full bg-primary transition-[width] duration-300 motion-reduce:transition-none"
|
|
21
|
+
:style="{ width: `${clamp(upload) * 100}%` }"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="h-1 flex-[2] overflow-hidden rounded-full bg-accented">
|
|
25
|
+
<div
|
|
26
|
+
v-if="processingActive"
|
|
27
|
+
class="h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none"
|
|
28
|
+
:class="[processColor, processing == null ? 'w-1/3 animate-pulse motion-reduce:animate-none' : '']"
|
|
29
|
+
:style="processing != null ? { width: `${Math.max(0.06, clamp(processing)) * 100}%` } : void 0"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** 0–1, bytes sent. */
|
|
3
|
+
upload: number;
|
|
4
|
+
/** 0–1 done, or null while processing reports no progress (shown pulsing). */
|
|
5
|
+
processing?: number | null;
|
|
6
|
+
/** Processing has started (draws the second part). */
|
|
7
|
+
processingActive?: boolean;
|
|
8
|
+
/** warning: retrying or waiting for input; error: failed. */
|
|
9
|
+
tone?: 'normal' | 'warning' | 'error';
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
12
|
+
processing: number | null;
|
|
13
|
+
processingActive: boolean;
|
|
14
|
+
tone: "normal" | "warning" | "error";
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MediaUpload } from '../../composables/useMLMediaUpload.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
upload: MediaUpload;
|
|
4
|
+
expanded?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
toggle: () => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onToggle?: (() => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
3
|
+
import { useToast } from "#imports";
|
|
4
|
+
import { useMLUploadQueue } from "../../composables/useMLUploadQueue";
|
|
5
|
+
import { assetKindMeta } from "../../utils/assetDisplay";
|
|
6
|
+
import { formatBytes } from "../../utils/assetFormat";
|
|
7
|
+
import { isBackgroundStep, stepLabel } from "../../utils/processingSteps";
|
|
8
|
+
import UploadProgress from "./UploadProgress.vue";
|
|
9
|
+
import UploadSteps from "./UploadSteps.vue";
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
upload: { type: Object, required: true },
|
|
12
|
+
expanded: { type: Boolean, required: false }
|
|
13
|
+
});
|
|
14
|
+
const emit = defineEmits(["toggle"]);
|
|
15
|
+
const queue = useMLUploadQueue();
|
|
16
|
+
const toast = useToast();
|
|
17
|
+
const u = computed(() => props.upload);
|
|
18
|
+
const kind = computed(() => assetKindMeta(u.value.kind));
|
|
19
|
+
const TILE = {
|
|
20
|
+
primary: "bg-primary/10 text-primary",
|
|
21
|
+
secondary: "bg-secondary/10 text-secondary",
|
|
22
|
+
success: "bg-success/10 text-success",
|
|
23
|
+
info: "bg-info/10 text-info",
|
|
24
|
+
warning: "bg-warning/10 text-warning",
|
|
25
|
+
error: "bg-error/10 text-error",
|
|
26
|
+
neutral: "bg-elevated text-muted"
|
|
27
|
+
};
|
|
28
|
+
const tileClass = computed(() => u.value.phase === "failed" ? TILE.error : TILE[kind.value.color]);
|
|
29
|
+
const preview = computed(() => u.value.localPreview && u.value.kind === "image" ? u.value.localPreview : null);
|
|
30
|
+
const previewBroken = ref(false);
|
|
31
|
+
const now = ref(Date.now());
|
|
32
|
+
let timer = null;
|
|
33
|
+
watch(() => u.value.phase === "processing", (on) => {
|
|
34
|
+
if (timer) clearInterval(timer);
|
|
35
|
+
timer = on ? setInterval(() => now.value = Date.now(), 1e3) : null;
|
|
36
|
+
}, { immediate: true });
|
|
37
|
+
onBeforeUnmount(() => timer && clearInterval(timer));
|
|
38
|
+
const elapsed = computed(() => {
|
|
39
|
+
if (!u.value.processingSince) return "";
|
|
40
|
+
const secs = Math.max(0, Math.floor((now.value - u.value.processingSince) / 1e3));
|
|
41
|
+
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
42
|
+
});
|
|
43
|
+
const where = computed(() => u.value.context?.label ?? "");
|
|
44
|
+
const subtitle = computed(() => {
|
|
45
|
+
const x = u.value;
|
|
46
|
+
switch (x.phase) {
|
|
47
|
+
case "uploading": {
|
|
48
|
+
const p = x.progress ?? 0;
|
|
49
|
+
return [`Uploading ${Math.round(p)}%`, `${formatBytes(x.size * p / 100)} of ${formatBytes(x.size)}`, where.value].filter(Boolean).join(" \xB7 ");
|
|
50
|
+
}
|
|
51
|
+
case "processing": {
|
|
52
|
+
if (x.needsInput) return ["Needs your input", x.step && stepLabel(x.step)].filter(Boolean).join(" \xB7 ");
|
|
53
|
+
if (x.notice) return x.notice;
|
|
54
|
+
if (x.usable) return `Ready to use \xB7 finishing ${x.step && isBackgroundStep(x.step) ? stepLabel(x.step).toLowerCase() : "up"}`;
|
|
55
|
+
const at = x.stepIndex && x.stepTotal ? `step ${x.stepIndex} of ${x.stepTotal}` : null;
|
|
56
|
+
return ["Processing", at, x.step && stepLabel(x.step), elapsed.value].filter(Boolean).join(" \xB7 ");
|
|
57
|
+
}
|
|
58
|
+
case "ready":
|
|
59
|
+
if (x.orphaned) return `Uploaded, not added${where.value ? ` to ${where.value}` : ""}`;
|
|
60
|
+
return ["Ready to use", where.value].filter(Boolean).join(" \xB7 ");
|
|
61
|
+
case "failed":
|
|
62
|
+
if (x.assetId && x.failedStep) return `Failed at ${stepLabel(x.failedStep)}`;
|
|
63
|
+
return x.error ?? (x.assetId ? "Processing failed" : "Upload failed");
|
|
64
|
+
case "canceled":
|
|
65
|
+
return x.assetId ? "Stopped following; processing goes on in the library" : "Canceled";
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
68
|
+
});
|
|
69
|
+
const subtitleClass = computed(() => {
|
|
70
|
+
const x = u.value;
|
|
71
|
+
if (x.phase === "failed") return "text-error";
|
|
72
|
+
if (x.phase === "ready" && x.orphaned) return "text-warning";
|
|
73
|
+
if (x.phase === "ready" || x.phase === "processing" && x.usable) return "text-success";
|
|
74
|
+
if (x.needsInput || x.notice) return "text-warning";
|
|
75
|
+
return "text-muted";
|
|
76
|
+
});
|
|
77
|
+
const showBar = computed(() => u.value.phase === "uploading" || u.value.phase === "processing");
|
|
78
|
+
const uploadShare = computed(() => u.value.phase === "uploading" ? (u.value.progress ?? 0) / 100 : 1);
|
|
79
|
+
const processShare = computed(() => {
|
|
80
|
+
const x = u.value;
|
|
81
|
+
if (x.phase !== "processing") return null;
|
|
82
|
+
if (x.usable) return 1;
|
|
83
|
+
if (x.stepIndex && x.stepTotal) return (x.stepIndex - 1) / x.stepTotal;
|
|
84
|
+
return x.progress != null ? x.progress / 100 : null;
|
|
85
|
+
});
|
|
86
|
+
const tone = computed(() => u.value.needsInput || u.value.notice ? "warning" : "normal");
|
|
87
|
+
const canExpand = computed(() => !!u.value.assetId && u.value.phase !== "canceled");
|
|
88
|
+
const canShow = computed(() => !!u.value.context?.to && (u.value.phase === "ready" || u.value.usable));
|
|
89
|
+
const finished = computed(() => ["ready", "canceled", "failed"].includes(u.value.phase));
|
|
90
|
+
const retrying = ref(false);
|
|
91
|
+
async function retryStep() {
|
|
92
|
+
retrying.value = true;
|
|
93
|
+
try {
|
|
94
|
+
await queue.retryStep(u.value);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err?.data?.detail ?? err?.data?.message ?? err?.statusMessage ?? "Could not retry this step";
|
|
97
|
+
toast.add({ title: "Retry failed", description: String(message), color: "error" });
|
|
98
|
+
} finally {
|
|
99
|
+
retrying.value = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<li
|
|
106
|
+
class="px-3 py-2.5"
|
|
107
|
+
:class="expanded ? 'bg-elevated/40' : ''"
|
|
108
|
+
>
|
|
109
|
+
<div class="flex items-center gap-2.5">
|
|
110
|
+
<!-- What it is -->
|
|
111
|
+
<div
|
|
112
|
+
class="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-md"
|
|
113
|
+
:class="preview && !previewBroken ? 'bg-elevated' : tileClass"
|
|
114
|
+
>
|
|
115
|
+
<img
|
|
116
|
+
v-if="preview && !previewBroken"
|
|
117
|
+
:src="preview"
|
|
118
|
+
alt=""
|
|
119
|
+
class="size-full object-cover"
|
|
120
|
+
@error="previewBroken = true"
|
|
121
|
+
>
|
|
122
|
+
<UIcon
|
|
123
|
+
v-else
|
|
124
|
+
:name="u.phase === 'failed' ? 'i-lucide-file-warning' : kind.icon"
|
|
125
|
+
class="size-4"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Name and state -->
|
|
130
|
+
<div class="min-w-0 flex-1">
|
|
131
|
+
<p
|
|
132
|
+
class="truncate text-sm"
|
|
133
|
+
:title="u.file.name"
|
|
134
|
+
>
|
|
135
|
+
{{ u.file.name }}
|
|
136
|
+
</p>
|
|
137
|
+
<p
|
|
138
|
+
class="truncate text-xs"
|
|
139
|
+
:class="subtitleClass"
|
|
140
|
+
:title="subtitle"
|
|
141
|
+
>
|
|
142
|
+
{{ subtitle }}
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- State icon -->
|
|
147
|
+
<UIcon
|
|
148
|
+
v-if="u.phase === 'processing' && !u.needsInput"
|
|
149
|
+
:name="u.notice ? 'i-lucide-refresh-cw' : 'i-lucide-loader-circle'"
|
|
150
|
+
class="size-4 shrink-0 animate-spin motion-reduce:animate-none"
|
|
151
|
+
:class="u.notice ? 'text-warning' : 'text-secondary'"
|
|
152
|
+
/>
|
|
153
|
+
<UIcon
|
|
154
|
+
v-else-if="u.phase === 'processing' && u.needsInput"
|
|
155
|
+
name="i-lucide-pencil-line"
|
|
156
|
+
class="size-4 shrink-0 text-warning"
|
|
157
|
+
/>
|
|
158
|
+
<UIcon
|
|
159
|
+
v-else-if="u.phase === 'ready' && !u.orphaned"
|
|
160
|
+
name="i-lucide-circle-check"
|
|
161
|
+
class="size-4 shrink-0 text-success"
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<!-- Actions -->
|
|
165
|
+
<UButton
|
|
166
|
+
v-if="u.phase === 'uploading'"
|
|
167
|
+
icon="i-lucide-x"
|
|
168
|
+
color="neutral"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
size="xs"
|
|
171
|
+
square
|
|
172
|
+
:aria-label="`Cancel ${u.file.name}`"
|
|
173
|
+
@click="u.cancel()"
|
|
174
|
+
/>
|
|
175
|
+
<UButton
|
|
176
|
+
v-else-if="u.phase === 'failed' && !u.assetId"
|
|
177
|
+
icon="i-lucide-rotate-ccw"
|
|
178
|
+
color="neutral"
|
|
179
|
+
variant="outline"
|
|
180
|
+
size="xs"
|
|
181
|
+
label="Retry"
|
|
182
|
+
@click="u.retry()"
|
|
183
|
+
/>
|
|
184
|
+
<UButton
|
|
185
|
+
v-else-if="u.phase === 'failed' && u.failedTaskId"
|
|
186
|
+
icon="i-lucide-rotate-ccw"
|
|
187
|
+
color="neutral"
|
|
188
|
+
variant="outline"
|
|
189
|
+
size="xs"
|
|
190
|
+
label="Retry step"
|
|
191
|
+
:loading="retrying"
|
|
192
|
+
@click="retryStep"
|
|
193
|
+
/>
|
|
194
|
+
<UButton
|
|
195
|
+
v-if="canShow"
|
|
196
|
+
:to="u.context.to"
|
|
197
|
+
size="xs"
|
|
198
|
+
variant="link"
|
|
199
|
+
label="Show"
|
|
200
|
+
class="px-1"
|
|
201
|
+
/>
|
|
202
|
+
<UButton
|
|
203
|
+
v-if="canExpand"
|
|
204
|
+
:icon="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
|
205
|
+
color="neutral"
|
|
206
|
+
variant="ghost"
|
|
207
|
+
size="xs"
|
|
208
|
+
square
|
|
209
|
+
:aria-label="expanded ? `Hide steps for ${u.file.name}` : `Show steps for ${u.file.name}`"
|
|
210
|
+
:aria-expanded="expanded"
|
|
211
|
+
@click="emit('toggle')"
|
|
212
|
+
/>
|
|
213
|
+
<UButton
|
|
214
|
+
v-if="finished"
|
|
215
|
+
icon="i-lucide-x"
|
|
216
|
+
color="neutral"
|
|
217
|
+
variant="ghost"
|
|
218
|
+
size="xs"
|
|
219
|
+
square
|
|
220
|
+
:aria-label="`Dismiss ${u.file.name}`"
|
|
221
|
+
@click="queue.dismiss(u)"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<UploadProgress
|
|
226
|
+
v-if="showBar"
|
|
227
|
+
class="ms-10.5 mt-2"
|
|
228
|
+
:upload="uploadShare"
|
|
229
|
+
:processing="processShare"
|
|
230
|
+
:processing-active="u.phase === 'processing'"
|
|
231
|
+
:tone="tone"
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<UploadSteps
|
|
235
|
+
v-if="expanded && canExpand"
|
|
236
|
+
class="ms-10.5 mt-2"
|
|
237
|
+
:asset-id="u.assetId"
|
|
238
|
+
:kind="u.kind"
|
|
239
|
+
@complete="u.retry()"
|
|
240
|
+
/>
|
|
241
|
+
</li>
|
|
242
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MediaUpload } from '../../composables/useMLMediaUpload.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
upload: MediaUpload;
|
|
4
|
+
expanded?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
toggle: () => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onToggle?: (() => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProcessingTask } from '../../types/asset.js';
|
|
2
|
+
import type { ProcessingEvent } from '../../composables/useMLProcessingStatus.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
task: ProcessingTask;
|
|
5
|
+
sseEvent?: ProcessingEvent | null;
|
|
6
|
+
/** Upstream step holding this one up (for waiting steps). */
|
|
7
|
+
blocker?: ProcessingTask | null;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
retry: (taskId: string) => any;
|
|
11
|
+
configure: (taskId: string) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onRetry?: ((taskId: string) => any) | undefined;
|
|
14
|
+
onConfigure?: ((taskId: string) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|