@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.
- package/dist/app/app.css +3 -3
- package/dist/app/delivery.d.ts +11 -0
- package/dist/app/delivery.js +46 -0
- package/dist/app/index.d.ts +4 -1
- package/dist/app/index.js +3 -2
- package/dist/app/lib/deliveries.d.ts +8 -0
- package/dist/app/lib/delivery-network.d.ts +25 -0
- package/dist/app/lib/delivery-network.js +159 -0
- package/dist/app/lib/delivery-upload.d.ts +29 -11
- package/dist/app/lib/delivery-upload.js +408 -131
- package/dist/app/lib/upload-queue.d.ts +21 -3
- package/dist/app/lib/upload-queue.js +76 -19
- package/dist/app/routes/Deliveries.js +108 -29
- package/dist/app/routes/UploadDock.js +6 -2
- package/dist/direct-publication.d.ts +20 -0
- package/dist/direct-publication.js +75 -0
- package/dist/functions/_shared/b2.js +22 -3
- package/dist/functions/_shared/delivery-crypto.js +5 -0
- package/dist/functions/_shared/delivery-limits.d.ts +9 -0
- package/dist/functions/_shared/delivery-limits.js +29 -0
- package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
- package/dist/functions/_shared/delivery-manifest.js +392 -0
- package/dist/functions/_shared/delivery-purge.js +17 -1
- package/dist/functions/api/deliveries.js +177 -49
- package/dist/functions/api/delivery/image.js +11 -6
- package/dist/functions/api/delivery/multipart.js +196 -64
- package/dist/functions/delivery-public/assets/client.js +246 -0
- package/dist/functions/delivery-public/assets/delivery.css +297 -0
- package/dist/functions/delivery-public/assets/gallery.js +243 -0
- package/dist/functions/delivery-public/assets/stage.js +107 -0
- package/dist/functions/delivery-public/index.js +637 -0
- package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
- package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
- package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
- package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
- package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
- package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
- package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
- package/dist/functions/index.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
const
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 })
|
|
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 || "
|
|
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 || "—", "
|
|
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: [
|
|
340
|
-
|
|
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."] })
|
|
350
|
-
|
|
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)}
|
|
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
|
-
:
|
|
423
|
-
:
|
|
424
|
-
: "
|
|
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}` : ""},
|
|
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>;
|