@unif/react-native-camera 2.6.1 → 2.7.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 (259) hide show
  1. package/README.md +7 -0
  2. package/lib/module/camera/Camera.js +96 -27
  3. package/lib/module/camera/Camera.js.map +1 -1
  4. package/lib/module/camera/CaptureFlash.js +38 -0
  5. package/lib/module/camera/CaptureFlash.js.map +1 -0
  6. package/lib/module/camera/CaptureFlash.test.js +19 -0
  7. package/lib/module/camera/CaptureFlash.test.js.map +1 -0
  8. package/lib/module/camera/Container.js +166 -64
  9. package/lib/module/camera/Container.js.map +1 -1
  10. package/lib/module/camera/FocusIndicator.js +99 -23
  11. package/lib/module/camera/FocusIndicator.js.map +1 -1
  12. package/lib/module/camera/FocusIndicator.test.js +27 -0
  13. package/lib/module/camera/FocusIndicator.test.js.map +1 -0
  14. package/lib/module/camera/colors/dark.js +22 -0
  15. package/lib/module/camera/colors/dark.js.map +1 -0
  16. package/lib/module/camera/colors/dark.test.js +10 -0
  17. package/lib/module/camera/colors/dark.test.js.map +1 -0
  18. package/lib/module/camera/footer/ActionRow.js +65 -0
  19. package/lib/module/camera/footer/ActionRow.js.map +1 -0
  20. package/lib/module/camera/footer/ActionRow.test.js +53 -0
  21. package/lib/module/camera/footer/ActionRow.test.js.map +1 -0
  22. package/lib/module/camera/footer/FlipButton.js +31 -0
  23. package/lib/module/camera/footer/FlipButton.js.map +1 -0
  24. package/lib/module/camera/footer/FlipButton.test.js +16 -0
  25. package/lib/module/camera/footer/FlipButton.test.js.map +1 -0
  26. package/lib/module/camera/footer/ModeSwitcherPill.js +89 -0
  27. package/lib/module/camera/footer/ModeSwitcherPill.js.map +1 -0
  28. package/lib/module/camera/footer/ModeSwitcherPill.test.js +43 -0
  29. package/lib/module/camera/footer/ModeSwitcherPill.test.js.map +1 -0
  30. package/lib/module/camera/footer/RecordingTimer.js +66 -0
  31. package/lib/module/camera/footer/RecordingTimer.js.map +1 -0
  32. package/lib/module/camera/footer/RecordingTimer.test.js +20 -0
  33. package/lib/module/camera/footer/RecordingTimer.test.js.map +1 -0
  34. package/lib/module/camera/footer/Shutter.js +70 -0
  35. package/lib/module/camera/footer/Shutter.js.map +1 -0
  36. package/lib/module/camera/footer/Shutter.test.js +42 -0
  37. package/lib/module/camera/footer/Shutter.test.js.map +1 -0
  38. package/lib/module/camera/footer/ThumbnailStack.js +71 -0
  39. package/lib/module/camera/footer/ThumbnailStack.js.map +1 -0
  40. package/lib/module/camera/footer/ThumbnailStack.test.js +30 -0
  41. package/lib/module/camera/footer/ThumbnailStack.test.js.map +1 -0
  42. package/lib/module/camera/footer/ZoomChips.js +57 -0
  43. package/lib/module/camera/footer/ZoomChips.js.map +1 -0
  44. package/lib/module/camera/footer/ZoomChips.test.js +17 -0
  45. package/lib/module/camera/footer/ZoomChips.test.js.map +1 -0
  46. package/lib/module/camera/footer/index.js +7 -1
  47. package/lib/module/camera/footer/index.js.map +1 -1
  48. package/lib/module/camera/icons/VolumeIcon.js +44 -0
  49. package/lib/module/camera/icons/VolumeIcon.js.map +1 -0
  50. package/lib/module/camera/icons/VolumeIcon.test.js +18 -0
  51. package/lib/module/camera/icons/VolumeIcon.test.js.map +1 -0
  52. package/lib/module/camera/index.js +3 -1
  53. package/lib/module/camera/index.js.map +1 -1
  54. package/lib/module/camera/preview/PreviewBottomBar.js +69 -0
  55. package/lib/module/camera/preview/PreviewBottomBar.js.map +1 -0
  56. package/lib/module/camera/preview/PreviewBottomBar.test.js +46 -0
  57. package/lib/module/camera/preview/PreviewBottomBar.test.js.map +1 -0
  58. package/lib/module/camera/preview/PreviewOverlay.js +89 -0
  59. package/lib/module/camera/preview/PreviewOverlay.js.map +1 -0
  60. package/lib/module/camera/preview/PreviewOverlay.test.js +45 -0
  61. package/lib/module/camera/preview/PreviewOverlay.test.js.map +1 -0
  62. package/lib/module/camera/preview/PreviewTopBar.js +96 -0
  63. package/lib/module/camera/preview/PreviewTopBar.js.map +1 -0
  64. package/lib/module/camera/preview/PreviewTopBar.test.js +44 -0
  65. package/lib/module/camera/preview/PreviewTopBar.test.js.map +1 -0
  66. package/lib/module/camera/preview/groupTypes.js +11 -0
  67. package/lib/module/camera/preview/groupTypes.js.map +1 -0
  68. package/lib/module/camera/preview/groupTypes.test.js +25 -0
  69. package/lib/module/camera/preview/groupTypes.test.js.map +1 -0
  70. package/lib/module/camera/preview/index.js +3 -4
  71. package/lib/module/camera/preview/index.js.map +1 -1
  72. package/lib/module/camera/setup/SideRail.js +138 -0
  73. package/lib/module/camera/setup/SideRail.js.map +1 -0
  74. package/lib/module/camera/setup/SideRail.test.js +49 -0
  75. package/lib/module/camera/setup/SideRail.test.js.map +1 -0
  76. package/lib/module/camera/setup/index.js +1 -1
  77. package/lib/module/camera/setup/index.js.map +1 -1
  78. package/lib/module/components/Carousel/SlideItem.js +5 -3
  79. package/lib/module/components/Carousel/SlideItem.js.map +1 -1
  80. package/lib/module/components/Carousel/SlideItem.test.js +39 -0
  81. package/lib/module/components/Carousel/SlideItem.test.js.map +1 -0
  82. package/lib/module/components/VideoPlayer.js +34 -0
  83. package/lib/module/components/VideoPlayer.js.map +1 -0
  84. package/lib/module/components/VideoPlayer.test.js +15 -0
  85. package/lib/module/components/VideoPlayer.test.js.map +1 -0
  86. package/lib/module/components/index.js +0 -1
  87. package/lib/module/components/index.js.map +1 -1
  88. package/lib/module/hooks/index.js +0 -1
  89. package/lib/module/hooks/index.js.map +1 -1
  90. package/lib/typescript/src/camera/Camera.d.ts +3 -0
  91. package/lib/typescript/src/camera/Camera.d.ts.map +1 -1
  92. package/lib/typescript/src/camera/CaptureFlash.d.ts +6 -0
  93. package/lib/typescript/src/camera/CaptureFlash.d.ts.map +1 -0
  94. package/lib/typescript/src/camera/CaptureFlash.test.d.ts +2 -0
  95. package/lib/typescript/src/camera/CaptureFlash.test.d.ts.map +1 -0
  96. package/lib/typescript/src/camera/Container.d.ts.map +1 -1
  97. package/lib/typescript/src/camera/FocusIndicator.d.ts.map +1 -1
  98. package/lib/typescript/src/camera/FocusIndicator.test.d.ts +2 -0
  99. package/lib/typescript/src/camera/FocusIndicator.test.d.ts.map +1 -0
  100. package/lib/typescript/src/camera/colors/dark.d.ts +18 -0
  101. package/lib/typescript/src/camera/colors/dark.d.ts.map +1 -0
  102. package/lib/typescript/src/camera/colors/dark.test.d.ts +2 -0
  103. package/lib/typescript/src/camera/colors/dark.test.d.ts.map +1 -0
  104. package/lib/typescript/src/camera/footer/ActionRow.d.ts +14 -0
  105. package/lib/typescript/src/camera/footer/ActionRow.d.ts.map +1 -0
  106. package/lib/typescript/src/camera/footer/ActionRow.test.d.ts +2 -0
  107. package/lib/typescript/src/camera/footer/ActionRow.test.d.ts.map +1 -0
  108. package/lib/typescript/src/camera/footer/FlipButton.d.ts +4 -0
  109. package/lib/typescript/src/camera/footer/FlipButton.d.ts.map +1 -0
  110. package/lib/typescript/src/camera/footer/FlipButton.test.d.ts +2 -0
  111. package/lib/typescript/src/camera/footer/FlipButton.test.d.ts.map +1 -0
  112. package/lib/typescript/src/camera/footer/ModeSwitcherPill.d.ts +10 -0
  113. package/lib/typescript/src/camera/footer/ModeSwitcherPill.d.ts.map +1 -0
  114. package/lib/typescript/src/camera/footer/ModeSwitcherPill.test.d.ts +2 -0
  115. package/lib/typescript/src/camera/footer/ModeSwitcherPill.test.d.ts.map +1 -0
  116. package/lib/typescript/src/camera/footer/RecordingTimer.d.ts +5 -0
  117. package/lib/typescript/src/camera/footer/RecordingTimer.d.ts.map +1 -0
  118. package/lib/typescript/src/camera/footer/RecordingTimer.test.d.ts +2 -0
  119. package/lib/typescript/src/camera/footer/RecordingTimer.test.d.ts.map +1 -0
  120. package/lib/typescript/src/camera/footer/Shutter.d.ts +9 -0
  121. package/lib/typescript/src/camera/footer/Shutter.d.ts.map +1 -0
  122. package/lib/typescript/src/camera/footer/Shutter.test.d.ts +2 -0
  123. package/lib/typescript/src/camera/footer/Shutter.test.d.ts.map +1 -0
  124. package/lib/typescript/src/camera/footer/ThumbnailStack.d.ts +8 -0
  125. package/lib/typescript/src/camera/footer/ThumbnailStack.d.ts.map +1 -0
  126. package/lib/typescript/src/camera/footer/ThumbnailStack.test.d.ts +2 -0
  127. package/lib/typescript/src/camera/footer/ThumbnailStack.test.d.ts.map +1 -0
  128. package/lib/typescript/src/camera/footer/ZoomChips.d.ts +5 -0
  129. package/lib/typescript/src/camera/footer/ZoomChips.d.ts.map +1 -0
  130. package/lib/typescript/src/camera/footer/ZoomChips.test.d.ts +2 -0
  131. package/lib/typescript/src/camera/footer/ZoomChips.test.d.ts.map +1 -0
  132. package/lib/typescript/src/camera/footer/index.d.ts +7 -1
  133. package/lib/typescript/src/camera/footer/index.d.ts.map +1 -1
  134. package/lib/typescript/src/camera/icons/VolumeIcon.d.ts +8 -0
  135. package/lib/typescript/src/camera/icons/VolumeIcon.d.ts.map +1 -0
  136. package/lib/typescript/src/camera/icons/VolumeIcon.test.d.ts +2 -0
  137. package/lib/typescript/src/camera/icons/VolumeIcon.test.d.ts.map +1 -0
  138. package/lib/typescript/src/camera/index.d.ts +3 -1
  139. package/lib/typescript/src/camera/index.d.ts.map +1 -1
  140. package/lib/typescript/src/camera/preview/PreviewBottomBar.d.ts +12 -0
  141. package/lib/typescript/src/camera/preview/PreviewBottomBar.d.ts.map +1 -0
  142. package/lib/typescript/src/camera/preview/PreviewBottomBar.test.d.ts +2 -0
  143. package/lib/typescript/src/camera/preview/PreviewBottomBar.test.d.ts.map +1 -0
  144. package/lib/typescript/src/camera/preview/PreviewOverlay.d.ts +12 -0
  145. package/lib/typescript/src/camera/preview/PreviewOverlay.d.ts.map +1 -0
  146. package/lib/typescript/src/camera/preview/PreviewOverlay.test.d.ts +2 -0
  147. package/lib/typescript/src/camera/preview/PreviewOverlay.test.d.ts.map +1 -0
  148. package/lib/typescript/src/camera/preview/PreviewTopBar.d.ts +10 -0
  149. package/lib/typescript/src/camera/preview/PreviewTopBar.d.ts.map +1 -0
  150. package/lib/typescript/src/camera/preview/PreviewTopBar.test.d.ts +2 -0
  151. package/lib/typescript/src/camera/preview/PreviewTopBar.test.d.ts.map +1 -0
  152. package/lib/typescript/src/camera/preview/groupTypes.d.ts +4 -0
  153. package/lib/typescript/src/camera/preview/groupTypes.d.ts.map +1 -0
  154. package/lib/typescript/src/camera/preview/groupTypes.test.d.ts +2 -0
  155. package/lib/typescript/src/camera/preview/groupTypes.test.d.ts.map +1 -0
  156. package/lib/typescript/src/camera/preview/index.d.ts +3 -4
  157. package/lib/typescript/src/camera/preview/index.d.ts.map +1 -1
  158. package/lib/typescript/src/camera/setup/SideRail.d.ts +15 -0
  159. package/lib/typescript/src/camera/setup/SideRail.d.ts.map +1 -0
  160. package/lib/typescript/src/camera/setup/SideRail.test.d.ts +2 -0
  161. package/lib/typescript/src/camera/setup/SideRail.test.d.ts.map +1 -0
  162. package/lib/typescript/src/camera/setup/index.d.ts +2 -1
  163. package/lib/typescript/src/camera/setup/index.d.ts.map +1 -1
  164. package/lib/typescript/src/components/Carousel/SlideItem.d.ts.map +1 -1
  165. package/lib/typescript/src/components/Carousel/SlideItem.test.d.ts +2 -0
  166. package/lib/typescript/src/components/Carousel/SlideItem.test.d.ts.map +1 -0
  167. package/lib/typescript/src/components/VideoPlayer.d.ts +4 -0
  168. package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -0
  169. package/lib/typescript/src/components/VideoPlayer.test.d.ts +2 -0
  170. package/lib/typescript/src/components/VideoPlayer.test.d.ts.map +1 -0
  171. package/lib/typescript/src/components/index.d.ts +0 -1
  172. package/lib/typescript/src/components/index.d.ts.map +1 -1
  173. package/lib/typescript/src/hooks/index.d.ts +0 -1
  174. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  175. package/package.json +4 -2
  176. package/src/camera/Camera.tsx +117 -26
  177. package/src/camera/CaptureFlash.test.tsx +11 -0
  178. package/src/camera/CaptureFlash.tsx +42 -0
  179. package/src/camera/Container.tsx +158 -64
  180. package/src/camera/FocusIndicator.test.tsx +17 -0
  181. package/src/camera/FocusIndicator.tsx +111 -27
  182. package/src/camera/colors/dark.test.ts +8 -0
  183. package/src/camera/colors/dark.ts +19 -0
  184. package/src/camera/footer/ActionRow.test.tsx +44 -0
  185. package/src/camera/footer/ActionRow.tsx +80 -0
  186. package/src/camera/footer/FlipButton.test.tsx +9 -0
  187. package/src/camera/footer/FlipButton.tsx +22 -0
  188. package/src/camera/footer/ModeSwitcherPill.test.tsx +29 -0
  189. package/src/camera/footer/ModeSwitcherPill.tsx +97 -0
  190. package/src/camera/footer/RecordingTimer.test.tsx +14 -0
  191. package/src/camera/footer/RecordingTimer.tsx +58 -0
  192. package/src/camera/footer/Shutter.test.tsx +26 -0
  193. package/src/camera/footer/Shutter.tsx +71 -0
  194. package/src/camera/footer/ThumbnailStack.test.tsx +19 -0
  195. package/src/camera/footer/ThumbnailStack.tsx +58 -0
  196. package/src/camera/footer/ZoomChips.test.tsx +9 -0
  197. package/src/camera/footer/ZoomChips.tsx +53 -0
  198. package/src/camera/footer/index.tsx +7 -1
  199. package/src/camera/icons/VolumeIcon.test.tsx +9 -0
  200. package/src/camera/icons/VolumeIcon.tsx +39 -0
  201. package/src/camera/index.tsx +11 -1
  202. package/src/camera/preview/PreviewBottomBar.test.tsx +43 -0
  203. package/src/camera/preview/PreviewBottomBar.tsx +79 -0
  204. package/src/camera/preview/PreviewOverlay.test.tsx +45 -0
  205. package/src/camera/preview/PreviewOverlay.tsx +99 -0
  206. package/src/camera/preview/PreviewTopBar.test.tsx +46 -0
  207. package/src/camera/preview/PreviewTopBar.tsx +91 -0
  208. package/src/camera/preview/groupTypes.test.ts +34 -0
  209. package/src/camera/preview/groupTypes.ts +15 -0
  210. package/src/camera/preview/index.tsx +3 -4
  211. package/src/camera/setup/SideRail.test.tsx +37 -0
  212. package/src/camera/setup/SideRail.tsx +161 -0
  213. package/src/camera/setup/index.tsx +2 -1
  214. package/src/components/Carousel/SlideItem.test.tsx +27 -0
  215. package/src/components/Carousel/SlideItem.tsx +11 -7
  216. package/src/components/VideoPlayer.test.tsx +8 -0
  217. package/src/components/VideoPlayer.tsx +30 -0
  218. package/src/components/index.tsx +0 -1
  219. package/src/hooks/index.ts +0 -1
  220. package/lib/module/camera/footer/Footer.js +0 -94
  221. package/lib/module/camera/footer/Footer.js.map +0 -1
  222. package/lib/module/camera/preview/PreView.js +0 -43
  223. package/lib/module/camera/preview/PreView.js.map +0 -1
  224. package/lib/module/camera/preview/PreViewContainer.js +0 -33
  225. package/lib/module/camera/preview/PreViewContainer.js.map +0 -1
  226. package/lib/module/camera/preview/PreviewFooter.js +0 -36
  227. package/lib/module/camera/preview/PreviewFooter.js.map +0 -1
  228. package/lib/module/camera/preview/SinglePre.js +0 -32
  229. package/lib/module/camera/preview/SinglePre.js.map +0 -1
  230. package/lib/module/camera/setup/SetUp.js +0 -74
  231. package/lib/module/camera/setup/SetUp.js.map +0 -1
  232. package/lib/module/components/PreviewThumbnail.js +0 -39
  233. package/lib/module/components/PreviewThumbnail.js.map +0 -1
  234. package/lib/module/hooks/useConfirm.js +0 -16
  235. package/lib/module/hooks/useConfirm.js.map +0 -1
  236. package/lib/typescript/src/camera/footer/Footer.d.ts +0 -14
  237. package/lib/typescript/src/camera/footer/Footer.d.ts.map +0 -1
  238. package/lib/typescript/src/camera/preview/PreView.d.ts +0 -7
  239. package/lib/typescript/src/camera/preview/PreView.d.ts.map +0 -1
  240. package/lib/typescript/src/camera/preview/PreViewContainer.d.ts +0 -9
  241. package/lib/typescript/src/camera/preview/PreViewContainer.d.ts.map +0 -1
  242. package/lib/typescript/src/camera/preview/PreviewFooter.d.ts +0 -7
  243. package/lib/typescript/src/camera/preview/PreviewFooter.d.ts.map +0 -1
  244. package/lib/typescript/src/camera/preview/SinglePre.d.ts +0 -7
  245. package/lib/typescript/src/camera/preview/SinglePre.d.ts.map +0 -1
  246. package/lib/typescript/src/camera/setup/SetUp.d.ts +0 -13
  247. package/lib/typescript/src/camera/setup/SetUp.d.ts.map +0 -1
  248. package/lib/typescript/src/components/PreviewThumbnail.d.ts +0 -9
  249. package/lib/typescript/src/components/PreviewThumbnail.d.ts.map +0 -1
  250. package/lib/typescript/src/hooks/useConfirm.d.ts +0 -2
  251. package/lib/typescript/src/hooks/useConfirm.d.ts.map +0 -1
  252. package/src/camera/footer/Footer.tsx +0 -113
  253. package/src/camera/preview/PreView.tsx +0 -32
  254. package/src/camera/preview/PreViewContainer.tsx +0 -30
  255. package/src/camera/preview/PreviewFooter.tsx +0 -38
  256. package/src/camera/preview/SinglePre.tsx +0 -30
  257. package/src/camera/setup/SetUp.tsx +0 -93
  258. package/src/components/PreviewThumbnail.tsx +0 -45
  259. package/src/hooks/useConfirm.tsx +0 -11
