@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
|
@@ -44,7 +44,7 @@ export function FolderMenu({ lib, prefix, label, openTo = "left" }) {
|
|
|
44
44
|
const [name, setName] = useState(label);
|
|
45
45
|
const [side, setSide] = useState("left");
|
|
46
46
|
const close = () => { setOpen(false); setRenaming(false); };
|
|
47
|
-
return (_jsxs("span", { className: "mf-actions", children: [_jsx("button", { className: "mf-more", title: "Altro", onClick: (e) => {
|
|
47
|
+
return (_jsxs("span", { className: "mf-actions", children: [_jsx("button", { className: "mf-more", title: "Altro", disabled: lib.busy || lib.mutating, onClick: (e) => {
|
|
48
48
|
e.stopPropagation();
|
|
49
49
|
if (openTo === "auto") {
|
|
50
50
|
const r = e.currentTarget.getBoundingClientRect();
|
|
@@ -53,23 +53,24 @@ export function FolderMenu({ lib, prefix, label, openTo = "left" }) {
|
|
|
53
53
|
setName(label);
|
|
54
54
|
setOpen((v) => !v);
|
|
55
55
|
setRenaming(false);
|
|
56
|
-
}, children: "\u2022\u2022\u2022" }), open && !renaming && (_jsxs("div", { className: `mf-menu is-${side}`, onMouseLeave: close, children: [_jsx("button", { onClick: () => setRenaming(true), children: "Rinomina\u2026" }), _jsx("button", { className: "mf-danger", onClick: () => { close(); void lib.removeFolder(prefix); }, children: "Elimina la cartella\u2026" })] })), open && renaming && (_jsxs("div", { className: `mf-menu mf-menu-form is-${side}`, children: [_jsx("input", { className: "ak-input", autoFocus: true, value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
56
|
+
}, children: "\u2022\u2022\u2022" }), open && !renaming && (_jsxs("div", { className: `mf-menu is-${side}`, onMouseLeave: close, children: [_jsx("button", { disabled: lib.busy || lib.mutating, onClick: () => setRenaming(true), children: "Rinomina\u2026" }), _jsx("button", { disabled: lib.busy || lib.mutating, className: "mf-danger", onClick: () => { close(); void lib.removeFolder(prefix); }, children: "Elimina la cartella\u2026" })] })), open && renaming && (_jsxs("div", { className: `mf-menu mf-menu-form is-${side}`, children: [_jsx("input", { className: "ak-input", autoFocus: true, value: name, disabled: lib.busy || lib.mutating, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
|
|
57
57
|
if (e.key === "Escape")
|
|
58
58
|
close();
|
|
59
59
|
if (e.key === "Enter" && name.trim()) {
|
|
60
60
|
close();
|
|
61
61
|
void lib.renameFolder(prefix, name);
|
|
62
62
|
}
|
|
63
|
-
} }), _jsx("span", { className: "ak-hint", children: "Cambia anche l'indirizzo delle foto dentro. Le pagine del sito vengono aggiornate; i link scritti a mano altrove no." }), _jsxs("div", { className: "mf-menu-row", children: [_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !name.trim(), onClick: () => { close(); void lib.renameFolder(prefix, name); }, children: "Rinomina" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: close, children: "Annulla" })] })] }))] }));
|
|
63
|
+
} }), _jsx("span", { className: "ak-hint", children: "Cambia anche l'indirizzo delle foto dentro. Le pagine del sito vengono aggiornate; i link scritti a mano altrove no." }), _jsxs("div", { className: "mf-menu-row", children: [_jsx("button", { className: "ak-btn ak-btn-primary", disabled: lib.busy || lib.mutating || !name.trim(), onClick: () => { close(); void lib.renameFolder(prefix, name); }, children: "Rinomina" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: close, children: "Annulla" })] })] }))] }));
|
|
64
64
|
}
|
|
65
65
|
const IcoTrash = () => (_jsx("svg", { className: "mf-ico", viewBox: "0 0 16 16", width: "15", height: "15", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round", d: "M3 4.5h10M6 4.5V3h4v1.5M4.5 4.5 5 13.5h6l.5-9" }) }));
|
|
66
|
+
const IcoSettings = () => (_jsxs("svg", { className: "mf-ico", viewBox: "0 0 16 16", width: "15", height: "15", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.35", strokeLinecap: "round", strokeLinejoin: "round", d: "M6.6 2.2h2.8l.4 1.5c.4.2.8.4 1.1.7l1.5-.4 1.4 2.4-1.1 1.1v1l1.1 1.1-1.4 2.4-1.5-.4c-.3.3-.7.5-1.1.7l-.4 1.5H6.6l-.4-1.5a5 5 0 0 1-1.1-.7l-1.5.4-1.4-2.4 1.1-1.1v-1L2.2 6.4 3.6 4l1.5.4c.3-.3.7-.5 1.1-.7l.4-1.5Z" }), _jsx("circle", { cx: "8", cy: "8", r: "2", fill: "none", stroke: "currentColor", strokeWidth: "1.35" })] }));
|
|
66
67
|
/* `trash` è facoltativo perché il cestino è una ROTTA, e le rotte le conosce l'applicazione, non
|
|
67
68
|
* questo componente. Sta qui in fondo, sotto «+ Cartella», perché è l'ultimo posto dove finiscono
|
|
68
69
|
* le foto: nella colonna che elenca dove stanno, non in una barra da un'altra parte. */
|
|
69
|
-
export function MediaFolders({ lib, trash }) {
|
|
70
|
+
export function MediaFolders({ lib, trash, settings }) {
|
|
70
71
|
const [over, setOver] = useState(null);
|
|
71
72
|
const [draggingFolder, setDraggingFolder] = useState(false);
|
|
72
|
-
const canDrop = lib.mode === "manage";
|
|
73
|
+
const canDrop = lib.mode === "manage" && !lib.busy && !lib.mutating;
|
|
73
74
|
/* Nel cestino non si è in nessuna cartella: senza questo, «Tutte» resterebbe accesa insieme a
|
|
74
75
|
«Cestino» e la colonna indicherebbe due posti nello stesso momento. */
|
|
75
76
|
const here = (p) => !trash?.active && (p === "" ? lib.prefix === "" : lib.prefix.startsWith(p));
|
|
@@ -104,7 +105,7 @@ export function MediaFolders({ lib, trash }) {
|
|
|
104
105
|
removeEventListener("drop", end);
|
|
105
106
|
};
|
|
106
107
|
}, [canDrop]);
|
|
107
|
-
return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : ""), children: _jsx("button", { className: "mf-open", onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
|
|
108
|
+
return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : ""), children: _jsx("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
|
|
108
109
|
/* Accesa anche quando si è DENTRO una sua sottocartella: la colonna risponde a
|
|
109
110
|
«dove sono», e stando in food/piatti-2024/ la risposta è ancora food. */
|
|
110
111
|
className: "mf-item"
|
|
@@ -112,5 +113,5 @@ export function MediaFolders({ lib, trash }) {
|
|
|
112
113
|
+ (over === f.prefix ? " is-over" : ""), onDragOver: (e) => { if (canDrop) {
|
|
113
114
|
e.preventDefault();
|
|
114
115
|
setOver(f.prefix);
|
|
115
|
-
} }, onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canDrop && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canDrop && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) }))] }));
|
|
116
|
+
} }, onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canDrop && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canDrop && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", disabled: lib.busy || lib.mutating, onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
|
|
116
117
|
}
|
|
@@ -23,6 +23,7 @@ import { SORT_LABELS } from "./useMediaLibrary.js";
|
|
|
23
23
|
import { DRAG_TYPE, DRAG_FOLDERS, readDragged, FolderMenu } from "./MediaFolders.js";
|
|
24
24
|
import { Lightbox } from "./Lightbox.js";
|
|
25
25
|
import { UploadDialog } from "./UploadDialog.js";
|
|
26
|
+
import { summarizeMediaSelection } from "./media-model.js";
|
|
26
27
|
const JOURNAL_LABELS = {
|
|
27
28
|
preparing: "Preparo le copie Media.",
|
|
28
29
|
ready: "Media pronta: pubblica per applicare la modifica.",
|
|
@@ -49,8 +50,9 @@ function download(urls) {
|
|
|
49
50
|
}
|
|
50
51
|
function MoveMenu({ lib, keys }) {
|
|
51
52
|
const [open, setOpen] = useState(false);
|
|
52
|
-
const
|
|
53
|
-
|
|
53
|
+
const locked = lib.busy || lib.mutating;
|
|
54
|
+
const dest = lib.folders.filter((f) => f.prefix !== lib.prefix && lib.canMoveTo(keys, f.prefix));
|
|
55
|
+
return (_jsxs("span", { className: "mg-menu-wrap", children: [_jsx("button", { className: "mg-act", onClick: () => setOpen((v) => !v), disabled: locked || !dest.length, children: "Sposta in\u2026" }), open && (_jsx("div", { className: "mg-menu", onMouseLeave: () => setOpen(false), children: dest.map((f) => (_jsxs("button", { onClick: () => { setOpen(false); void lib.move(keys, f.prefix); }, children: [f.label || f.name, " ", _jsx("span", { className: "mf-count", children: f.count })] }, f.prefix))) }))] }));
|
|
54
56
|
}
|
|
55
57
|
/* Una cartella dentro la griglia. Fa tre cose che un chip non poteva fare: si apre, si
|
|
56
58
|
* seleziona insieme alle foto, e si trascina — dentro un'altra cartella qui, o sulla colonna di
|
|
@@ -60,7 +62,12 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
60
62
|
const [over, setOver] = useState(false);
|
|
61
63
|
const on = lib.selected.has(folder.prefix);
|
|
62
64
|
const name = folder.label || folder.name;
|
|
63
|
-
|
|
65
|
+
const locked = lib.busy || lib.mutating;
|
|
66
|
+
return (_jsxs("figure", { className: "mg-item mg-folder" + (on ? " is-on" : "") + (over ? " is-over" : ""), draggable: manage && !locked, onDragStart: (e) => {
|
|
67
|
+
if (locked) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
const ids = on && lib.selected.size ? [...lib.selected] : [folder.prefix];
|
|
65
72
|
// Due tipi, non uno: durante il trascinamento i valori non si leggono, i tipi sì —
|
|
66
73
|
// ed è così che la colonna sa di avere in mano una cartella e offre il primo livello.
|
|
@@ -77,7 +84,7 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
77
84
|
"è roba nostra": il tipo dei nostri dati non è leggibile in ogni passaggio del trascinamento,
|
|
78
85
|
e un bersaglio che non si accende sembra un bersaglio che non funziona. */
|
|
79
86
|
onDragOver: (e) => {
|
|
80
|
-
if (!manage || e.dataTransfer.types.includes("Files"))
|
|
87
|
+
if (!manage || locked || e.dataTransfer.types.includes("Files"))
|
|
81
88
|
return;
|
|
82
89
|
/* Si accende anche quando la cartella trascinata è questa stessa: durante il
|
|
83
90
|
trascinamento i valori non sono leggibili, quindi non si può sapere. Il rifiuto
|
|
@@ -86,19 +93,22 @@ function FolderCell({ lib, folder, manage }) {
|
|
|
86
93
|
setOver(true);
|
|
87
94
|
}, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
88
95
|
setOver(false);
|
|
89
|
-
if (!manage)
|
|
96
|
+
if (!manage || locked)
|
|
90
97
|
return;
|
|
91
98
|
const ids = readDragged(e.dataTransfer);
|
|
92
99
|
if (!ids.length)
|
|
93
100
|
return; // non è roba nostra: lascia fare a chi sta sopra
|
|
94
101
|
e.preventDefault();
|
|
95
102
|
void lib.move(ids, folder.prefix);
|
|
96
|
-
}, children: [_jsxs("div", { className: "mg-frame", onClick: () =>
|
|
103
|
+
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => { if (!locked)
|
|
104
|
+
lib.go(folder.prefix); }, children: [_jsx("svg", { className: "mg-folder-ico", viewBox: "0 0 48 40", "aria-hidden": "true", children: _jsx("path", { d: "M2 8a4 4 0 0 1 4-4h12l4 5h20a4 4 0 0 1 4 4v21a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8Z" }) }), manage && (_jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(folder.prefix); }, children: on ? "✓" : "" }))] }), _jsx("figcaption", { className: "mg-cap", title: name, children: name }), manage && (_jsx("span", { className: "mg-folder-menu", children: _jsx(FolderMenu, { lib: lib, prefix: folder.prefix, label: name, openTo: "auto" }) }))] }));
|
|
97
105
|
}
|
|
98
106
|
export function MediaGrid({ lib, onPick, onCancel }) {
|
|
99
107
|
const manage = lib.mode === "manage";
|
|
100
108
|
const sel = lib.selected;
|
|
101
|
-
const allOn = lib.
|
|
109
|
+
const allOn = lib.allSelected;
|
|
110
|
+
const locked = lib.busy || lib.mutating;
|
|
111
|
+
const selection = summarizeMediaSelection(sel);
|
|
102
112
|
/* `dragDepth` e non un booleano: `dragenter`/`dragleave` scattano anche passando da un figlio
|
|
103
113
|
all'altro, quindi con un booleano la zona si spegne mentre ci si muove sopra. Si conta. */
|
|
104
114
|
const [dragDepth, setDragDepth] = useState(0);
|
|
@@ -106,8 +116,8 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
106
116
|
zona di caricamento non c'entra. Il controllo è "NON è roba nostra" invece di "è un file":
|
|
107
117
|
`types` non contiene "Files" allo stesso modo in ogni browser, e con quel controllo il
|
|
108
118
|
caricamento non partiva affatto dove il nome era diverso. */
|
|
109
|
-
const external = (e) => manage && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
|
|
110
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mg-wrap" + (dragDepth > 0 ? " is-dropping" : ""),
|
|
119
|
+
const external = (e) => manage && !locked && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
|
|
120
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mg-wrap" + (dragDepth > 0 ? " is-dropping" : ""), "aria-busy": lib.mutating ? "true" : "false",
|
|
111
121
|
/* Trascinare file dal Finder sulla griglia carica, senza passare dal dialogo: è il gesto
|
|
112
122
|
più corto e va lasciato funzionare. Il dialogo serve a chi cerca un bottone.
|
|
113
123
|
La zona è ALTA QUANTO IL PANNELLO (`.mg-wrap { min-height: 100% }`): prima era alta
|
|
@@ -120,15 +130,19 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
120
130
|
e.preventDefault(); }, onDragLeave: (e) => { if (external(e))
|
|
121
131
|
setDragDepth((n) => Math.max(0, n - 1)); }, onDrop: (e) => {
|
|
122
132
|
setDragDepth(0);
|
|
123
|
-
if (!manage || !e.dataTransfer.files.length)
|
|
133
|
+
if (!manage || locked || !e.dataTransfer.files.length)
|
|
124
134
|
return;
|
|
125
135
|
e.preventDefault();
|
|
126
136
|
void lib.upload(e.dataTransfer.files);
|
|
127
|
-
}, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] }) })), _jsx("header", { className: "mg-bar", children: sel.size === 0 ? (_jsxs(_Fragment, { children: [(manage || lib.multiple) && (_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: false, ref: (el) => { if (el)
|
|
128
|
-
el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, ref: (el) => { if (el)
|
|
129
|
-
el.indeterminate =
|
|
137
|
+
}, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] }) })), _jsx("header", { className: "mg-bar", children: sel.size === 0 ? (_jsxs(_Fragment, { children: [(manage || lib.multiple) && (_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: false, disabled: locked || !lib.selectableIds.length, ref: (el) => { if (el)
|
|
138
|
+
el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, disabled: locked, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", disabled: locked, onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, disabled: locked, ref: (el) => { if (el)
|
|
139
|
+
el.indeterminate = lib.someSelected; }, onChange: () => lib.setAll(!allOn) }), _jsx("span", { children: selection.label })] }), !allOn && lib.selectableIds.length > 0 && (_jsxs("button", { className: "mg-act is-link", disabled: locked, onClick: () => lib.setAll(true), children: ["Seleziona tutti gli elementi (", lib.selectableIds.length, ")"] })), manage && (_jsxs(_Fragment, { children: [lib.selectedFiles.length > 0 && (_jsxs("button", { className: "mg-act", disabled: locked, onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: ["Scarica ", lib.selectedFiles.length, " file"] })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", disabled: locked, onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), manage && lib.mediaOperations.length > 0 && (_jsx("section", { className: "mg-journal", "aria-live": "polite", "aria-label": "Stato operazioni Media", children: lib.mediaOperations.map((operation) => (_jsxs("div", { className: `mg-journal-row is-${operation.state}`, children: [_jsx("span", { className: "mg-journal-badge", children: operation.type === "move" ? "Spostamento" : "Eliminazione" }), _jsx("span", { children: JOURNAL_LABELS[operation.state] ?? "Operazione Media in attesa." }), operation.failedEntries > 0 && _jsxs("span", { className: "ak-hint", children: [operation.failedEntries, " file da riprovare"] }), _jsx("span", { className: "ak-spacer" }), operation.state === "completed_with_warning" && operation.releaseId && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.resumeMedia(operation), children: "Riprendi" })), ["preparing", "ready", "blocked_in_use", "failed"].includes(operation.state) && !operation.releaseId && (_jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.cancelMedia(operation), children: "Annulla" }))] }, operation.operationId))) })), lib.progress && (_jsxs("p", { className: "mg-progress", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026 \u2014 non chiudere la pagina"] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && (_jsxs("section", { className: "mg-section", children: [_jsxs("div", { className: "mg-section-head", children: [_jsx("h2", { className: "mg-section-title", children: "Cartelle" }), manage && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.newFolder(lib.prefix), children: "+ Cartella" }))] }), lib.subfolders.length > 0 && (_jsx("div", { className: "mg", children: lib.subfolders.map((f) => (_jsx(FolderCell, { lib: lib, folder: f, manage: manage }, f.prefix))) }))] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && lib.shown.length > 0 && (_jsx("h2", { className: "mg-section-title", children: "Foto" })), _jsx("div", { className: "mg", children: lib.shown.map((f, i) => {
|
|
130
140
|
const on = sel.has(f.key);
|
|
131
|
-
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage, onDragStart: (e) => {
|
|
141
|
+
return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage && !locked, onDragStart: (e) => {
|
|
142
|
+
if (locked) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
132
146
|
// Si trascina la SELEZIONE se la foto ne fa parte, altrimenti solo quella.
|
|
133
147
|
const ids = on && sel.size ? [...sel] : [f.key];
|
|
134
148
|
const files = ids.filter((i) => !i.endsWith("/"));
|
|
@@ -138,11 +152,12 @@ export function MediaGrid({ lib, onPick, onCancel }) {
|
|
|
138
152
|
if (dirs.length)
|
|
139
153
|
e.dataTransfer.setData(DRAG_FOLDERS, JSON.stringify(dirs));
|
|
140
154
|
e.dataTransfer.effectAllowed = "move";
|
|
141
|
-
}, children: [_jsxs("div", { className: "mg-frame", onClick: () =>
|
|
155
|
+
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => { if (!locked)
|
|
156
|
+
(manage ? lib.view(i) : lib.toggle(f.key)); }, children: [_jsx("img", { className: "mg-img", src: f.url, alt: "", loading: "lazy",
|
|
142
157
|
/* Il rapporto è noto prima che l'immagine arrivi (lo dà l'elenco): la cella
|
|
143
158
|
riserva subito l'ingombro giusto e la griglia non salta mentre si carica. */
|
|
144
|
-
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
|
159
|
+
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
|
145
160
|
}) }), !lib.busy && lib.shown.length === 0 && (_jsx("p", { className: "ak-hint", children: lib.prefix === "" && lib.folders.length
|
|
146
161
|
? "Nessuna foto fuori dalle cartelle. Scegline una qui a fianco."
|
|
147
|
-
: "Nessun file qui." })), lib.mode === "pick" && (_jsxs("footer", { className: "mg-foot", children: [_jsxs("span", { className: "ak-hint", children: [sel.size, " selezionate"] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), manage && _jsx(UploadDialog, { lib: lib })] }));
|
|
162
|
+
: "Nessun file qui." })), lib.mode === "pick" && (_jsxs("footer", { className: "mg-foot", children: [_jsxs("span", { className: "ak-hint", children: [sel.size, " selezionate"] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: locked, onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: locked || !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), manage && _jsx(UploadDialog, { lib: lib })] }));
|
|
148
163
|
}
|
|
@@ -15,23 +15,29 @@ export function UploadDialog({ lib }) {
|
|
|
15
15
|
if (!lib.uploadOpen)
|
|
16
16
|
return null;
|
|
17
17
|
const send = async (list) => {
|
|
18
|
-
if (!list || !list.length)
|
|
18
|
+
if (lib.mutating || !list || !list.length)
|
|
19
19
|
return;
|
|
20
20
|
// `upload` dice se sono passati tutti. Guardare `lib.error` da qui non funzionerebbe: è il
|
|
21
21
|
// valore catturato prima del render, quindi sarebbe sempre uguale a sé stesso.
|
|
22
22
|
if (await lib.upload(list))
|
|
23
23
|
lib.setUploadOpen(false);
|
|
24
24
|
};
|
|
25
|
-
return (_jsx("div", { className: "up", onClick: (e) => { if (e.target === e.currentTarget)
|
|
26
|
-
lib.setUploadOpen(false); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", onClick: () => lib.setUploadOpen(false), "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
|
|
25
|
+
return (_jsx("div", { className: "up", onClick: (e) => { if (!lib.mutating && e.target === e.currentTarget)
|
|
26
|
+
lib.setUploadOpen(false); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", disabled: lib.mutating, onClick: () => lib.setUploadOpen(false), "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
|
|
27
27
|
/* `stopPropagation` oltre a `preventDefault`: il dialogo è una sovrapposizione, ma
|
|
28
28
|
per React resta un nodo dell'albero e l'evento continuerebbe a salire. Oggi sopra
|
|
29
29
|
non c'è più nessuna zona di rilascio (il dialogo è fuori da `.mg-wrap`), e questa
|
|
30
30
|
riga serve a che non torni a essercene una senza che nessuno se ne accorga. */
|
|
31
|
-
onDragOver: (e) => {
|
|
31
|
+
onDragOver: (e) => { if (!lib.mutating) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
setOver(true);
|
|
35
|
+
} }, onDragLeave: () => setOver(false), onDrop: (e) => {
|
|
36
|
+
if (lib.mutating)
|
|
37
|
+
return;
|
|
32
38
|
e.preventDefault();
|
|
33
39
|
e.stopPropagation();
|
|
34
40
|
setOver(false);
|
|
35
41
|
void send(e.dataTransfer.files);
|
|
36
|
-
}, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children: "Trascina qui le foto da caricare" }), _jsx("input", { ref: input, type: "file", multiple: true, accept: "image/*,video/*", hidden: true, onChange: (e) => void send(e.target.files) }), _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => input.current?.click(), children: "Scegli i file" })] }), _jsxs("p", { className: "up-dest", children: ["Finiranno in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella (la radice del bucket)" })] }), lib.progress && (_jsxs("p", { className: "ak-hint", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026"] })), lib.error && _jsx("p", { className: "ak-error", children: lib.error })] }) }));
|
|
42
|
+
}, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children: "Trascina qui le foto da caricare" }), _jsx("input", { ref: input, type: "file", multiple: true, accept: "image/*,video/*", hidden: true, disabled: lib.mutating, onChange: (e) => void send(e.target.files) }), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating, onClick: () => input.current?.click(), children: "Scegli i file" })] }), _jsxs("p", { className: "up-dest", children: ["Finiranno in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella (la radice del bucket)" })] }), lib.progress && (_jsxs("p", { className: "ak-hint", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026"] })), lib.error && _jsx("p", { className: "ak-error", children: lib.error })] }) }));
|
|
37
43
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const MEDIA_WEBP_EDGE = 2500;
|
|
2
|
+
export declare const MEDIA_WEBP_QUALITY = 0.8;
|
|
3
|
+
export declare function isConvertibleImage(file: File): boolean;
|
|
4
|
+
export declare function webpName(name: string): string;
|
|
5
|
+
export declare function fitLongEdge(width: number, height: number, edge?: number): {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
/** La conversione canvas rimuove i metadati. Conserviamo almeno DateTimeOriginal, già usato
|
|
10
|
+
* dalla libreria per l'ordinamento per scatto, leggendo solo l'inizio del JPEG. */
|
|
11
|
+
export declare function jpegCapturedAt(file: File): Promise<string | null>;
|
|
12
|
+
/** I video e gli altri file non-immagine restano intatti. Retry e fallback all'originale sono
|
|
13
|
+
* responsabilità della coda: questa funzione fa un singolo tentativo e fallisce in modo netto. */
|
|
14
|
+
export declare function convertMediaImage(file: File): Promise<File>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* Conversione prima dell'upload della libreria Media.
|
|
2
|
+
*
|
|
3
|
+
* Sta nel browser: il file originale non attraversa la rete e ogni worker della coda può
|
|
4
|
+
* convertire e caricare in parallelo. HEIC/HEIF è l'unica eccezione ai codec del browser e
|
|
5
|
+
* viene decodificato, solo quando serve, dal modulo dedicato caricato dinamicamente.
|
|
6
|
+
*/
|
|
7
|
+
export const MEDIA_WEBP_EDGE = 2500;
|
|
8
|
+
export const MEDIA_WEBP_QUALITY = .8;
|
|
9
|
+
const IMAGE_EXT = /\.(?:avif|bmp|gif|heic|heif|jfif|jpe?g|png|tiff?|webp)$/i;
|
|
10
|
+
const HEIC_EXT = /\.(?:heic|heif)$/i;
|
|
11
|
+
export function isConvertibleImage(file) {
|
|
12
|
+
return file.type.startsWith("image/") || IMAGE_EXT.test(file.name);
|
|
13
|
+
}
|
|
14
|
+
export function webpName(name) {
|
|
15
|
+
const base = name.replace(/\.[^./\\]+$/, "") || "immagine";
|
|
16
|
+
return `${base}.webp`;
|
|
17
|
+
}
|
|
18
|
+
export function fitLongEdge(width, height, edge = MEDIA_WEBP_EDGE) {
|
|
19
|
+
const scale = Math.min(1, edge / Math.max(width, height));
|
|
20
|
+
return {
|
|
21
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
22
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** La conversione canvas rimuove i metadati. Conserviamo almeno DateTimeOriginal, già usato
|
|
26
|
+
* dalla libreria per l'ordinamento per scatto, leggendo solo l'inizio del JPEG. */
|
|
27
|
+
export async function jpegCapturedAt(file) {
|
|
28
|
+
if (!(file.type === "image/jpeg" || /\.(?:jpe?g|jfif)$/i.test(file.name)))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const buffer = await file.slice(0, 512 * 1024).arrayBuffer();
|
|
32
|
+
const view = new DataView(buffer);
|
|
33
|
+
if (view.byteLength < 4 || view.getUint16(0) !== 0xffd8)
|
|
34
|
+
return null;
|
|
35
|
+
let offset = 2;
|
|
36
|
+
while (offset + 12 < view.byteLength) {
|
|
37
|
+
if (view.getUint8(offset) !== 0xff)
|
|
38
|
+
return null;
|
|
39
|
+
const marker = view.getUint8(offset + 1);
|
|
40
|
+
const length = view.getUint16(offset + 2);
|
|
41
|
+
if (marker === 0xe1) {
|
|
42
|
+
const app1 = offset + 4;
|
|
43
|
+
if (app1 + 14 >= view.byteLength || view.getUint32(app1) !== 0x45786966)
|
|
44
|
+
return null;
|
|
45
|
+
const tiff = app1 + 6;
|
|
46
|
+
const little = view.getUint16(tiff) === 0x4949;
|
|
47
|
+
const u16 = (at) => view.getUint16(at, little);
|
|
48
|
+
const u32 = (at) => view.getUint32(at, little);
|
|
49
|
+
const ifd0 = tiff + u32(tiff + 4);
|
|
50
|
+
let exif = 0;
|
|
51
|
+
for (let i = 0, count = u16(ifd0); i < count; i++) {
|
|
52
|
+
const entry = ifd0 + 2 + i * 12;
|
|
53
|
+
if (u16(entry) === 0x8769) {
|
|
54
|
+
exif = tiff + u32(entry + 8);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!exif)
|
|
59
|
+
return null;
|
|
60
|
+
for (let i = 0, count = u16(exif); i < count; i++) {
|
|
61
|
+
const entry = exif + 2 + i * 12;
|
|
62
|
+
if (u16(entry) !== 0x9003)
|
|
63
|
+
continue;
|
|
64
|
+
const start = tiff + u32(entry + 8);
|
|
65
|
+
let raw = "";
|
|
66
|
+
for (let j = 0; j < 19 && start + j < view.byteLength; j++)
|
|
67
|
+
raw += String.fromCharCode(view.getUint8(start + j));
|
|
68
|
+
const match = raw.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})/);
|
|
69
|
+
return match ? `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}` : null;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (marker === 0xda || length < 2)
|
|
74
|
+
return null;
|
|
75
|
+
offset += 2 + length;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* metadati assenti o corrotti: non impediscono una conversione valida */ }
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function bitmapOf(file) {
|
|
82
|
+
const heic = HEIC_EXT.test(file.name) || /image\/(?:hei[cf]|heif-sequence)/i.test(file.type);
|
|
83
|
+
const options = {
|
|
84
|
+
imageOrientation: "from-image",
|
|
85
|
+
/* `none` lascerebbe i numeri del profilo sorgente senza trasformazione: disegnarli poi su
|
|
86
|
+
un canvas sRGB è il percorso classico verso colori slavati o troppo saturi. */
|
|
87
|
+
colorSpaceConversion: "default",
|
|
88
|
+
premultiplyAlpha: "default",
|
|
89
|
+
};
|
|
90
|
+
if (heic) {
|
|
91
|
+
/* Prima il codec di sistema: quando Safari/il browser sa leggere HEIC, è la strada che ha
|
|
92
|
+
accesso completo al suo profilo ICC/NCLX. Il decoder WASM è il ripiego interoperabile. */
|
|
93
|
+
try {
|
|
94
|
+
return await createImageBitmap(file, options);
|
|
95
|
+
}
|
|
96
|
+
catch { /* HEIC non supportato nativamente */ }
|
|
97
|
+
/* La variante CSP non usa `unsafe-eval`, vietato normalmente dall'admin. Il chunk grande
|
|
98
|
+
del decoder non entra nel caricamento iniziale: viene scaricato soltanto al primo HEIC. */
|
|
99
|
+
const { heicTo } = await import("heic-to/csp");
|
|
100
|
+
return heicTo({ blob: file, type: "bitmap", options });
|
|
101
|
+
}
|
|
102
|
+
return createImageBitmap(file, options);
|
|
103
|
+
}
|
|
104
|
+
async function drawWebp(bitmap, width, height) {
|
|
105
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
106
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
107
|
+
/* Il WebP generato dal canvas normalmente non porta con sé il profilo sorgente. Rendendo
|
|
108
|
+
esplicitamente in sRGB, i pixel sono già corretti; la specifica WebP prescrive sRGB
|
|
109
|
+
quando ICCP manca. Non copiamo l'ICC originale su pixel ormai convertiti. */
|
|
110
|
+
const context = canvas.getContext("2d", { alpha: true, colorSpace: "srgb" });
|
|
111
|
+
if (!context)
|
|
112
|
+
throw new Error("Canvas 2D non disponibile");
|
|
113
|
+
context.imageSmoothingEnabled = true;
|
|
114
|
+
context.imageSmoothingQuality = "high";
|
|
115
|
+
try {
|
|
116
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
117
|
+
const blob = await canvas.convertToBlob({ type: "image/webp", quality: MEDIA_WEBP_QUALITY });
|
|
118
|
+
if (blob.type !== "image/webp")
|
|
119
|
+
throw new Error("Questo browser non supporta la codifica WebP");
|
|
120
|
+
return blob;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
canvas.width = 1;
|
|
124
|
+
canvas.height = 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const canvas = document.createElement("canvas");
|
|
128
|
+
canvas.width = width;
|
|
129
|
+
canvas.height = height;
|
|
130
|
+
const context = canvas.getContext("2d", { alpha: true, colorSpace: "srgb" });
|
|
131
|
+
if (!context)
|
|
132
|
+
throw new Error("Canvas 2D non disponibile");
|
|
133
|
+
context.imageSmoothingEnabled = true;
|
|
134
|
+
context.imageSmoothingQuality = "high";
|
|
135
|
+
try {
|
|
136
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
137
|
+
return await new Promise((resolve, reject) => canvas.toBlob((blob) => blob?.type === "image/webp"
|
|
138
|
+
? resolve(blob)
|
|
139
|
+
: reject(new Error("Questo browser non supporta la codifica WebP")), "image/webp", MEDIA_WEBP_QUALITY));
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
canvas.width = 1;
|
|
143
|
+
canvas.height = 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** I video e gli altri file non-immagine restano intatti. Retry e fallback all'originale sono
|
|
147
|
+
* responsabilità della coda: questa funzione fa un singolo tentativo e fallisce in modo netto. */
|
|
148
|
+
export async function convertMediaImage(file) {
|
|
149
|
+
if (!isConvertibleImage(file))
|
|
150
|
+
return file;
|
|
151
|
+
const bitmap = await bitmapOf(file);
|
|
152
|
+
try {
|
|
153
|
+
const size = fitLongEdge(bitmap.width, bitmap.height);
|
|
154
|
+
const blob = await drawWebp(bitmap, size.width, size.height);
|
|
155
|
+
return new File([blob], webpName(file.name), {
|
|
156
|
+
type: "image/webp",
|
|
157
|
+
lastModified: file.lastModified,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
bitmap.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AssetFile, AssetFolder } from "../../api.js";
|
|
2
|
+
export type MediaSelectionKind = "file" | "folder";
|
|
3
|
+
export interface MediaSelectableItem {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: MediaSelectionKind;
|
|
6
|
+
}
|
|
7
|
+
export interface MediaSelectionSummary {
|
|
8
|
+
files: string[];
|
|
9
|
+
folders: string[];
|
|
10
|
+
total: number;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
/** L'unico dominio su cui "Seleziona tutto" e la relativa checkbox possono ragionare. */
|
|
14
|
+
export declare function mediaSelectableItems({ mode, prefix, shown, subfolders, }: {
|
|
15
|
+
mode: "manage" | "pick";
|
|
16
|
+
prefix: string;
|
|
17
|
+
shown: Pick<AssetFile, "key">[];
|
|
18
|
+
subfolders: Pick<AssetFolder, "prefix">[];
|
|
19
|
+
}): MediaSelectableItem[];
|
|
20
|
+
export declare function mediaSelectionState(selectableIds: string[], selected: ReadonlySet<string>): {
|
|
21
|
+
selectedCount: number;
|
|
22
|
+
allSelected: boolean;
|
|
23
|
+
someSelected: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function summarizeMediaSelection(ids: Iterable<string>): MediaSelectionSummary;
|
|
26
|
+
/**
|
|
27
|
+
* Deduplica la selezione e rimuove discendenti già coperti da una cartella selezionata.
|
|
28
|
+
* L'ordine delle cartelle va dalla più esterna alla più interna per rendere il risultato stabile.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeMediaSelection(ids: Iterable<string>): {
|
|
31
|
+
files: string[];
|
|
32
|
+
folders: string[];
|
|
33
|
+
ids: string[];
|
|
34
|
+
};
|
|
35
|
+
export declare function invalidMediaMoveReason(ids: Iterable<string>, destination: string): string | null;
|
|
36
|
+
export declare function isValidMediaMoveDestination(ids: Iterable<string>, destination: string): boolean;
|
|
37
|
+
export declare function mediaFolderSlug(name: string): string;
|
|
38
|
+
/** Guardia sincrona condivisibile con una ref React; il secondo avvio perde immediatamente. */
|
|
39
|
+
export declare function tryBeginMediaMutation(lock: {
|
|
40
|
+
current: boolean;
|
|
41
|
+
}): boolean;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** L'unico dominio su cui "Seleziona tutto" e la relativa checkbox possono ragionare. */
|
|
2
|
+
export function mediaSelectableItems({ mode, prefix, shown, subfolders, }) {
|
|
3
|
+
const visibleFolders = mode === "manage" && prefix !== ""
|
|
4
|
+
? subfolders.map((folder) => ({ id: folder.prefix, kind: "folder" }))
|
|
5
|
+
: [];
|
|
6
|
+
return [
|
|
7
|
+
...visibleFolders,
|
|
8
|
+
...shown.map((file) => ({ id: file.key, kind: "file" })),
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
export function mediaSelectionState(selectableIds, selected) {
|
|
12
|
+
const selectedCount = selectableIds.reduce((count, id) => count + Number(selected.has(id)), 0);
|
|
13
|
+
return {
|
|
14
|
+
selectedCount,
|
|
15
|
+
allSelected: selectableIds.length > 0 && selectedCount === selectableIds.length,
|
|
16
|
+
someSelected: selectedCount > 0 && selectedCount < selectableIds.length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function summarizeMediaSelection(ids) {
|
|
20
|
+
const unique = [...new Set(ids)];
|
|
21
|
+
const files = unique.filter((id) => !id.endsWith("/"));
|
|
22
|
+
const folders = unique.filter((id) => id.endsWith("/"));
|
|
23
|
+
const parts = [
|
|
24
|
+
files.length ? `${files.length} file` : "",
|
|
25
|
+
folders.length ? `${folders.length} ${folders.length === 1 ? "cartella" : "cartelle"}` : "",
|
|
26
|
+
].filter(Boolean);
|
|
27
|
+
const agreement = folders.length && !files.length
|
|
28
|
+
? (folders.length === 1 ? "selezionata" : "selezionate")
|
|
29
|
+
: (unique.length === 1 ? "selezionato" : "selezionati");
|
|
30
|
+
return {
|
|
31
|
+
files,
|
|
32
|
+
folders,
|
|
33
|
+
total: unique.length,
|
|
34
|
+
label: `${parts.join(" e ")} ${agreement}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Deduplica la selezione e rimuove discendenti già coperti da una cartella selezionata.
|
|
39
|
+
* L'ordine delle cartelle va dalla più esterna alla più interna per rendere il risultato stabile.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeMediaSelection(ids) {
|
|
42
|
+
const unique = [...new Set(ids)].filter((id) => typeof id === "string" && id.length > 0);
|
|
43
|
+
const folders = unique.filter((id) => id.endsWith("/")).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
44
|
+
const roots = [];
|
|
45
|
+
for (const folder of folders) {
|
|
46
|
+
if (!roots.some((root) => folder.startsWith(root)))
|
|
47
|
+
roots.push(folder);
|
|
48
|
+
}
|
|
49
|
+
const files = unique
|
|
50
|
+
.filter((id) => !id.endsWith("/") && !roots.some((root) => id.startsWith(root)))
|
|
51
|
+
.sort();
|
|
52
|
+
return { files, folders: roots, ids: [...roots, ...files] };
|
|
53
|
+
}
|
|
54
|
+
export function invalidMediaMoveReason(ids, destination) {
|
|
55
|
+
const { folders } = normalizeMediaSelection(ids);
|
|
56
|
+
for (const source of folders) {
|
|
57
|
+
if (destination === source) {
|
|
58
|
+
return `La destinazione «${source}» coincide con una cartella selezionata.`;
|
|
59
|
+
}
|
|
60
|
+
if (destination.startsWith(source)) {
|
|
61
|
+
return `La destinazione «${destination}» si trova dentro la cartella selezionata «${source}».`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function isValidMediaMoveDestination(ids, destination) {
|
|
67
|
+
return invalidMediaMoveReason(ids, destination) === null;
|
|
68
|
+
}
|
|
69
|
+
export function mediaFolderSlug(name) {
|
|
70
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
71
|
+
}
|
|
72
|
+
/** Guardia sincrona condivisibile con una ref React; il secondo avvio perde immediatamente. */
|
|
73
|
+
export function tryBeginMediaMutation(lock) {
|
|
74
|
+
if (lock.current)
|
|
75
|
+
return false;
|
|
76
|
+
lock.current = true;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AssetFile, AssetFolder } from "../../api.js";
|
|
2
|
+
export interface PlannedFolderMove {
|
|
3
|
+
source: string;
|
|
4
|
+
name: string;
|
|
5
|
+
target: string;
|
|
6
|
+
files: AssetFile[];
|
|
7
|
+
folders: AssetFolder[];
|
|
8
|
+
}
|
|
9
|
+
export interface MediaMovePlan {
|
|
10
|
+
directFiles: string[];
|
|
11
|
+
folders: PlannedFolderMove[];
|
|
12
|
+
totalFiles: number;
|
|
13
|
+
}
|
|
14
|
+
interface MoveResult {
|
|
15
|
+
moved?: number;
|
|
16
|
+
urlMap?: Record<string, string>;
|
|
17
|
+
failed?: {
|
|
18
|
+
key: string;
|
|
19
|
+
error: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface MediaMoveExecutor {
|
|
23
|
+
createFolder(parent: string, name: string, opts: {
|
|
24
|
+
label: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
prefix: string;
|
|
27
|
+
exists?: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
move(keys: string[], destination: string): Promise<MoveResult>;
|
|
30
|
+
applyReferences(urlMap: Record<string, string>): Promise<{
|
|
31
|
+
failed?: {
|
|
32
|
+
key: string;
|
|
33
|
+
error: string;
|
|
34
|
+
}[];
|
|
35
|
+
}>;
|
|
36
|
+
removeSourceFolders(prefixes: string[]): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare function mediaMoveNeedsConfirmation(plan: MediaMovePlan, multiFileThreshold?: number): boolean;
|
|
39
|
+
export declare function mediaMoveConfirmation(plan: MediaMovePlan, destinationLabel: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Esegue un piano già validato. Al primo errore non avvia altri gruppi né applica i riferimenti;
|
|
42
|
+
* le copie riuscite restano duplicati sicuri e gli originali restano disponibili.
|
|
43
|
+
*/
|
|
44
|
+
export declare function executeMediaMovePlan(plan: MediaMovePlan, destination: string, executor: MediaMoveExecutor, onProgress: (done: number, total: number) => void, chunkSize?: number): Promise<{
|
|
45
|
+
completed: number;
|
|
46
|
+
total: number;
|
|
47
|
+
}>;
|
|
48
|
+
export {};
|