@haklex/rich-plugin-image-editor 0.42.1 → 0.43.0
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/ImageEditModal.d.ts +5 -2
- package/dist/ImageEditModal.d.ts.map +1 -1
- package/dist/ImageEditModalPlugin.d.ts +5 -1
- package/dist/ImageEditModalPlugin.d.ts.map +1 -1
- package/dist/ImageInsertSheet.d.ts +12 -0
- package/dist/ImageInsertSheet.d.ts.map +1 -0
- package/dist/MosaicOptionsBar.d.ts +12 -0
- package/dist/MosaicOptionsBar.d.ts.map +1 -0
- package/dist/OutputSizeControl.d.ts +18 -0
- package/dist/OutputSizeControl.d.ts.map +1 -0
- package/dist/SheetRowItem.d.ts +10 -0
- package/dist/SheetRowItem.d.ts.map +1 -0
- package/dist/ToolRail.d.ts +9 -0
- package/dist/ToolRail.d.ts.map +1 -0
- package/dist/annotation.d.ts +1 -1
- package/dist/annotation.d.ts.map +1 -1
- package/dist/edit-summary.d.ts +9 -0
- package/dist/edit-summary.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +846 -144
- package/dist/insert-sheet-model.d.ts +22 -0
- package/dist/insert-sheet-model.d.ts.map +1 -0
- package/dist/mosaic.d.ts +14 -0
- package/dist/mosaic.d.ts.map +1 -0
- package/dist/pipeline.d.ts +2 -1
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/resize.d.ts +9 -0
- package/dist/resize.d.ts.map +1 -0
- package/dist/rich-plugin-image-editor.css +1 -1
- package/dist/sheet.css.d.ts +22 -0
- package/dist/sheet.css.d.ts.map +1 -0
- package/dist/styles.css.d.ts +9 -0
- package/dist/styles.css.d.ts.map +1 -1
- package/dist/useEscapeDismissesTopDialog.d.ts +2 -0
- package/dist/useEscapeDismissesTopDialog.d.ts.map +1 -0
- package/dist/useImageEditorState.d.ts +6 -1
- package/dist/useImageEditorState.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Circle, Crop, Hash, ImageOff, MoveUpRight, PaintBucket, Pen, Redo2, Square, Type, Undo2 } from "lucide-react";
|
|
1
|
+
import { Circle, Crop, Grid3x3, Hash, ImageOff, MapPin, MoveUpRight, PaintBucket, Pen, Pencil, Redo2, Square, Type, Undo2, X } from "lucide-react";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { ReactCrop } from "react-image-crop";
|
|
4
4
|
import { MarkerArea, Renderer, TextMarkerEditor } from "@markerjs/markerjs3";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { dismissTopDialog, presentDialog } from "@haklex/rich-editor-ui";
|
|
6
7
|
import { useImagePreprocess } from "@haklex/rich-editor/plugins";
|
|
7
|
-
import { presentDialog } from "@haklex/rich-editor-ui";
|
|
8
8
|
import { usePortalTheme } from "@haklex/rich-style-token";
|
|
9
9
|
//#region src/annotation.ts
|
|
10
10
|
var MARKER_TYPE_BY_TOOL = {
|
|
@@ -17,7 +17,7 @@ var MARKER_TYPE_BY_TOOL = {
|
|
|
17
17
|
text: "TextMarker"
|
|
18
18
|
};
|
|
19
19
|
function isAnnotationTool(tool) {
|
|
20
|
-
return tool !== "crop";
|
|
20
|
+
return tool !== "crop" && tool !== "mosaic";
|
|
21
21
|
}
|
|
22
22
|
var SWATCH_COLORS = [
|
|
23
23
|
"#ef4444",
|
|
@@ -71,19 +71,29 @@ var toolRail = "_1257qq29";
|
|
|
71
71
|
var toolButton = "_1257qq2a";
|
|
72
72
|
var toolButtonActive = "_1257qq2b";
|
|
73
73
|
var canvasArea = "_1257qq2d";
|
|
74
|
+
var cropSurface = "_1257qq2e";
|
|
75
|
+
var cropImage = "_1257qq2f";
|
|
74
76
|
var annotationSurface = "_1257qq2g";
|
|
75
77
|
var optionsBar = "_1257qq2h";
|
|
76
78
|
var swatch = "_1257qq2i";
|
|
77
79
|
var swatchActive = "_1257qq2j";
|
|
78
80
|
var optionButton = "_1257qq2k";
|
|
81
|
+
var optionButtonActive = "_1257qq2l";
|
|
79
82
|
var optionDivider = "_1257qq2n";
|
|
80
83
|
var errorState = "_1257qq2o";
|
|
81
84
|
var attribution = "_1257qq2p";
|
|
82
|
-
var footer = "_1257qq2q";
|
|
85
|
+
var footer$1 = "_1257qq2q";
|
|
83
86
|
var footerSpacer = "_1257qq2r";
|
|
84
|
-
var ghostButton = "_1257qq2t
|
|
85
|
-
var
|
|
86
|
-
var
|
|
87
|
+
var ghostButton = "_1257qq2u _1257qq2t";
|
|
88
|
+
var topBarGhostButton = "_1257qq2v _1257qq2u _1257qq2t";
|
|
89
|
+
var secondaryButton = "_1257qq2w _1257qq2t";
|
|
90
|
+
var primaryButton = "_1257qq2x _1257qq2t";
|
|
91
|
+
var segmentButton = "_1257qq2y _1257qq2k";
|
|
92
|
+
var rangeField = "_1257qq2z";
|
|
93
|
+
var rangeInput = "_1257qq210";
|
|
94
|
+
var rangeValue = "_1257qq211";
|
|
95
|
+
var sizeControl = "_1257qq212";
|
|
96
|
+
var sizeSelect = "_1257qq213";
|
|
87
97
|
//#endregion
|
|
88
98
|
//#region src/AnnotationSurface.tsx
|
|
89
99
|
var TEXT_TOOLS = /* @__PURE__ */ new Set(["counter", "text"]);
|
|
@@ -325,6 +335,228 @@ async function canvasToObjectUrl(canvas) {
|
|
|
325
335
|
return URL.createObjectURL(blob);
|
|
326
336
|
}
|
|
327
337
|
//#endregion
|
|
338
|
+
//#region src/edit-summary.ts
|
|
339
|
+
function buildEditSummary({ cropped, marks, mosaics, resizedTo }) {
|
|
340
|
+
const parts = [];
|
|
341
|
+
if (cropped) parts.push("Cropped");
|
|
342
|
+
if (marks > 0) parts.push(`${marks} mark${marks > 1 ? "s" : ""}`);
|
|
343
|
+
if (mosaics > 0) parts.push(`${mosaics} mosaic`);
|
|
344
|
+
if (resizedTo) parts.push(`${resizedTo.width} × ${resizedTo.height}`);
|
|
345
|
+
return parts.join(" · ");
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/mosaic.ts
|
|
349
|
+
var MOSAIC_STRENGTH_RANGE = {
|
|
350
|
+
blur: {
|
|
351
|
+
max: 48,
|
|
352
|
+
min: 12
|
|
353
|
+
},
|
|
354
|
+
pixelate: {
|
|
355
|
+
max: 48,
|
|
356
|
+
min: 8
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var MOSAIC_DEFAULT_STRENGTH = {
|
|
360
|
+
blur: 20,
|
|
361
|
+
pixelate: 16
|
|
362
|
+
};
|
|
363
|
+
function clampMosaicStrength(mode, value) {
|
|
364
|
+
const { max, min } = MOSAIC_STRENGTH_RANGE[mode];
|
|
365
|
+
return Math.min(Math.max(Math.round(value), min), max);
|
|
366
|
+
}
|
|
367
|
+
function mosaicSampleSize(rect, block) {
|
|
368
|
+
return {
|
|
369
|
+
height: Math.max(1, Math.round(rect.height / block)),
|
|
370
|
+
width: Math.max(1, Math.round(rect.width / block))
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async function applyMosaic(sourceUrl, area, mode, block) {
|
|
374
|
+
const canvas = await sourceToCanvas(sourceUrl);
|
|
375
|
+
const rect = clampCropRect(area, canvas.width, canvas.height);
|
|
376
|
+
if (rect.width === 0 || rect.height === 0) throw new Error("Mosaic region is empty");
|
|
377
|
+
const sample = mosaicSampleSize(rect, block);
|
|
378
|
+
const small = document.createElement("canvas");
|
|
379
|
+
small.width = sample.width;
|
|
380
|
+
small.height = sample.height;
|
|
381
|
+
const smallCtx = small.getContext("2d");
|
|
382
|
+
const ctx = canvas.getContext("2d");
|
|
383
|
+
if (!smallCtx || !ctx) throw new Error("Canvas 2d context unavailable");
|
|
384
|
+
const smooth = mode === "blur";
|
|
385
|
+
smallCtx.imageSmoothingEnabled = smooth;
|
|
386
|
+
smallCtx.imageSmoothingQuality = "high";
|
|
387
|
+
smallCtx.drawImage(canvas, rect.x, rect.y, rect.width, rect.height, 0, 0, sample.width, sample.height);
|
|
388
|
+
ctx.imageSmoothingEnabled = smooth;
|
|
389
|
+
ctx.imageSmoothingQuality = "high";
|
|
390
|
+
ctx.drawImage(small, 0, 0, sample.width, sample.height, rect.x, rect.y, rect.width, rect.height);
|
|
391
|
+
return canvasToObjectUrl(canvas);
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/MosaicOptionsBar.tsx
|
|
395
|
+
var MODES = [{
|
|
396
|
+
id: "pixelate",
|
|
397
|
+
label: "Pixelate"
|
|
398
|
+
}, {
|
|
399
|
+
id: "blur",
|
|
400
|
+
label: "Blur"
|
|
401
|
+
}];
|
|
402
|
+
var MosaicOptionsBar = ({ canUndo, mode, onModeChange, onStrengthChange, onUndo, strength }) => {
|
|
403
|
+
const range = MOSAIC_STRENGTH_RANGE[mode];
|
|
404
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
405
|
+
className: optionsBar,
|
|
406
|
+
children: [
|
|
407
|
+
MODES.map((item) => /* @__PURE__ */ jsx("button", {
|
|
408
|
+
"aria-pressed": mode === item.id,
|
|
409
|
+
type: "button",
|
|
410
|
+
className: mode === item.id ? `${segmentButton} ${optionButtonActive}` : segmentButton,
|
|
411
|
+
onClick: () => onModeChange(item.id),
|
|
412
|
+
children: item.label
|
|
413
|
+
}, item.id)),
|
|
414
|
+
/* @__PURE__ */ jsx("div", { className: optionDivider }),
|
|
415
|
+
/* @__PURE__ */ jsxs("label", {
|
|
416
|
+
className: rangeField,
|
|
417
|
+
children: [
|
|
418
|
+
mode === "blur" ? "Radius" : "Block size",
|
|
419
|
+
/* @__PURE__ */ jsx("input", {
|
|
420
|
+
className: rangeInput,
|
|
421
|
+
max: range.max,
|
|
422
|
+
min: range.min,
|
|
423
|
+
type: "range",
|
|
424
|
+
value: strength,
|
|
425
|
+
onChange: (event) => onStrengthChange(Number(event.currentTarget.value))
|
|
426
|
+
}),
|
|
427
|
+
/* @__PURE__ */ jsxs("span", {
|
|
428
|
+
className: rangeValue,
|
|
429
|
+
children: [strength, " px"]
|
|
430
|
+
})
|
|
431
|
+
]
|
|
432
|
+
}),
|
|
433
|
+
/* @__PURE__ */ jsx("div", { className: optionDivider }),
|
|
434
|
+
/* @__PURE__ */ jsx("button", {
|
|
435
|
+
"aria-label": "Undo",
|
|
436
|
+
className: optionButton,
|
|
437
|
+
disabled: !canUndo,
|
|
438
|
+
title: "Undo",
|
|
439
|
+
type: "button",
|
|
440
|
+
onClick: onUndo,
|
|
441
|
+
children: /* @__PURE__ */ jsx(Undo2, { size: 16 })
|
|
442
|
+
})
|
|
443
|
+
]
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/resize.ts
|
|
448
|
+
var LONG_EDGE_PRESETS = [
|
|
449
|
+
3840,
|
|
450
|
+
2560,
|
|
451
|
+
1920,
|
|
452
|
+
1280
|
|
453
|
+
];
|
|
454
|
+
function canShrinkTo(size, longEdge) {
|
|
455
|
+
return longEdge < Math.max(size.width, size.height);
|
|
456
|
+
}
|
|
457
|
+
function scaleToLongEdge(size, longEdge) {
|
|
458
|
+
if (!canShrinkTo(size, longEdge)) return size;
|
|
459
|
+
const ratio = longEdge / Math.max(size.width, size.height);
|
|
460
|
+
return {
|
|
461
|
+
height: Math.max(1, Math.round(size.height * ratio)),
|
|
462
|
+
width: Math.max(1, Math.round(size.width * ratio))
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function scaleCanvasToLongEdge(source, longEdge) {
|
|
466
|
+
const target = scaleToLongEdge({
|
|
467
|
+
height: source.height,
|
|
468
|
+
width: source.width
|
|
469
|
+
}, longEdge);
|
|
470
|
+
if (target.width === source.width && target.height === source.height) return source;
|
|
471
|
+
const canvas = document.createElement("canvas");
|
|
472
|
+
canvas.width = target.width;
|
|
473
|
+
canvas.height = target.height;
|
|
474
|
+
const ctx = canvas.getContext("2d");
|
|
475
|
+
if (!ctx) throw new Error("Canvas 2d context unavailable");
|
|
476
|
+
ctx.imageSmoothingEnabled = true;
|
|
477
|
+
ctx.imageSmoothingQuality = "high";
|
|
478
|
+
ctx.drawImage(source, 0, 0, target.width, target.height);
|
|
479
|
+
return canvas;
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/OutputSizeControl.tsx
|
|
483
|
+
var CUSTOM = "custom";
|
|
484
|
+
function useOutputSize(size) {
|
|
485
|
+
const [mode, setMode] = useState("original");
|
|
486
|
+
const [presetEdge, setPresetEdge] = useState(null);
|
|
487
|
+
const [customEdge, setCustomEdge] = useState("");
|
|
488
|
+
const requested = mode === "custom" ? Number(customEdge) : presetEdge;
|
|
489
|
+
const maxLongEdge = requested && size && canShrinkTo(size, requested) ? requested : null;
|
|
490
|
+
return {
|
|
491
|
+
controlProps: {
|
|
492
|
+
customEdge,
|
|
493
|
+
maxLongEdge,
|
|
494
|
+
mode,
|
|
495
|
+
size,
|
|
496
|
+
onCustomEdgeChange: setCustomEdge,
|
|
497
|
+
onModeChange: (nextMode, edge) => {
|
|
498
|
+
setMode(nextMode);
|
|
499
|
+
setPresetEdge(edge);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
maxLongEdge
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
var OutputSizeControl = ({ customEdge, maxLongEdge, mode, onCustomEdgeChange, onModeChange, size }) => {
|
|
506
|
+
const output = size && maxLongEdge !== null ? scaleToLongEdge(size, maxLongEdge) : size;
|
|
507
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
508
|
+
className: sizeControl,
|
|
509
|
+
children: [
|
|
510
|
+
/* @__PURE__ */ jsxs("label", {
|
|
511
|
+
className: rangeField,
|
|
512
|
+
children: ["Long edge", /* @__PURE__ */ jsxs("select", {
|
|
513
|
+
className: sizeSelect,
|
|
514
|
+
disabled: !size,
|
|
515
|
+
value: mode === "preset" ? String(maxLongEdge) : mode,
|
|
516
|
+
onChange: (event) => {
|
|
517
|
+
const { value } = event.currentTarget;
|
|
518
|
+
if (value === "original") onModeChange("original", null);
|
|
519
|
+
else if (value === CUSTOM) onModeChange("custom", null);
|
|
520
|
+
else onModeChange("preset", Number(value));
|
|
521
|
+
},
|
|
522
|
+
children: [
|
|
523
|
+
/* @__PURE__ */ jsx("option", {
|
|
524
|
+
value: "original",
|
|
525
|
+
children: "Original"
|
|
526
|
+
}),
|
|
527
|
+
LONG_EDGE_PRESETS.map((edge) => /* @__PURE__ */ jsxs("option", {
|
|
528
|
+
disabled: !size || !canShrinkTo(size, edge),
|
|
529
|
+
value: edge,
|
|
530
|
+
children: [edge, " px"]
|
|
531
|
+
}, edge)),
|
|
532
|
+
/* @__PURE__ */ jsx("option", {
|
|
533
|
+
value: CUSTOM,
|
|
534
|
+
children: "Custom…"
|
|
535
|
+
})
|
|
536
|
+
]
|
|
537
|
+
})]
|
|
538
|
+
}),
|
|
539
|
+
mode === "custom" && /* @__PURE__ */ jsx("input", {
|
|
540
|
+
"aria-label": "Custom long edge in pixels",
|
|
541
|
+
className: "_1257qq214",
|
|
542
|
+
inputMode: "numeric",
|
|
543
|
+
placeholder: "1600",
|
|
544
|
+
value: customEdge,
|
|
545
|
+
onChange: (event) => onCustomEdgeChange(event.currentTarget.value.replaceAll(/\D/g, ""))
|
|
546
|
+
}),
|
|
547
|
+
output && /* @__PURE__ */ jsxs("span", {
|
|
548
|
+
className: "_1257qq215",
|
|
549
|
+
children: [
|
|
550
|
+
"→ ",
|
|
551
|
+
output.width,
|
|
552
|
+
" × ",
|
|
553
|
+
output.height
|
|
554
|
+
]
|
|
555
|
+
})
|
|
556
|
+
]
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
//#endregion
|
|
328
560
|
//#region src/build-result.ts
|
|
329
561
|
async function buildResult(original, canvas) {
|
|
330
562
|
if (!canvas) return original;
|
|
@@ -357,14 +589,15 @@ async function applyCropRebase(sourceUrl, rect, markerState) {
|
|
|
357
589
|
} : null
|
|
358
590
|
};
|
|
359
591
|
}
|
|
360
|
-
async function exportResult({ hasRebasedBitmap, markerState, original, pendingCropRect, sourceUrl }) {
|
|
592
|
+
async function exportResult({ hasRebasedBitmap, markerState, maxLongEdge, original, pendingCropRect, sourceUrl }) {
|
|
361
593
|
const hasMarkers = markerState !== null && markerState.markers.length > 0;
|
|
362
594
|
let canvas = null;
|
|
363
595
|
if (hasMarkers && markerState) {
|
|
364
596
|
canvas = await rasterizeAnnotations(sourceUrl, markerState);
|
|
365
597
|
if (pendingCropRect) canvas = cropCanvas(canvas, pendingCropRect);
|
|
366
598
|
} else if (pendingCropRect) canvas = await extractCrop(sourceUrl, pendingCropRect);
|
|
367
|
-
else if (hasRebasedBitmap) canvas = await sourceToCanvas(sourceUrl);
|
|
599
|
+
else if (hasRebasedBitmap || maxLongEdge !== null) canvas = await sourceToCanvas(sourceUrl);
|
|
600
|
+
if (canvas && maxLongEdge !== null) canvas = scaleCanvasToLongEdge(canvas, maxLongEdge);
|
|
368
601
|
return buildResult(original, canvas);
|
|
369
602
|
}
|
|
370
603
|
//#endregion
|
|
@@ -418,6 +651,85 @@ var ToolOptionsBar = ({ canRedo, canUndo, onRedo, onStrokeColorChange, onStrokeW
|
|
|
418
651
|
]
|
|
419
652
|
});
|
|
420
653
|
//#endregion
|
|
654
|
+
//#region src/ToolRail.tsx
|
|
655
|
+
var TOOLS = [
|
|
656
|
+
{
|
|
657
|
+
icon: Crop,
|
|
658
|
+
id: "crop",
|
|
659
|
+
label: "Crop"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
icon: MoveUpRight,
|
|
663
|
+
id: "arrow",
|
|
664
|
+
label: "Arrow"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
icon: Pen,
|
|
668
|
+
id: "pen",
|
|
669
|
+
label: "Pen"
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
icon: Square,
|
|
673
|
+
id: "rect",
|
|
674
|
+
label: "Rectangle"
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
icon: Circle,
|
|
678
|
+
id: "ellipse",
|
|
679
|
+
label: "Ellipse"
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
icon: Type,
|
|
683
|
+
id: "text",
|
|
684
|
+
label: "Text"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
icon: Hash,
|
|
688
|
+
id: "counter",
|
|
689
|
+
label: "Counter"
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
icon: PaintBucket,
|
|
693
|
+
id: "cover",
|
|
694
|
+
label: "Cover"
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
icon: Grid3x3,
|
|
698
|
+
id: "mosaic",
|
|
699
|
+
label: "Mosaic"
|
|
700
|
+
}
|
|
701
|
+
];
|
|
702
|
+
var ToolRail = ({ activeTool, disabled, onSelect }) => /* @__PURE__ */ jsx("div", {
|
|
703
|
+
className: toolRail,
|
|
704
|
+
children: TOOLS.map((tool, index) => /* @__PURE__ */ jsxs("span", {
|
|
705
|
+
className: toolGroup,
|
|
706
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
707
|
+
"aria-label": tool.label,
|
|
708
|
+
"aria-pressed": activeTool === tool.id,
|
|
709
|
+
disabled,
|
|
710
|
+
title: tool.label,
|
|
711
|
+
type: "button",
|
|
712
|
+
className: activeTool === tool.id ? `${toolButton} ${toolButtonActive}` : toolButton,
|
|
713
|
+
onClick: () => onSelect(tool.id),
|
|
714
|
+
children: /* @__PURE__ */ jsx(tool.icon, { size: 18 })
|
|
715
|
+
}), index === 0 && /* @__PURE__ */ jsx("div", { className: "_1257qq2c" })]
|
|
716
|
+
}, tool.id))
|
|
717
|
+
});
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/useEscapeDismissesTopDialog.ts
|
|
720
|
+
function useEscapeDismissesTopDialog() {
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
const handleKeyDown = (event) => {
|
|
723
|
+
if (event.key !== "Escape") return;
|
|
724
|
+
event.preventDefault();
|
|
725
|
+
event.stopImmediatePropagation();
|
|
726
|
+
dismissTopDialog();
|
|
727
|
+
};
|
|
728
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
729
|
+
return () => document.removeEventListener("keydown", handleKeyDown, true);
|
|
730
|
+
}, []);
|
|
731
|
+
}
|
|
732
|
+
//#endregion
|
|
421
733
|
//#region src/useImageEditorState.ts
|
|
422
734
|
var FULL_IMAGE_CROP = {
|
|
423
735
|
height: 100,
|
|
@@ -433,12 +745,25 @@ function useImageEditorState(objectUrl) {
|
|
|
433
745
|
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
|
|
434
746
|
const [naturalSize, setNaturalSize] = useState(null);
|
|
435
747
|
const markerStateRef = useRef(null);
|
|
748
|
+
const [mosaicHistory, setMosaicHistory] = useState([]);
|
|
436
749
|
const rebasedUrlRef = useRef(null);
|
|
437
750
|
rebasedUrlRef.current = rebasedBitmapUrl;
|
|
438
|
-
|
|
439
|
-
|
|
751
|
+
const mosaicHistoryRef = useRef(mosaicHistory);
|
|
752
|
+
mosaicHistoryRef.current = mosaicHistory;
|
|
753
|
+
const mountedRef = useRef(false);
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
mountedRef.current = true;
|
|
756
|
+
return () => {
|
|
757
|
+
mountedRef.current = false;
|
|
758
|
+
if (rebasedUrlRef.current) URL.revokeObjectURL(rebasedUrlRef.current);
|
|
759
|
+
for (const url of mosaicHistoryRef.current) if (url) URL.revokeObjectURL(url);
|
|
760
|
+
};
|
|
440
761
|
}, []);
|
|
441
762
|
const sourceUrl = rebasedBitmapUrl ?? objectUrl;
|
|
763
|
+
const sourceUrlRef = useRef(sourceUrl);
|
|
764
|
+
sourceUrlRef.current = sourceUrl;
|
|
765
|
+
const [mosaicBusy, setMosaicBusy] = useState(false);
|
|
766
|
+
const mosaicBusyRef = useRef(false);
|
|
442
767
|
const hasPendingCrop = croppedAreaPixels !== null && naturalSize !== null && croppedAreaPixels.width >= 1 && croppedAreaPixels.height >= 1 && !isFullImageCrop(croppedAreaPixels, naturalSize.width, naturalSize.height);
|
|
443
768
|
const resetCrop = () => {
|
|
444
769
|
setCrop(FULL_IMAGE_CROP);
|
|
@@ -450,6 +775,8 @@ function useImageEditorState(objectUrl) {
|
|
|
450
775
|
try {
|
|
451
776
|
const rebase = await applyCropRebase(sourceUrl, rect, markerStateRef.current);
|
|
452
777
|
if (rebasedBitmapUrl) URL.revokeObjectURL(rebasedBitmapUrl);
|
|
778
|
+
for (const url of mosaicHistory) if (url) URL.revokeObjectURL(url);
|
|
779
|
+
setMosaicHistory([]);
|
|
453
780
|
setRebasedBitmapUrl(rebase.bitmapUrl);
|
|
454
781
|
markerStateRef.current = rebase.markerState;
|
|
455
782
|
resetCrop();
|
|
@@ -460,13 +787,44 @@ function useImageEditorState(objectUrl) {
|
|
|
460
787
|
setActiveTool(tool);
|
|
461
788
|
} catch {}
|
|
462
789
|
};
|
|
790
|
+
const applyMosaicArea = async (area, mode, block) => {
|
|
791
|
+
if (!naturalSize || mosaicBusyRef.current) return;
|
|
792
|
+
const rect = clampCropRect(area, naturalSize.width, naturalSize.height);
|
|
793
|
+
if (rect.width < 1 || rect.height < 1) return;
|
|
794
|
+
mosaicBusyRef.current = true;
|
|
795
|
+
setMosaicBusy(true);
|
|
796
|
+
try {
|
|
797
|
+
const bitmapUrl = await applyMosaic(sourceUrlRef.current, rect, mode, block);
|
|
798
|
+
if (!mountedRef.current) {
|
|
799
|
+
URL.revokeObjectURL(bitmapUrl);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const previous = rebasedUrlRef.current;
|
|
803
|
+
rebasedUrlRef.current = bitmapUrl;
|
|
804
|
+
setMosaicHistory((history) => [...history, previous]);
|
|
805
|
+
setRebasedBitmapUrl(bitmapUrl);
|
|
806
|
+
} finally {
|
|
807
|
+
mosaicBusyRef.current = false;
|
|
808
|
+
if (mountedRef.current) setMosaicBusy(false);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
const undoMosaic = () => {
|
|
812
|
+
if (mosaicHistory.length === 0 || mosaicBusyRef.current) return;
|
|
813
|
+
const previous = mosaicHistory.at(-1) ?? null;
|
|
814
|
+
if (rebasedBitmapUrl) URL.revokeObjectURL(rebasedBitmapUrl);
|
|
815
|
+
setMosaicHistory(mosaicHistory.slice(0, -1));
|
|
816
|
+
setRebasedBitmapUrl(previous);
|
|
817
|
+
};
|
|
463
818
|
return {
|
|
464
819
|
activeTool,
|
|
820
|
+
applyMosaicArea,
|
|
465
821
|
confirmCropAndSwitch,
|
|
466
822
|
crop,
|
|
467
823
|
croppedAreaPixels,
|
|
468
824
|
hasPendingCrop,
|
|
469
825
|
markerStateRef,
|
|
826
|
+
mosaicBusy,
|
|
827
|
+
mosaicCount: mosaicHistory.length,
|
|
470
828
|
naturalSize,
|
|
471
829
|
rebasedBitmapUrl,
|
|
472
830
|
resetCrop,
|
|
@@ -474,84 +832,75 @@ function useImageEditorState(objectUrl) {
|
|
|
474
832
|
setCrop,
|
|
475
833
|
setCroppedAreaPixels,
|
|
476
834
|
setNaturalSize,
|
|
477
|
-
sourceUrl
|
|
835
|
+
sourceUrl,
|
|
836
|
+
undoMosaic
|
|
478
837
|
};
|
|
479
838
|
}
|
|
480
839
|
//#endregion
|
|
481
840
|
//#region src/ImageEditModal.tsx
|
|
482
|
-
var
|
|
483
|
-
{
|
|
484
|
-
icon: Crop,
|
|
485
|
-
id: "crop",
|
|
486
|
-
label: "Crop"
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
icon: MoveUpRight,
|
|
490
|
-
id: "arrow",
|
|
491
|
-
label: "Arrow"
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
icon: Pen,
|
|
495
|
-
id: "pen",
|
|
496
|
-
label: "Pen"
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
icon: Square,
|
|
500
|
-
id: "rect",
|
|
501
|
-
label: "Rectangle"
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
icon: Circle,
|
|
505
|
-
id: "ellipse",
|
|
506
|
-
label: "Ellipse"
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
icon: Type,
|
|
510
|
-
id: "text",
|
|
511
|
-
label: "Text"
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
icon: Hash,
|
|
515
|
-
id: "counter",
|
|
516
|
-
label: "Counter"
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
icon: PaintBucket,
|
|
520
|
-
id: "cover",
|
|
521
|
-
label: "Cover"
|
|
522
|
-
}
|
|
523
|
-
];
|
|
524
|
-
var ImageEditModal = ({ file, onCancel, onConfirm, onSkip }) => {
|
|
841
|
+
var ImageEditModal = ({ file, onCancel, onConfirm }) => {
|
|
525
842
|
const [objectUrl, setObjectUrl] = useState("");
|
|
526
843
|
const [decodeError, setDecodeError] = useState(false);
|
|
527
844
|
const [exporting, setExporting] = useState(false);
|
|
845
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
528
846
|
const [strokeColor, setStrokeColor] = useState(SWATCH_COLORS[0]);
|
|
529
847
|
const [strokeWidth, setStrokeWidth] = useState(STROKE_WIDTHS[1]);
|
|
530
848
|
const [history, setHistory] = useState({
|
|
531
849
|
canRedo: false,
|
|
532
850
|
canUndo: false
|
|
533
851
|
});
|
|
852
|
+
const [mosaicMode, setMosaicMode] = useState("pixelate");
|
|
853
|
+
const [mosaicStrength, setMosaicStrength] = useState(MOSAIC_DEFAULT_STRENGTH.pixelate);
|
|
854
|
+
const [mosaicCrop, setMosaicCrop] = useState();
|
|
855
|
+
const [originalSize, setOriginalSize] = useState(null);
|
|
534
856
|
const imgRef = useRef(null);
|
|
535
857
|
const counterRef = useRef(1);
|
|
536
858
|
const annotationApiRef = useRef(null);
|
|
537
859
|
const editor = useImageEditorState(objectUrl);
|
|
860
|
+
useEscapeDismissesTopDialog();
|
|
538
861
|
const { hasPendingCrop, markerStateRef, sourceUrl } = editor;
|
|
539
862
|
useEffect(() => {
|
|
540
863
|
const url = URL.createObjectURL(file);
|
|
541
864
|
setObjectUrl(url);
|
|
542
865
|
return () => URL.revokeObjectURL(url);
|
|
543
866
|
}, [file]);
|
|
867
|
+
const pendingCropRect = editor.activeTool === "crop" && hasPendingCrop ? editor.croppedAreaPixels : null;
|
|
868
|
+
const { controlProps: outputSizeProps, maxLongEdge } = useOutputSize(pendingCropRect ?? editor.naturalSize);
|
|
544
869
|
const handleImageLoad = (event) => {
|
|
545
870
|
const { naturalHeight, naturalWidth } = event.currentTarget;
|
|
546
871
|
editor.setNaturalSize({
|
|
547
872
|
height: naturalHeight,
|
|
548
873
|
width: naturalWidth
|
|
549
874
|
});
|
|
875
|
+
setOriginalSize((size) => size ?? {
|
|
876
|
+
height: naturalHeight,
|
|
877
|
+
width: naturalWidth
|
|
878
|
+
});
|
|
550
879
|
};
|
|
551
|
-
const
|
|
880
|
+
const toNatural = (pixelCrop) => {
|
|
552
881
|
const img = imgRef.current;
|
|
553
|
-
if (!img || img.width === 0 || img.height === 0) return;
|
|
554
|
-
|
|
882
|
+
if (!img || img.width === 0 || img.height === 0) return null;
|
|
883
|
+
return {
|
|
884
|
+
rect: displayedToNatural(pixelCrop, img.naturalWidth / img.width, img.naturalHeight / img.height),
|
|
885
|
+
scale: img.naturalWidth / img.width
|
|
886
|
+
};
|
|
887
|
+
};
|
|
888
|
+
const handleCropComplete = (pixelCrop) => {
|
|
889
|
+
const natural = toNatural(pixelCrop);
|
|
890
|
+
if (natural) editor.setCroppedAreaPixels(natural.rect);
|
|
891
|
+
};
|
|
892
|
+
const handleMosaicComplete = (pixelCrop) => {
|
|
893
|
+
const natural = toNatural(pixelCrop);
|
|
894
|
+
setMosaicCrop(void 0);
|
|
895
|
+
if (!natural || pixelCrop.width < 2 || pixelCrop.height < 2) return;
|
|
896
|
+
editor.applyMosaicArea(natural.rect, mosaicMode, mosaicStrength * natural.scale).then(() => setErrorMessage(null)).catch((error) => {
|
|
897
|
+
console.error("[ImageEditModal] mosaic failed", error);
|
|
898
|
+
setErrorMessage("Could not apply mosaic to this area");
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
const handleMosaicModeChange = (mode) => {
|
|
902
|
+
setMosaicMode(mode);
|
|
903
|
+
setMosaicStrength(clampMosaicStrength(mode, mosaicStrength));
|
|
555
904
|
};
|
|
556
905
|
const handleToolClick = (tool) => {
|
|
557
906
|
if (tool === editor.activeTool) return;
|
|
@@ -561,23 +910,105 @@ var ImageEditModal = ({ file, onCancel, onConfirm, onSkip }) => {
|
|
|
561
910
|
}
|
|
562
911
|
editor.setActiveTool(tool);
|
|
563
912
|
};
|
|
913
|
+
const buildSummary = (output) => {
|
|
914
|
+
if (output === file) return "";
|
|
915
|
+
const size = pendingCropRect ?? editor.naturalSize;
|
|
916
|
+
return buildEditSummary({
|
|
917
|
+
cropped: pendingCropRect !== null || originalSize !== null && size !== null && (size.width !== originalSize.width || size.height !== originalSize.height),
|
|
918
|
+
marks: markerStateRef.current?.markers.length ?? 0,
|
|
919
|
+
mosaics: editor.mosaicCount,
|
|
920
|
+
resizedTo: maxLongEdge !== null && size ? scaleToLongEdge(size, maxLongEdge) : null
|
|
921
|
+
});
|
|
922
|
+
};
|
|
564
923
|
const handleConfirm = async () => {
|
|
565
924
|
setExporting(true);
|
|
925
|
+
setErrorMessage(null);
|
|
566
926
|
try {
|
|
567
|
-
const
|
|
568
|
-
onConfirm(await exportResult({
|
|
927
|
+
const output = await exportResult({
|
|
569
928
|
hasRebasedBitmap: editor.rebasedBitmapUrl !== null,
|
|
570
929
|
markerState: markerStateRef.current,
|
|
930
|
+
maxLongEdge,
|
|
571
931
|
original: file,
|
|
572
932
|
pendingCropRect,
|
|
573
933
|
sourceUrl
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
934
|
+
});
|
|
935
|
+
onConfirm({
|
|
936
|
+
file: output,
|
|
937
|
+
summary: buildSummary(output)
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error("[ImageEditModal] export failed", error);
|
|
941
|
+
setErrorMessage("Could not export the edited image. Try again or discard edits.");
|
|
578
942
|
setExporting(false);
|
|
579
943
|
}
|
|
580
944
|
};
|
|
945
|
+
const renderOptions = () => {
|
|
946
|
+
if (decodeError) return null;
|
|
947
|
+
if (editor.activeTool === "crop") return /* @__PURE__ */ jsx("button", {
|
|
948
|
+
className: topBarGhostButton,
|
|
949
|
+
disabled: !hasPendingCrop,
|
|
950
|
+
type: "button",
|
|
951
|
+
onClick: editor.resetCrop,
|
|
952
|
+
children: "Reset crop"
|
|
953
|
+
});
|
|
954
|
+
if (editor.activeTool === "mosaic") return /* @__PURE__ */ jsx(MosaicOptionsBar, {
|
|
955
|
+
canUndo: editor.mosaicCount > 0 && !editor.mosaicBusy,
|
|
956
|
+
mode: mosaicMode,
|
|
957
|
+
strength: mosaicStrength,
|
|
958
|
+
onModeChange: handleMosaicModeChange,
|
|
959
|
+
onStrengthChange: (value) => setMosaicStrength(clampMosaicStrength(mosaicMode, value)),
|
|
960
|
+
onUndo: editor.undoMosaic
|
|
961
|
+
});
|
|
962
|
+
return /* @__PURE__ */ jsx(ToolOptionsBar, {
|
|
963
|
+
canRedo: history.canRedo,
|
|
964
|
+
canUndo: history.canUndo,
|
|
965
|
+
showStrokeWidth: STROKE_TOOLS.has(editor.activeTool),
|
|
966
|
+
strokeColor,
|
|
967
|
+
strokeWidth,
|
|
968
|
+
onRedo: () => annotationApiRef.current?.redo(),
|
|
969
|
+
onStrokeColorChange: setStrokeColor,
|
|
970
|
+
onStrokeWidthChange: setStrokeWidth,
|
|
971
|
+
onUndo: () => annotationApiRef.current?.undo()
|
|
972
|
+
});
|
|
973
|
+
};
|
|
974
|
+
const renderCanvas = () => {
|
|
975
|
+
if (decodeError) return /* @__PURE__ */ jsxs("div", {
|
|
976
|
+
className: errorState,
|
|
977
|
+
children: [/* @__PURE__ */ jsx(ImageOff, { size: 24 }), /* @__PURE__ */ jsx("span", { children: "This image could not be displayed for editing." })]
|
|
978
|
+
});
|
|
979
|
+
if (!sourceUrl) return null;
|
|
980
|
+
if (isAnnotationTool(editor.activeTool)) return /* @__PURE__ */ jsx(AnnotationSurface, {
|
|
981
|
+
apiRef: annotationApiRef,
|
|
982
|
+
counterRef,
|
|
983
|
+
sourceUrl,
|
|
984
|
+
stateRef: markerStateRef,
|
|
985
|
+
strokeColor,
|
|
986
|
+
strokeWidth,
|
|
987
|
+
tool: editor.activeTool,
|
|
988
|
+
onDecodeError: () => setDecodeError(true),
|
|
989
|
+
onHistoryChange: (canUndo, canRedo) => setHistory({
|
|
990
|
+
canRedo,
|
|
991
|
+
canUndo
|
|
992
|
+
})
|
|
993
|
+
});
|
|
994
|
+
const isMosaic = editor.activeTool === "mosaic";
|
|
995
|
+
return /* @__PURE__ */ jsx(ReactCrop, {
|
|
996
|
+
className: cropSurface,
|
|
997
|
+
crop: isMosaic ? mosaicCrop : editor.crop,
|
|
998
|
+
disabled: isMosaic && editor.mosaicBusy,
|
|
999
|
+
keepSelection: !isMosaic,
|
|
1000
|
+
onComplete: isMosaic ? handleMosaicComplete : handleCropComplete,
|
|
1001
|
+
onChange: (_, percentCrop) => isMosaic ? setMosaicCrop(percentCrop) : editor.setCrop(percentCrop),
|
|
1002
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1003
|
+
alt: file.name,
|
|
1004
|
+
className: cropImage,
|
|
1005
|
+
ref: imgRef,
|
|
1006
|
+
src: sourceUrl,
|
|
1007
|
+
onError: () => setDecodeError(true),
|
|
1008
|
+
onLoad: handleImageLoad
|
|
1009
|
+
})
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
581
1012
|
return /* @__PURE__ */ jsxs("div", {
|
|
582
1013
|
className: root,
|
|
583
1014
|
children: [
|
|
@@ -588,75 +1019,18 @@ var ImageEditModal = ({ file, onCancel, onConfirm, onSkip }) => {
|
|
|
588
1019
|
children: "Edit image"
|
|
589
1020
|
}), /* @__PURE__ */ jsx("div", {
|
|
590
1021
|
className: topBarControls,
|
|
591
|
-
children:
|
|
592
|
-
className: "_1257qq2u _1257qq2t _1257qq2s",
|
|
593
|
-
disabled: !hasPendingCrop,
|
|
594
|
-
type: "button",
|
|
595
|
-
onClick: editor.resetCrop,
|
|
596
|
-
children: "Reset crop"
|
|
597
|
-
}) : /* @__PURE__ */ jsx(ToolOptionsBar, {
|
|
598
|
-
canRedo: history.canRedo,
|
|
599
|
-
canUndo: history.canUndo,
|
|
600
|
-
showStrokeWidth: STROKE_TOOLS.has(editor.activeTool),
|
|
601
|
-
strokeColor,
|
|
602
|
-
strokeWidth,
|
|
603
|
-
onRedo: () => annotationApiRef.current?.redo(),
|
|
604
|
-
onStrokeColorChange: setStrokeColor,
|
|
605
|
-
onStrokeWidthChange: setStrokeWidth,
|
|
606
|
-
onUndo: () => annotationApiRef.current?.undo()
|
|
607
|
-
}))
|
|
1022
|
+
children: renderOptions()
|
|
608
1023
|
})]
|
|
609
1024
|
}),
|
|
610
1025
|
/* @__PURE__ */ jsxs("div", {
|
|
611
1026
|
className: body,
|
|
612
|
-
children: [/* @__PURE__ */ jsx(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
children: [/* @__PURE__ */ jsx("button", {
|
|
617
|
-
"aria-label": tool.label,
|
|
618
|
-
"aria-pressed": editor.activeTool === tool.id,
|
|
619
|
-
disabled: decodeError,
|
|
620
|
-
title: tool.label,
|
|
621
|
-
type: "button",
|
|
622
|
-
className: editor.activeTool === tool.id ? `${toolButton} ${toolButtonActive}` : toolButton,
|
|
623
|
-
onClick: () => handleToolClick(tool.id),
|
|
624
|
-
children: /* @__PURE__ */ jsx(tool.icon, { size: 18 })
|
|
625
|
-
}), index === 0 && /* @__PURE__ */ jsx("div", { className: "_1257qq2c" })]
|
|
626
|
-
}, tool.id))
|
|
1027
|
+
children: [/* @__PURE__ */ jsx(ToolRail, {
|
|
1028
|
+
activeTool: editor.activeTool,
|
|
1029
|
+
disabled: decodeError,
|
|
1030
|
+
onSelect: handleToolClick
|
|
627
1031
|
}), /* @__PURE__ */ jsxs("div", {
|
|
628
1032
|
className: canvasArea,
|
|
629
|
-
children: [
|
|
630
|
-
className: errorState,
|
|
631
|
-
children: [/* @__PURE__ */ jsx(ImageOff, { size: 24 }), /* @__PURE__ */ jsx("span", { children: "This image could not be displayed for editing." })]
|
|
632
|
-
}) : sourceUrl && (isAnnotationTool(editor.activeTool) ? /* @__PURE__ */ jsx(AnnotationSurface, {
|
|
633
|
-
apiRef: annotationApiRef,
|
|
634
|
-
counterRef,
|
|
635
|
-
sourceUrl,
|
|
636
|
-
stateRef: markerStateRef,
|
|
637
|
-
strokeColor,
|
|
638
|
-
strokeWidth,
|
|
639
|
-
tool: editor.activeTool,
|
|
640
|
-
onDecodeError: () => setDecodeError(true),
|
|
641
|
-
onHistoryChange: (canUndo, canRedo) => setHistory({
|
|
642
|
-
canRedo,
|
|
643
|
-
canUndo
|
|
644
|
-
})
|
|
645
|
-
}) : /* @__PURE__ */ jsx(ReactCrop, {
|
|
646
|
-
keepSelection: true,
|
|
647
|
-
className: "_1257qq2e",
|
|
648
|
-
crop: editor.crop,
|
|
649
|
-
onChange: (_, percentCrop) => editor.setCrop(percentCrop),
|
|
650
|
-
onComplete: handleCropComplete,
|
|
651
|
-
children: /* @__PURE__ */ jsx("img", {
|
|
652
|
-
alt: file.name,
|
|
653
|
-
className: "_1257qq2f",
|
|
654
|
-
ref: imgRef,
|
|
655
|
-
src: sourceUrl,
|
|
656
|
-
onError: () => setDecodeError(true),
|
|
657
|
-
onLoad: handleImageLoad
|
|
658
|
-
})
|
|
659
|
-
})), /* @__PURE__ */ jsx("a", {
|
|
1033
|
+
children: [renderCanvas(), /* @__PURE__ */ jsx("a", {
|
|
660
1034
|
className: attribution,
|
|
661
1035
|
href: "https://markerjs.com",
|
|
662
1036
|
rel: "noreferrer",
|
|
@@ -666,27 +1040,326 @@ var ImageEditModal = ({ file, onCancel, onConfirm, onSkip }) => {
|
|
|
666
1040
|
})]
|
|
667
1041
|
}),
|
|
668
1042
|
/* @__PURE__ */ jsxs("div", {
|
|
669
|
-
className: footer,
|
|
1043
|
+
className: footer$1,
|
|
670
1044
|
children: [
|
|
1045
|
+
/* @__PURE__ */ jsx(OutputSizeControl, { ...outputSizeProps }),
|
|
1046
|
+
/* @__PURE__ */ jsx("div", {
|
|
1047
|
+
className: footerSpacer,
|
|
1048
|
+
children: errorMessage && /* @__PURE__ */ jsx("span", {
|
|
1049
|
+
className: "_1257qq2s",
|
|
1050
|
+
role: "alert",
|
|
1051
|
+
children: errorMessage
|
|
1052
|
+
})
|
|
1053
|
+
}),
|
|
671
1054
|
/* @__PURE__ */ jsx("button", {
|
|
672
|
-
className:
|
|
1055
|
+
className: secondaryButton,
|
|
673
1056
|
type: "button",
|
|
674
|
-
onClick:
|
|
675
|
-
children: "
|
|
1057
|
+
onClick: onCancel,
|
|
1058
|
+
children: "Discard edits"
|
|
676
1059
|
}),
|
|
677
|
-
/* @__PURE__ */ jsx("div", { className: footerSpacer }),
|
|
678
1060
|
/* @__PURE__ */ jsx("button", {
|
|
679
|
-
className:
|
|
1061
|
+
className: primaryButton,
|
|
1062
|
+
disabled: decodeError || exporting || editor.mosaicBusy,
|
|
1063
|
+
type: "button",
|
|
1064
|
+
onClick: handleConfirm,
|
|
1065
|
+
children: "Done"
|
|
1066
|
+
})
|
|
1067
|
+
]
|
|
1068
|
+
})
|
|
1069
|
+
]
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
//#endregion
|
|
1073
|
+
//#region src/insert-sheet-model.ts
|
|
1074
|
+
var FLATTENED_BY_CANVAS = /* @__PURE__ */ new Set(["image/gif", "image/svg+xml"]);
|
|
1075
|
+
function isEditableImage(file) {
|
|
1076
|
+
return !FLATTENED_BY_CANVAS.has(file.type);
|
|
1077
|
+
}
|
|
1078
|
+
function formatBytes(bytes) {
|
|
1079
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1080
|
+
if (bytes < 1048576) return `${Math.round(bytes / 1024)} KB`;
|
|
1081
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
1082
|
+
}
|
|
1083
|
+
async function finalizeRows(rows, removeGps, stripGps) {
|
|
1084
|
+
const failedIds = [];
|
|
1085
|
+
return {
|
|
1086
|
+
failedIds,
|
|
1087
|
+
files: (await Promise.all(rows.map(async (row) => {
|
|
1088
|
+
if (row.current !== row.original || !removeGps || !row.gps) return row.current;
|
|
1089
|
+
try {
|
|
1090
|
+
if (!stripGps) throw new Error("No GPS stripper configured");
|
|
1091
|
+
return await stripGps(row.original);
|
|
1092
|
+
} catch {
|
|
1093
|
+
failedIds.push(row.id);
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
}))).filter((file) => file !== null)
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/sheet.css.ts
|
|
1101
|
+
var sheetPopup = "_1us6jj40";
|
|
1102
|
+
var sheet = "_1us6jj41";
|
|
1103
|
+
var rows = "_1us6jj42";
|
|
1104
|
+
var row = "_1us6jj43";
|
|
1105
|
+
var rowFailed = "_1us6jj44";
|
|
1106
|
+
var thumb = "_1us6jj45";
|
|
1107
|
+
var rowBody = "_1us6jj46";
|
|
1108
|
+
var rowTitle = "_1us6jj47";
|
|
1109
|
+
var fileName = "_1us6jj48";
|
|
1110
|
+
var meta = "_1us6jj4a";
|
|
1111
|
+
var footer = "_1us6jj4h";
|
|
1112
|
+
var hint = "_1us6jj4i";
|
|
1113
|
+
var iconButton = "_1us6jj4j";
|
|
1114
|
+
//#endregion
|
|
1115
|
+
//#region src/SheetRowItem.tsx
|
|
1116
|
+
function useImageInfo(file) {
|
|
1117
|
+
const [info, setInfo] = useState(null);
|
|
1118
|
+
const [failed, setFailed] = useState(false);
|
|
1119
|
+
useEffect(() => {
|
|
1120
|
+
const url = URL.createObjectURL(file);
|
|
1121
|
+
let active = true;
|
|
1122
|
+
setFailed(false);
|
|
1123
|
+
const image = new Image();
|
|
1124
|
+
image.onload = () => {
|
|
1125
|
+
if (active) setInfo({
|
|
1126
|
+
height: image.naturalHeight,
|
|
1127
|
+
url,
|
|
1128
|
+
width: image.naturalWidth
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
image.onerror = () => {
|
|
1132
|
+
if (active) setFailed(true);
|
|
1133
|
+
};
|
|
1134
|
+
image.src = url;
|
|
1135
|
+
return () => {
|
|
1136
|
+
active = false;
|
|
1137
|
+
URL.revokeObjectURL(url);
|
|
1138
|
+
};
|
|
1139
|
+
}, [file]);
|
|
1140
|
+
return {
|
|
1141
|
+
failed,
|
|
1142
|
+
info
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
var SheetRowItem = ({ failed, onEdit, onRemove, row: row$1 }) => {
|
|
1146
|
+
const { failed: decodeFailed, info } = useImageInfo(row$1.current);
|
|
1147
|
+
const edited = row$1.current !== row$1.original;
|
|
1148
|
+
const editable = isEditableImage(row$1.current) && !decodeFailed;
|
|
1149
|
+
const type = row$1.current.type.replace("image/", "").toUpperCase();
|
|
1150
|
+
const meta$1 = [
|
|
1151
|
+
info ? `${info.width} × ${info.height}` : null,
|
|
1152
|
+
formatBytes(row$1.current.size),
|
|
1153
|
+
type || null
|
|
1154
|
+
].filter(Boolean).join(" · ");
|
|
1155
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1156
|
+
className: failed ? `${row} ${rowFailed}` : row,
|
|
1157
|
+
children: [
|
|
1158
|
+
info ? /* @__PURE__ */ jsx("img", {
|
|
1159
|
+
alt: "",
|
|
1160
|
+
className: thumb,
|
|
1161
|
+
src: info.url
|
|
1162
|
+
}) : /* @__PURE__ */ jsx("div", { className: thumb }),
|
|
1163
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1164
|
+
className: rowBody,
|
|
1165
|
+
children: [
|
|
1166
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1167
|
+
className: rowTitle,
|
|
1168
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1169
|
+
className: fileName,
|
|
1170
|
+
title: row$1.original.name,
|
|
1171
|
+
children: row$1.original.name
|
|
1172
|
+
}), edited && /* @__PURE__ */ jsx("span", {
|
|
1173
|
+
className: "_1us6jj49",
|
|
1174
|
+
children: "Edited"
|
|
1175
|
+
})]
|
|
1176
|
+
}),
|
|
1177
|
+
/* @__PURE__ */ jsx("span", {
|
|
1178
|
+
className: meta,
|
|
1179
|
+
children: meta$1
|
|
1180
|
+
}),
|
|
1181
|
+
edited && row$1.summary && /* @__PURE__ */ jsx("span", {
|
|
1182
|
+
className: "_1us6jj4a",
|
|
1183
|
+
children: row$1.summary
|
|
1184
|
+
}),
|
|
1185
|
+
!edited && row$1.gps && /* @__PURE__ */ jsxs("span", {
|
|
1186
|
+
className: "_1us6jj4b",
|
|
1187
|
+
children: [/* @__PURE__ */ jsx(MapPin, {
|
|
1188
|
+
"aria-hidden": true,
|
|
1189
|
+
size: 13
|
|
1190
|
+
}), "Has location"]
|
|
1191
|
+
}),
|
|
1192
|
+
!isEditableImage(row$1.current) && /* @__PURE__ */ jsx("span", {
|
|
1193
|
+
className: "_1us6jj4a",
|
|
1194
|
+
children: "Animated or vector — editing would flatten it"
|
|
1195
|
+
}),
|
|
1196
|
+
failed && /* @__PURE__ */ jsx("span", {
|
|
1197
|
+
className: "_1us6jj4c",
|
|
1198
|
+
children: "Could not remove location from this image"
|
|
1199
|
+
})
|
|
1200
|
+
]
|
|
1201
|
+
}),
|
|
1202
|
+
/* @__PURE__ */ jsxs("button", {
|
|
1203
|
+
className: secondaryButton,
|
|
1204
|
+
disabled: !editable,
|
|
1205
|
+
type: "button",
|
|
1206
|
+
onClick: onEdit,
|
|
1207
|
+
children: [/* @__PURE__ */ jsx(Pencil, {
|
|
1208
|
+
"aria-hidden": true,
|
|
1209
|
+
size: 14,
|
|
1210
|
+
style: { marginRight: 6 }
|
|
1211
|
+
}), edited ? "Re-edit" : "Edit…"]
|
|
1212
|
+
}),
|
|
1213
|
+
/* @__PURE__ */ jsx("button", {
|
|
1214
|
+
"aria-label": `Remove ${row$1.original.name}`,
|
|
1215
|
+
className: iconButton,
|
|
1216
|
+
title: "Remove",
|
|
1217
|
+
type: "button",
|
|
1218
|
+
onClick: onRemove,
|
|
1219
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
1220
|
+
"aria-hidden": true,
|
|
1221
|
+
size: 16
|
|
1222
|
+
})
|
|
1223
|
+
})
|
|
1224
|
+
]
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
//#endregion
|
|
1228
|
+
//#region src/ImageInsertSheet.tsx
|
|
1229
|
+
function mapUrl({ latitude, longitude }) {
|
|
1230
|
+
return `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`;
|
|
1231
|
+
}
|
|
1232
|
+
function withDetectedGps(rows, files, found) {
|
|
1233
|
+
return rows.map((row) => {
|
|
1234
|
+
const index = files.indexOf(row.original);
|
|
1235
|
+
return index === -1 ? row : {
|
|
1236
|
+
...row,
|
|
1237
|
+
gps: found[index] ?? null
|
|
1238
|
+
};
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
var ImageInsertSheet = ({ files, onCancel, onEdit, onInsert, privacy }) => {
|
|
1242
|
+
const [rows$1, setRows] = useState(() => files.map((file, index) => ({
|
|
1243
|
+
current: file,
|
|
1244
|
+
gps: null,
|
|
1245
|
+
id: `${index}-${file.name}`,
|
|
1246
|
+
original: file,
|
|
1247
|
+
summary: ""
|
|
1248
|
+
})));
|
|
1249
|
+
const [removeGps, setRemoveGps] = useState(true);
|
|
1250
|
+
const [failedIds, setFailedIds] = useState([]);
|
|
1251
|
+
const [inserting, setInserting] = useState(false);
|
|
1252
|
+
useEscapeDismissesTopDialog();
|
|
1253
|
+
const detectionRef = useRef(Promise.resolve([]));
|
|
1254
|
+
useEffect(() => {
|
|
1255
|
+
if (!privacy) return;
|
|
1256
|
+
let active = true;
|
|
1257
|
+
const detection = Promise.all(files.map((file) => privacy.detectGps(file).catch(() => null)));
|
|
1258
|
+
detectionRef.current = detection;
|
|
1259
|
+
detection.then((found) => {
|
|
1260
|
+
if (active) setRows((current) => withDetectedGps(current, files, found));
|
|
1261
|
+
});
|
|
1262
|
+
return () => {
|
|
1263
|
+
active = false;
|
|
1264
|
+
};
|
|
1265
|
+
}, [files, privacy]);
|
|
1266
|
+
const gpsRows = rows$1.filter((row) => row.gps && row.current === row.original);
|
|
1267
|
+
const blocked = removeGps && failedIds.some((id) => gpsRows.some((row) => row.id === id));
|
|
1268
|
+
const handleRemove = (id) => {
|
|
1269
|
+
const next = rows$1.filter((row) => row.id !== id);
|
|
1270
|
+
if (next.length === 0) {
|
|
1271
|
+
onCancel();
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
setRows(next);
|
|
1275
|
+
};
|
|
1276
|
+
const handleEdit = async (target) => {
|
|
1277
|
+
const result = await onEdit(target.current);
|
|
1278
|
+
if (!result || result.file === target.current) return;
|
|
1279
|
+
setRows((current) => current.map((row) => row.id === target.id ? {
|
|
1280
|
+
...row,
|
|
1281
|
+
current: result.file,
|
|
1282
|
+
summary: [row.summary, result.summary].filter(Boolean).join(" · ")
|
|
1283
|
+
} : row));
|
|
1284
|
+
};
|
|
1285
|
+
const handleInsert = async () => {
|
|
1286
|
+
setInserting(true);
|
|
1287
|
+
const found = await detectionRef.current;
|
|
1288
|
+
const result = await finalizeRows(withDetectedGps(rows$1, files, found), removeGps, privacy?.stripGps);
|
|
1289
|
+
setInserting(false);
|
|
1290
|
+
if (result.failedIds.length > 0) {
|
|
1291
|
+
setFailedIds(result.failedIds);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
onInsert(result.files);
|
|
1295
|
+
};
|
|
1296
|
+
const single = rows$1.length === 1 ? gpsRows[0] : void 0;
|
|
1297
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1298
|
+
className: sheet,
|
|
1299
|
+
children: [
|
|
1300
|
+
/* @__PURE__ */ jsx("div", {
|
|
1301
|
+
className: rows,
|
|
1302
|
+
children: rows$1.map((row) => /* @__PURE__ */ jsx(SheetRowItem, {
|
|
1303
|
+
failed: removeGps && failedIds.includes(row.id),
|
|
1304
|
+
row,
|
|
1305
|
+
onEdit: () => void handleEdit(row),
|
|
1306
|
+
onRemove: () => handleRemove(row.id)
|
|
1307
|
+
}, row.id))
|
|
1308
|
+
}),
|
|
1309
|
+
gpsRows.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
1310
|
+
className: "_1us6jj4d",
|
|
1311
|
+
children: [
|
|
1312
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1313
|
+
className: "_1us6jj4e",
|
|
1314
|
+
children: [/* @__PURE__ */ jsx(MapPin, {
|
|
1315
|
+
"aria-hidden": true,
|
|
1316
|
+
color: "#d97706",
|
|
1317
|
+
size: 16
|
|
1318
|
+
}), single ? "This photo records where it was taken" : `${gpsRows.length} of ${rows$1.length} record where they were taken`]
|
|
1319
|
+
}),
|
|
1320
|
+
single?.gps && /* @__PURE__ */ jsxs("div", {
|
|
1321
|
+
className: "_1us6jj4f",
|
|
1322
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
1323
|
+
single.gps.latitude.toFixed(4),
|
|
1324
|
+
", ",
|
|
1325
|
+
single.gps.longitude.toFixed(4)
|
|
1326
|
+
] }), /* @__PURE__ */ jsx("a", {
|
|
1327
|
+
href: mapUrl(single.gps),
|
|
1328
|
+
rel: "noreferrer",
|
|
1329
|
+
target: "_blank",
|
|
1330
|
+
children: "View on map"
|
|
1331
|
+
})]
|
|
1332
|
+
}),
|
|
1333
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1334
|
+
className: "_1us6jj4g",
|
|
1335
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1336
|
+
checked: removeGps,
|
|
1337
|
+
type: "checkbox",
|
|
1338
|
+
onChange: (event) => setRemoveGps(event.currentTarget.checked)
|
|
1339
|
+
}), "Remove location before upload"]
|
|
1340
|
+
})
|
|
1341
|
+
]
|
|
1342
|
+
}),
|
|
1343
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1344
|
+
className: footer,
|
|
1345
|
+
children: [
|
|
1346
|
+
/* @__PURE__ */ jsx("span", {
|
|
1347
|
+
className: hint,
|
|
1348
|
+
children: blocked ? "Remove the flagged images or keep their location to continue" : "Edited images drop all metadata"
|
|
1349
|
+
}),
|
|
1350
|
+
/* @__PURE__ */ jsx("button", {
|
|
1351
|
+
className: ghostButton,
|
|
680
1352
|
type: "button",
|
|
681
1353
|
onClick: onCancel,
|
|
682
1354
|
children: "Cancel"
|
|
683
1355
|
}),
|
|
684
1356
|
/* @__PURE__ */ jsx("button", {
|
|
1357
|
+
autoFocus: true,
|
|
685
1358
|
className: primaryButton,
|
|
686
|
-
disabled:
|
|
1359
|
+
disabled: inserting || blocked,
|
|
687
1360
|
type: "button",
|
|
688
|
-
onClick:
|
|
689
|
-
children: "
|
|
1361
|
+
onClick: () => void handleInsert(),
|
|
1362
|
+
children: rows$1.length > 1 ? `Insert ${rows$1.length} images` : "Insert"
|
|
690
1363
|
})
|
|
691
1364
|
]
|
|
692
1365
|
})
|
|
@@ -695,14 +1368,16 @@ var ImageEditModal = ({ file, onCancel, onConfirm, onSkip }) => {
|
|
|
695
1368
|
};
|
|
696
1369
|
//#endregion
|
|
697
1370
|
//#region src/ImageEditModalPlugin.tsx
|
|
698
|
-
var ImageEditModalPlugin = () => {
|
|
1371
|
+
var ImageEditModalPlugin = ({ privacy }) => {
|
|
699
1372
|
const preprocess = useImagePreprocess();
|
|
700
1373
|
const portalTheme = usePortalTheme();
|
|
701
1374
|
const portalThemeRef = useRef(portalTheme);
|
|
702
1375
|
portalThemeRef.current = portalTheme;
|
|
1376
|
+
const privacyRef = useRef(privacy);
|
|
1377
|
+
privacyRef.current = privacy;
|
|
703
1378
|
useEffect(() => {
|
|
704
1379
|
if (!preprocess) return;
|
|
705
|
-
const
|
|
1380
|
+
const openEditor = (file) => new Promise((resolve) => {
|
|
706
1381
|
let settled = false;
|
|
707
1382
|
const settle = (result) => {
|
|
708
1383
|
if (settled) return;
|
|
@@ -717,19 +1392,46 @@ var ImageEditModalPlugin = () => {
|
|
|
717
1392
|
settle(null);
|
|
718
1393
|
dismiss();
|
|
719
1394
|
},
|
|
720
|
-
onConfirm: (
|
|
721
|
-
settle(
|
|
1395
|
+
onConfirm: (result) => {
|
|
1396
|
+
settle(result);
|
|
1397
|
+
dismiss();
|
|
1398
|
+
}
|
|
1399
|
+
}),
|
|
1400
|
+
onClose: () => settle(null),
|
|
1401
|
+
portalClassName: portalThemeRef.current.className,
|
|
1402
|
+
showCloseButton: false,
|
|
1403
|
+
theme: portalThemeRef.current.theme
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
const fn = (files) => new Promise((resolve) => {
|
|
1407
|
+
let settled = false;
|
|
1408
|
+
const settle = (result) => {
|
|
1409
|
+
if (settled) return;
|
|
1410
|
+
settled = true;
|
|
1411
|
+
resolve(result);
|
|
1412
|
+
};
|
|
1413
|
+
const dismiss = presentDialog({
|
|
1414
|
+
className: sheetPopup,
|
|
1415
|
+
clickOutsideToDismiss: false,
|
|
1416
|
+
content: () => /* @__PURE__ */ jsx(ImageInsertSheet, {
|
|
1417
|
+
files,
|
|
1418
|
+
privacy: privacyRef.current,
|
|
1419
|
+
onEdit: openEditor,
|
|
1420
|
+
onCancel: () => {
|
|
1421
|
+
settle(null);
|
|
722
1422
|
dismiss();
|
|
723
1423
|
},
|
|
724
|
-
|
|
725
|
-
settle(
|
|
1424
|
+
onInsert: (result) => {
|
|
1425
|
+
settle(result);
|
|
726
1426
|
dismiss();
|
|
727
1427
|
}
|
|
728
1428
|
}),
|
|
1429
|
+
description: "Check before it goes up. Nothing is uploaded yet.",
|
|
729
1430
|
onClose: () => settle(null),
|
|
730
1431
|
portalClassName: portalThemeRef.current.className,
|
|
731
1432
|
showCloseButton: false,
|
|
732
|
-
theme: portalThemeRef.current.theme
|
|
1433
|
+
theme: portalThemeRef.current.theme,
|
|
1434
|
+
title: files.length > 1 ? "Insert images" : "Insert image"
|
|
733
1435
|
});
|
|
734
1436
|
});
|
|
735
1437
|
return preprocess.register(fn);
|
|
@@ -737,4 +1439,4 @@ var ImageEditModalPlugin = () => {
|
|
|
737
1439
|
return null;
|
|
738
1440
|
};
|
|
739
1441
|
//#endregion
|
|
740
|
-
export { ImageEditModal, ImageEditModalPlugin };
|
|
1442
|
+
export { ImageEditModal, ImageEditModalPlugin, ImageInsertSheet };
|