@masters-union/union-stack 0.3.9 → 0.3.10
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/cdn/loader.v1.global.js +4 -2
- package/dist/cdn/loader.v1.global.js.map +1 -1
- package/dist/{chunk-ZOOUTUWU.cjs → chunk-F4EVNUJZ.cjs} +3 -3
- package/dist/{chunk-ZOOUTUWU.cjs.map → chunk-F4EVNUJZ.cjs.map} +1 -1
- package/dist/{chunk-DV5YSBNS.js → chunk-IIVRPVNX.js} +3 -3
- package/dist/{chunk-DV5YSBNS.js.map → chunk-IIVRPVNX.js.map} +1 -1
- package/dist/{chunk-27RCQJAE.cjs → chunk-IV76YPDA.cjs} +7 -7
- package/dist/{chunk-27RCQJAE.cjs.map → chunk-IV76YPDA.cjs.map} +1 -1
- package/dist/{chunk-YNWENXR2.js → chunk-MFZ2JGLP.js} +3 -3
- package/dist/{chunk-YNWENXR2.js.map → chunk-MFZ2JGLP.js.map} +1 -1
- package/dist/{client-B1KPVLJR.d.ts → client-C0qMOfb3.d.ts} +18 -1
- package/dist/{client-DP2jyZzm.d.cts → client-DOTa3Mai.d.cts} +18 -1
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/node.cjs +7 -7
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +1 -1
- package/dist/picker.cjs +213 -11
- package/dist/picker.cjs.map +1 -1
- package/dist/picker.d.cts +10 -3
- package/dist/picker.d.ts +10 -3
- package/dist/picker.js +213 -11
- package/dist/picker.js.map +1 -1
- package/dist/react.cjs +3 -3
- package/dist/react.d.cts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +2 -2
- package/dist/{types-50o0pbff.d.ts → types-ZCfjNZls.d.cts} +13 -0
- package/dist/{types-50o0pbff.d.cts → types-ZCfjNZls.d.ts} +13 -0
- package/package.json +1 -1
package/dist/picker.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { U as UnionStackClient, P as PickerOptions, a as PickerHandle } from './client-
|
|
2
|
-
export { b as PickerBranding, c as PickerTheme } from './client-
|
|
3
|
-
import { P as PickResponse } from './types-
|
|
1
|
+
import { U as UnionStackClient, P as PickerOptions, a as PickerHandle } from './client-DOTa3Mai.cjs';
|
|
2
|
+
export { b as PickerBranding, c as PickerTheme } from './client-DOTa3Mai.cjs';
|
|
3
|
+
import { P as PickResponse } from './types-ZCfjNZls.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Single-shot file picker modal. Built imperatively (no framework) so it can
|
|
@@ -56,6 +56,13 @@ declare class Picker {
|
|
|
56
56
|
private statusLabel;
|
|
57
57
|
private setItemState;
|
|
58
58
|
private updateSummary;
|
|
59
|
+
private needsCrop;
|
|
60
|
+
private hasUncroppedImages;
|
|
61
|
+
private anyValidating;
|
|
62
|
+
private shouldCheckDimensions;
|
|
63
|
+
private updateItemBadge;
|
|
64
|
+
private validateDimensions;
|
|
65
|
+
private maybeAutoUpload;
|
|
59
66
|
private refreshConfirm;
|
|
60
67
|
private flashNotice;
|
|
61
68
|
private startUpload;
|
package/dist/picker.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { U as UnionStackClient, P as PickerOptions, a as PickerHandle } from './client-
|
|
2
|
-
export { b as PickerBranding, c as PickerTheme } from './client-
|
|
3
|
-
import { P as PickResponse } from './types-
|
|
1
|
+
import { U as UnionStackClient, P as PickerOptions, a as PickerHandle } from './client-C0qMOfb3.js';
|
|
2
|
+
export { b as PickerBranding, c as PickerTheme } from './client-C0qMOfb3.js';
|
|
3
|
+
import { P as PickResponse } from './types-ZCfjNZls.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Single-shot file picker modal. Built imperatively (no framework) so it can
|
|
@@ -56,6 +56,13 @@ declare class Picker {
|
|
|
56
56
|
private statusLabel;
|
|
57
57
|
private setItemState;
|
|
58
58
|
private updateSummary;
|
|
59
|
+
private needsCrop;
|
|
60
|
+
private hasUncroppedImages;
|
|
61
|
+
private anyValidating;
|
|
62
|
+
private shouldCheckDimensions;
|
|
63
|
+
private updateItemBadge;
|
|
64
|
+
private validateDimensions;
|
|
65
|
+
private maybeAutoUpload;
|
|
59
66
|
private refreshConfirm;
|
|
60
67
|
private flashNotice;
|
|
61
68
|
private startUpload;
|
package/dist/picker.js
CHANGED
|
@@ -249,6 +249,8 @@ var BASE_CSS = `
|
|
|
249
249
|
}
|
|
250
250
|
.us-file[data-state="done"] { border-color: color-mix(in srgb, var(--us-success) 35%, var(--us-border)); }
|
|
251
251
|
.us-file[data-state="failed"] { border-color: color-mix(in srgb, var(--us-danger) 35%, var(--us-border)); }
|
|
252
|
+
/* Force-crop: image queued but not yet cropped to the required ratio. */
|
|
253
|
+
.us-file[data-crop-required="true"] { border-color: color-mix(in srgb, var(--us-danger) 35%, var(--us-border)); }
|
|
252
254
|
|
|
253
255
|
.us-file-thumb {
|
|
254
256
|
width: 40px; height: 40px; flex-shrink: 0;
|
|
@@ -613,6 +615,7 @@ var BASE_CSS = `
|
|
|
613
615
|
|
|
614
616
|
// src/picker/imageEditor.ts
|
|
615
617
|
var MAX_DISPLAY = 720;
|
|
618
|
+
var MIN_CROP = 24;
|
|
616
619
|
var ICON = {
|
|
617
620
|
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`,
|
|
618
621
|
crop: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>`,
|
|
@@ -643,7 +646,31 @@ var ImageEditor = class {
|
|
|
643
646
|
const start = this.dragStart.rect;
|
|
644
647
|
const maxW = this.displayCanvas.width;
|
|
645
648
|
const maxH = this.displayCanvas.height;
|
|
646
|
-
const min =
|
|
649
|
+
const min = MIN_CROP;
|
|
650
|
+
if (this.aspect && this.dragKind !== "move") {
|
|
651
|
+
switch (this.dragKind) {
|
|
652
|
+
case "se":
|
|
653
|
+
this.cropRect = this.lockedRectFromDrag(start.x, start.y, start.w + dx, 1, 1);
|
|
654
|
+
break;
|
|
655
|
+
case "nw": {
|
|
656
|
+
const ax = start.x + start.w, ay = start.y + start.h;
|
|
657
|
+
this.cropRect = this.lockedRectFromDrag(ax, ay, ax - (start.x + dx), -1, -1);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "ne": {
|
|
661
|
+
const ay = start.y + start.h;
|
|
662
|
+
this.cropRect = this.lockedRectFromDrag(start.x, ay, start.w + dx, 1, -1);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "sw": {
|
|
666
|
+
const ax = start.x + start.w;
|
|
667
|
+
this.cropRect = this.lockedRectFromDrag(ax, start.y, ax - (start.x + dx), -1, 1);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.updateCropBox();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
647
674
|
let { x, y, w, h } = start;
|
|
648
675
|
switch (this.dragKind) {
|
|
649
676
|
case "move":
|
|
@@ -690,6 +717,12 @@ var ImageEditor = class {
|
|
|
690
717
|
document.removeEventListener("pointerup", this.onPointerUp);
|
|
691
718
|
};
|
|
692
719
|
}
|
|
720
|
+
// Locked crop ratio (width / height), or null for free-form. Derived from
|
|
721
|
+
// opts so there's a single source of truth.
|
|
722
|
+
get aspect() {
|
|
723
|
+
const a = this.opts.lockedAspect;
|
|
724
|
+
return a && a.width > 0 && a.height > 0 ? a.width / a.height : null;
|
|
725
|
+
}
|
|
693
726
|
async open() {
|
|
694
727
|
this.mount();
|
|
695
728
|
try {
|
|
@@ -820,8 +853,33 @@ var ImageEditor = class {
|
|
|
820
853
|
this.cropTool.innerHTML = `${ICON.check} <span>Apply crop</span>`;
|
|
821
854
|
const dw = this.displayCanvas.width;
|
|
822
855
|
const dh = this.displayCanvas.height;
|
|
823
|
-
|
|
824
|
-
|
|
856
|
+
if (this.aspect) {
|
|
857
|
+
let w = dw;
|
|
858
|
+
let h = w / this.aspect;
|
|
859
|
+
if (h > dh) {
|
|
860
|
+
h = dh;
|
|
861
|
+
w = h * this.aspect;
|
|
862
|
+
}
|
|
863
|
+
w = Math.max(MIN_CROP, Math.round(w * 0.9));
|
|
864
|
+
h = Math.max(MIN_CROP, Math.round(w / this.aspect));
|
|
865
|
+
if (w > dw) {
|
|
866
|
+
w = dw;
|
|
867
|
+
h = w / this.aspect;
|
|
868
|
+
}
|
|
869
|
+
if (h > dh) {
|
|
870
|
+
h = dh;
|
|
871
|
+
w = h * this.aspect;
|
|
872
|
+
}
|
|
873
|
+
this.cropRect = {
|
|
874
|
+
x: Math.round((dw - w) / 2),
|
|
875
|
+
y: Math.round((dh - h) / 2),
|
|
876
|
+
w,
|
|
877
|
+
h
|
|
878
|
+
};
|
|
879
|
+
} else {
|
|
880
|
+
const inset = Math.round(Math.min(dw, dh) * 0.1);
|
|
881
|
+
this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
|
|
882
|
+
}
|
|
825
883
|
this.renderCropBox();
|
|
826
884
|
}
|
|
827
885
|
exitCropMode() {
|
|
@@ -873,6 +931,28 @@ var ImageEditor = class {
|
|
|
873
931
|
document.addEventListener("pointermove", this.onPointerMove);
|
|
874
932
|
document.addEventListener("pointerup", this.onPointerUp);
|
|
875
933
|
}
|
|
934
|
+
// Build a ratio-locked rect from a fixed anchor corner and a desired width.
|
|
935
|
+
// signX/signY say whether the dragged corner sits to the right/below the
|
|
936
|
+
// anchor (+1) or left/above (-1). Height is always derived from width to keep
|
|
937
|
+
// the ratio exact; both are clamped so the rect stays on-canvas.
|
|
938
|
+
lockedRectFromDrag(anchorX, anchorY, desiredW, signX, signY) {
|
|
939
|
+
const a = this.aspect;
|
|
940
|
+
const maxW = this.displayCanvas.width;
|
|
941
|
+
const maxH = this.displayCanvas.height;
|
|
942
|
+
let w = Math.max(MIN_CROP, desiredW);
|
|
943
|
+
let h = w / a;
|
|
944
|
+
if (h < MIN_CROP) {
|
|
945
|
+
h = MIN_CROP;
|
|
946
|
+
w = h * a;
|
|
947
|
+
}
|
|
948
|
+
const roomX = signX > 0 ? maxW - anchorX : anchorX;
|
|
949
|
+
const roomY = signY > 0 ? maxH - anchorY : anchorY;
|
|
950
|
+
w = Math.min(w, roomX, roomY * a);
|
|
951
|
+
h = w / a;
|
|
952
|
+
const x = signX > 0 ? anchorX : anchorX - w;
|
|
953
|
+
const y = signY > 0 ? anchorY : anchorY - h;
|
|
954
|
+
return { x, y, w, h };
|
|
955
|
+
}
|
|
876
956
|
// ---- operations (mutate working canvas) --------------------------------
|
|
877
957
|
applyCrop() {
|
|
878
958
|
if (!this.cropRect) {
|
|
@@ -893,6 +973,7 @@ var ImageEditor = class {
|
|
|
893
973
|
this.working = next;
|
|
894
974
|
this.exitCropMode();
|
|
895
975
|
this.markEdited(true);
|
|
976
|
+
this.opts.onCropApplied?.();
|
|
896
977
|
this.draw();
|
|
897
978
|
}
|
|
898
979
|
applyRotate() {
|
|
@@ -1039,6 +1120,12 @@ function mergeConfig(server, runtime) {
|
|
|
1039
1120
|
const types = server.constraints.allowedMimeTypes.filter((t) => t !== "*/*");
|
|
1040
1121
|
if (types.length > 0) merged.accept = types.join(",");
|
|
1041
1122
|
}
|
|
1123
|
+
const srvImg = server.image ?? null;
|
|
1124
|
+
const ratio = runtime.cropAspectRatio ?? (srvImg && srvImg.cropRatioW && srvImg.cropRatioH ? { width: srvImg.cropRatioW, height: srvImg.cropRatioH } : void 0);
|
|
1125
|
+
merged.cropAspectRatio = ratio;
|
|
1126
|
+
merged.forceCrop = (runtime.forceCrop ?? srvImg?.forceCrop ?? false) && !!ratio && runtime.imageEditing !== false;
|
|
1127
|
+
merged.minImageWidth = runtime.minImageWidth ?? srvImg?.minWidth ?? void 0;
|
|
1128
|
+
merged.minImageHeight = runtime.minImageHeight ?? srvImg?.minHeight ?? void 0;
|
|
1042
1129
|
return merged;
|
|
1043
1130
|
}
|
|
1044
1131
|
var DEFAULT_TITLE = "Upload files";
|
|
@@ -1511,15 +1598,13 @@ var Picker = class {
|
|
|
1511
1598
|
};
|
|
1512
1599
|
this.items.push(item);
|
|
1513
1600
|
this.renderItem(item);
|
|
1601
|
+
if (this.shouldCheckDimensions(file)) {
|
|
1602
|
+
item.validating = true;
|
|
1603
|
+
this.validateDimensions(item);
|
|
1604
|
+
}
|
|
1514
1605
|
}
|
|
1515
1606
|
this.refreshConfirm();
|
|
1516
|
-
|
|
1517
|
-
const hasQueued = this.items.some((i) => i.state === "queued");
|
|
1518
|
-
if (autoUpload && hasQueued && !this.uploadStarted) {
|
|
1519
|
-
requestAnimationFrame(() => {
|
|
1520
|
-
if (!this.uploadStarted) this.startUpload();
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1607
|
+
this.maybeAutoUpload();
|
|
1523
1608
|
}
|
|
1524
1609
|
renderItem(item) {
|
|
1525
1610
|
if (!this.$list) return;
|
|
@@ -1578,6 +1663,7 @@ var Picker = class {
|
|
|
1578
1663
|
item.$thumb = thumb;
|
|
1579
1664
|
this.$list.appendChild(row);
|
|
1580
1665
|
this.updateSummary();
|
|
1666
|
+
this.updateItemBadge(item);
|
|
1581
1667
|
}
|
|
1582
1668
|
// Swap a queued item's file with an edited version. Refreshes thumbnail and
|
|
1583
1669
|
// meta in place so the row identity is preserved (uploadId, position).
|
|
@@ -1615,15 +1701,27 @@ var Picker = class {
|
|
|
1615
1701
|
// nothing (Cancel/Back).
|
|
1616
1702
|
openEditor(item) {
|
|
1617
1703
|
if (!this.$panel || this.editor || item.state !== "queued") return;
|
|
1704
|
+
let didRatioCrop = false;
|
|
1618
1705
|
this.editor = new ImageEditor({
|
|
1619
1706
|
host: this.$panel,
|
|
1620
1707
|
file: item.file,
|
|
1621
1708
|
originalFile: item.originalFile,
|
|
1622
1709
|
title: item.originalFile.name,
|
|
1710
|
+
lockedAspect: this.opts.cropAspectRatio,
|
|
1711
|
+
onCropApplied: () => {
|
|
1712
|
+
didRatioCrop = true;
|
|
1713
|
+
},
|
|
1623
1714
|
onApply: (edited) => {
|
|
1624
1715
|
const wasEdited = edited !== item.originalFile;
|
|
1625
1716
|
this.replaceItemFile(item, edited, wasEdited);
|
|
1717
|
+
if (didRatioCrop) item.ratioCropped = true;
|
|
1626
1718
|
this.editor = null;
|
|
1719
|
+
if (this.shouldCheckDimensions(item.file)) {
|
|
1720
|
+
item.validating = true;
|
|
1721
|
+
this.validateDimensions(item);
|
|
1722
|
+
}
|
|
1723
|
+
this.updateItemBadge(item);
|
|
1724
|
+
this.refreshConfirm();
|
|
1627
1725
|
},
|
|
1628
1726
|
onCancel: () => {
|
|
1629
1727
|
this.editor = null;
|
|
@@ -1712,10 +1810,77 @@ var Picker = class {
|
|
|
1712
1810
|
this.$summary.textContent = `${total} file${total === 1 ? "" : "s"} ready`;
|
|
1713
1811
|
}
|
|
1714
1812
|
}
|
|
1813
|
+
// ---- image rules (force-crop + min dimensions) --------------------------
|
|
1814
|
+
// True when force-crop is on and this queued raster image hasn't been cropped
|
|
1815
|
+
// to the locked ratio yet. SVGs are exempt (vector→raster crop is lossy).
|
|
1816
|
+
needsCrop(item) {
|
|
1817
|
+
return this.opts.forceCrop === true && !!this.opts.cropAspectRatio && item.state === "queued" && item.file.type.startsWith("image/") && item.file.type !== "image/svg+xml" && !item.ratioCropped;
|
|
1818
|
+
}
|
|
1819
|
+
hasUncroppedImages() {
|
|
1820
|
+
return this.items.some((i) => this.needsCrop(i));
|
|
1821
|
+
}
|
|
1822
|
+
anyValidating() {
|
|
1823
|
+
return this.items.some((i) => i.validating === true);
|
|
1824
|
+
}
|
|
1825
|
+
// Whether a min-dimension rule applies to this file (image, non-SVG, rule set).
|
|
1826
|
+
shouldCheckDimensions(file) {
|
|
1827
|
+
const minConfigured = !!(this.opts.minImageWidth || this.opts.minImageHeight);
|
|
1828
|
+
return minConfigured && file.type.startsWith("image/") && file.type !== "image/svg+xml";
|
|
1829
|
+
}
|
|
1830
|
+
// Toggle the per-row "crop required" cue. Only touches the meta line for
|
|
1831
|
+
// queued rows (other states own their meta via setItemState).
|
|
1832
|
+
updateItemBadge(item) {
|
|
1833
|
+
if (!item.$row) return;
|
|
1834
|
+
if (this.needsCrop(item)) {
|
|
1835
|
+
item.$row.dataset.cropRequired = "true";
|
|
1836
|
+
if (item.$meta) item.$meta.textContent = "Crop to ratio required";
|
|
1837
|
+
} else {
|
|
1838
|
+
delete item.$row.dataset.cropRequired;
|
|
1839
|
+
if (item.state === "queued" && item.$meta && !item.validating) {
|
|
1840
|
+
item.$meta.textContent = formatBytes(item.file.size);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Decode an image and fail the row if it's below the configured minimum.
|
|
1845
|
+
// Non-decodable inputs are left queued — we can't prove they're too small.
|
|
1846
|
+
async validateDimensions(item) {
|
|
1847
|
+
const minW = this.opts.minImageWidth ?? 0;
|
|
1848
|
+
const minH = this.opts.minImageHeight ?? 0;
|
|
1849
|
+
let dim = null;
|
|
1850
|
+
try {
|
|
1851
|
+
dim = await readImageSize(item.file);
|
|
1852
|
+
} catch {
|
|
1853
|
+
dim = null;
|
|
1854
|
+
}
|
|
1855
|
+
if (item.state !== "queued") {
|
|
1856
|
+
item.validating = false;
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
item.validating = false;
|
|
1860
|
+
if (dim && dim.w > 0 && dim.h > 0 && (dim.w < minW || dim.h < minH)) {
|
|
1861
|
+
const need = `${minW || "?"}\xD7${minH || "?"}`;
|
|
1862
|
+
item.error = `Image too small \u2014 needs at least ${need}px (got ${dim.w}\xD7${dim.h})`;
|
|
1863
|
+
this.setItemState(item, "failed");
|
|
1864
|
+
} else {
|
|
1865
|
+
this.updateItemBadge(item);
|
|
1866
|
+
}
|
|
1867
|
+
this.refreshConfirm();
|
|
1868
|
+
this.maybeAutoUpload();
|
|
1869
|
+
}
|
|
1870
|
+
// Fire auto-upload once nothing is blocking it (no pending validation, no
|
|
1871
|
+
// pending force-crops). Re-checked whenever a validation settles.
|
|
1872
|
+
maybeAutoUpload() {
|
|
1873
|
+
if (this.opts.autoUpload !== true || this.uploadStarted) return;
|
|
1874
|
+
if (this.anyValidating() || this.hasUncroppedImages()) return;
|
|
1875
|
+
if (!this.items.some((i) => i.state === "queued")) return;
|
|
1876
|
+
requestAnimationFrame(() => {
|
|
1877
|
+
if (!this.uploadStarted) this.startUpload();
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1715
1880
|
refreshConfirm() {
|
|
1716
1881
|
if (!this.$confirm) return;
|
|
1717
1882
|
const queued = this.items.filter((i) => i.state === "queued").length;
|
|
1718
|
-
this.$confirm.disabled = queued === 0 || this.uploadStarted;
|
|
1883
|
+
this.$confirm.disabled = queued === 0 || this.uploadStarted || this.hasUncroppedImages() || this.anyValidating();
|
|
1719
1884
|
}
|
|
1720
1885
|
// Transient message in the actions bar (e.g. "2 files skipped — too large").
|
|
1721
1886
|
flashNotice(text) {
|
|
@@ -1734,6 +1899,15 @@ var Picker = class {
|
|
|
1734
1899
|
// ---- upload -------------------------------------------------------------
|
|
1735
1900
|
async startUpload() {
|
|
1736
1901
|
if (this.uploadStarted) return;
|
|
1902
|
+
if (this.anyValidating()) {
|
|
1903
|
+
this.flashNotice("Checking image dimensions\u2026");
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (this.hasUncroppedImages()) {
|
|
1907
|
+
this.items.forEach((i) => this.updateItemBadge(i));
|
|
1908
|
+
this.flashNotice("Crop required \u2014 crop every image to the set ratio before uploading.");
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1737
1911
|
const queued = this.items.filter((i) => i.state === "queued");
|
|
1738
1912
|
if (queued.length === 0) return;
|
|
1739
1913
|
this.uploadStarted = true;
|
|
@@ -1889,6 +2063,34 @@ function cryptoId() {
|
|
|
1889
2063
|
}
|
|
1890
2064
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1891
2065
|
}
|
|
2066
|
+
async function readImageSize(file) {
|
|
2067
|
+
if (typeof createImageBitmap === "function") {
|
|
2068
|
+
try {
|
|
2069
|
+
const bmp = await createImageBitmap(file);
|
|
2070
|
+
const w = bmp.width, h = bmp.height;
|
|
2071
|
+
bmp.close?.();
|
|
2072
|
+
return { w, h };
|
|
2073
|
+
} catch {
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
return new Promise((resolve, reject) => {
|
|
2077
|
+
if (typeof URL === "undefined" || !URL.createObjectURL) {
|
|
2078
|
+
reject(new Error("no object URL support"));
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const url = URL.createObjectURL(file);
|
|
2082
|
+
const img = new Image();
|
|
2083
|
+
img.onload = () => {
|
|
2084
|
+
URL.revokeObjectURL(url);
|
|
2085
|
+
resolve({ w: img.naturalWidth, h: img.naturalHeight });
|
|
2086
|
+
};
|
|
2087
|
+
img.onerror = () => {
|
|
2088
|
+
URL.revokeObjectURL(url);
|
|
2089
|
+
reject(new Error("decode failed"));
|
|
2090
|
+
};
|
|
2091
|
+
img.src = url;
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
1892
2094
|
async function fetchUrlAsFile(url) {
|
|
1893
2095
|
let res;
|
|
1894
2096
|
try {
|