@cardinali-studio/adminkit 2.2.11 → 2.2.13
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/api.d.ts +43 -2
- package/dist/api.js +13 -3
- package/dist/app/App.js +11 -5
- package/dist/app/app.css +6 -0
- package/dist/app/lib/publish.js +4 -1
- package/dist/app/lib/settings.d.ts +2 -0
- package/dist/app/lib/settings.js +54 -8
- package/dist/app/routes/MediaSettingsEditor.d.ts +4 -0
- package/dist/app/routes/MediaSettingsEditor.js +4 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +3 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/routes/UploadDock.js +31 -9
- package/dist/app/site.d.ts +4 -0
- package/dist/deployment.js +14 -1
- package/dist/form/media/MediaFolders.d.ts +5 -1
- package/dist/form/media/MediaFolders.js +8 -7
- package/dist/form/media/MediaGrid.js +32 -17
- package/dist/form/media/UploadDialog.js +11 -5
- package/dist/form/media/image-conversion.d.ts +14 -0
- package/dist/form/media/image-conversion.js +163 -0
- package/dist/form/media/media-model.d.ts +41 -0
- package/dist/form/media/media-model.js +78 -0
- package/dist/form/media/media-move.d.ts +48 -0
- package/dist/form/media/media-move.js +92 -0
- package/dist/form/media/media-upload-queue.d.ts +31 -0
- package/dist/form/media/media-upload-queue.js +230 -0
- package/dist/form/media/useMediaLibrary.d.ts +12 -0
- package/dist/form/media/useMediaLibrary.js +215 -78
- package/dist/functions/_shared/assets.js +3 -2
- package/dist/functions/api/assets/folders.js +2 -1
- package/dist/functions/api/assets/settings.js +40 -0
- package/dist/functions/api/assets/upload.js +4 -1
- package/dist/functions/api/assets.js +4 -3
- package/dist/functions/api/site-access.js +78 -0
- package/dist/functions/index.js +4 -0
- package/dist/functions/site.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/migrations/0004_site_access.sql +41 -0
- package/dist/persistence.d.ts +1 -0
- package/dist/persistence.js +22 -4
- package/dist/release.d.ts +3 -0
- package/dist/release.js +7 -0
- package/dist/site-access.d.ts +48 -0
- package/dist/site-access.js +180 -0
- package/dist/theme.css +4 -0
- package/package.json +4 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export function mediaMoveNeedsConfirmation(plan, multiFileThreshold = 2) {
|
|
2
|
+
return plan.folders.length > 0 || plan.directFiles.length >= multiFileThreshold;
|
|
3
|
+
}
|
|
4
|
+
export function mediaMoveConfirmation(plan, destinationLabel) {
|
|
5
|
+
const selectedParts = [
|
|
6
|
+
plan.directFiles.length
|
|
7
|
+
? `${plan.directFiles.length} file ${plan.directFiles.length === 1 ? "selezionato direttamente" : "selezionati direttamente"}`
|
|
8
|
+
: "",
|
|
9
|
+
plan.folders.length ? `${plan.folders.length} ${plan.folders.length === 1 ? "cartella" : "cartelle"}` : "",
|
|
10
|
+
].filter(Boolean).join(" e ");
|
|
11
|
+
return `Stai per spostare ${selectedParts} in ${destinationLabel}.\n\n`
|
|
12
|
+
+ `File complessivi coinvolti, inclusi i discendenti: ${plan.totalFiles}.\n\n`
|
|
13
|
+
+ "Gli indirizzi pubblici cambieranno e i riferimenti gestiti verranno aggiornati. Continuare?";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Esegue un piano già validato. Al primo errore non avvia altri gruppi né applica i riferimenti;
|
|
17
|
+
* le copie riuscite restano duplicati sicuri e gli originali restano disponibili.
|
|
18
|
+
*/
|
|
19
|
+
export async function executeMediaMovePlan(plan, destination, executor, onProgress, chunkSize = 10) {
|
|
20
|
+
for (const folder of plan.folders) {
|
|
21
|
+
const created = await executor.createFolder(destination, folder.name, { label: folder.name });
|
|
22
|
+
if (created.exists || created.prefix !== folder.target) {
|
|
23
|
+
throw new Error(`La destinazione della cartella «${folder.name}» non è più disponibile.`);
|
|
24
|
+
}
|
|
25
|
+
for (const sub of [...folder.folders].sort((a, b) => a.prefix.length - b.prefix.length)) {
|
|
26
|
+
const rel = sub.prefix.slice(folder.source.length);
|
|
27
|
+
const subName = rel.replace(/\/$/, "").split("/").pop();
|
|
28
|
+
await executor.createFolder(folder.target + rel.replace(/[^/]+\/$/, ""), sub.label || subName, {
|
|
29
|
+
label: sub.label || subName,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const transfers = [];
|
|
34
|
+
for (const folder of plan.folders) {
|
|
35
|
+
const byDir = new Map();
|
|
36
|
+
for (const file of folder.files) {
|
|
37
|
+
const rel = file.key.slice(folder.source.length);
|
|
38
|
+
const cut = rel.lastIndexOf("/");
|
|
39
|
+
const dir = cut >= 0 ? rel.slice(0, cut + 1) : "";
|
|
40
|
+
const keys = byDir.get(dir) ?? [];
|
|
41
|
+
keys.push(file.key);
|
|
42
|
+
byDir.set(dir, keys);
|
|
43
|
+
}
|
|
44
|
+
for (const [dir, keys] of byDir)
|
|
45
|
+
transfers.push({ keys, destination: folder.target + dir });
|
|
46
|
+
}
|
|
47
|
+
if (plan.directFiles.length)
|
|
48
|
+
transfers.push({ keys: plan.directFiles, destination });
|
|
49
|
+
let done = 0;
|
|
50
|
+
const urlMap = {};
|
|
51
|
+
onProgress(done, plan.totalFiles);
|
|
52
|
+
for (const transfer of transfers) {
|
|
53
|
+
for (let i = 0; i < transfer.keys.length; i += chunkSize) {
|
|
54
|
+
const group = transfer.keys.slice(i, i + chunkSize);
|
|
55
|
+
let result;
|
|
56
|
+
try {
|
|
57
|
+
result = await executor.move(group, transfer.destination);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const notStarted = Math.max(0, plan.totalFiles - done - group.length);
|
|
61
|
+
throw new Error(`Spostamento interrotto: ${done} file copiati, ${group.length} con esito non confermato, ${notStarted} non iniziati. `
|
|
62
|
+
+ `Gli originali sono stati conservati. Dettaglio: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
Object.assign(urlMap, result.urlMap ?? {});
|
|
65
|
+
done += result.moved ?? 0;
|
|
66
|
+
onProgress(done, plan.totalFiles);
|
|
67
|
+
if (result.failed?.length || (result.moved ?? 0) !== group.length) {
|
|
68
|
+
const failed = result.failed?.length ?? group.length - (result.moved ?? 0);
|
|
69
|
+
const notStarted = Math.max(0, plan.totalFiles - done - failed);
|
|
70
|
+
throw new Error(`Spostamento interrotto: ${done} file copiati, ${failed} falliti, ${notStarted} non iniziati. `
|
|
71
|
+
+ "Gli originali sono stati conservati; le copie già create possono restare come duplicati.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (Object.keys(urlMap).length) {
|
|
76
|
+
let applied;
|
|
77
|
+
try {
|
|
78
|
+
applied = await executor.applyReferences(urlMap);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`Tutte le ${done} copie sono state create, ma i riferimenti non sono stati applicati. `
|
|
82
|
+
+ `Gli originali sono stati conservati e le copie possono restare come duplicati. Dettaglio: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
if (applied.failed?.length) {
|
|
85
|
+
throw new Error(`Spostamento applicato con ${applied.failed.length} originali rimasti come duplicati.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (plan.folders.length) {
|
|
89
|
+
await executor.removeSourceFolders(plan.folders.map((folder) => folder.source));
|
|
90
|
+
}
|
|
91
|
+
return { completed: done, total: plan.totalFiles };
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type MediaUploadState = "running" | "done" | "failed";
|
|
2
|
+
export interface MediaUploadJob {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
prefix: string;
|
|
6
|
+
state: MediaUploadState;
|
|
7
|
+
total: number;
|
|
8
|
+
conversionDone: number;
|
|
9
|
+
uploadDone: number;
|
|
10
|
+
converted: number;
|
|
11
|
+
originals: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
autoWebp: boolean;
|
|
14
|
+
errors: string[];
|
|
15
|
+
}
|
|
16
|
+
interface StartInput {
|
|
17
|
+
prefix: string;
|
|
18
|
+
title: string;
|
|
19
|
+
files: File[];
|
|
20
|
+
autoWebp: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function useMediaUploads(): MediaUploadJob[];
|
|
23
|
+
export declare function useMediaUploadCompletions(): number;
|
|
24
|
+
/** Snapshot non mutabile per diagnostica e test della coda fuori da React. */
|
|
25
|
+
export declare function mediaUploadsSnapshot(): MediaUploadJob[];
|
|
26
|
+
export declare function dismissMediaUpload(id: string): void;
|
|
27
|
+
/** Accoda e torna subito: la finestra di scelta può chiudersi, il lavoro resta nel dock. */
|
|
28
|
+
export declare function startMediaUpload(input: StartInput): string;
|
|
29
|
+
export declare function adaptiveConversionConcurrency(files: File[], enabled?: boolean): number;
|
|
30
|
+
export declare function adaptiveUploadConcurrency(): number;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* Upload persistenti della libreria Media.
|
|
2
|
+
*
|
|
3
|
+
* Come per i transfer, lo stato vive fuori da React: cambiare rotta nell'admin non smonta la
|
|
4
|
+
* conversione, non perde l'avanzamento e non nasconde gli errori. Le due fasi hanno pool
|
|
5
|
+
* distinti e una coda limitata fra loro: conversione e rete si sovrappongono, ma non si
|
|
6
|
+
* accumulano in RAM decine di WebP già pronti.
|
|
7
|
+
*/
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
import { api, ApiError } from "../../api.js";
|
|
10
|
+
import { convertMediaImage, isConvertibleImage, jpegCapturedAt } from "./image-conversion.js";
|
|
11
|
+
let jobs = [];
|
|
12
|
+
let completions = 0;
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
function emit() {
|
|
15
|
+
jobs = [...jobs];
|
|
16
|
+
for (const listener of listeners)
|
|
17
|
+
listener();
|
|
18
|
+
}
|
|
19
|
+
function patch(id, change) {
|
|
20
|
+
jobs = jobs.map((job) => job.id === id ? { ...job, ...change } : job);
|
|
21
|
+
emit();
|
|
22
|
+
}
|
|
23
|
+
function current(id) {
|
|
24
|
+
return jobs.find((job) => job.id === id);
|
|
25
|
+
}
|
|
26
|
+
function subscribe(listener) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => { listeners.delete(listener); };
|
|
29
|
+
}
|
|
30
|
+
export function useMediaUploads() {
|
|
31
|
+
return useSyncExternalStore(subscribe, () => jobs, () => jobs);
|
|
32
|
+
}
|
|
33
|
+
export function useMediaUploadCompletions() {
|
|
34
|
+
return useSyncExternalStore(subscribe, () => completions, () => completions);
|
|
35
|
+
}
|
|
36
|
+
/** Snapshot non mutabile per diagnostica e test della coda fuori da React. */
|
|
37
|
+
export function mediaUploadsSnapshot() { return jobs; }
|
|
38
|
+
export function dismissMediaUpload(id) {
|
|
39
|
+
jobs = jobs.filter((job) => job.id !== id || job.state === "running");
|
|
40
|
+
emit();
|
|
41
|
+
}
|
|
42
|
+
/** Accoda e torna subito: la finestra di scelta può chiudersi, il lavoro resta nel dock. */
|
|
43
|
+
export function startMediaUpload(input) {
|
|
44
|
+
const id = crypto.randomUUID();
|
|
45
|
+
jobs = [...jobs, {
|
|
46
|
+
id, title: input.title, prefix: input.prefix, state: "running", total: input.files.length,
|
|
47
|
+
conversionDone: 0, uploadDone: 0, converted: 0, originals: 0, failed: 0,
|
|
48
|
+
autoWebp: input.autoWebp, errors: [],
|
|
49
|
+
}];
|
|
50
|
+
emit();
|
|
51
|
+
void run(id, input);
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
async function run(id, input) {
|
|
55
|
+
const conversionWorkers = adaptiveConversionConcurrency(input.files, input.autoWebp);
|
|
56
|
+
const uploadWorkers = adaptiveUploadConcurrency();
|
|
57
|
+
/* Due soli elementi pronti: abbastanza per non lasciare la rete in attesa, pochi abbastanza
|
|
58
|
+
* da non tenere in memoria un intero servizio fotografico già convertito. */
|
|
59
|
+
const ready = new BoundedQueue(2);
|
|
60
|
+
let next = 0;
|
|
61
|
+
const convert = async () => {
|
|
62
|
+
while (next < input.files.length) {
|
|
63
|
+
const file = input.files[next++];
|
|
64
|
+
const prepared = await prepare(file, input.autoWebp);
|
|
65
|
+
const job = current(id);
|
|
66
|
+
patch(id, {
|
|
67
|
+
conversionDone: job.conversionDone + 1,
|
|
68
|
+
converted: job.converted + (prepared.converted ? 1 : 0),
|
|
69
|
+
originals: job.originals + (prepared.originalFallback ? 1 : 0),
|
|
70
|
+
errors: prepared.conversionError
|
|
71
|
+
? appendError(job.errors, `${file.name}: conversione non riuscita, carico l’originale (${prepared.conversionError})`)
|
|
72
|
+
: job.errors,
|
|
73
|
+
});
|
|
74
|
+
await ready.push(prepared);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const upload = async () => {
|
|
78
|
+
for (;;) {
|
|
79
|
+
const prepared = await ready.shift();
|
|
80
|
+
if (!prepared)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
await uploadWithRetry(input.prefix, prepared.upload, prepared.capturedAt);
|
|
84
|
+
const job = current(id);
|
|
85
|
+
patch(id, { uploadDone: job.uploadDone + 1 });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const job = current(id);
|
|
89
|
+
patch(id, {
|
|
90
|
+
uploadDone: job.uploadDone + 1,
|
|
91
|
+
failed: job.failed + 1,
|
|
92
|
+
errors: appendError(job.errors, `${prepared.source.name}: ${readable(error)}`),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
const consumers = Array.from({ length: Math.min(uploadWorkers, input.files.length) }, upload);
|
|
99
|
+
await Promise.all(Array.from({ length: Math.min(conversionWorkers, input.files.length) }, convert));
|
|
100
|
+
ready.close();
|
|
101
|
+
await Promise.all(consumers);
|
|
102
|
+
patch(id, { state: current(id).failed ? "failed" : "done" });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
/* Un errore della coda è un guasto nostro, non di una foto. Chiude comunque i consumer e
|
|
106
|
+
resta visibile: un job "in corso" per sempre sarebbe impossibile da diagnosticare. */
|
|
107
|
+
ready.close();
|
|
108
|
+
const job = current(id);
|
|
109
|
+
patch(id, { state: "failed", failed: Math.max(1, job.failed), errors: appendError(job.errors, readable(error)) });
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
completions += 1;
|
|
113
|
+
emit();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function prepare(file, autoWebp) {
|
|
117
|
+
const capturedAt = await jpegCapturedAt(file);
|
|
118
|
+
if (!autoWebp || !isConvertibleImage(file)) {
|
|
119
|
+
return { source: file, upload: file, converted: false, originalFallback: false, capturedAt };
|
|
120
|
+
}
|
|
121
|
+
let last;
|
|
122
|
+
/* Tre tentativi reali. Fra uno e l'altro restituiamo il controllo al browser: dopo una
|
|
123
|
+
* pressione di memoria può liberare bitmap/canvas chiusi prima di riprovare. */
|
|
124
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const converted = await convertMediaImage(file);
|
|
127
|
+
return { source: file, upload: converted, converted: true, originalFallback: false, capturedAt };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
last = error;
|
|
131
|
+
if (attempt < 3)
|
|
132
|
+
await delay(attempt * 180);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
source: file, upload: file, converted: false, originalFallback: true,
|
|
137
|
+
conversionError: readable(last), capturedAt,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function uploadWithRetry(prefix, file, capturedAt) {
|
|
141
|
+
let last;
|
|
142
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
143
|
+
try {
|
|
144
|
+
return await api.upload(prefix, file, { capturedAt });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
last = error;
|
|
148
|
+
if (attempt === 3 || !retryable(error))
|
|
149
|
+
throw error;
|
|
150
|
+
await delay(attempt * 300);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw last;
|
|
154
|
+
}
|
|
155
|
+
function retryable(error) {
|
|
156
|
+
return !(error instanceof ApiError) || error.status === 408 || error.status === 429 || error.status >= 500;
|
|
157
|
+
}
|
|
158
|
+
export function adaptiveConversionConcurrency(files, enabled = true) {
|
|
159
|
+
if (!enabled || !files.some(isConvertibleImage))
|
|
160
|
+
return Math.min(4, Math.max(1, files.length));
|
|
161
|
+
const cores = typeof navigator === "undefined" ? 4 : navigator.hardwareConcurrency || 4;
|
|
162
|
+
const memory = typeof navigator === "undefined"
|
|
163
|
+
? 4
|
|
164
|
+
: navigator.deviceMemory || 4;
|
|
165
|
+
let count = cores >= 12 && memory >= 8 ? 3 : cores >= 6 && memory >= 4 ? 2 : 1;
|
|
166
|
+
/* Il decoder HEIC ha un buffer compresso, un RGBA completo e il canvas di uscita. Due in
|
|
167
|
+
* parallelo sono già una quantità importante di memoria anche su una workstation. */
|
|
168
|
+
if (files.some((file) => /\.(?:heic|heif)$/i.test(file.name)))
|
|
169
|
+
count = Math.min(count, 2);
|
|
170
|
+
if (files.some((file) => file.size >= 30 * 1024 * 1024))
|
|
171
|
+
count = 1;
|
|
172
|
+
return Math.min(count, Math.max(1, files.length));
|
|
173
|
+
}
|
|
174
|
+
export function adaptiveUploadConcurrency() {
|
|
175
|
+
if (typeof navigator === "undefined")
|
|
176
|
+
return 3;
|
|
177
|
+
const connection = navigator.connection;
|
|
178
|
+
if (connection?.saveData || connection?.effectiveType === "slow-2g" || connection?.effectiveType === "2g")
|
|
179
|
+
return 2;
|
|
180
|
+
const memory = navigator.deviceMemory || 4;
|
|
181
|
+
return (navigator.hardwareConcurrency || 4) >= 8 && memory >= 8 ? 4 : 3;
|
|
182
|
+
}
|
|
183
|
+
function appendError(errors, next) {
|
|
184
|
+
/* Nel dock servono esempi utili, non duecento righe. Il conteggio completo resta in `failed`
|
|
185
|
+
e `originals`; le prime otto dicono quali file guardare. */
|
|
186
|
+
return errors.length >= 8 ? errors : [...errors, next];
|
|
187
|
+
}
|
|
188
|
+
function readable(error) {
|
|
189
|
+
return error instanceof Error ? error.message : String(error);
|
|
190
|
+
}
|
|
191
|
+
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
192
|
+
class BoundedQueue {
|
|
193
|
+
capacity;
|
|
194
|
+
values = [];
|
|
195
|
+
readers = [];
|
|
196
|
+
writers = [];
|
|
197
|
+
closed = false;
|
|
198
|
+
constructor(capacity) {
|
|
199
|
+
this.capacity = capacity;
|
|
200
|
+
}
|
|
201
|
+
async push(value) {
|
|
202
|
+
while (!this.closed && this.values.length >= this.capacity && this.readers.length === 0) {
|
|
203
|
+
await new Promise((resolve) => this.writers.push(resolve));
|
|
204
|
+
}
|
|
205
|
+
if (this.closed)
|
|
206
|
+
throw new Error("coda Media chiusa");
|
|
207
|
+
const reader = this.readers.shift();
|
|
208
|
+
if (reader)
|
|
209
|
+
reader(value);
|
|
210
|
+
else
|
|
211
|
+
this.values.push(value);
|
|
212
|
+
}
|
|
213
|
+
async shift() {
|
|
214
|
+
const value = this.values.shift();
|
|
215
|
+
if (value) {
|
|
216
|
+
this.writers.shift()?.();
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (this.closed)
|
|
220
|
+
return null;
|
|
221
|
+
return new Promise((resolve) => this.readers.push(resolve));
|
|
222
|
+
}
|
|
223
|
+
close() {
|
|
224
|
+
this.closed = true;
|
|
225
|
+
for (const reader of this.readers.splice(0))
|
|
226
|
+
reader(null);
|
|
227
|
+
for (const writer of this.writers.splice(0))
|
|
228
|
+
writer();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AssetFile, type AssetFolder } from "../../api.js";
|
|
2
2
|
import type { MediaOperationStatus } from "../../media.js";
|
|
3
|
+
export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "journal";
|
|
3
4
|
export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
|
|
4
5
|
export declare const SORT_LABELS: Record<SortKey, string>;
|
|
5
6
|
export interface MediaLibrary {
|
|
@@ -10,7 +11,14 @@ export interface MediaLibrary {
|
|
|
10
11
|
subfolders: AssetFolder[];
|
|
11
12
|
shown: AssetFile[];
|
|
12
13
|
selected: Set<string>;
|
|
14
|
+
selectableIds: string[];
|
|
15
|
+
selectedFiles: string[];
|
|
16
|
+
selectedFolders: string[];
|
|
17
|
+
allSelected: boolean;
|
|
18
|
+
someSelected: boolean;
|
|
13
19
|
busy: boolean;
|
|
20
|
+
mutating: boolean;
|
|
21
|
+
operationState: MediaOperationState;
|
|
14
22
|
error: string | null;
|
|
15
23
|
mediaOperations: MediaOperationStatus[];
|
|
16
24
|
/** Avanzamento di un'operazione lunga. `verb` è quello che si sta facendo ("Carico",
|
|
@@ -25,6 +33,8 @@ export interface MediaLibrary {
|
|
|
25
33
|
sortDir: "asc" | "desc";
|
|
26
34
|
uploadOpen: boolean;
|
|
27
35
|
viewing: number | null;
|
|
36
|
+
autoWebp: boolean;
|
|
37
|
+
settingsBusy: boolean;
|
|
28
38
|
go(prefix: string): void;
|
|
29
39
|
reload(): void;
|
|
30
40
|
toggle(key: string): void;
|
|
@@ -34,10 +44,12 @@ export interface MediaLibrary {
|
|
|
34
44
|
/** Vero se sono passati tutti: chi ha aperto un dialogo lo chiude solo in quel caso. */
|
|
35
45
|
upload(files: File[] | FileList): Promise<boolean>;
|
|
36
46
|
setUploadOpen(open: boolean): void;
|
|
47
|
+
setAutoWebp(on: boolean): Promise<void>;
|
|
37
48
|
view(i: number | null): void;
|
|
38
49
|
step(delta: number): void;
|
|
39
50
|
/** File e cartelle insieme: una cartella si riconosce dalla chiave, che finisce con "/". */
|
|
40
51
|
move(ids: string[], dest: string): Promise<void>;
|
|
52
|
+
canMoveTo(ids: string[], dest: string): boolean;
|
|
41
53
|
remove(ids: string[]): Promise<void>;
|
|
42
54
|
/** `parent` esplicito: chi preme il pulsante sa dove sta creando, il pulsante non lo indovina. */
|
|
43
55
|
newFolder(parent: string): Promise<void>;
|