@@ -5,16 +5,21 @@ import {
5
5
  useCameraPermission,
6
6
  } from 'react-native-vision-camera';
7
7
  import { useSharedValue } from 'react-native-reanimated';
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
+ import { r } from '@unif/react-native-design';
8
10
  import type { CameraResult, CustomPhotoFile, OpenConfig } from '../utils';
9
11
  import { NoCamera } from './NoCamera';
10
12
  import { NoPermission } from './NoPermission';
11
13
  import { Loading } from '../components/Loading';
12
14
  import { Camera, type CameraHandle } from './Camera';
13
- import { PreViewContainer } from './preview';
14
- import { Footer } from './footer';
15
- import { SetUp, type AspectRatio, type FlashMode } from './setup';
16
-
17
- const NEUTRAL_ZOOM = 1;
15
+ import { PreviewOverlay } from './preview';
16
+ import { CaptureFlash } from './CaptureFlash';
17
+ import { SideRail, type AspectRatio, type FlashMode } from './setup';
18
+ import { ZoomChips } from './footer/ZoomChips';
19
+ import { ModeSwitcherPill, type ModeItem } from './footer/ModeSwitcherPill';
20
+ import { ActionRow } from './footer/ActionRow';
21
+ import { RecordingTimer } from './footer/RecordingTimer';
22
+ import { DARK } from './colors/dark';
18
23
 
