@cardinali-studio/adminkit 2.2.25 → 2.2.33

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 (42) hide show
  1. package/dist/app/app.css +3 -3
  2. package/dist/app/delivery.d.ts +11 -0
  3. package/dist/app/delivery.js +46 -0
  4. package/dist/app/index.d.ts +4 -1
  5. package/dist/app/index.js +3 -2
  6. package/dist/app/lib/deliveries.d.ts +8 -0
  7. package/dist/app/lib/delivery-network.d.ts +25 -0
  8. package/dist/app/lib/delivery-network.js +159 -0
  9. package/dist/app/lib/delivery-upload.d.ts +29 -11
  10. package/dist/app/lib/delivery-upload.js +408 -131
  11. package/dist/app/lib/upload-queue.d.ts +21 -3
  12. package/dist/app/lib/upload-queue.js +76 -19
  13. package/dist/app/routes/Deliveries.js +108 -29
  14. package/dist/app/routes/UploadDock.js +6 -2
  15. package/dist/direct-publication.d.ts +20 -0
  16. package/dist/direct-publication.js +75 -0
  17. package/dist/functions/_shared/b2.js +22 -3
  18. package/dist/functions/_shared/delivery-crypto.js +5 -0
  19. package/dist/functions/_shared/delivery-limits.d.ts +9 -0
  20. package/dist/functions/_shared/delivery-limits.js +29 -0
  21. package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
  22. package/dist/functions/_shared/delivery-manifest.js +392 -0
  23. package/dist/functions/_shared/delivery-purge.js +17 -1
  24. package/dist/functions/api/deliveries.js +177 -49
  25. package/dist/functions/api/delivery/image.js +11 -6
  26. package/dist/functions/api/delivery/multipart.js +196 -64
  27. package/dist/functions/delivery-public/assets/client.js +246 -0
  28. package/dist/functions/delivery-public/assets/delivery.css +297 -0
  29. package/dist/functions/delivery-public/assets/gallery.js +243 -0
  30. package/dist/functions/delivery-public/assets/stage.js +107 -0
  31. package/dist/functions/delivery-public/index.js +637 -0
  32. package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
  33. package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
  34. package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
  35. package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
  36. package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
  37. package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
  38. package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
  39. package/dist/functions/index.js +3 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +1 -0
  42. package/package.json +10 -3
