@@ -63,6 +63,26 @@ function clamp(v: number, a = 0, b = 1) {
6363 return Math . max ( a , Math . min ( b , v ) )
6464}
6565
66+ type HapticLevel = 'light' | 'medium' | 'heavy'
67+
68+ function fireHaptic ( level : HapticLevel ) {
69+ try {
70+ const h = ( globalThis as any ) . HapticFeedback
71+ if ( ! h ) return
72+ if ( level === 'light' ) {
73+ if ( typeof h . lightImpact === 'function' ) h . lightImpact ( )
74+ else if ( typeof h . mediumImpact === 'function' ) h . mediumImpact ( )
75+ return
76+ }
77+ if ( level === 'heavy' ) {
78+ if ( typeof h . heavyImpact === 'function' ) h . heavyImpact ( )
79+ else if ( typeof h . mediumImpact === 'function' ) h . mediumImpact ( )
80+ return
81+ }
82+ if ( typeof h . mediumImpact === 'function' ) h . mediumImpact ( )
83+ } catch { }
84+ }
85+
6686function RectOverlay ( {
6787 box,
6888 selected,
@@ -75,22 +95,25 @@ function RectOverlay({
7595 const stroke = selected ? 'rgba(0,122,255,1)' : 'rgba(0,200,0,1)'
7696 const fill = selected ? 'rgba(0,122,255,0.18)' : 'rgba(0,200,0,0.06)'
7797 const hitPad = 6
98+ const handleTap = ( ) => {
99+ fireHaptic ( 'light' )
100+ onTap ( )
101+ }
78102 return (
79103 < >
80104 < Rectangle
81105 // larger invisible hit area for easier tapping
82106 fill = { 'rgba(0,0,0,0.001)' }
83107 frame = { { width : box . width + hitPad * 2 , height : box . height + hitPad * 2 } }
84108 position = { { x : box . left + box . width / 2 , y : box . top + box . height / 2 } }
85- onTapGesture = { ( ) => onTap ( ) }
109+ onTapGesture = { handleTap }
86110 />
87111 < Rectangle
88112 // a faint green fill so it's visible and hit-testable
89113 fill = { fill }
90114 stroke = { stroke }
91115 frame = { { width : box . width , height : box . height } }
92116 position = { { x : box . left + box . width / 2 , y : box . top + box . height / 2 } }
93- onTapGesture = { ( ) => onTap ( ) }
94117 />
95118 </ >
96119 )
@@ -105,6 +128,7 @@ function ToolbarButton(props: {
105128 font ?: number | Font | { name : string ; size : number }
106129 imageScale ?: 'small' | 'medium' | 'large'
107130 background ?: boolean
131+ haptic ?: HapticLevel
108132} ) {
109133 const isActive = props . active ?? false
110134 const foreground = isActive ? 'systemBlue' : undefined
@@ -115,9 +139,13 @@ function ToolbarButton(props: {
115139 const padding = layout === 'vertical'
116140 ? { top : 10 , bottom : 10 , left : 12 , right : 12 }
117141 : { top : 6 , bottom : 6 , left : 10 , right : 10 }
142+ const handlePress = ( ) => {
143+ fireHaptic ( props . haptic ?? 'medium' )
144+ props . onPress ( )
145+ }
118146 return (
119147 < Button
120- action = { props . onPress }
148+ action = { handlePress }
121149 buttonStyle = "plain"
122150 background = { hasBackground ? { style : 'secondarySystemBackground' , shape : { type : 'rect' , cornerRadius : 10 } } : undefined }
123151 >
@@ -146,6 +174,10 @@ function ToolbarButton(props: {
146174
147175export default function App ( { initialImage } : { initialImage ?: UIImage | null } ) {
148176 const dismiss = Navigation . useDismiss ( )
177+ const withHaptic = ( action : ( ) => void | Promise < void > , level : HapticLevel = 'medium' ) => ( ) => {
178+ fireHaptic ( level )
179+ void action ( )
180+ }
149181
150182 const [ image , setImage ] = useState < UIImage | null > ( null )
151183 const [ items , setItems ] = useState < RecognizedItem [ ] > ( [ ] )
@@ -486,7 +518,7 @@ export default function App({ initialImage }: { initialImage?: UIImage | null })
486518 < ZStack >
487519 < VStack spacing = { 8 } padding toast = { { message : toastMsg , isPresented : toastShown , onChanged : ( v : boolean ) => setToastShown ( v ) , duration : 2 , position : 'bottom' } } >
488520 < HStack spacing = { 8 } alignment = "center" zIndex = { 2 } >
489- < Button title = "" systemImage = "xmark" buttonStyle = "plain" action = { ( ) => dismiss ( ) } />
521+ < Button title = "" systemImage = "xmark" buttonStyle = "plain" action = { withHaptic ( ( ) => dismiss ( ) , 'light' ) } />
490522 < Text font = "headline" > Vision OCR</ Text >
491523 < Spacer />
492524 { loading ? < ProgressView value = { 0.5 } /> : null }
@@ -496,14 +528,14 @@ export default function App({ initialImage }: { initialImage?: UIImage | null })
496528 < VStack frame = { { maxWidth : 'infinity' , maxHeight : 'infinity' } } alignment = "center" >
497529 < Spacer />
498530 < VStack spacing = { 10 } alignment = "center" >
499- < ToolbarButton title = "相册" systemImage = "photo" onPress = { pickPhoto } layout = "vertical" font = { 18 } imageScale = "large" background = { false } />
500- < ToolbarButton title = "文件" systemImage = "doc" onPress = { pickFile } layout = "vertical" font = { 18 } imageScale = "large" background = { false } />
501- < ToolbarButton title = "拍照" systemImage = "camera" onPress = { takePhoto } layout = "vertical" font = { 18 } imageScale = "large" background = { false } />
502- < ToolbarButton title = "粘贴" systemImage = "doc.on.clipboard" onPress = { pasteImage } layout = "vertical" font = { 18 } imageScale = "large" background = { false } />
531+ < ToolbarButton title = "相册" systemImage = "photo" onPress = { pickPhoto } layout = "vertical" font = { 18 } imageScale = "large" background = { false } haptic = "medium" />
532+ < ToolbarButton title = "文件" systemImage = "doc" onPress = { pickFile } layout = "vertical" font = { 18 } imageScale = "large" background = { false } haptic = "medium" />
533+ < ToolbarButton title = "拍照" systemImage = "camera" onPress = { takePhoto } layout = "vertical" font = { 18 } imageScale = "large" background = { false } haptic = "medium" />
534+ < ToolbarButton title = "粘贴" systemImage = "doc.on.clipboard" onPress = { pasteImage } layout = "vertical" font = { 18 } imageScale = "large" background = { false } haptic = "medium" />
503535 < Toggle
504536 title = "自动读取剪贴板"
505537 value = { autoPaste }
506- onChanged = { ( v : boolean ) => setAutoPaste ( v ) }
538+ onChanged = { ( v : boolean ) => { fireHaptic ( 'light' ) ; setAutoPaste ( v ) } }
507539 toggleStyle = "switch"
508540 />
509541 </ VStack >
@@ -514,19 +546,21 @@ export default function App({ initialImage }: { initialImage?: UIImage | null })
514546 { image ? (
515547 < GroupBox label = { < Label title = "操作" systemImage = "slider.horizontal.3" /> } zIndex = { 1 } >
516548 < HStack spacing = { 8 } >
517- < ToolbarButton title = "重置图片" systemImage = "xmark.bin" onPress = { resetImage } />
549+ < ToolbarButton title = "重置图片" systemImage = "xmark.bin" onPress = { resetImage } haptic = "heavy" />
518550 < ToolbarButton
519551 title = { multiSelect ? '合并复制' : '复制全部' }
520552 systemImage = "doc.on.doc"
521553 onPress = { multiSelect ? copySelectedMulti : copyAll }
554+ haptic = "medium"
522555 />
523556 < ToolbarButton
524557 title = "多选"
525558 systemImage = { multiSelect ? 'checkmark.circle.fill' : 'checkmark.circle' }
526559 onPress = { toggleMultiSelect }
527560 active = { multiSelect }
561+ haptic = "light"
528562 />
529- { selectedItem ? < ToolbarButton title = "取消选择" systemImage = "xmark.circle" onPress = { ( ) => setSelectedId ( null ) } /> : null }
563+ { selectedItem ? < ToolbarButton title = "取消选择" systemImage = "xmark.circle" onPress = { ( ) => setSelectedId ( null ) } haptic = "light" /> : null }
530564 </ HStack >
531565 </ GroupBox >
532566 ) : null }
@@ -715,11 +749,11 @@ export default function App({ initialImage }: { initialImage?: UIImage | null })
715749 { /* <Text font="footnote">识别结果:{selectedItem.content}</Text> */ }
716750 < TextField title = "编辑" value = { editorValue } onChanged = { setEditorValue } prompt = "编辑文本" />
717751 < HStack spacing = { 8 } >
718- < Button title = "应用" action = { ( ) => { applyEdit ( ) ; showToast ( '更改已保存' ) } } />
719- < Button title = "重置" action = { ( ) => { resetItem ( selectedItem . id ) ; setEditorValue ( selectedItem . content ) } } />
720- < Button title = "复制" action = { copySelected } />
752+ < Button title = "应用" action = { withHaptic ( ( ) => { applyEdit ( ) ; showToast ( '更改已保存' ) } , 'medium' ) } />
753+ < Button title = "重置" action = { withHaptic ( ( ) => { resetItem ( selectedItem . id ) ; setEditorValue ( selectedItem . content ) } , 'light' ) } />
754+ < Button title = "复制" action = { withHaptic ( copySelected , 'medium' ) } />
721755 < Spacer />
722- < Button title = "关闭" action = { ( ) => setSelectedId ( null ) } />
756+ < Button title = "关闭" action = { withHaptic ( ( ) => setSelectedId ( null ) , 'light' ) } />
723757 </ HStack >
724758 </ VStack >
725759 </ GroupBox >
0 commit comments