flex-image-viewer 1.0.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 (67) hide show
  1. package/README.md +160 -0
  2. package/dist/FlexImageViewer/Content/index.js +193 -0
  3. package/dist/FlexImageViewer/Footer/Actions.js +115 -0
  4. package/dist/FlexImageViewer/Footer/index.js +56 -0
  5. package/dist/FlexImageViewer/Header/Actions.js +80 -0
  6. package/dist/FlexImageViewer/Header/index.js +42 -0
  7. package/dist/FlexImageViewer/Info/index.js +52 -0
  8. package/dist/FlexImageViewer/Thumbnail/index.js +90 -0
  9. package/dist/FlexImageViewer/index.css +582 -0
  10. package/dist/FlexImageViewer/index.js +117 -0
  11. package/dist/components/ActionItem.js +10 -0
  12. package/dist/components/BaseActions.js +69 -0
  13. package/dist/components/Icon/index.js +48 -0
  14. package/dist/components/Icon/svg/arrow-left.js +15 -0
  15. package/dist/components/Icon/svg/arrow-right.js +15 -0
  16. package/dist/components/Icon/svg/caret-right.js +15 -0
  17. package/dist/components/Icon/svg/check.js +15 -0
  18. package/dist/components/Icon/svg/clear.js +15 -0
  19. package/dist/components/Icon/svg/close.js +16 -0
  20. package/dist/components/Icon/svg/default-image.js +17 -0
  21. package/dist/components/Icon/svg/down.js +15 -0
  22. package/dist/components/Icon/svg/info-circle.js +20 -0
  23. package/dist/components/Icon/svg/rotate-left.js +20 -0
  24. package/dist/components/Icon/svg/rotate-right.js +20 -0
  25. package/dist/components/Icon/svg/save.js +15 -0
  26. package/dist/components/Icon/svg/size.js +19 -0
  27. package/dist/components/Icon/svg/thumbnail.js +19 -0
  28. package/dist/components/Icon/svg/zoom-adapt.js +24 -0
  29. package/dist/components/Icon/svg/zoom-in.js +15 -0
  30. package/dist/components/Icon/svg/zoom-out.js +15 -0
  31. package/dist/components/Image.js +148 -0
  32. package/dist/components/Modal.js +98 -0
  33. package/dist/components/PercentSelect.js +160 -0
  34. package/dist/components/ThumbnailIamge.js +65 -0
  35. package/dist/components/index.js +6 -0
  36. package/dist/context/ViewerContext.js +208 -0
  37. package/dist/hooks/useImageOperations.js +113 -0
  38. package/dist/hooks/useImageSource.js +52 -0
  39. package/dist/index.js +2 -0
  40. package/dist/rspress.config.d.ts +2 -0
  41. package/dist/src/FlexImageViewer/Content/index.d.ts +11 -0
  42. package/dist/src/FlexImageViewer/Footer/Actions.d.ts +13 -0
  43. package/dist/src/FlexImageViewer/Footer/index.d.ts +8 -0
  44. package/dist/src/FlexImageViewer/Header/Actions.d.ts +15 -0
  45. package/dist/src/FlexImageViewer/Header/index.d.ts +11 -0
  46. package/dist/src/FlexImageViewer/Info/index.d.ts +5 -0
  47. package/dist/src/FlexImageViewer/Thumbnail/index.d.ts +5 -0
  48. package/dist/src/FlexImageViewer/index.d.ts +21 -0
  49. package/dist/src/components/ActionItem.d.ts +6 -0
  50. package/dist/src/components/BaseActions.d.ts +17 -0
  51. package/dist/src/components/Icon/index.d.ts +6 -0
  52. package/dist/src/components/Image.d.ts +18 -0
  53. package/dist/src/components/Modal.d.ts +9 -0
  54. package/dist/src/components/PercentSelect.d.ts +17 -0
  55. package/dist/src/components/ThumbnailIamge.d.ts +8 -0
  56. package/dist/src/components/index.d.ts +13 -0
  57. package/dist/src/context/ViewerContext.d.ts +13 -0
  58. package/dist/src/hooks/useImageOperations.d.ts +12 -0
  59. package/dist/src/hooks/useImageSource.d.ts +7 -0
  60. package/dist/src/index.d.ts +6 -0
  61. package/dist/src/utils/dict.d.ts +1 -0
  62. package/dist/src/utils/exif.d.ts +2 -0
  63. package/dist/src/utils/source.d.ts +1 -0
  64. package/dist/utils/dict.js +16 -0
  65. package/dist/utils/exif.js +17 -0
  66. package/dist/utils/source.js +2 -0
  67. package/package.json +74 -0