@@ -1,5 +1,5 @@
1
- import { type DeliveryUploadProgress } from "./delivery-upload";
2
- export type UploadState = "running" | "paused" | "done" | "failed";
1
+ import { type DeliveryUploadProgress, type UploadControl } from "./delivery-upload.js";
2
+ export type UploadState = "running" | "paused" | "cancelling" | "done" | "failed";
3
3
  export interface UploadJob {
4
4
  /** L'id del pacchetto: uno per caricamento. */
5
5
  id: string;
@@ -11,6 +11,8 @@ export interface UploadJob {
11
11
  progress: DeliveryUploadProgress | null;
12
12
  error?: string;
13
13
  skipped?: number;
14
+ waitingForNetwork?: boolean;
15
+ retryable?: boolean;
14
16
  }
15
17
  interface StartInput {
16
18
  deliveryId: string;
@@ -20,16 +22,32 @@ interface StartInput {
20
22
  options: {
21
23
  galleryEnabled: boolean;
22
24
  webEnabled: boolean;
25
+ webFormat?: "webp" | "jpg";
26
+ manifestVersion?: 1 | 2;
23
27
  };
24
28
  }
25
29
  export declare function useUploads(): UploadJob[];
26
30
  export declare function useUploadCompletions(): number;
27
31
  export declare function uploadFor(deliveryId: string, list: UploadJob[]): UploadJob | undefined;
28
32
  export declare function isBusy(job: UploadJob | undefined): boolean;
29
- export declare function startUpload(input: StartInput): void;
33
+ export declare class Gate implements UploadControl {
34
+ private id;
35
+ readonly controller: AbortController;
36
+ readonly signal: AbortSignal;
37
+ private waitingRequests;
38
+ constructor(id: string);
39
+ networkWaiting(waiting: boolean): void;
40
+ paused: boolean;
41
+ cancelled: boolean;
42
+ private waiting;
43
+ gate(): Promise<void>;
44
+ wake(): void;
45
+ }
46
+ export declare function startUpload(input: StartInput): Promise<void>;
30
47
  export declare function pauseUpload(id: string): void;
31
48
  export declare function resumeUpload(id: string): void;
32
49
  export declare function cancelUpload(id: string): void;
50
+ export declare function retryUpload(id: string): Promise<void> | undefined;
33
51
  export declare function dismissUpload(id: string): void;
34
52
  export declare function hasRunningUploads(): boolean;
35
53
  export {};
@@ -15,12 +15,14 @@
15
15
  * App.tsx ricarica l'elenco quando uno finisce (`useUploadCompletions`)
16
16
  */
17
17
  import { useSyncExternalStore } from "react";
18
- import { UploadCancelled, uploadDeliveryPackage, } from "./delivery-upload";
19
- import { deliveryAction } from "./deliveries";
18
+ import { UploadCancelled, uploadDeliveryPackage, } from "./delivery-upload.js";
19
+ import { deliveryJson } from "./delivery-network.js";
20
20
  /* ---- lo stato ------------------------------------------------------------------------ */
21
21
  let jobs = [];
22
22
  let completions = 0;
23
23
  const gates = new Map();
24
+ const running = new Map();
25
+ const retries = new Map();
24
26
  const listeners = new Set();
25
27
  function emit() {
26
28
  jobs = [...jobs];
@@ -47,15 +49,27 @@ export function useUploadCompletions() {
47
49
  return useSyncExternalStore(subscribe, () => completions, () => completions);
48
50
  }
49
51
  export function uploadFor(deliveryId, list) {
50
- return list.find((job) => job.deliveryId === deliveryId);
52
+ const matching = list.filter((job) => job.deliveryId === deliveryId);
53
+ return matching.find(isBusy) || matching[matching.length - 1];
51
54
  }
52
55
  export function isBusy(job) {
53
- return !!job && (job.state === "running" || job.state === "paused");
56
+ return !!job && (job.state === "running" || job.state === "paused" || job.state === "cancelling");
54
57
  }
55
58
  /* ---- il cancello --------------------------------------------------------------------- */
56
59
  /* Pausa e annullamento passano da qui: il motore (delivery-upload.ts) chiama `gate()` a ogni
57
60
  file e a ogni parte, e questa promessa lo tiene fermo finché non si riprende. */
58
- class Gate {
61
+ export class Gate {
62
+ id;
63
+ controller = new AbortController();
64
+ signal = this.controller.signal;
65
+ waitingRequests = 0;
66
+ constructor(id) {
67
+ this.id = id;
68
+ }
69
+ networkWaiting(waiting) {
70
+ this.waitingRequests += waiting ? 1 : -1;
71
+ patch(this.id, { waitingForNetwork: this.waitingRequests > 0 });
72
+ }
59
73
  paused = false;
60
74
  cancelled = false;
61
75
  waiting = [];
@@ -77,7 +91,10 @@ class Gate {
77
91
  }
78
92
  /* ---- i comandi ------------------------------------------------------------------------ */
79
93
  export function startUpload(input) {
80
- const gate = new Gate();
94
+ const existing = running.get(input.packageId);
95
+ if (existing)
96
+ return existing;
97
+ const gate = new Gate(input.packageId);
81
98
  gates.set(input.packageId, gate);
82
99
  jobs = [...jobs.filter((job) => job.id !== input.packageId), {
83
100
  id: input.packageId,
@@ -89,33 +106,64 @@ export function startUpload(input) {
89
106
  progress: null,
90
107
  }];
91
108
  emit();
92
- void run(input, gate);
109
+ const promise = run(input, gate).finally(() => running.delete(input.packageId));
110
+ running.set(input.packageId, promise);
111
+ return promise;
93
112
  }
94
113
  async function run(input, gate) {
95
114
  const id = input.packageId;
96
115
  try {
97
- const result = await uploadDeliveryPackage(input.deliveryId, id, input.files, input.options, (progress) => patch(id, { progress }), gate);
116
+ const retry = retries.get(id);
117
+ if (retry?.cleanup)
118
+ throw new UploadCancelled();
119
+ const result = retry?.result || await uploadDeliveryPackage(input.deliveryId, id, input.files, input.options, (progress) => patch(id, { progress }), gate);
98
120
  /* Chiusura e attivazione stanno QUI e non nella schermata: se finiscono mentre il
99
121
  fotografo sta guardando un'altra sezione, devono succedere lo stesso. */
100
- await deliveryAction({
101
- action: "finalizePackage", id: input.deliveryId, packageId: id,
102
- manifest: result.manifest, fileCount: result.fileCount,
103
- sourceBytes: result.sourceBytes, storedBytes: result.storedBytes,
104
- });
105
- await deliveryAction({ action: "activate", id: input.deliveryId }).catch(() => { });
122
+ retries.set(id, { input, result });
123
+ await gate.gate();
124
+ if (result.manifest.version === 2) {
125
+ let done = false;
126
+ while (!done) {
127
+ await gate.gate();
128
+ const step = await deliveryJson("/admin/api/deliveries", {
129
+ action: "manifestFinalize", id: input.deliveryId, packageId: id,
130
+ }, gate);
131
+ done = step.done;
132
+ }
133
+ }
134
+ else
135
+ await deliveryJson("/admin/api/deliveries", {
136
+ action: "finalizePackage", id: input.deliveryId, packageId: id,
137
+ manifest: result.manifest, fileCount: result.fileCount,
138
+ sourceBytes: result.sourceBytes, storedBytes: result.storedBytes,
139
+ }, gate);
140
+ await gate.gate();
141
+ await deliveryJson("/admin/api/deliveries", { action: "activate", id: input.deliveryId, packageId: id }, gate);
142
+ await gate.gate();
143
+ retries.delete(id);
106
144
  patch(id, { state: "done", progress: null, skipped: result.manifest.skipped.length });
107
145
  }
108
146
  catch (error) {
109
147
  if (error instanceof UploadCancelled) {
110
148
  /* Annullare vuol dire «come se non l'avessi mai iniziato»: il pacchetto sparisce, e
111
149
  con lui il multipart aperto su B2. */
112
- await deliveryAction({
113
- action: "deletePackage", id: input.deliveryId, packageId: id,
114
- }).catch(() => { });
115
- jobs = jobs.filter((job) => job.id !== id);
150
+ patch(id, { state: "cancelling", progress: null });
151
+ try {
152
+ await deliveryJson("/admin/api/deliveries", {
153
+ action: "deletePackage", id: input.deliveryId, packageId: id,
154
+ }, { gate: async () => { }, networkWaiting: (waiting) => gate.networkWaiting(waiting) });
155
+ retries.delete(id);
156
+ jobs = jobs.filter((job) => job.id !== id);
157
+ }
158
+ catch (cleanupError) {
159
+ retries.set(id, { input, cleanup: true });
160
+ patch(id, { state: "failed", retryable: true, error: "Upload fermato. Riprova la rimozione dei file: " + readable(cleanupError) });
161
+ }
116
162
  }
117
163
  else {
118
- patch(id, { state: "failed", progress: null, error: readable(error) });
164
+ if (!retries.has(id))
165
+ retries.set(id, { input });
166
+ patch(id, { state: "failed", progress: null, retryable: true, error: readable(error) });
119
167
  }
120
168
  }
121
169
  finally {
@@ -144,12 +192,21 @@ export function cancelUpload(id) {
144
192
  if (!gate)
145
193
  return;
146
194
  gate.cancelled = true;
195
+ gate.controller.abort();
196
+ patch(id, { state: "cancelling" });
147
197
  gate.paused = false;
148
198
  gate.wake();
149
199
  }
150
200
  /* Toglie dalla vista un caricamento già finito. Su uno in corso non fa niente: per quello
151
201
  c'è `cancelUpload`, che è un'altra cosa e va detta con un'altra parola. */
202
+ export function retryUpload(id) {
203
+ const retry = retries.get(id);
204
+ if (retry)
205
+ return startUpload(retry.input);
206
+ }
152
207
  export function dismissUpload(id) {
208
+ if (!gates.has(id))
209
+ retries.delete(id);
153
210
  jobs = jobs.filter((job) => job.id === id ? isBusy(job) : true);
154
211
  emit();
155
212
  }
@@ -22,7 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
22
22
  * Il vocabolario completo (e il perché di ogni parola) sta in DELIVERY-REDESIGN.md §2.
23
23
  */
24
24
  import { useEffect, useRef, useState } from "react";
25
- import { deliverySelectionName, deliverySourceFingerprint, uploadDeliveryHeroObject, } from "../lib/delivery-upload";
25
+ import { deliverySelectionName, deliverySourceFingerprint, uploadDeliveryHeroObject, preflightDeliverySelection, } from "../lib/delivery-upload";
26
26
  import { isBusy, startUpload, uploadFor, useUploadCompletions, useUploads, } from "../lib/upload-queue";
27
27
  import { filesFromDrop } from "../lib/delivery-drop";
28
28
  /* ============================================================================================
@@ -56,12 +56,30 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
56
56
  const [heroes, setHeroes] = useState(fromServer?.heroes || []);
57
57
  const [heroesClean, setHeroesClean] = useState(fromServer?.heroes || []);
58
58
  const [saving, setSaving] = useState(false);
59
+ const [jpegEnabled, setJpegEnabled] = useState(false);
60
+ const [downloadLabelsEnabled, setDownloadLabelsEnabled] = useState(false);
61
+ useEffect(() => {
62
+ const controller = new AbortController();
63
+ fetch("/admin/api/deliveries?capabilities=1", { signal: controller.signal, credentials: "same-origin" })
64
+ .then(response => response.ok ? response.json() : null)
65
+ .then(value => {
66
+ if (!controller.signal.aborted) {
67
+ setJpegEnabled(value?.jpegEnabled === true);
68
+ setDownloadLabelsEnabled(value?.downloadLabelsEnabled === true);
69
+ }
70
+ })
71
+ .catch(() => { });
72
+ return () => controller.abort();
73
+ }, []);
59
74
  const [failure, setFailure] = useState(null);
60
75
  const [notice, setNotice] = useState(null);
61
76
  const [confirmDelete, setConfirmDelete] = useState(false);
62
77
  /* Anche «Rimuovi» su un pacchetto cancella i byte all'istante: chiede conferma
63
78
  * come il fondo pagina, ma dentro la riga, che è dove si è guardato. */
64
79
  const [confirmPackage, setConfirmPackage] = useState(null);
80
+ const [moreImages, setMoreImages] = useState({});
81
+ const [loadingImages, setLoadingImages] = useState(false);
82
+ const [previewAttempt, setPreviewAttempt] = useState(0);
65
83
  const [copied, setCopied] = useState(null);
66
84
  const folderRef = useRef(null);
67
85
  const filesRef = useRef(null);
@@ -76,8 +94,34 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
76
94
  const jobs = useUploads();
77
95
  const job = delivery ? uploadFor(delivery.id, jobs) : undefined;
78
96
  const uploading = isBusy(job) || startingRef.current;
79
- const dirty = !same(draft, clean) || !sameHeroes(heroes, heroesClean);
80
- const photos = packages.flatMap((pkg) => pkg.images.map((image) => ({ pkg, image })));
97
+ const heroesDirty = !sameHeroes(heroes, heroesClean);
98
+ const dirty = !same(draft, clean) || heroesDirty;
99
+ const photos = packages.filter(pkg => pkg.state === "ready").flatMap((pkg) => [...pkg.images, ...(moreImages[pkg.id]?.images || [])].map((image) => ({ pkg, image })));
100
+ async function loadMoreImages() {
101
+ if (loadingImages || !delivery)
102
+ return;
103
+ setLoadingImages(true);
104
+ try {
105
+ for (const pkg of packages) {
106
+ const cursor = moreImages[pkg.id] ? moreImages[pkg.id].nextCursor : pkg.nextCursor;
107
+ if (cursor == null)
108
+ continue;
109
+ const page = await onAction({
110
+ action: "manifestPage", id: delivery.id, packageId: pkg.id, after: cursor,
111
+ }, { reload: false });
112
+ setMoreImages(current => ({ ...current, [pkg.id]: {
113
+ images: [...(current[pkg.id]?.images || []), ...page.images], nextCursor: page.nextCursor,
114
+ } }));
115
+ }
116
+ }
117
+ catch (error) {
118
+ setFailure(errorText(error));
119
+ }
120
+ finally {
121
+ setLoadingImages(false);
122
+ }
123
+ }
124
+ useEffect(() => { setMoreImages({}); }, [delivery?.id]);
81
125
  useEffect(() => {
82
126
  if (!fromServer)
83
127
  return;
@@ -105,7 +149,8 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
105
149
  const change = (patch) => setDraft((current) => ({ ...current, ...patch }));
106
150
  /* ---- caricamento ------------------------------------------------------------------- */
107
151
  /* Qui si prepara e si consegna alla coda: transfer, pacchetto, impronta. Poi si esce
108
- subito. Il caricamento vero, la chiusura del pacchetto e l'attivazione li fa la coda,
152
+ dalla preparazione; la promessa resta aperta per conservare il file input fino a lettura finita.
153
+ Il caricamento vero, la chiusura del pacchetto e l'attivazione li fa la coda,
109
154
  che non muore quando questa schermata sparisce. */
110
155
  const beginUpload = async (files, resume = null) => {
111
156
  if (!files.length || startingRef.current)
@@ -115,16 +160,23 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
115
160
  setNotice(null);
116
161
  let target = delivery;
117
162
  let packageId = resume?.id || "";
118
- let options = { galleryEnabled: draft.gallery, webEnabled: draft.light };
163
+ let options = resume ? { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled, webFormat: resume.webFormat || "webp" } : { galleryEnabled: draft.gallery, webEnabled: draft.light, webFormat: draft.light ? draft.webFormat : "webp" };
119
164
  try {
165
+ // Verifica i limiti fisici prima della richiesta delle capacità del sito.
166
+ preflightDeliverySelection(files, { ...options, manifestVersion: 2 });
167
+ const capabilities = resume ? { manifestVersion: resume.manifestVersion || 1 } :
168
+ await onAction({ action: "capabilities" }, { reload: false });
169
+ options.manifestVersion = capabilities.manifestVersion;
170
+ const { sourceBytes } = preflightDeliverySelection(files, options);
120
171
  /* L'impronta si legge PRIMA di creare qualsiasi cosa: se il browser ha perso il
121
172
  permesso su un file, non resta un transfer a metà né un oggetto orfano. */
122
- const fingerprint = await deliverySourceFingerprint(files);
173
+ const fingerprint = await deliverySourceFingerprint(files, options.manifestVersion);
123
174
  const title = draft.title.trim() || deliverySelectionName(files) || "Transfer";
124
175
  if (!target) {
125
176
  change({ title });
126
177
  target = (await onAction({
127
178
  action: "quickCreate", title, filesExpiresAt: expiryFrom(draft.duration),
179
+ ...(downloadLabelsEnabled ? { downloadOriginalsLabel: draft.downloadOriginalsLabel, downloadWebLabel: draft.downloadWebLabel } : {}),
128
180
  }, { reload: false })).delivery;
129
181
  setMade(target);
130
182
  }
@@ -133,18 +185,25 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
133
185
  throw new Error("Questi non sono gli stessi file di prima. Riseleziona esattamente la cartella " +
134
186
  "(o i file) del caricamento interrotto.");
135
187
  }
136
- options = { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled };
188
+ options = { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled, webFormat: resume.webFormat || "webp", manifestVersion: resume.manifestVersion || 1 };
137
189
  }
138
190
  else {
139
191
  const created = await onAction({
140
192
  action: "createPackage", id: target.id, label: title,
141
193
  ...options, sourceFingerprint: fingerprint, fileCount: files.length,
142
- sourceBytes: files.reduce((sum, file) => sum + file.size, 0),
194
+ sourceBytes,
143
195
  }, { reload: false });
144
196
  packageId = created.package.id;
197
+ options.webFormat = created.package.webFormat || "webp";
198
+ }
199
+ const completion = startUpload({ deliveryId: target.id, packageId, title, files, options });
200
+ try {
201
+ await refreshAll(target.id);
202
+ }
203
+ finally {
204
+ startingRef.current = false;
205
+ await completion;
145
206
  }
146
- startUpload({ deliveryId: target.id, packageId, title, files, options });
147
- await refreshAll(target.id);
148
207
  }
149
208
  catch (error) {
150
209
  setFailure(uploadFailure(error) + (packageId
@@ -184,7 +243,7 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
184
243
  if (files.length)
185
244
  void beginUpload(files, resume).finally(() => { input.value = ""; });
186
245
  };
187
- const choose = (input, resume = null) => {
246
+ const choose = (input, resume = resumeRef.current) => {
188
247
  resumeRef.current = resume;
189
248
  if (input)
190
249
  input.value = "";
@@ -203,7 +262,8 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
203
262
  clientName: draft.clientName, description: draft.description,
204
263
  licenseNotes: draft.licenseNotes, ctaEnabled: draft.ctaEnabled,
205
264
  ctaLabel: draft.ctaLabel, ctaUrl: draft.ctaUrl, syncPackageLabel: true,
206
- expiresAt: null,
265
+ ...(downloadLabelsEnabled ? { downloadOriginalsLabel: draft.downloadOriginalsLabel, downloadWebLabel: draft.downloadWebLabel } : {}),
266
+ expiresAt: delivery.expiresAt,
207
267
  filesExpiresAt: draft.duration === "keep"
208
268
  ? delivery.filesExpiresAt : expiryFrom(draft.duration),
209
269
  }, { reload: false })).delivery;
@@ -223,8 +283,9 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
223
283
  action: "createLink", id: delivery.id, label: "Link del cliente", pin: draft.pin,
224
284
  }, { reload: false });
225
285
  }
226
- if (ready)
227
- await onAction({ action: "activate", id: delivery.id }, { reload: false }).catch(() => { });
286
+ if (ready && delivery.status !== "archived") {
287
+ await onAction({ action: "activate", id: delivery.id }, { reload: false });
288
+ }
228
289
  setClean({ ...draft, duration: "keep" });
229
290
  setDraft((current) => ({ ...current, duration: "keep" }));
230
291
  setHeroesClean(heroes);
@@ -324,35 +385,47 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
324
385
  const state = delivery ? transferState(delivery) : null;
325
386
  const expiry = draft.duration === "keep"
326
387
  ? delivery?.filesExpiresAt || null : expiryFrom(draft.duration);
327
- return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsxs("div", { className: "dl-work-title", children: [state && _jsx("span", { className: "dl-pill is-" + state.kind, children: state.label }), _jsx("h2", { children: draft.title.trim() || (delivery ? delivery.title : "Nuovo transfer") })] }), _jsxs("div", { className: "dl-work-actions", children: [link && (_jsx("a", { className: "ak-btn", href: link.url, target: "_blank", rel: "noreferrer", children: "Guarda come lo vede il cliente" })), link && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva" }))] })] }), (failure || notice) && (_jsxs("p", { className: failure ? "ak-error dl-feedback" : "dl-feedback is-ok", children: [failure || notice, _jsx("button", { type: "button", "aria-label": "Chiudi", onClick: () => { setFailure(null); setNotice(null); }, children: "\u00D7" })] })), _jsxs("div", { className: "dl-grid", children: [_jsxs("div", { className: "dl-main", children: [_jsxs("section", { className: "dl-card", children: [_jsxs("div", { className: "dl-pick", children: [_jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(filesRef.current), children: [_jsx(FileIcon, {}), "Aggiungi file"] }), _jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(folderRef.current), children: [_jsx(FolderIcon, {}), "Aggiungi una cartella"] }), _jsx("input", { ref: folderRef, type: "file", multiple: true, hidden: true, onChange: picked }), _jsx("input", { ref: filesRef, type: "file", multiple: true, hidden: true, onChange: picked })] }), _jsx(DropArea, { disabled: uploading, resuming: !!resumeRef.current, onFiles: (files) => {
388
+ return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsxs("div", { className: "dl-work-title", children: [state && _jsx("span", { className: "dl-pill is-" + state.kind, children: state.label }), _jsx("h2", { children: draft.title.trim() || (delivery ? delivery.title : "Nuovo transfer") })] }), _jsxs("div", { className: "dl-work-actions", children: [link && (_jsx("a", { className: "ak-btn", href: link.url, target: "_blank", rel: "noreferrer", children: "Guarda come lo vede il cliente" })), link && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva" }))] })] }), (failure || notice) && (_jsxs("p", { className: failure ? "ak-error dl-feedback" : "dl-feedback is-ok", children: [failure || notice, _jsx("button", { type: "button", "aria-label": "Chiudi", onClick: () => { setFailure(null); setNotice(null); }, children: "\u00D7" })] })), _jsxs("div", { className: "dl-grid", children: [_jsxs("div", { className: "dl-main", children: [_jsxs("section", { className: "dl-card", children: [_jsxs("div", { className: "dl-pick", children: [_jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(filesRef.current), children: [_jsx(FileIcon, {}), "Aggiungi file"] }), _jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(folderRef.current), children: [_jsx(FolderIcon, {}), "Aggiungi una cartella"] }), _jsx("input", { ref: folderRef, type: "file", multiple: true, hidden: true, onChange: picked }), _jsx("input", { ref: filesRef, type: "file", multiple: true, hidden: true, onChange: picked })] }), _jsx("p", { className: "ak-help", children: "Fino a 150 GB di file per invio." }), _jsx(DropArea, { disabled: uploading, resuming: !!resumeRef.current, onFiles: (files) => {
328
389
  const resume = resumeRef.current;
329
390
  resumeRef.current = null;
330
391
  void beginUpload(files, resume);
331
- }, onError: (error) => setFailure(errorText(error)) }), job && isBusy(job) && _jsx(Progress, { job: job }), !!packages.length && (_jsx("ul", { className: "dl-files", children: packages.map((pkg) => (_jsxs("li", { className: "dl-file is-" + pkg.state + (confirmPackage === pkg.id ? " is-confirming" : ""), children: [_jsx("span", { className: "dl-file-icon", "aria-hidden": "true", children: pkg.state === "ready" ? "✓" : "…" }), confirmPackage === pkg.id ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: ["Rimuovo ", pkg.fileCount || "queste", " foto?"] }), _jsx("span", { children: "Spariscono adesso, e non si torna indietro." })] }), _jsxs("span", { className: "dl-file-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => setConfirmPackage(null), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", disabled: uploading, onClick: () => {
392
+ }, onError: (error) => setFailure(errorText(error)) }), job && isBusy(job) && _jsx(Progress, { job: job }), !!packages.length && (_jsx("ul", { className: "dl-files", children: packages.map((pkg) => (_jsxs("li", { className: "dl-file is-" + pkg.state + (confirmPackage === pkg.id ? " is-confirming" : ""), children: [_jsx("span", { className: "dl-file-icon", "aria-hidden": "true", children: pkg.state === "ready" ? "✓" : "…" }), confirmPackage === pkg.id ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: ["Rimuovo ", pkg.fileCount || "questi", " file?"] }), _jsx("span", { children: "Spariscono adesso, e non si torna indietro." })] }), _jsxs("span", { className: "dl-file-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => setConfirmPackage(null), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", disabled: uploading, onClick: () => {
332
393
  setConfirmPackage(null);
333
394
  void run({ action: "deletePackage", id: delivery?.id, packageId: pkg.id }, "File rimossi");
334
- }, children: "S\u00EC, rimuovi" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: [pkg.fileCount || "—", " foto \u00B7 ", formatBytes(pkg.sourceBytes)] }), _jsx("span", { children: pkg.state === "ready"
395
+ }, children: "S\u00EC, rimuovi" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: [pkg.fileCount || "—", " file \u00B7 ", formatBytes(pkg.sourceBytes)] }), _jsx("span", { children: pkg.state === "ready"
335
396
  ? [pkg.galleryEnabled ? "si vedono sulla pagina" : "", pkg.webEnabled ? "versione leggera" : ""]
336
397
  .filter(Boolean).join(" · ") || "solo originali"
337
398
  : job?.id === pkg.id && isBusy(job)
338
399
  ? (job.state === "paused" ? "caricamento in pausa" : "caricamento in corso")
339
- : "caricamento interrotto" })] }), _jsxs("span", { className: "dl-file-actions", children: [pkg.state !== "ready" && !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn", disabled: uploading, onClick: () => { resumeRef.current = pkg; setNotice(resumeHint); }, children: "Riprendi" })), !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: uploading, onClick: () => setConfirmPackage(pkg.id), children: "Rimuovi" }))] })] }))] }, pkg.id))) }))] }), _jsxs("section", { className: "dl-card dl-about", children: [_jsx(Field, { label: "Nome del transfer", hint: "Lo vede il cliente, ed \u00E8 il nome dello ZIP che scarica.", children: _jsx("input", { value: draft.title, placeholder: "Es. Matrimonio Rossi", onChange: (e) => change({ title: e.target.value }) }) }), _jsx(Field, { label: "Per", hint: "Il nome del cliente, grande in cima alla pagina.", children: _jsx("input", { value: draft.clientName, placeholder: "Es. Marta Rossi", onChange: (e) => change({ clientName: e.target.value }) }) }), _jsx(Field, { label: "Messaggio", wide: true, hint: "Due righe di accompagnamento. Si pu\u00F2 lasciare vuoto.", children: _jsx("textarea", { rows: 3, value: draft.description, onChange: (e) => change({ description: e.target.value }) }) })] }), link ? (_jsxs("section", { className: "dl-card dl-link", children: [_jsxs("div", { className: "dl-link-head", children: [_jsx("span", { className: "dl-step", children: "Il link per il cliente" }), _jsx("code", { children: link.url })] }), _jsxs("div", { className: "dl-link-actions", children: [_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => void copy(link.url, "link"), children: copied === "link" ? "Copiato" : "Copia il link" }), _jsx("button", { className: "ak-btn", onClick: () => void copy(messageFor(draft, link, expiry), "messaggio"), children: copied === "messaggio" ? "Copiato" : "Copia il messaggio" }), _jsx("a", { className: "ak-btn", target: "_blank", rel: "noreferrer", href: `https://wa.me/?text=${encodeURIComponent(messageFor(draft, link, expiry))}`, children: "Apri in WhatsApp" })] }), _jsx("p", { className: "dl-link-stats", children: linkStory(link) }), draft.pin && (_jsxs("p", { className: "dl-link-stats", children: ["Codice di accesso ", _jsx("strong", { children: draft.pin }), " \u2014 \u00E8 gi\u00E0 dentro il messaggio."] }))] })) : (_jsxs("section", { className: "dl-card dl-get", children: [_jsx("button", { className: "ak-btn ak-btn-primary dl-get-btn", disabled: !delivery || saving || busy, onClick: () => void save(), children: saving ? "Preparo…" : "Ottieni il link" }), _jsx("p", { children: delivery
340
- ? "Il link vale già mentre l'upload continua: chi lo apre vede che le foto stanno arrivando."
400
+ : "caricamento interrotto" })] }), _jsxs("span", { className: "dl-file-actions", children: [pkg.state !== "ready" && !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn", disabled: uploading, onClick: () => { resumeRef.current = pkg; setNotice(resumeHint); }, children: "Riprendi" })), !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: uploading, onClick: () => setConfirmPackage(pkg.id), children: "Rimuovi" }))] })] }))] }, pkg.id))) }))] }), _jsxs("section", { className: "dl-card dl-about", children: [_jsx(Field, { label: "Nome del transfer", hint: "Lo vede il cliente, ed \u00E8 il nome dello ZIP che scarica.", children: _jsx("input", { value: draft.title, placeholder: "Es. Matrimonio Rossi", onChange: (e) => change({ title: e.target.value }) }) }), _jsx(Field, { label: "Per", hint: "Il nome del cliente, grande in cima alla pagina.", children: _jsx("input", { value: draft.clientName, placeholder: "Es. Marta Rossi", onChange: (e) => change({ clientName: e.target.value }) }) }), _jsx(Field, { label: "Messaggio", wide: true, hint: "Due righe di accompagnamento. Si pu\u00F2 lasciare vuoto.", children: _jsx("textarea", { rows: 3, value: draft.description, onChange: (e) => change({ description: e.target.value }) }) })] }), link ? (_jsxs("section", { className: "dl-card dl-link", children: [_jsxs("div", { className: "dl-link-head", children: [_jsx("span", { className: "dl-step", children: "Il link per il cliente" }), _jsx("code", { children: link.url })] }), _jsxs("div", { className: "dl-link-actions", children: [delivery?.canShortenLinks && link.token.length > 16 && (_jsx("button", { className: "ak-btn", disabled: dirty || saving || busy, title: dirty ? "Salva prima le modifiche del transfer" : "Il vecchio indirizzo continuerà a funzionare", onClick: async () => {
401
+ setSaving(true);
402
+ try {
403
+ await run({ action: "shortenLink", id: delivery.id, linkId: link.id }, "Link accorciato. Anche il vecchio indirizzo resta valido.");
404
+ }
405
+ finally {
406
+ setSaving(false);
407
+ }
408
+ }, children: "Accorcia link" })), _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => void copy(link.url, "link"), children: copied === "link" ? "Copiato" : "Copia il link" }), _jsx("button", { className: "ak-btn", onClick: () => void copy(messageFor(draft, link, expiry), "messaggio"), children: copied === "messaggio" ? "Copiato" : "Copia il messaggio" }), _jsx("a", { className: "ak-btn", target: "_blank", rel: "noreferrer", href: `https://wa.me/?text=${encodeURIComponent(messageFor(draft, link, expiry))}`, children: "Apri in WhatsApp" })] }), _jsx("p", { className: "dl-link-stats", children: linkStory(link) }), draft.pin && (_jsxs("p", { className: "dl-link-stats", children: ["Codice di accesso ", _jsx("strong", { children: draft.pin }), " \u2014 \u00E8 gi\u00E0 dentro il messaggio."] }))] })) : (_jsxs("section", { className: "dl-card dl-get", children: [_jsx("button", { className: "ak-btn ak-btn-primary dl-get-btn", disabled: !delivery || saving || busy, onClick: () => void save(), children: saving ? "Preparo…" : "Ottieni il link" }), _jsx("p", { children: delivery
409
+ ? "Il link vale già mentre l'upload continua: chi lo apre vede che i file stanno arrivando."
341
410
  : "Aggiungi i file: il caricamento parte subito e il resto si compila mentre va." })] }))] }), _jsxs("div", { className: "dl-side", children: [_jsxs(Group, { title: "Chi pu\u00F2 aprirlo", children: [_jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !draft.pin, onChange: () => change({ pin: "" }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiunque abbia il link" }), _jsx("small", { children: "Il link \u00E8 lungo e casuale: non si indovina." })] })] }), _jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !!draft.pin, onChange: () => change({ pin: draft.pin || makeCode() }) }), _jsxs("span", { children: [_jsx("strong", { children: "Serve anche un codice" }), _jsx("small", { children: "Da mandare insieme al link. Dopo dieci errori si blocca per un'ora." })] })] }), !!draft.pin && (_jsxs("div", { className: "dl-code", children: [_jsx("input", { value: draft.pin, spellCheck: false, onChange: (e) => change({ pin: e.target.value.toUpperCase() }) }), _jsx("button", { className: "ak-btn", onClick: () => change({ pin: makeCode() }), children: "Altro" })] }))] }), _jsxs(Group, { title: "Cosa riceve", note: packages.length
342
411
  ? "Si decide prima di caricare: per cambiarle bisognerebbe ricaricare i file."
343
- : undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e un Transfer." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] })] }), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
412
+ : undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e un Transfer." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] }), draft.light && (_jsxs("label", { className: "dl-field", children: [_jsx("span", { children: "Formato della versione leggera" }), _jsxs("select", { className: "dl-select", "aria-label": "Formato della versione leggera", value: draft.webFormat, disabled: !!packages.length || uploading, onChange: (e) => change({ webFormat: e.target.value === "jpg" ? "jpg" : "webp" }), children: [_jsx("option", { value: "webp", children: "WebP \u2014 file pi\u00F9 piccoli" }), _jsx("option", { value: "jpg", disabled: !jpegEnabled && draft.webFormat !== "jpg", children: "JPG \u2014 massima compatibilit\u00E0" })] }), _jsxs("small", { children: ["2048 px sul lato lungo, senza ingrandire le foto pi\u00F9 piccole. Qualit\u00E0 ", draft.webFormat === "jpg" ? "82%" : "80%", "."] })] }))] }), downloadLabelsEnabled && (_jsxs(Group, { title: "Testi dei pulsanti di download", note: "Lascia vuoto per usare il testo predefinito. Puoi cambiarli anche dopo l\u2019invio, senza ricaricare i file.", children: [delivery?.status !== "archived" && (_jsx(Field, { label: "Pulsante degli originali", children: _jsx("input", { value: draft.downloadOriginalsLabel, maxLength: 80, placeholder: "Scarica i file", onChange: (e) => change({ downloadOriginalsLabel: e.target.value }) }) })), (packages.length ? packages.some(pkg => pkg.webEnabled) : draft.light) && (_jsx(Field, { label: "Pulsante della versione leggera", children: _jsx("input", { value: draft.downloadWebLabel, maxLength: 80, placeholder: "Versione leggera", onChange: (e) => change({ downloadWebLabel: e.target.value }) }) }))] })), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
344
413
  ? `Fino al ${formatDate(delivery.filesExpiresAt)}`
345
414
  : "Per sempre" }), _jsx("option", { value: "7", children: "7 giorni" }), _jsx("option", { value: "30", children: "30 giorni" }), _jsx("option", { value: "180", children: "6 mesi" }), _jsx("option", { value: "365", children: "1 anno" }), _jsx("option", { value: "forever", children: "Per sempre" })] }), _jsx("p", { className: "dl-note", children: expiry
346
415
  ? `Dopo il ${formatDate(expiry)} il link continua ad aprirsi, ma i file non si scaricano più.`
347
416
  : "I file restano finché non li togli tu." })] }), _jsxs(Group, { title: "Alla fine della pagina", children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.ctaEnabled, onChange: (e) => change({ ctaEnabled: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiedi una recensione" }), _jsx("small", { children: "Un pulsante in fondo, dopo le fotografie." })] })] }), draft.ctaEnabled && (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Testo del pulsante", children: _jsx("input", { value: draft.ctaLabel, onChange: (e) => change({ ctaLabel: e.target.value }) }) }), _jsx(Field, { label: "Indirizzo", hint: "Il link alla tua pagina delle recensioni.", children: _jsx("input", { type: "url", value: draft.ctaUrl, placeholder: "https://\u2026", onChange: (e) => change({ ctaUrl: e.target.value }) }) })] })), _jsx(Field, { label: "Note d'uso", hint: "Cosa pu\u00F2 farci il cliente. Appaiono in fondo alla pagina.", children: _jsx("textarea", { rows: 3, value: draft.licenseNotes, onChange: (e) => change({ licenseNotes: e.target.value }) }) })] })] })] }), !!delivery && (_jsxs("section", { className: "dl-card dl-cover", children: [_jsxs("div", { className: "dl-cover-head", children: [_jsxs("div", { children: [_jsx("span", { className: "dl-step", children: "Copertina" }), _jsxs("p", { children: ["Le foto grandi in cima alla pagina del cliente, fino a otto.", " ", heroes.length
348
417
  ? `Ne hai scelte ${heroes.length}.`
349
- : "Nessuna scelta: si userà quella predefinita."] })] }), _jsxs("div", { className: "dl-cover-actions", children: [_jsx("button", { className: "ak-btn", disabled: saving || !ready, onClick: () => { if (coverRef.current)
350
- coverRef.current.value = ""; coverRef.current?.click(); }, children: "Carica un'immagine" }), !!heroes.length && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setHeroes([]), children: "Usa quella predefinita" })), _jsx("input", { ref: coverRef, type: "file", accept: "image/jpeg,image/png,image/webp", multiple: true, hidden: true, onChange: (event) => void uploadCovers(event) })] })] }), photos.length ? (_jsx("div", { className: "dl-cover-grid", children: photos.map(({ pkg, image }) => {
418
+ : "Nessuna scelta: si userà quella predefinita."] }), _jsx("p", { role: "status", children: heroesDirty
419
+ ? "Scelta non ancora salvata: premi Salva modifiche per mostrarla al cliente."
420
+ : heroes.length
421
+ ? "Copertine salvate. Una sola immagine resta fissa; più immagini compongono la sequenza."
422
+ : "Scegli le copertine qui: la galleria sotto nella pagina del cliente è separata." })] }), _jsxs("div", { className: "dl-cover-actions", children: [!!photos.length && (_jsx("button", { className: "ak-btn", onClick: () => setPreviewAttempt(value => value + 1), title: "Riprova a caricare le miniature senza cambiare le copertine selezionate", children: "Ricarica anteprime" })), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva modifiche" }), _jsx("button", { className: "ak-btn", disabled: saving || !ready, onClick: () => { if (coverRef.current)
423
+ coverRef.current.value = ""; coverRef.current?.click(); }, children: "Carica un'immagine" }), !!heroes.length && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: saving, onClick: () => setHeroes([]), children: "Usa quella predefinita" })), _jsx("input", { ref: coverRef, type: "file", accept: "image/jpeg,image/png,image/webp", multiple: true, hidden: true, onChange: (event) => void uploadCovers(event) })] })] }), packages.some(pkg => (moreImages[pkg.id] ? moreImages[pkg.id].nextCursor : pkg.nextCursor) != null) && (_jsx("button", { className: "ak-btn", disabled: loadingImages, onClick: () => void loadMoreImages(), children: loadingImages ? "Caricamento…" : "Mostra altre foto" })), photos.length ? (_jsx("div", { className: "dl-cover-grid", children: photos.map(({ pkg, image }) => {
351
424
  const chosen = heroes.some((item) => item.type === "package" && item.packageId === pkg.id && item.index === image.index);
352
- return (_jsxs("button", { type: "button", className: chosen ? "is-chosen" : "", "aria-pressed": chosen, onClick: () => toggleCover(pkg.id, image.index, image.previewUrl), children: [_jsx("img", { src: image.previewUrl, alt: image.name, loading: "lazy" }), chosen && _jsx("em", { "aria-hidden": "true", children: "\u2713" })] }, `${pkg.id}-${image.index}`));
425
+ return (_jsxs("button", { type: "button", className: chosen ? "is-chosen" : "", disabled: saving, "aria-pressed": chosen, onClick: () => toggleCover(pkg.id, image.index, image.previewUrl), children: [_jsx("img", { src: previewAttempt ? `${image.previewUrl}&previewRetry=${previewAttempt}` : image.previewUrl, alt: image.name, width: image.width, height: image.height, loading: "lazy" }), chosen && _jsx("em", { "aria-hidden": "true", children: "\u2713" })] }, `${pkg.id}-${image.index}`));
353
426
  }) })) : (_jsx("p", { className: "dl-note dl-cover-empty", children: heroes.some((item) => item.type === "object")
354
427
  ? `${heroes.length} immagini caricate a mano.`
355
- : "Questo transfer non ha anteprime da cui scegliere: carica un'immagine, oppure resta quella predefinita." }))] })), !!delivery && (_jsxs("section", { className: "dl-danger", children: [_jsxs("div", { children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void duplicate(), children: "Usa come modello" }), delivery.status === "active" && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void run({ action: "archive", id: delivery.id }, "Originali tolti: recuperabili per sette giorni"), children: "Libera spazio: togli gli originali" }))] }), confirmDelete ? (_jsxs("div", { className: "dl-confirm", children: [_jsxs("p", { children: ["Elimino ", _jsx("strong", { children: delivery.title }), photos.length ? ` (${countPhotos(packages)} foto, ${formatBytes(delivery.storedBytes)})` : "", "? Il link smette subito di funzionare e i file vengono cancellati adesso. Non si torna indietro."] }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmDelete(false), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => void remove(), children: "S\u00EC, elimina" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmDelete(true), children: "Elimina il transfer" }))] }))] }));
428
+ : "Questo transfer non ha anteprime da cui scegliere: carica un'immagine, oppure resta quella predefinita." }))] })), !!delivery && (_jsxs("section", { className: "dl-danger", children: [_jsxs("div", { children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void duplicate(), children: "Usa come modello" }), delivery.status === "active" && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void run({ action: "archive", id: delivery.id }, "Originali tolti: recuperabili per sette giorni"), children: "Libera spazio: togli gli originali" }))] }), confirmDelete ? (_jsxs("div", { className: "dl-confirm", children: [_jsxs("p", { children: ["Elimino ", _jsx("strong", { children: delivery.title }), photos.length ? ` (${countPhotos(packages)} file, ${formatBytes(delivery.storedBytes)})` : "", "? Il link smette subito di funzionare e i file vengono cancellati adesso. Non si torna indietro."] }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmDelete(false), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => void remove(), children: "S\u00EC, elimina" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmDelete(true), children: "Elimina il transfer" }))] }))] }));
356
429
  }
