@haklex/rich-plugin-image-editor 0.42.0 → 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.
Files changed (39) hide show
  1. package/dist/ImageEditModal.d.ts +5 -2
  2. package/dist/ImageEditModal.d.ts.map +1 -1
  3. package/dist/ImageEditModalPlugin.d.ts +5 -1
  4. package/dist/ImageEditModalPlugin.d.ts.map +1 -1
  5. package/dist/ImageInsertSheet.d.ts +12 -0
  6. package/dist/ImageInsertSheet.d.ts.map +1 -0
  7. package/dist/MosaicOptionsBar.d.ts +12 -0
  8. package/dist/MosaicOptionsBar.d.ts.map +1 -0
  9. package/dist/OutputSizeControl.d.ts +18 -0
  10. package/dist/OutputSizeControl.d.ts.map +1 -0
  11. package/dist/SheetRowItem.d.ts +10 -0
  12. package/dist/SheetRowItem.d.ts.map +1 -0
  13. package/dist/ToolRail.d.ts +9 -0
  14. package/dist/ToolRail.d.ts.map +1 -0
  15. package/dist/annotation.d.ts +1 -1
  16. package/dist/annotation.d.ts.map +1 -1
  17. package/dist/edit-summary.d.ts +9 -0
  18. package/dist/edit-summary.d.ts.map +1 -0
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.mjs +846 -144
  22. package/dist/insert-sheet-model.d.ts +22 -0
  23. package/dist/insert-sheet-model.d.ts.map +1 -0
  24. package/dist/mosaic.d.ts +14 -0
  25. package/dist/mosaic.d.ts.map +1 -0
  26. package/dist/pipeline.d.ts +2 -1
  27. package/dist/pipeline.d.ts.map +1 -1
  28. package/dist/resize.d.ts +9 -0
  29. package/dist/resize.d.ts.map +1 -0
  30. package/dist/rich-plugin-image-editor.css +1 -1
  31. package/dist/sheet.css.d.ts +22 -0
  32. package/dist/sheet.css.d.ts.map +1 -0
  33. package/dist/styles.css.d.ts +9 -0
  34. package/dist/styles.css.d.ts.map +1 -1
  35. package/dist/useEscapeDismissesTopDialog.d.ts +2 -0
  36. package/dist/useEscapeDismissesTopDialog.d.ts.map +1 -0
  37. package/dist/useImageEditorState.d.ts +6 -1
  38. package/dist/useImageEditorState.d.ts.map +1 -1
  39. 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 _1257qq2s";
85
- var secondaryButton = "_1257qq2v _1257qq2s";
86
- var primaryButton = "_1257qq2w _1257qq2s";
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
- useEffect(() => () => {
439
- if (rebasedUrlRef.current) URL.revokeObjectURL(rebasedUrlRef.current);
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 TOOLS = [
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 handleCropComplete = (pixelCrop) => {
880
+ const toNatural = (pixelCrop) => {
552
881
  const img = imgRef.current;
553
- if (!img || img.width === 0 || img.height === 0) return;
554
- editor.setCroppedAreaPixels(displayedToNatural(pixelCrop, img.naturalWidth / img.width, img.naturalHeight / img.height));
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 pendingCropRect = editor.activeTool === "crop" && hasPendingCrop ? editor.croppedAreaPixels : null;
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
- } catch {
576
- onSkip();
577
- } finally {
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: !decodeError && (editor.activeTool === "crop" ? /* @__PURE__ */ jsx("button", {
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("div", {
613
- className: toolRail,
614
- children: TOOLS.map((tool, index) => /* @__PURE__ */ jsxs("span", {
615
- className: toolGroup,
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: [decodeError ? /* @__PURE__ */ jsxs("div", {
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: ghostButton,
1055
+ className: secondaryButton,
673
1056
  type: "button",
674
- onClick: onSkip,
675
- children: "Upload without editing"
1057
+ onClick: onCancel,
1058
+ children: "Discard edits"
676
1059
  }),
677
- /* @__PURE__ */ jsx("div", { className: footerSpacer }),
678
1060
  /* @__PURE__ */ jsx("button", {
679
- className: secondaryButton,
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: decodeError || exporting,
1359
+ disabled: inserting || blocked,
687
1360
  type: "button",
688
- onClick: handleConfirm,
689
- children: "Upload"
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 fn = (file) => new Promise((resolve) => {
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: (edited) => {
721
- settle(edited);
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
- onSkip: () => {
725
- settle("skip");
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 };