@@ -0,0 +1,148 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useImageSource } from "../hooks/useImageSource.js";
4
+ const MIN_SCALE = 0.1;
5
+ const MAX_SCALE = 8;
6
+ const DEFAULT_WHEEL_ZOOM_STEP = 0.1;
7
+ function Image(props) {
8
+ const { file, angle = 0, scale: oriScale = 1, getSrc, isAdapt = true, onAdapt, wheelZoom = true, wheelZoomStep = DEFAULT_WHEEL_ZOOM_STEP, onWheelZoom, onOrientation, disableRightClick = false, style } = props;
9
+ const containerRef = useRef(null);
10
+ const internalImgRef = useRef(null);
11
+ const scaleRef = useRef(oriScale);
12
+ const wheelZoomStepRef = useRef(wheelZoomStep);
13
+ const onWheelZoomRef = useRef(onWheelZoom);
14
+ const [scale, setScale] = useState(oriScale);
15
+ const [isLoaded, setIsLoaded] = useState(false);
16
+ const [hasError, setHasError] = useState(false);
17
+ const { src, loading, orientation } = useImageSource(file, getSrc);
18
+ useEffect(()=>{
19
+ setIsLoaded(false);
20
+ setHasError(false);
21
+ }, [
22
+ src
23
+ ]);
24
+ useEffect(()=>{
25
+ if (void 0 !== orientation) onOrientation?.(orientation);
26
+ }, [
27
+ orientation,
28
+ onOrientation
29
+ ]);
30
+ useEffect(()=>{
31
+ scaleRef.current = scale;
32
+ }, [
33
+ scale
34
+ ]);
35
+ useEffect(()=>{
36
+ wheelZoomStepRef.current = wheelZoomStep;
37
+ }, [
38
+ wheelZoomStep
39
+ ]);
40
+ useEffect(()=>{
41
+ onWheelZoomRef.current = onWheelZoom;
42
+ }, [
43
+ onWheelZoom
44
+ ]);
45
+ useEffect(()=>{
46
+ if (!isAdapt) return void setScale(oriScale);
47
+ const container = containerRef.current;
48
+ const img = internalImgRef.current;
49
+ const calculateScale = ()=>{
50
+ if (!img || !container) return;
51
+ const containerWidth = container.clientWidth;
52
+ const containerHeight = container.clientHeight;
53
+ const imgWidth = img.naturalWidth;
54
+ const imgHeight = img.naturalHeight;
55
+ if (!imgWidth || !imgHeight || !containerWidth || !containerHeight) return;
56
+ const normalizedAngle = Math.abs(angle % 360);
57
+ const isRotated90Or270 = 90 === normalizedAngle || 270 === normalizedAngle;
58
+ const effectiveImgWidth = isRotated90Or270 ? imgHeight : imgWidth;
59
+ const effectiveImgHeight = isRotated90Or270 ? imgWidth : imgHeight;
60
+ const scaleX = containerWidth / effectiveImgWidth;
61
+ const scaleY = containerHeight / effectiveImgHeight;
62
+ const newScale = Math.min(scaleX, scaleY);
63
+ setScale(newScale);
64
+ onAdapt?.(newScale);
65
+ setIsLoaded(true);
66
+ };
67
+ calculateScale();
68
+ let resizeObserver = null;
69
+ if (container && "u" > typeof ResizeObserver) {
70
+ resizeObserver = new ResizeObserver(calculateScale);
71
+ resizeObserver.observe(container);
72
+ }
73
+ if (img) img.addEventListener('load', calculateScale);
74
+ return ()=>{
75
+ if (resizeObserver) resizeObserver.disconnect();
76
+ if (img) img.removeEventListener('load', calculateScale);
77
+ };
78
+ }, [
79
+ angle,
80
+ src,
81
+ isAdapt,
82
+ oriScale,
83
+ onAdapt
84
+ ]);
85
+ useEffect(()=>{
86
+ const container = containerRef.current;
87
+ if (!container || !wheelZoom) return;
88
+ const handleWheel = (e)=>{
89
+ e.preventDefault();
90
+ const delta = e.deltaY < 0 ? 1 : -1;
91
+ const step = wheelZoomStepRef.current;
92
+ const nextScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scaleRef.current + delta * step));
93
+ if (nextScale !== scaleRef.current) {
94
+ setScale(nextScale);
95
+ onWheelZoomRef.current?.(nextScale);
96
+ }
97
+ };
98
+ container.addEventListener('wheel', handleWheel, {
99
+ passive: false
100
+ });
101
+ return ()=>{
102
+ container.removeEventListener('wheel', handleWheel);
103
+ };
104
+ }, [
105
+ wheelZoom
106
+ ]);
107
+ const handleLoad = ()=>{
108
+ setIsLoaded(true);
109
+ setHasError(false);
110
+ };
111
+ const handleError = ()=>{
112
+ setHasError(true);
113
+ setIsLoaded(true);
114
+ };
115
+ return /*#__PURE__*/ jsxs("figure", {
116
+ ref: containerRef,
117
+ className: "flex-image-viewer-container",
118
+ style: style,
119
+ children: [
120
+ /*#__PURE__*/ jsx("img", {
121
+ ref: internalImgRef,
122
+ className: "flex-image-viewer-image",
123
+ src: src,
124
+ alt: file?.alt,
125
+ onLoad: handleLoad,
126
+ onError: handleError,
127
+ onContextMenu: (e)=>{
128
+ if (disableRightClick) e.preventDefault();
129
+ },
130
+ style: {
131
+ transform: `rotate(${angle}deg) scale(${scale})`,
132
+ opacity: isLoaded && !hasError ? 1 : 0
133
+ }
134
+ }),
135
+ loading && /*#__PURE__*/ jsx("div", {
136
+ className: "flex-image-viewer-loading"
137
+ }),
138
+ hasError && /*#__PURE__*/ jsx("div", {
139
+ className: "flex-image-viewer-error",
140
+ style: {
141
+ position: 'absolute'
142
+ }
143
+ })
144
+ ]
145
+ });
146
+ }
147
+ const components_Image = Image;
148
+ export default components_Image;
@@ -0,0 +1,98 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ import react_dom from "react-dom";
4
+ const getContainer = null;
5
+ function getFocusableElements(container) {
6
+ const selector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
7
+ return Array.from(container.querySelectorAll(selector));
8
+ }
9
+ function Modal(props) {
10
+ const { visible, onClose, children, getContainer: getContainerProp } = props;
11
+ const modalRef = useRef(null);
12
+ const previousFocusRef = useRef(null);
13
+ const getContainerElement = useCallback(()=>{
14
+ if (!getContainerProp) return document.body;
15
+ if ('string' == typeof getContainerProp) {
16
+ const el = document.querySelector(getContainerProp);
17
+ if (!el || !(el instanceof HTMLElement)) {
18
+ console.warn(`Modal: 选择器 "${getContainerProp}" 未找到有效元素,已回退到 body`);
19
+ return document.body;
20
+ }
21
+ return el;
22
+ }
23
+ if ('function' == typeof getContainerProp) {
24
+ const result = getContainerProp();
25
+ if (result && 1 === result.nodeType) return result;
26
+ console.warn('Modal: getContainer 函数返回无效 DOM 元素,已回退到 body');
27
+ return document.body;
28
+ }
29
+ if (getContainerProp instanceof HTMLElement && 1 === getContainerProp.nodeType) return getContainerProp;
30
+ console.warn('Modal: getContainer 参数无效,已回退到 body');
31
+ return document.body;
32
+ }, [
33
+ getContainerProp
34
+ ]);
35
+ useEffect(()=>{
36
+ if (visible) {
37
+ previousFocusRef.current = document.activeElement;
38
+ document.body.style.overflow = 'hidden';
39
+ const timer = setTimeout(()=>{
40
+ if (modalRef.current) {
41
+ const focusable = getFocusableElements(modalRef.current);
42
+ if (focusable.length > 0) focusable[0].focus();
43
+ else modalRef.current.focus();
44
+ }
45
+ }, 0);
46
+ return ()=>{
47
+ clearTimeout(timer);
48
+ };
49
+ }
50
+ document.body.style.overflow = '';
51
+ if (previousFocusRef.current) {
52
+ previousFocusRef.current.focus();
53
+ previousFocusRef.current = null;
54
+ }
55
+ }, [
56
+ visible
57
+ ]);
58
+ useEffect(()=>{
59
+ const handleKeyDown = (e)=>{
60
+ if ('Escape' === e.key && visible && onClose) return void onClose();
61
+ if ('Tab' === e.key && visible && modalRef.current) {
62
+ const focusable = getFocusableElements(modalRef.current);
63
+ if (0 === focusable.length) return void e.preventDefault();
64
+ const first = focusable[0];
65
+ const last = focusable[focusable.length - 1];
66
+ if (e.shiftKey) {
67
+ if (document.activeElement === first) {
68
+ e.preventDefault();
69
+ last.focus();
70
+ }
71
+ } else if (document.activeElement === last) {
72
+ e.preventDefault();
73
+ first.focus();
74
+ }
75
+ }
76
+ };
77
+ document.addEventListener('keydown', handleKeyDown);
78
+ return ()=>{
79
+ document.removeEventListener('keydown', handleKeyDown);
80
+ };
81
+ }, [
82
+ visible,
83
+ onClose
84
+ ]);
85
+ if (!visible) return null;
86
+ const targetElement = getContainerElement();
87
+ const modalContent = /*#__PURE__*/ jsx("div", {
88
+ ref: modalRef,
89
+ className: "flex-image-viewer-modal",
90
+ role: "dialog",
91
+ "aria-modal": "true",
92
+ tabIndex: -1,
93
+ children: children
94
+ });
95
+ return /*#__PURE__*/ react_dom.createPortal(modalContent, targetElement);
96
+ }
97
+ export default Modal;
98
+ export { getContainer };
@@ -0,0 +1,160 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { min } from "es-toolkit/compat";
4
+ import { Icon } from "./index.js";
5
+ const MAX_SCALE = 800;
6
+ const DEFAULT_PERCENT_OPTIONS = Array.from({
7
+ length: 8
8
+ }, (_, i)=>MAX_SCALE - 100 * i).concat([
9
+ 75,
10
+ 50,
11
+ 25,
12
+ 10
13
+ ]);
14
+ const unit = MAX_SCALE / 100;
15
+ const getScaleValue = (sliderValue)=>Math.round(sliderValue * unit);
16
+ const getSliderValue = (scale)=>Math.round(scale / unit);
17
+ function PercentSelect({ value = 100, onChange, disabled = false, className = '', style, options = DEFAULT_PERCENT_OPTIONS }) {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [inputValue, setInputValue] = useState(value.toString());
20
+ const [isEditing, setIsEditing] = useState(false);
21
+ const selectRef = useRef(null);
22
+ const inputRef = useRef(null);
23
+ const optionList = options.map((opt)=>({
24
+ value: opt,
25
+ label: `${opt}%`
26
+ }));
27
+ useEffect(()=>{
28
+ if (!isEditing) setInputValue(value.toString());
29
+ }, [
30
+ value,
31
+ isEditing
32
+ ]);
33
+ useEffect(()=>{
34
+ const handleClickOutside = (event)=>{
35
+ if (selectRef.current && !selectRef.current.contains(event.target)) {
36
+ setIsOpen(false);
37
+ if (isEditing) handleInputBlur();
38
+ }
39
+ };
40
+ document.addEventListener('mousedown', handleClickOutside);
41
+ return ()=>document.removeEventListener('mousedown', handleClickOutside);
42
+ }, [
43
+ isEditing,
44
+ inputValue,
45
+ value
46
+ ]);
47
+ const handleOptionClick = (optionValue)=>{
48
+ if (disabled) return;
49
+ onChange?.(optionValue);
50
+ setInputValue(optionValue.toString());
51
+ setIsOpen(false);
52
+ setIsEditing(false);
53
+ };
54
+ const handleInputChange = (e)=>{
55
+ const val = e.target.value;
56
+ if ('' === val || /^\d*\.?\d*$/.test(val)) {
57
+ const num = min([
58
+ parseFloat(val),
59
+ MAX_SCALE
60
+ ]);
61
+ setInputValue(void 0 === num ? value.toString() : num.toString());
62
+ }
63
+ };
64
+ const handleInputBlur = ()=>{
65
+ const num = min([
66
+ parseFloat(inputValue),
67
+ MAX_SCALE
68
+ ]);
69
+ if (void 0 === num || isNaN(num) || num <= 0 || '' === inputValue) setInputValue(value.toString());
70
+ else {
71
+ if (num !== value) onChange?.(num);
72
+ setInputValue(value.toString());
73
+ }
74
+ setIsEditing(false);
75
+ };
76
+ const handleInputKeyDown = (e)=>{
77
+ if ('Enter' === e.key) {
78
+ handleInputBlur();
79
+ inputRef.current?.blur();
80
+ }
81
+ };
82
+ const handleToggle = (e)=>{
83
+ if (disabled) return;
84
+ e.stopPropagation();
85
+ setIsOpen(!isOpen);
86
+ };
87
+ const handleInputClick = (e)=>{
88
+ if (disabled) return;
89
+ e.stopPropagation();
90
+ if (!isEditing) {
91
+ setIsEditing(true);
92
+ setTimeout(()=>{
93
+ inputRef.current?.select();
94
+ }, 0);
95
+ }
96
+ };
97
+ return /*#__PURE__*/ jsxs("div", {
98
+ ref: selectRef,
99
+ className: `flex-image-viewer-percent-select ${isOpen ? 'flex-image-viewer-percent-select-open' : ''} ${disabled ? 'flex-image-viewer-percent-select-disabled' : ''} ${className}`,
100
+ style: style,
101
+ children: [
102
+ /*#__PURE__*/ jsxs("div", {
103
+ className: "flex-image-viewer-percent-select-control",
104
+ children: [
105
+ /*#__PURE__*/ jsx("input", {
106
+ ref: inputRef,
107
+ type: "text",
108
+ readOnly: !isEditing,
109
+ className: `flex-image-viewer-percent-select-input ${isEditing ? 'flex-image-viewer-percent-select-input-editing' : ''}`,
110
+ value: isEditing ? inputValue : value.toString(),
111
+ onChange: handleInputChange,
112
+ onBlur: handleInputBlur,
113
+ onKeyDown: handleInputKeyDown,
114
+ onClick: handleInputClick,
115
+ disabled: disabled,
116
+ placeholder: "缩放比例"
117
+ }),
118
+ /*#__PURE__*/ jsx("span", {
119
+ className: "flex-image-viewer-percent-select-suffix",
120
+ children: "%"
121
+ }),
122
+ /*#__PURE__*/ jsx("div", {
123
+ className: `flex-image-viewer-percent-select-arrow ${isOpen ? 'flex-image-viewer-percent-select-arrow-open' : ''}`,
124
+ onClick: handleToggle,
125
+ children: /*#__PURE__*/ jsx(Icon, {
126
+ className: "flex-image-viewer-percent-select-arrow-icon",
127
+ name: "Down"
128
+ })
129
+ })
130
+ ]
131
+ }),
132
+ isOpen && /*#__PURE__*/ jsx("div", {
133
+ className: "flex-image-viewer-percent-select-dropdown flex-image-viewer-percent-select-dropdown-up",
134
+ children: /*#__PURE__*/ jsx("div", {
135
+ className: "flex-image-viewer-percent-select-options",
136
+ children: optionList.map((option)=>{
137
+ const isSelected = option.value === value;
138
+ return /*#__PURE__*/ jsxs("div", {
139
+ className: `flex-image-viewer-percent-select-option ${isSelected ? 'flex-image-viewer-percent-select-option-selected' : ''}`,
140
+ onClick: ()=>handleOptionClick(option.value),
141
+ children: [
142
+ option.label,
143
+ isSelected && /*#__PURE__*/ jsx("span", {
144
+ className: "flex-image-viewer-percent-select-check",
145
+ children: /*#__PURE__*/ jsx(Icon, {
146
+ className: "flex-image-viewer-percent-select-check-icon",
147
+ name: "Check"
148
+ })
149
+ })
150
+ ]
151
+ }, option.value);
152
+ })
153
+ })
154
+ })
155
+ ]
156
+ });
157
+ }
158
+ const components_PercentSelect = PercentSelect;
159
+ export default components_PercentSelect;
160
+ export { getScaleValue, getSliderValue };
@@ -0,0 +1,65 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useImageSource } from "../hooks/useImageSource.js";
4
+ function ThumbnailImage(props) {
5
+ const { file, angle = 0, getThumbnail } = props;
6
+ const containerRef = useRef(null);
7
+ const imgRef = useRef(null);
8
+ const [scale, setScale] = useState(1);
9
+ const [isLoaded, setIsLoaded] = useState(false);
10
+ const { src, loading } = useImageSource(file, getThumbnail);
11
+ useEffect(()=>{
12
+ const img = imgRef.current;
13
+ if (!img || !containerRef.current) return;
14
+ const container = containerRef.current;
15
+ const calculateScale = ()=>{
16
+ const containerWidth = container.clientWidth;
17
+ const containerHeight = container.clientHeight;
18
+ const imgWidth = img.naturalWidth || img.width;
19
+ const imgHeight = img.naturalHeight || img.height;
20
+ if (!imgWidth || !imgHeight || !containerWidth || !containerHeight) return;
21
+ const normalizedAngle = Math.abs(angle % 360);
22
+ const isRotated90Or270 = 90 === normalizedAngle || 270 === normalizedAngle;
23
+ const effectiveImgWidth = isRotated90Or270 ? imgHeight : imgWidth;
24
+ const effectiveImgHeight = isRotated90Or270 ? imgWidth : imgHeight;
25
+ const scaleX = containerWidth / effectiveImgWidth;
26
+ const scaleY = containerHeight / effectiveImgHeight;
27
+ const newScale = Math.min(scaleX, scaleY);
28
+ setScale(newScale);
29
+ setIsLoaded(true);
30
+ };
31
+ calculateScale();
32
+ img.addEventListener('load', calculateScale);
33
+ return ()=>{
34
+ img.removeEventListener('load', calculateScale);
35
+ };
36
+ }, [
37
+ angle,
38
+ src
39
+ ]);
40
+ const handleError = ()=>{
41
+ setIsLoaded(true);
42
+ };
43
+ return /*#__PURE__*/ jsxs("div", {
44
+ ref: containerRef,
45
+ className: "flex-image-viewer-thumbnail-container",
46
+ children: [
47
+ /*#__PURE__*/ jsx("img", {
48
+ ref: imgRef,
49
+ className: "flex-image-viewer-thumbnail-image",
50
+ src: src,
51
+ alt: file?.alt,
52
+ onError: handleError,
53
+ style: {
54
+ transform: `rotate(${angle}deg) scale(${scale})`,
55
+ opacity: isLoaded ? 1 : 0
56
+ }
57
+ }),
58
+ loading && /*#__PURE__*/ jsx("div", {
59
+ className: "flex-image-viewer-loading"
60
+ })
61
+ ]
62
+ });
63
+ }
64
+ const ThumbnailIamge = ThumbnailImage;
65
+ export default ThumbnailIamge;
@@ -0,0 +1,6 @@
1
+ export { default as Icon } from "./Icon/index.js";
2
+ export { default as Image } from "./Image.js";
3
+ export { default as Modal } from "./Modal.js";
4
+ export { default as PercentSelect } from "./PercentSelect.js";
5
+ export { default as ActionItem } from "./ActionItem.js";
6
+ export { default as BaseActions } from "./BaseActions.js";
@@ -0,0 +1,208 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useReducer } from "react";
3
+ const ZOOM_STEP = 0.1;
4
+ const MIN_SCALE = 0.1;
5
+ const MAX_SCALE = 8;
6
+ const ViewerStateContext = /*#__PURE__*/ createContext(null);
7
+ const ViewerDispatchContext = /*#__PURE__*/ createContext(null);
8
+ function useViewerState() {
9
+ const context = useContext(ViewerStateContext);
10
+ if (!context) throw new Error('useViewerState must be used within a ViewerProvider');
11
+ return context;
12
+ }
13
+ function useViewerDispatch() {
14
+ const context = useContext(ViewerDispatchContext);
15
+ if (!context) throw new Error('useViewerDispatch must be used within a ViewerProvider');
16
+ return context;
17
+ }
18
+ function viewerReducer(state, action) {
19
+ switch(action.type){
20
+ case 'SET_CURRENT_INDEX':
21
+ return {
22
+ ...state,
23
+ currentIndex: action.payload
24
+ };
25
+ case 'PREV_IMAGE':
26
+ {
27
+ const total = state.filesLength;
28
+ if (total <= 1) return state;
29
+ let index = state.currentIndex - 1;
30
+ if (state.loop && index < 1) index = total;
31
+ else if (index < 1) index = 1;
32
+ return {
33
+ ...state,
34
+ currentIndex: index
35
+ };
36
+ }
37
+ case 'NEXT_IMAGE':
38
+ {
39
+ const total = state.filesLength;
40
+ if (total <= 1) return state;
41
+ let index = state.currentIndex + 1;
42
+ if (state.loop && index > total) index = 1;
43
+ else if (index > total) index = total;
44
+ return {
45
+ ...state,
46
+ currentIndex: index
47
+ };
48
+ }
49
+ case 'SET_LOOP':
50
+ return {
51
+ ...state,
52
+ loop: action.payload
53
+ };
54
+ case 'ROTATE_LEFT':
55
+ {
56
+ const { index } = action.payload;
57
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
58
+ ...opt,
59
+ angle: (opt.angle - 90 + 360) % 360
60
+ } : opt);
61
+ return {
62
+ ...state,
63
+ imageOptions: newOptions
64
+ };
65
+ }
66
+ case 'ROTATE_RIGHT':
67
+ {
68
+ const { index } = action.payload;
69
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
70
+ ...opt,
71
+ angle: (opt.angle + 90) % 360
72
+ } : opt);
73
+ return {
74
+ ...state,
75
+ imageOptions: newOptions
76
+ };
77
+ }
78
+ case 'SET_ZOOM':
79
+ {
80
+ const { index, scale } = action.payload;
81
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
82
+ ...opt,
83
+ scale,
84
+ isAdapt: false
85
+ } : opt);
86
+ return {
87
+ ...state,
88
+ imageOptions: newOptions
89
+ };
90
+ }
91
+ case 'ZOOM_IN':
92
+ {
93
+ const { index, step = ZOOM_STEP } = action.payload;
94
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
95
+ ...opt,
96
+ scale: Math.min(MAX_SCALE, opt.scale + step),
97
+ isAdapt: false
98
+ } : opt);
99
+ return {
100
+ ...state,
101
+ imageOptions: newOptions
102
+ };
103
+ }
104
+ case 'ZOOM_OUT':
105
+ {
106
+ const { index, step = ZOOM_STEP } = action.payload;
107
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
108
+ ...opt,
109
+ scale: Math.max(MIN_SCALE, opt.scale - step),
110
+ isAdapt: false
111
+ } : opt);
112
+ return {
113
+ ...state,
114
+ imageOptions: newOptions
115
+ };
116
+ }
117
+ case 'ADAPT_ZOOM':
118
+ {
119
+ const { index, scale } = action.payload;
120
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
121
+ ...opt,
122
+ scale,
123
+ isAdapt: true
124
+ } : opt);
125
+ return {
126
+ ...state,
127
+ imageOptions: newOptions
128
+ };
129
+ }
130
+ case 'CLEAR_IMAGE':
131
+ {
132
+ const { index, angle, scale } = action.payload;
133
+ const newOptions = state.imageOptions.map((opt, i)=>i === index ? {
134
+ ...opt,
135
+ angle,
136
+ scale,
137
+ isAdapt: true
138
+ } : opt);
139
+ return {
140
+ ...state,
141
+ imageOptions: newOptions
142
+ };
143
+ }
144
+ case 'TOGGLE_INFO':
145
+ return {
146
+ ...state,
147
+ infoVisible: !state.infoVisible
148
+ };
149
+ case 'TOGGLE_THUMBNAIL':
150
+ return {
151
+ ...state,
152
+ thumbnailVisible: !state.thumbnailVisible
153
+ };
154
+ case 'INITIALIZE_FILES':
155
+ {
156
+ const { files } = action.payload;
157
+ const count = files.length;
158
+ return {
159
+ ...state,
160
+ filesLength: count,
161
+ files,
162
+ imageOptions: Array.from({
163
+ length: count
164
+ }, (_, i)=>({
165
+ angle: files[i]?.angle ?? 0,
166
+ scale: files[i]?.scale ?? 1,
167
+ isAdapt: true
168
+ }))
169
+ };
170
+ }
171
+ case 'UPDATE_FILE':
172
+ {
173
+ const { index, file } = action.payload;
174
+ const newFiles = state.files.map((f, i)=>i === index ? {
175
+ ...f,
176
+ ...file
177
+ } : f);
178
+ return {
179
+ ...state,
180
+ files: newFiles
181
+ };
182
+ }
183
+ default:
184
+ return state;
185
+ }
186
+ }
187
+ function createInitialState(initialCurrent = 1, initialFilesLength = 0) {
188
+ return {
189
+ currentIndex: initialCurrent,
190
+ filesLength: initialFilesLength,
191
+ imageOptions: [],
192
+ infoVisible: false,
193
+ thumbnailVisible: false,
194
+ loop: false,
195
+ files: []
196
+ };
197
+ }
198
+ function ViewerProvider({ children, initialCurrent = 1, initialFilesLength = 0 }) {
199
+ const [state, dispatch] = useReducer(viewerReducer, createInitialState(initialCurrent, initialFilesLength));
200
+ return /*#__PURE__*/ jsx(ViewerStateContext.Provider, {
201
+ value: state,
202
+ children: /*#__PURE__*/ jsx(ViewerDispatchContext.Provider, {
203
+ value: dispatch,
204
+ children: children
205
+ })
206
+ });
207
+ }
208
+ export { ViewerDispatchContext, ViewerProvider, ViewerStateContext, useViewerDispatch, useViewerState, viewerReducer };