357
430
  /* ============================================================================================
358
431
  Impostazioni
@@ -419,9 +492,11 @@ function Progress({ job }) {
419
492
  }
420
493
  const done = progress.totalFiles ? Math.min(1, progress.doneFiles / progress.totalFiles) : 0;
421
494
  const phase = job.state === "paused" ? "In pausa"
422
- : progress.phase === "originals" ? "Carico le foto"
423
- : progress.phase === "derivatives" ? "Preparo le anteprime"
424
- : "Ultimi controlli";
495
+ : job.state === "cancelling" ? "Annullamento in corso"
496
+ : job.waitingForNetwork ? "Attendo la connessione e riprovo…"
497
+ : progress.phase === "originals" ? "Carico gli originali"
498
+ : progress.phase === "derivatives" ? "Preparo le anteprime"
499
+ : "Ultimi controlli";
425
500
  const seconds = progress.transferStartedAt
426
501
  ? Math.max(1, (Date.now() - progress.transferStartedAt) / 1000) : 0;
427
502
  const mbps = seconds ? progress.transferredBytes * 8 / seconds / 1_000_000 : 0;
@@ -440,12 +515,15 @@ function draftOf(delivery, keep) {
440
515
  return {
441
516
  title: "", clientName: "", description: "", licenseNotes: "",
442
517
  ctaEnabled: false, ctaLabel: "Lascia una recensione", ctaUrl: "",
443
- duration: "30", pin: "", gallery: true, light: true,
518
+ duration: "30", pin: "", gallery: true, light: true, webFormat: "webp",
519
+ downloadOriginalsLabel: "", downloadWebLabel: "",
444
520
  };
445
521
  }
446
522
  const link = primaryLink(delivery);
447
523
  return {
448
524
  title: delivery.title,
525
+ downloadOriginalsLabel: delivery.downloadOriginalsLabel || "",
526
+ downloadWebLabel: delivery.downloadWebLabel || "",
449
527
  clientName: delivery.clientName,
450
528
  description: delivery.description,
451
529
  licenseNotes: delivery.licenseNotes,
@@ -458,6 +536,7 @@ function draftOf(delivery, keep) {
458
536
  transfer che ha già i file, la verità è quella dei file. */
