nuxt-media-library 0.0.55 → 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.
Files changed (38) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/MLMedia/Field.vue +1 -0
  3. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -0
  4. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +5 -1
  5. package/dist/runtime/components/MLMedia/Gallery.vue +106 -4
  6. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +5 -1
  7. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +6 -0
  8. package/dist/runtime/components/MLMedia/GallerySection.vue +10 -2
  9. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +6 -0
  10. package/dist/runtime/components/MLMedia/UploadProgress.d.vue.ts +17 -0
  11. package/dist/runtime/components/MLMedia/UploadProgress.vue +33 -0
  12. package/dist/runtime/components/MLMedia/UploadProgress.vue.d.ts +17 -0
  13. package/dist/runtime/components/MLMedia/UploadRow.d.vue.ts +12 -0
  14. package/dist/runtime/components/MLMedia/UploadRow.vue +242 -0
  15. package/dist/runtime/components/MLMedia/UploadRow.vue.d.ts +12 -0
  16. package/dist/runtime/components/MLMedia/UploadStep.d.vue.ts +17 -0
  17. package/dist/runtime/components/MLMedia/UploadStep.vue +168 -0
  18. package/dist/runtime/components/MLMedia/UploadStep.vue.d.ts +17 -0
  19. package/dist/runtime/components/MLMedia/UploadSteps.d.vue.ts +14 -0
  20. package/dist/runtime/components/MLMedia/UploadSteps.vue +129 -0
  21. package/dist/runtime/components/MLMedia/UploadSteps.vue.d.ts +14 -0
  22. package/dist/runtime/components/MLMedia/UploadTray.d.vue.ts +12 -0
  23. package/dist/runtime/components/MLMedia/UploadTray.vue +222 -0
  24. package/dist/runtime/components/MLMedia/UploadTray.vue.d.ts +12 -0
  25. package/dist/runtime/composables/useMLMediaField.js +5 -3
  26. package/dist/runtime/composables/useMLMediaGallery.js +4 -1
  27. package/dist/runtime/composables/useMLMediaUpload.d.ts +96 -4
  28. package/dist/runtime/composables/useMLMediaUpload.js +26 -168
  29. package/dist/runtime/composables/useMLUploadQueue.d.ts +31 -0
  30. package/dist/runtime/composables/useMLUploadQueue.js +354 -0
  31. package/dist/runtime/index.d.ts +2 -0
  32. package/dist/runtime/index.js +2 -0
  33. package/dist/runtime/utils/fileAccept.d.ts +6 -0
  34. package/dist/runtime/utils/fileAccept.js +29 -0
  35. package/dist/runtime/utils/processingSteps.d.ts +5 -0
  36. package/dist/runtime/utils/processingSteps.js +36 -0
  37. package/dist/runtime/utils/uploader/serviceUpload.js +3 -2
  38. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.55",
4
+ "version": "0.0.57",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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;
@@ -72,6 +72,7 @@ function onFiles(files) {
72
72
  const u = uploader.start(file);
73
73
  void u.created.then((id) => {
74
74
  if (!id) return;
75
+ u.attached = true;
75
76
  props.onUploaded?.(id, u.sessionId);
76
77
  if (!isChosen(id)) choose(id);
77
78
  });
@@ -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
- /** Role new uploads and picked files go to. Defaults to the first role that holds more than one file. */
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 key = pendingRole.get(id) ?? defaultKey.value;
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
- emit("uploaded", id, uploadedRole.get(id) ?? defaultKey.value, sessionId);
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="defaultRole?.accept ?? accept"
174
- :max-size="defaultRole?.maxSize ?? maxSize"
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
- /** Role new uploads and picked files go to. Defaults to the first role that holds more than one file. */
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="gallery.onFiles"
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;