19
24
  type Props = {
20
25
  config: OpenConfig;
@@ -27,10 +32,10 @@ export function Container({ config, onSettle }: Props) {
27
32
  const settledRef = useRef(false);
28
33
 
29
34
  const settle = useCallback(
30
- (r: CameraResult) => {
35
+ (result: CameraResult) => {
31
36
  if (settledRef.current) return;
32
37
  settledRef.current = true;
33
- onSettle(r);
38
+ onSettle(result);
34
39
  },
35
40
  [onSettle]
36
41
  );
@@ -72,25 +77,47 @@ export function Container({ config, onSettle }: Props) {
72
77
  };
73
78
  }, [hasPermission, requestPermission]);
74
79
 
80
+ const insets = useSafeAreaInsets();
75
81
  // 初始前/后摄由 config 首个 mode 的 type 决定(H5 传入),缺省 back。
76
- // 运行时前后摄翻转是独立功能,不在本次范围。
77
- // 5.x:physicalDevices 字符串不带 -camera;单 'wide-angle' 规避 iOS #3773
82
+ // 运行时翻转(S7):position state + flipNonce 触发 rotateY 动画。
83
+ // 5.x:physicalDevices 字符串不带 -camera;单 'wide-angle' 规避 iOS #3773
78
84
  const initialPosition = config.cameraMode[0]?.type ?? 'back';
79
- const device = useCameraDevice(initialPosition, {
85
+ const [position, setPosition] = useState<'back' | 'front'>(initialPosition);
86
+ const [flipNonce, setFlipNonce] = useState(0);
87
+ const device = useCameraDevice(position, {
80
88
  physicalDevices: ['wide-angle'],
81
89
  });
82
90
 
83
91
  const cameraRef = useRef<CameraHandle>(null);
84
92
  const [photos, setPhotos] = useState<CustomPhotoFile[]>([]);
85
93
  const [previewing, setPreviewing] = useState(false);
94
+ const [previewVariant, setPreviewVariant] = useState<'confirm' | 'gallery'>(
95
+ 'gallery'
96
+ );
86
97
  const [recording, setRecording] = useState(false);
87
98
  const [modeIndex, setModeIndex] = useState(0);
88
99
  const currentMode = config.cameraMode[modeIndex];
89
100
 
90
- const [flash, setFlash] = useState<FlashMode>('off');
101
+ // 初始闪光从 config 首个 mode 接线(API 兼容),缺省 off
102
+ const [flash, setFlash] = useState<FlashMode>(
103
+ config.cameraMode[0]?.flashMode ?? 'off'
104
+ );
105
+ const [sound, setSound] = useState(true);
106
+ const [grid, setGrid] = useState(false);
91
107
  const [aspectRatio, setAspectRatio] = useState<AspectRatio>('4:3');
92
- const zoomShared = useSharedValue(NEUTRAL_ZOOM);
93
- const [lensLabel, setLensLabel] = useState(`${NEUTRAL_ZOOM.toFixed(1)}x`);
108
+ const [flashNonce, setFlashNonce] = useState(0);
109
+ const [recSeconds, setRecSeconds] = useState(0);
110
+ const [zoom, setZoom] = useState(1);
111
+ const zoomShared = useSharedValue(1);
112
+
113
+ useEffect(() => {
114
+ if (!recording) {
115
+ setRecSeconds(0);
116
+ return;
117
+ }
118
+ const id = setInterval(() => setRecSeconds((s) => s + 1), 1000);
119
+ return () => clearInterval(id);
120
+ }, [recording]);
94
121
 
95
122
  const onShutter = async () => {
96
123
  if (currentMode?.mode === 'video') {
@@ -100,12 +127,8 @@ export function Container({ config, onSettle }: Props) {
100
127
  } else {
101
128
  const f = await cameraRef.current?.stopVideo();
102
129
  setRecording(false);
103
- if (f) {
104
- setPhotos([f]);
105
- setPreviewing(true);
106
- } else {
107
- settle({ code: 503, data: [], message: 'video_failed' });
108
- }
130
+ if (f) setPhotos((prev) => [...prev, f]);
131
+ else settle({ code: 503, data: [], message: 'video_failed' });
109
132
  }
110
133
  return;
111
134
  }
@@ -114,12 +137,25 @@ export function Container({ config, onSettle }: Props) {
114
137
  settle({ code: 500, data: photos, message: 'capture_failed' });
115
138
  return;
116
139
  }
140
+ setFlashNonce((n) => n + 1);
117
141
  setPhotos((prev) => [...prev, f]);
118
- if (currentMode?.mode !== 'continuous') {
142
+ // 自动预览规则:仅「非保留(clear) + 单拍」拍完进预览;其余累积
143
+ if (currentMode?.mode === 'single' && config.dataRetainedMode === 'clear') {
144
+ setPreviewVariant('confirm');
119
145
  setPreviewing(true);
120
146
  }
121
147
  };
122
148
 
149
+ const onFlip = () => {
150
+ setPosition((p) => (p === 'back' ? 'front' : 'back'));
151
+ setFlipNonce((n) => n + 1);
152
+ };
153
+
154
+ const onSelectMode = (i: number) => {
155
+ if (config.dataRetainedMode === 'clear' && i !== modeIndex) setPhotos([]);
156
+ setModeIndex(i);
157
+ };
158
+
123
159
  if (state === 'denied') {
124
160
  return (
125
161
  <NoPermission
@@ -141,13 +177,20 @@ export function Container({ config, onSettle }: Props) {
141
177
 
142
178
  if (previewing) {
143
179
  return (
144
- <PreViewContainer
180
+ <PreviewOverlay
145
181
  files={photos}
182
+ variant={previewVariant}
146
183
  onRetake={() => {
147
184
  setPhotos([]);
148
185
  setPreviewing(false);
149
186
  }}
150
- onConfirm={() => settle({ code: 200, data: photos, message: 'ok' })}
187
+ onSave={() => settle({ code: 200, data: photos, message: 'ok' })}
188
+ onBack={() => setPreviewing(false)}
189
+ onDelete={(f) => {
190
+ const next = photos.filter((x) => x !== f);
191
+ setPhotos(next);
192
+ if (next.length === 0) setPreviewing(false);
193
+ }}
151
194
  />
152
195
  );
153
196
  }
@@ -170,15 +213,11 @@ export function Container({ config, onSettle }: Props) {
170
213
  );
171
214
  }
172
215
 
173
- const onToggleLens = () => {
174
- if (lensLabel.startsWith(device.minZoom.toFixed(1))) {
175
- zoomShared.value = NEUTRAL_ZOOM;
176
- setLensLabel(`${NEUTRAL_ZOOM.toFixed(1)}x`);
177
- } else {
178
- zoomShared.value = device.minZoom;
179
- setLensLabel(`${device.minZoom.toFixed(1)}x`);
180
- }
181
- };
216
+ const modeItems: ModeItem[] = config.cameraMode.map((m, i) => ({
217
+ key: `${m.mode}-${i}`,
218
+ label:
219
+ m.mode === 'single' ? '单拍' : m.mode === 'continuous' ? '连拍' : '视频',
220
+ }));
182
221
 
183
222
  return (
184
223
  <View style={styles.root} testID="device-ready">
@@ -189,49 +228,104 @@ export function Container({ config, onSettle }: Props) {
189
228
  flash={flash}
190
229
  aspectRatio={aspectRatio}
191
230
  zoomShared={zoomShared}
231
+ sound={sound}
232
+ grid={grid}
233
+ flipNonce={flipNonce}
192
234
  />
193
- {!previewing && (
194
- <SetUp
195
- flash={flash}
196
- aspectRatio={aspectRatio}
197
- onChangeFlash={setFlash}
198
- onChangeAspectRatio={setAspectRatio}
199
- onToggleLens={onToggleLens}
200
- lensLabel={lensLabel}
201
- />
235
+
236
+ {!recording && (
237
+ <View style={[styles.sideRail, { bottom: insets.bottom + r(172) }]}>
238
+ <SideRail
239
+ flash={flash}
240
+ aspectRatio={aspectRatio}
241
+ sound={sound}
242
+ grid={grid}
243
+ onChangeFlash={setFlash}
244
+ onChangeAspectRatio={setAspectRatio}
245
+ onToggleSound={() => setSound((v) => !v)}
246
+ onToggleGrid={() => setGrid((v) => !v)}
247
+ />
248
+ </View>
202
249
  )}
203
- <Footer
204
- modes={config.cameraMode}
205
- currentIndex={modeIndex}
206
- recording={recording}
207
- onShutter={onShutter}
208
- onSelectMode={(i) => {
209
- if (config.dataRetainedMode === 'clear' && i !== modeIndex) {
210
- setPhotos([]);
211
- }
212
- setModeIndex(i);
213
- }}
214
- onCancel={() => settle({ code: 0, data: [], message: 'cancelled' })}
215
- onFinishBurst={
216
- currentMode?.mode === 'continuous'
217
- ? () => setPreviewing(true)
218
- : undefined
219
- }
220
- burstCount={
221
- currentMode?.mode === 'continuous' ? photos.length : undefined
222
- }
223
- />
250
+ {!recording && (
251
+ <View style={[styles.zoomChips, { bottom: insets.bottom + r(184) }]}>
252
+ <ZoomChips
253
+ zoom={zoom}
254
+ onSelect={(z) => {
255
+ // clamp 到设备变焦范围:无超广角设备 minZoom===1,点 0.5x 不应越界
256
+ const clamped = Math.min(
257
+ Math.max(z, device.minZoom),
258
+ device.maxZoom
259
+ );
260
+ setZoom(clamped);
261
+ zoomShared.value = clamped;
262
+ }}
263
+ />
264
+ </View>
265
+ )}
266
+
267
+ <View style={[styles.bottom, { paddingBottom: insets.bottom + r(20) }]}>
268
+ {recording ? (
269
+ <View style={styles.center}>
270
+ <RecordingTimer seconds={recSeconds} />
271
+ </View>
272
+ ) : (
273
+ <View style={styles.center}>
274
+ <ModeSwitcherPill
275
+ items={modeItems}
276
+ currentIndex={modeIndex}
277
+ onSelect={onSelectMode}
278
+ />
279
+ </View>
280
+ )}
281
+ <ActionRow
282
+ mode={currentMode.mode}
283
+ recording={recording}
284
+ latestUri={photos.at(-1)?.uri}
285
+ count={photos.length}
286
+ onShutter={onShutter}
287
+ onBack={() => settle({ code: 0, data: [], message: 'cancelled' })}
288
+ onSave={() => settle({ code: 200, data: photos, message: 'ok' })}
289
+ onFlip={onFlip}
290
+ onOpenPreview={() => {
291
+ if (photos.length > 0) {
292
+ setPreviewVariant('gallery');
293
+ setPreviewing(true);
294
+ }
295
+ }}
296
+ />
297
+ </View>
298
+
299
+ <CaptureFlash trigger={flashNonce} />
224
300
  </View>
225
301
  );
226
302
  }
227
303
 
228
304
  const styles = StyleSheet.create({
305
+ // 相机主容器固定黑底:相机 UX 惯例(预览 / 拍照取景需要黑底凸显),
306
+ // 不走 c.background token。
229
307
  root: {
230
308
  flex: 1,
231
- // 相机主容器固定黑底:相机 UX 惯例(预览 / 拍照取景需要黑底凸显),
232
- // 不走 c.background token.
233
- backgroundColor: '#000',
309
+ backgroundColor: DARK.black,
234
310
  justifyContent: 'center',
235
311
  alignItems: 'center',
236
312
  },
313
+ sideRail: { position: 'absolute', left: r(12), zIndex: 9 },
314
+ zoomChips: {
315
+ position: 'absolute',
316
+ left: 0,
317
+ right: 0,
318
+ alignItems: 'center',
319
+ zIndex: 7,
320
+ },
321
+ bottom: {
322
+ position: 'absolute',
323
+ left: 0,
324
+ right: 0,
325
+ bottom: 0,
326
+ paddingTop: r(14),
327
+ zIndex: 8,
328
+ gap: r(16),
329
+ },
330
+ center: { alignItems: 'center' },
237
331
  });
@@ -0,0 +1,17 @@
1
+ import { render } from '@testing-library/react-native';
2
+ import { FocusIndicator } from './FocusIndicator';
3
+
4
+ it('renders focus brackets without crashing', () => {
5
+ expect(() =>
6
+ render(
7
+ <FocusIndicator point={{ x: 100, y: 200 }} onAnimationEnd={() => {}} />
8
+ )
9
+ ).not.toThrow();
10
+ });
11
+
12
+ it('exposes the focus-indicator testID', () => {
13
+ const { getByTestId } = render(
14
+ <FocusIndicator point={{ x: 100, y: 200 }} onAnimationEnd={() => {}} />
15
+ );
16
+ expect(getByTestId('focus-indicator')).toBeTruthy();
17
+ });
@@ -1,60 +1,144 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { Animated, StyleSheet } from 'react-native';
3
+ import Svg, { Circle, Line, Path } from 'react-native-svg';
3
4
  import { r } from '@unif/react-native-design';
5
+ import { DARK } from './colors/dark';
4
6
  import type { Point } from '../utils';
5
7
 
6
8
  type Props = { point: Point; onAnimationEnd: () => void };
7
9
 
8
- const SIZE = r(80);
10
+ // 聚焦指示器(取景态固定深色,品牌橙):四角括号 + 中心点 + 右侧曝光小太阳。
11
+ // 设计稿 viewBox 390×844 视口下盒子 110×88,transformOrigin 居中(44,44)。
12
+ // 多段动画:淡入放大 1.35 → 回弹 0.94 → 1 → 定格(opacity 0.72),约 1.3s 后 onAnimationEnd()。
13
+ const VB_W = 110;
14
+ const VB_H = 88;
15
+ const W = r(VB_W);
16
+ const H = r(VB_H);
17
+
18
+ // 曝光小太阳中心(右侧)与半径,8 道短射线绕一圈。
19
+ const SUN_CX = 97;
20
+ const SUN_CY = 44;
21
+ const SUN_R = 4.5;
22
+ const RAY_INNER = SUN_R + 2.5;
23
+ const RAY_OUTER = SUN_R + 6;
24
+ const RAY_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];
25
+
26
+ const AnimatedSvg = Animated.createAnimatedComponent(Svg);
9
27
 
10
28
  export function FocusIndicator({ point, onAnimationEnd }: Props) {
11
- const scale = useRef(new Animated.Value(1.6)).current;
12
- const opacity = useRef(new Animated.Value(1)).current;
29
+ const scale = useRef(new Animated.Value(1.35)).current;
30
+ const opacity = useRef(new Animated.Value(0)).current;
13
31
 
14
32
  useEffect(() => {
15
- Animated.parallel([
16
- Animated.timing(scale, {
17
- toValue: 1,
18
- duration: 200,
19
- useNativeDriver: true,
20
- }),
21
- Animated.sequence([
22
- Animated.delay(600),
33
+ const anim = Animated.sequence([
34
+ Animated.parallel([
23
35
  Animated.timing(opacity, {
24
- toValue: 0,
25
- duration: 200,
36
+ toValue: 1,
37
+ duration: 120,
38
+ useNativeDriver: true,
39
+ }),
40
+ Animated.spring(scale, {
41
+ toValue: 0.94,
26
42
  useNativeDriver: true,
27
43
  }),
28
44
  ]),
29
- ]).start(() => onAnimationEnd());
45
+ Animated.spring(scale, {
46
+ toValue: 1,
47
+ useNativeDriver: true,
48
+ }),
49
+ Animated.delay(800),
50
+ Animated.timing(opacity, {
51
+ toValue: 0.72,
52
+ duration: 180,
53
+ useNativeDriver: true,
54
+ }),
55
+ ]);
56
+ anim.start(() => onAnimationEnd());
57
+ return () => anim.stop();
30
58
  }, [scale, opacity, onAnimationEnd]);
31
59
 
32
60
  return (
33
61
  <Animated.View
62
+ pointerEvents="none"
63
+ testID="focus-indicator"
34
64
  style={[
35
65
  styles.box,
36
66
  {
37
- left: point.x - SIZE / 2,
38
- top: point.y - SIZE / 2,
39
- transform: [{ scale }],
67
+ left: point.x - W / 2,
68
+ top: point.y - H / 2,
40
69
  opacity,
70
+ transform: [{ scale }],
41
71
  },
42
72
  ]}
43
- pointerEvents="none"
44
- testID="focus-indicator"
45
- />
73
+ >
74
+ <AnimatedSvg width={W} height={H} viewBox={`0 0 ${VB_W} ${VB_H}`}>
75
+ {/* 四角括号(约 64×64 居中,中心 55,44) */}
76
+ <Path
77
+ d="M23 31v-8h8 M79 23h8v8 M87 57v8h-8 M31 65h-8v-8"
78
+ stroke={DARK.orange}
79
+ strokeWidth={2}
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ fill="none"
83
+ />
84
+ {/* 中心点 */}
85
+ <Circle cx={55} cy={44} r={2.4} fill={DARK.orange} />
86
+ {/* 曝光小太阳:圆 + 8 道短射线 + 上下引导线 */}
87
+ <Circle
88
+ cx={SUN_CX}
89
+ cy={SUN_CY}
90
+ r={SUN_R}
91
+ stroke={DARK.orange}
92
+ strokeWidth={1.6}
93
+ fill="rgba(235,110,0,0.2)"
94
+ />
95
+ {RAY_ANGLES.map((deg) => {
96
+ const rad = (deg * Math.PI) / 180;
97
+ const cos = Math.cos(rad);
98
+ const sin = Math.sin(rad);
99
+ return (
100
+ <Line
101
+ key={deg}
102
+ x1={SUN_CX + cos * RAY_INNER}
103
+ y1={SUN_CY + sin * RAY_INNER}
104
+ x2={SUN_CX + cos * RAY_OUTER}
105
+ y2={SUN_CY + sin * RAY_OUTER}
106
+ stroke={DARK.orange}
107
+ strokeWidth={1.4}
108
+ strokeLinecap="round"
109
+ />
110
+ );
111
+ })}
112
+ {/* 拖动调曝光的引导线(上下各一条,半透明) */}
113
+ <Line
114
+ x1={SUN_CX}
115
+ y1={20}
116
+ x2={SUN_CX}
117
+ y2={34}
118
+ stroke={DARK.orange}
119
+ strokeWidth={1.6}
120
+ opacity={0.45}
121
+ strokeLinecap="round"
122
+ />
123
+ <Line
124
+ x1={SUN_CX}
125
+ y1={54}
126
+ x2={SUN_CX}
127
+ y2={68}
128
+ stroke={DARK.orange}
129
+ strokeWidth={1.6}
130
+ opacity={0.45}
131
+ strokeLinecap="round"
132
+ />
133
+ </AnimatedSvg>
134
+ </Animated.View>
46
135
  );
47
136
  }
48
137
 
49
138
  const styles = StyleSheet.create({
50
139
  box: {
51
140
  position: 'absolute',
52
- width: SIZE,
53
- height: SIZE,
54
- borderWidth: 1.5,
55
- // 对焦黄框:相机 UX 惯例(iOS / Android 系统相机统一用饱和黄高对比度),
56
- // 不走主题 token —— 任何主题下都需要在镜头预览(黑底为主)上一眼可见.
57
- borderColor: 'yellow',
58
- borderRadius: r(6),
141
+ width: W,
142
+ height: H,
59
143
  },
60
144
  });
@@ -0,0 +1,8 @@
1
+ import { DARK } from './dark';
2
+
3
+ it('exposes brand orange and core dark tokens', () => {
4
+ expect(DARK.orange).toBe('#EB6E00');
5
+ expect(DARK.white).toBe('#fff');
6
+ expect(DARK.recRed).toBe('#ff3b30');
7
+ expect(DARK.orange16).toBe('rgba(235,110,0,0.16)');
8
+ });
@@ -0,0 +1,19 @@
1
+ // 取景态固定深色常量。取景器是相机画面,永远深色,不跟随 light/dark 主题;
2
+ // 只有预览/弹窗/Toast(2b)走 useColors()。品牌橙两主题都不变。
3
+ export const DARK = {
4
+ white: '#fff',
5
+ white95: 'rgba(255,255,255,0.95)',
6
+ white65: 'rgba(255,255,255,0.65)',
7
+ white40: 'rgba(255,255,255,0.4)',
8
+ white25: 'rgba(255,255,255,0.25)',
9
+ white12: 'rgba(255,255,255,0.12)',
10
+ white08: 'rgba(255,255,255,0.08)',
11
+ black: '#000',
12
+ black42: 'rgba(0,0,0,0.42)',
13
+ black45: 'rgba(0,0,0,0.45)',
14
+ orange: '#EB6E00',
15
+ orange16: 'rgba(235,110,0,0.16)',
16
+ orange18: 'rgba(235,110,0,0.18)',
17
+ orange95: 'rgba(235,110,0,0.95)',
18
+ recRed: '#ff3b30',
19
+ } as const;
@@ -0,0 +1,44 @@
1
+ import { render, fireEvent } from '@testing-library/react-native';
2
+ import { ActionRow } from './ActionRow';
3
+
4
+ const base = {
5
+ mode: 'single' as const,
6
+ recording: false,
7
+ latestUri: undefined,
8
+ count: 0,
9
+ onShutter: jest.fn(),
10
+ onBack: jest.fn(),
11
+ onSave: jest.fn(),
12
+ onFlip: jest.fn(),
13
+ onOpenPreview: jest.fn(),
14
+ };
15
+
16
+ it('wires shutter/back/save/flip', () => {
17
+ const p = {
18
+ ...base,
19
+ onShutter: jest.fn(),
20
+ onBack: jest.fn(),
21
+ onSave: jest.fn(),
22
+ onFlip: jest.fn(),
23
+ };
24
+ const { getByTestId } = render(<ActionRow {...p} />);
25
+ fireEvent.press(getByTestId('shutter-btn'));
26
+ expect(p.onShutter).toHaveBeenCalled();
27
+ fireEvent.press(getByTestId('back-btn'));
28
+ expect(p.onBack).toHaveBeenCalled();
29
+ fireEvent.press(getByTestId('save-btn'));
30
+ expect(p.onSave).toHaveBeenCalled();
31
+ fireEvent.press(getByTestId('flip-btn'));
32
+ expect(p.onFlip).toHaveBeenCalled();
33
+ });
34
+
35
+ it('recording hides back/save/flip/thumb', () => {
36
+ const { queryByTestId, getByTestId } = render(
37
+ <ActionRow {...base} recording />
38
+ );
39
+ expect(getByTestId('shutter-btn')).toBeTruthy();
40
+ expect(queryByTestId('back-btn')).toBeNull();
41
+ expect(queryByTestId('save-btn')).toBeNull();
42
+ expect(queryByTestId('flip-btn')).toBeNull();
43
+ expect(queryByTestId('thumbnail-stack')).toBeNull();
44
+ });
@@ -0,0 +1,80 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+ import { Button, r } from '@unif/react-native-design';
3
+ import { Shutter } from './Shutter';
4
+ import { ThumbnailStack } from './ThumbnailStack';
5
+ import { FlipButton } from './FlipButton';
6
+
7
+ type Props = {
8
+ mode: 'single' | 'continuous' | 'video';
9
+ recording: boolean;
10
+ latestUri?: string;
11
+ count: number;
12
+ onShutter: () => void;
13
+ onBack: () => void;
14
+ onSave: () => void;
15
+ onFlip: () => void;
16
+ onOpenPreview: () => void;
17
+ };
18
+
19
+ export function ActionRow({
20
+ mode,
21
+ recording,
22
+ latestUri,
23
+ count,
24
+ onShutter,
25
+ onBack,
26
+ onSave,
27
+ onFlip,
28
+ onOpenPreview,
29
+ }: Props) {
30
+ return (
31
+ <View style={styles.row}>
32
+ {!recording ? (
33
+ <ThumbnailStack
34
+ latestUri={latestUri}
35
+ count={count}
36
+ onPress={onOpenPreview}
37
+ />
38
+ ) : (
39
+ <View style={styles.slot} />
40
+ )}
41
+ {!recording ? (
42
+ <Button
43
+ variant="ghost"
44
+ label="返回"
45
+ onPress={onBack}
46
+ testID="back-btn"
47
+ />
48
+ ) : (
49
+ <View style={styles.slot} />
50
+ )}
51
+ <Shutter mode={mode} recording={recording} onPress={onShutter} />
52
+ {!recording ? (
53
+ <Button
54
+ variant="primary"
55
+ label="保存"
56
+ onPress={onSave}
57
+ testID="save-btn"
58
+ />
59
+ ) : (
60
+ <View style={styles.slot} />
61
+ )}
62
+ {!recording ? (
63
+ <FlipButton onFlip={onFlip} />
64
+ ) : (
65
+ <View style={styles.slot} />
66
+ )}
67
+ </View>
68
+ );
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ row: {
73
+ flexDirection: 'row',
74
+ alignItems: 'center',
75
+ justifyContent: 'space-between',
76
+ width: '100%',
77
+ paddingHorizontal: r(20),
78
+ },
79
+ slot: { width: r(44) },
80
+ });
@@ -0,0 +1,9 @@
1
+ import { render, fireEvent } from '@testing-library/react-native';
2
+ import { FlipButton } from './FlipButton';
3
+
4
+ it('fires onFlip', () => {
5
+ const onFlip = jest.fn();
6
+ const { getByTestId } = render(<FlipButton onFlip={onFlip} />);
7
+ fireEvent.press(getByTestId('flip-btn'));
8
+ expect(onFlip).toHaveBeenCalled();
9
+ });