459
537
  gallery: delivery.packages[0]?.galleryEnabled ?? keep?.gallery ?? true,
460
538
  light: delivery.packages[0]?.webEnabled ?? keep?.light ?? true,
539
+ webFormat: delivery.packages.length ? (delivery.packages[0].webFormat || "webp") : keep?.webFormat || "webp",
461
540
  };
462
541
  }
463
542
  function same(a, b) {
@@ -506,7 +585,7 @@ function makeCode() {
506
585
  function messageFor(draft, link, expiry) {
507
586
  const who = draft.clientName.trim().split(/\s+/)[0];
508
587
  return [
509
- `Ciao${who ? ` ${who}` : ""}, le fotografie di ${draft.title.trim() || "questo lavoro"} sono pronte:`,
588
+ `Ciao${who ? ` ${who}` : ""}, i file di ${draft.title.trim() || "questo lavoro"} sono pronti:`,
510
589
  link.url,
511
590
  draft.pin ? `Codice di accesso: ${draft.pin}` : "",
512
591
  expiry ? `Puoi scaricarle fino al ${formatDate(expiry)}.` : "",
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
11
11
  * sempre.
12
12
  */
13
13
  import { useEffect, useState } from "react";
14
- import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, useUploads, } from "../lib/upload-queue";
14
+ import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, retryUpload, useUploads, } from "../lib/upload-queue";
15
15
  import { dismissMediaUpload, useMediaUploads, } from "../../form/media/media-upload-queue.js";
16
16
  export function UploadDock() {
17
17
  const jobs = useUploads();
@@ -28,7 +28,7 @@ export function UploadDock() {
28
28
  }, [busy]);
29
29
  if (!jobs.length && !mediaJobs.length)
30
30
  return null;
31
- return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs, mediaJobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (_jsxs("ul", { className: "uk-list", children: [mediaJobs.map((job) => _jsx(MediaJob, { job: job }, job.id)), jobs.map((job) => (_jsxs("li", { className: "uk-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: line(job) })] }), isBusy(job) && (_jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${percent(job)}%` } }) })), job.error && _jsx("p", { className: "uk-error", children: job.error }), _jsxs("div", { className: "uk-job-actions", children: [job.state === "running" && (_jsx("button", { className: "ak-btn", onClick: () => pauseUpload(job.id), children: "Pausa" })), job.state === "paused" && (_jsx("button", { className: "ak-btn", onClick: () => resumeUpload(job.id), children: "Riprendi" })), isBusy(job) && (confirmStop === job.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "uk-confirm", children: "I file gi\u00E0 caricati verranno buttati." }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmStop(null), children: "No" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => { cancelUpload(job.id); setConfirmStop(null); }, children: "Annulla il caricamento" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmStop(job.id), children: "Annulla" }))), !isBusy(job) && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissUpload(job.id), children: "Togli dall'elenco" }))] })] }, job.id)))] }))] }));
31
+ return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs, mediaJobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (_jsxs("ul", { className: "uk-list", children: [mediaJobs.map((job) => _jsx(MediaJob, { job: job }, job.id)), jobs.map((job) => (_jsxs("li", { className: "uk-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: line(job) })] }), isBusy(job) && (_jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${percent(job)}%` } }) })), job.error && _jsx("p", { className: "uk-error", children: job.error }), _jsxs("div", { className: "uk-job-actions", children: [job.state === "running" && (_jsx("button", { className: "ak-btn", onClick: () => pauseUpload(job.id), children: "Pausa" })), job.state === "paused" && (_jsx("button", { className: "ak-btn", onClick: () => resumeUpload(job.id), children: "Riprendi" })), isBusy(job) && job.state !== "cancelling" && (confirmStop === job.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "uk-confirm", children: "I file gi\u00E0 caricati verranno buttati." }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmStop(null), children: "No" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => { cancelUpload(job.id); setConfirmStop(null); }, children: "Annulla il caricamento" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmStop(job.id), children: "Annulla" }))), job.state === "failed" && job.retryable && _jsx("button", { className: "ak-btn", onClick: () => void retryUpload(job.id), children: "Riprova" }), !isBusy(job) && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissUpload(job.id), children: "Togli dall'elenco" }))] })] }, job.id)))] }))] }));
32
32
  }
33
33
  function MediaJob({ job }) {
34
34
  const conversionPercent = job.total ? (job.conversionDone / job.total) * 100 : 0;
@@ -68,10 +68,14 @@ function line(job) {
68
68
  ? `Pronto · ${job.skipped} senza anteprima`
69
69
  : "Pronto";
70
70
  }
71
+ if (job.state === "cancelling")
72
+ return "Annullamento in corso";
71
73
  if (job.state === "failed")
72
74
  return "Interrotto";
73
75
  if (job.state === "paused")
74
76
  return "In pausa";
77
+ if (job.waitingForNetwork)
78
+ return "Attendo la connessione e riprovo…";
75
79
  const progress = job.progress;
76
80
  if (!progress)
77
81
  return "Preparo…";
@@ -0,0 +1,20 @@
1
+ import { type D1DatabaseLike, type D1Statement } from "./persistence.js";
2
+ import { type ImmutableReleaseExport } from "./release.js";
3
+ /** A SELECT guard belongs inside the batch: a failed conditional must abort
4
+ * every mutation, including when an UPDATE would silently affect zero rows. */
5
+ export declare function transactionAssertion(db: D1DatabaseLike, condition: string, values?: unknown[]): D1Statement;
6
+ export declare function previewCandidateChecksum(baseReleaseId: string, entries: readonly {
7
+ documentId: string;
8
+ revisionId: string;
9
+ payloadChecksum: string;
10
+ }[]): Promise<string>;
11
+ export declare function commitDirectRelease(db: D1DatabaseLike, input: {
12
+ release: ImmutableReleaseExport;
13
+ expectedBaseReleaseId: string;
14
+ selectedDrafts: readonly {
15
+ documentId: string;
16
+ revisionId: string;
17
+ }[];
18
+ authorSubject: string;
19
+ now: number;
20
+ }): Promise<void>;