@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
@@ -0,0 +1,45 @@
1
+ import { render } from '@testing-library/react-native';
2
+ import { PreviewOverlay } from './PreviewOverlay';
3
+ import type { CustomPhotoFile } from '../../utils';
4
+
5
+ const f = (
6
+ cameraMode: CustomPhotoFile['cameraMode'],
7
+ id: string
8
+ ): CustomPhotoFile => ({
9
+ id,
10
+ cameraType: 'back',
11
+ cameraMode,
12
+ path: `/${id}`,
13
+ uri: `file:///${id}`,
14
+ width: 1,
15
+ height: 1,
16
+ mime: cameraMode === 'video' ? 'video/mp4' : 'image/jpeg',
17
+ mode: cameraMode,
18
+ });
19
+
20
+ const noop = {
21
+ onRetake: () => {},
22
+ onSave: () => {},
23
+ onBack: () => {},
24
+ onDelete: () => {},
25
+ };
26
+
27
+ it('confirm 变体: 重拍/保存 在', () => {
28
+ const { getByTestId } = render(
29
+ <PreviewOverlay files={[f('single', 'a')]} variant="confirm" {...noop} />
30
+ );
31
+ expect(getByTestId('retake-btn')).toBeTruthy();
32
+ expect(getByTestId('save-btn')).toBeTruthy();
33
+ });
34
+
35
+ it('gallery 变体: 返回/删除 在', () => {
36
+ const { getByTestId } = render(
37
+ <PreviewOverlay
38
+ files={[f('single', 'a'), f('single', 'b')]}
39
+ variant="gallery"
40
+ {...noop}
41
+ />
42
+ );
43
+ expect(getByTestId('back-btn')).toBeTruthy();
44
+ expect(getByTestId('delete-btn')).toBeTruthy();
45
+ });
@@ -0,0 +1,99 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { confirm, toast, useThemedStyles } from '@unif/react-native-design';
4
+ import type { ColorTokens } from '@unif/react-native-design';
5
+ import type { CustomPhotoFile, CameraModeName } from '../../utils';
6
+ import { Carousel } from '../../components/Carousel';
7
+ import { distinctTypes, filesOfType } from './groupTypes';
8
+ import { PreviewTopBar } from './PreviewTopBar';
9
+ import { PreviewBottomBar } from './PreviewBottomBar';
10
+
11
+ type Props = {
12
+ files: CustomPhotoFile[];
13
+ variant: 'confirm' | 'gallery';
14
+ onRetake: () => void;
15
+ onSave: () => void;
16
+ onBack: () => void;
17
+ onDelete: (f: CustomPhotoFile) => void;
18
+ };
19
+
20
+ export function PreviewOverlay({
21
+ files,
22
+ variant,
23
+ onRetake,
24
+ onSave,
25
+ onBack,
26
+ onDelete,
27
+ }: Props) {
28
+ const styles = useThemedStyles(makeStyles);
29
+ const types = useMemo(() => distinctTypes(files), [files]);
30
+ const [activeType, setActiveType] = useState<CameraModeName>(
31
+ types[0] ?? 'single'
32
+ );
33
+ const [index, setIndex] = useState(0);
34
+
35
+ // 删除回收:当前类型被删空 → 切到剩余首个类型(无则关由 Container 处理)
36
+ useEffect(() => {
37
+ if (!types.includes(activeType)) {
38
+ setActiveType(types[0] ?? 'single');
39
+ setIndex(0);
40
+ }
41
+ }, [types, activeType]);
42
+
43
+ // confirm 不分 tab(全 files);gallery 按 activeType 过滤
44
+ const data = variant === 'confirm' ? files : filesOfType(files, activeType);
45
+ const current = data[index] ?? data[0];
46
+
47
+ // 删除后 index 越界 → 夹紧到末张(避免「第 X/Y」计数错位)
48
+ useEffect(() => {
49
+ if (index >= data.length && data.length > 0) setIndex(data.length - 1);
50
+ }, [index, data.length]);
51
+
52
+ const handleSave = () => {
53
+ toast.success('已保存');
54
+ onSave();
55
+ };
56
+ const handleDelete = async () => {
57
+ const ok = await confirm({
58
+ title: '确认删除?',
59
+ message: '图片删除后无法恢复',
60
+ });
61
+ if (ok && current) onDelete(current);
62
+ };
63
+
64
+ return (
65
+ <View style={styles.root} testID="preview-overlay">
66
+ <PreviewTopBar
67
+ variant={variant}
68
+ files={files}
69
+ activeType={activeType}
70
+ onSelectType={(t) => {
71
+ setActiveType(t);
72
+ setIndex(0);
73
+ }}
74
+ />
75
+ <View style={styles.pager}>
76
+ <Carousel data={data} onIndexChange={setIndex} />
77
+ </View>
78
+ <PreviewBottomBar
79
+ variant={variant}
80
+ index={index}
81
+ total={data.length}
82
+ onRetake={onRetake}
83
+ onSave={handleSave}
84
+ onBack={onBack}
85
+ onDelete={handleDelete}
86
+ />
87
+ </View>
88
+ );
89
+ }
90
+
91
+ const makeStyles = (c: ColorTokens) =>
92
+ StyleSheet.create({
93
+ root: {
94
+ ...StyleSheet.absoluteFill,
95
+ backgroundColor: c.background,
96
+ zIndex: 50,
97
+ },
98
+ pager: { flex: 1 },
99
+ });
@@ -0,0 +1,46 @@
1
+ import { render, fireEvent } from '@testing-library/react-native';
2
+ import { PreviewTopBar } from './PreviewTopBar';
3
+ import type { CustomPhotoFile } from '../../utils';
4
+
5
+ const f = (
6
+ cameraMode: CustomPhotoFile['cameraMode'],
7
+ id: string
8
+ ): CustomPhotoFile => ({
9
+ id,
10
+ cameraType: 'back',
11
+ cameraMode,
12
+ path: `/${id}`,
13
+ uri: `file:///${id}`,
14
+ width: 1,
15
+ height: 1,
16
+ mime: cameraMode === 'video' ? 'video/mp4' : 'image/jpeg',
17
+ mode: cameraMode,
18
+ });
19
+
20
+ it('confirm 变体显示类型 label', () => {
21
+ const { getByText } = render(
22
+ <PreviewTopBar
23
+ variant="confirm"
24
+ files={[f('single', 'a')]}
25
+ activeType="single"
26
+ onSelectType={() => {}}
27
+ />
28
+ );
29
+ expect(getByText('单拍')).toBeTruthy();
30
+ });
31
+
32
+ it('gallery 多类型显示 tab + 共N张,点 tab 回调', () => {
33
+ const onSelectType = jest.fn();
34
+ const files = [f('continuous', 'a'), f('continuous', 'b'), f('single', 'c')];
35
+ const { getByTestId, getByText } = render(
36
+ <PreviewTopBar
37
+ variant="gallery"
38
+ files={files}
39
+ activeType="continuous"
40
+ onSelectType={onSelectType}
41
+ />
42
+ );
43
+ expect(getByText('共 3 张')).toBeTruthy();
44
+ fireEvent.press(getByTestId('type-tab-single'));
45
+ expect(onSelectType).toHaveBeenCalledWith('single');
46
+ });
@@ -0,0 +1,91 @@
1
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2
+ import { r, useThemedStyles } from '@unif/react-native-design';
3
+ import type { ColorTokens } from '@unif/react-native-design';
4
+ import type { CustomPhotoFile, CameraModeName } from '../../utils';
5
+ import { distinctTypes, filesOfType } from './groupTypes';
6
+
7
+ const LABEL: Record<CameraModeName, string> = {
8
+ continuous: '连拍',
9
+ single: '单拍',
10
+ video: '视频',
11
+ };
12
+
13
+ type Props = {
14
+ variant: 'confirm' | 'gallery';
15
+ files: CustomPhotoFile[];
16
+ activeType: CameraModeName;
17
+ onSelectType: (t: CameraModeName) => void;
18
+ };
19
+
20
+ export function PreviewTopBar({
21
+ variant,
22
+ files,
23
+ activeType,
24
+ onSelectType,
25
+ }: Props) {
26
+ const styles = useThemedStyles(makeStyles);
27
+ if (variant === 'confirm') {
28
+ return (
29
+ <View style={styles.root}>
30
+ <Text style={styles.label}>{LABEL[activeType]}</Text>
31
+ </View>
32
+ );
33
+ }
34
+ const types = distinctTypes(files);
35
+ const total = files.length;
36
+ return (
37
+ <View style={styles.root}>
38
+ {types.length > 1 && (
39
+ <View style={styles.tabs}>
40
+ {types.map((t) => {
41
+ const sel = t === activeType;
42
+ return (
43
+ <TouchableOpacity
44
+ key={t}
45
+ testID={`type-tab-${t}`}
46
+ onPress={() => onSelectType(t)}
47
+ style={[styles.tab, sel && styles.tabSel]}
48
+ >
49
+ <Text style={[styles.tabTxt, sel && styles.tabTxtSel]}>
50
+ {LABEL[t]}
51
+ {filesOfType(files, t).length}
52
+ </Text>
53
+ </TouchableOpacity>
54
+ );
55
+ })}
56
+ </View>
57
+ )}
58
+ <Text style={styles.total}>共 {total} 张</Text>
59
+ </View>
60
+ );
61
+ }
62
+
63
+ const makeStyles = (c: ColorTokens) =>
64
+ StyleSheet.create({
65
+ root: {
66
+ minHeight: r(46),
67
+ flexDirection: 'row',
68
+ alignItems: 'center',
69
+ justifyContent: 'center',
70
+ gap: r(10),
71
+ paddingHorizontal: r(14),
72
+ paddingTop: r(14),
73
+ },
74
+ label: { color: c.foreground, fontSize: r(14), fontWeight: '500' },
75
+ tabs: {
76
+ flexDirection: 'row',
77
+ gap: r(4),
78
+ padding: r(4),
79
+ borderRadius: r(999),
80
+ backgroundColor: c.surface,
81
+ },
82
+ tab: {
83
+ paddingVertical: r(7),
84
+ paddingHorizontal: r(16),
85
+ borderRadius: r(999),
86
+ },
87
+ tabSel: { backgroundColor: '#EB6E00' },
88
+ tabTxt: { color: c.foreground, fontSize: r(13), fontWeight: '600' },
89
+ tabTxtSel: { color: '#fff' },
90
+ total: { color: c.foreground, fontSize: r(13), opacity: 0.7 },
91
+ });
@@ -0,0 +1,34 @@
1
+ import { distinctTypes, filesOfType } from './groupTypes';
2
+ import type { CustomPhotoFile } from '../../utils';
3
+
4
+ const f = (
5
+ cameraMode: CustomPhotoFile['cameraMode'],
6
+ id: string
7
+ ): CustomPhotoFile => ({
8
+ id,
9
+ cameraType: 'back',
10
+ cameraMode,
11
+ path: `/${id}.jpg`,
12
+ uri: `file:///${id}.jpg`,
13
+ width: 1,
14
+ height: 1,
15
+ mime: cameraMode === 'video' ? 'video/mp4' : 'image/jpeg',
16
+ mode: cameraMode,
17
+ });
18
+
19
+ it('distinctTypes 按 连拍/单拍/视频 顺序去重', () => {
20
+ const files = [
21
+ f('single', 'a'),
22
+ f('video', 'b'),
23
+ f('single', 'c'),
24
+ f('continuous', 'd'),
25
+ ];
26
+ expect(distinctTypes(files)).toEqual(['continuous', 'single', 'video']);
27
+ expect(distinctTypes([f('single', 'a')])).toEqual(['single']);
28
+ expect(distinctTypes([])).toEqual([]);
29
+ });
30
+
31
+ it('filesOfType 过滤', () => {
32
+ const files = [f('single', 'a'), f('video', 'b'), f('single', 'c')];
33
+ expect(filesOfType(files, 'single').map((x) => x.id)).toEqual(['a', 'c']);
34
+ });
@@ -0,0 +1,15 @@
1
+ import type { CustomPhotoFile, CameraModeName } from '../../utils';
2
+
3
+ const ORDER: CameraModeName[] = ['continuous', 'single', 'video'];
4
+
5
+ export function distinctTypes(files: CustomPhotoFile[]): CameraModeName[] {
6
+ const present = new Set(files.map((f) => f.cameraMode));
7
+ return ORDER.filter((t) => present.has(t));
8
+ }
9
+
10
+ export function filesOfType(
11
+ files: CustomPhotoFile[],
12
+ type: CameraModeName
13
+ ): CustomPhotoFile[] {
14
+ return files.filter((f) => f.cameraMode === type);
15
+ }
@@ -1,4 +1,3 @@
1
- export { SinglePre } from './SinglePre';
2
- export { PreView } from './PreView';
3
- export { PreviewFooter } from './PreviewFooter';
4
- export { PreViewContainer } from './PreViewContainer';
1
+ export { PreviewOverlay } from './PreviewOverlay';
2
+ export { PreviewTopBar } from './PreviewTopBar';
3
+ export { PreviewBottomBar } from './PreviewBottomBar';
@@ -0,0 +1,37 @@
1
+ import { render, fireEvent } from '@testing-library/react-native';
2
+ import { SideRail } from './SideRail';
3
+
4
+ const base = {
5
+ flash: 'off' as const,
6
+ aspectRatio: '4:3' as const,
7
+ sound: true,
8
+ grid: false,
9
+ onChangeFlash: jest.fn(),
10
+ onChangeAspectRatio: jest.fn(),
11
+ onToggleSound: jest.fn(),
12
+ onToggleGrid: jest.fn(),
13
+ };
14
+
15
+ it('toggles aspect / sound / grid', () => {
16
+ const p = {
17
+ ...base,
18
+ onChangeAspectRatio: jest.fn(),
19
+ onToggleSound: jest.fn(),
20
+ onToggleGrid: jest.fn(),
21
+ };
22
+ const { getByTestId } = render(<SideRail {...p} />);
23
+ fireEvent.press(getByTestId('aspect-btn'));
24
+ expect(p.onChangeAspectRatio).toHaveBeenCalledWith('16:9');
25
+ fireEvent.press(getByTestId('sound-btn'));
26
+ expect(p.onToggleSound).toHaveBeenCalled();
27
+ fireEvent.press(getByTestId('grid-btn'));
28
+ expect(p.onToggleGrid).toHaveBeenCalled();
29
+ });
30
+
31
+ it('flash dropdown selects a mode', () => {
32
+ const p = { ...base, onChangeFlash: jest.fn() };
33
+ const { getByTestId } = render(<SideRail {...p} />);
34
+ fireEvent.press(getByTestId('flash-btn'));
35
+ fireEvent.press(getByTestId('flash-opt-on'));
36
+ expect(p.onChangeFlash).toHaveBeenCalledWith('on');
37
+ });
@@ -0,0 +1,161 @@
1
+ import { useState } from 'react';
2
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
+ import { Icon, r, type IconName } from '@unif/react-native-design';
4
+ import { DARK } from '../colors/dark';
5
+ import { VolumeIcon } from '../icons/VolumeIcon';
6
+
7
+ export type FlashMode = 'off' | 'on' | 'auto';
8
+ export type AspectRatio = '4:3' | '16:9';
9
+
10
+ type Props = {
11
+ flash: FlashMode;
12
+ aspectRatio: AspectRatio;
13
+ sound: boolean;
14
+ grid: boolean;
15
+ onChangeFlash: (m: FlashMode) => void;
16
+ onChangeAspectRatio: (r: AspectRatio) => void;
17
+ onToggleSound: () => void;
18
+ onToggleGrid: () => void;
19
+ };
20
+
21
+ const flashIcon: Record<FlashMode, IconName> = {
22
+ off: 'flash-off',
23
+ on: 'flash-on',
24
+ auto: 'flash-auto',
25
+ };
26
+
27
+ const FLASH_OPTS: { key: FlashMode; label: string }[] = [
28
+ { key: 'auto', label: '自动' },
29
+ { key: 'on', label: '打开' },
30
+ { key: 'off', label: '关闭' },
31
+ ];
32
+
33
+ export function SideRail({
34
+ flash,
35
+ aspectRatio,
36
+ sound,
37
+ grid,
38
+ onChangeFlash,
39
+ onChangeAspectRatio,
40
+ onToggleSound,
41
+ onToggleGrid,
42
+ }: Props) {
43
+ const [flashOpen, setFlashOpen] = useState(false);
44
+ return (
45
+ <View style={styles.rail}>
46
+ <TouchableOpacity
47
+ testID="aspect-btn"
48
+ style={styles.btn}
49
+ onPress={() =>
50
+ onChangeAspectRatio(aspectRatio === '4:3' ? '16:9' : '4:3')
51
+ }
52
+ >
53
+ <Icon
54
+ name={aspectRatio === '4:3' ? 'aspect-4-3' : 'aspect-16-9'}
55
+ size={r(20)}
56
+ color={DARK.white95}
57
+ />
58
+ </TouchableOpacity>
59
+
60
+ <View>
61
+ <TouchableOpacity
62
+ testID="flash-btn"
63
+ style={[styles.btn, flash !== 'off' && styles.btnActive]}
64
+ onPress={() => setFlashOpen((v) => !v)}
65
+ >
66
+ <Icon
67
+ name={flashIcon[flash]}
68
+ size={r(20)}
69
+ color={flash !== 'off' ? DARK.white : DARK.white95}
70
+ />
71
+ </TouchableOpacity>
72
+ {flashOpen && (
73
+ <View style={styles.dropdown}>
74
+ {FLASH_OPTS.map((o) => (
75
+ <TouchableOpacity
76
+ key={o.key}
77
+ testID={`flash-opt-${o.key}`}
78
+ style={styles.opt}
79
+ onPress={() => {
80
+ onChangeFlash(o.key);
81
+ setFlashOpen(false);
82
+ }}
83
+ >
84
+ <Icon
85
+ name={flashIcon[o.key]}
86
+ size={r(18)}
87
+ color={flash === o.key ? DARK.orange : DARK.white}
88
+ />
89
+ <Text
90
+ style={[styles.optTxt, flash === o.key && styles.optTxtSel]}
91
+ >
92
+ {o.label}
93
+ </Text>
94
+ </TouchableOpacity>
95
+ ))}
96
+ </View>
97
+ )}
98
+ </View>
99
+
100
+ <TouchableOpacity
101
+ testID="sound-btn"
102
+ style={[styles.btn, sound && styles.btnActive]}
103
+ onPress={onToggleSound}
104
+ >
105
+ <VolumeIcon
106
+ on={sound}
107
+ size={r(20)}
108
+ color={sound ? DARK.white : DARK.white95}
109
+ />
110
+ </TouchableOpacity>
111
+
112
+ <TouchableOpacity
113
+ testID="grid-btn"
114
+ style={[styles.btn, grid && styles.btnActive]}
115
+ onPress={onToggleGrid}
116
+ >
117
+ <Icon
118
+ name="grid"
119
+ size={r(18)}
120
+ color={grid ? DARK.white : DARK.white95}
121
+ />
122
+ </TouchableOpacity>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ rail: {
129
+ gap: r(8),
130
+ padding: r(6),
131
+ paddingVertical: r(10),
132
+ borderRadius: r(26),
133
+ backgroundColor: DARK.black42,
134
+ },
135
+ btn: {
136
+ width: r(40),
137
+ height: r(40),
138
+ borderRadius: r(999),
139
+ alignItems: 'center',
140
+ justifyContent: 'center',
141
+ },
142
+ btnActive: { backgroundColor: DARK.orange95 },
143
+ dropdown: {
144
+ position: 'absolute',
145
+ left: r(52),
146
+ top: 0,
147
+ minWidth: r(130),
148
+ padding: r(6),
149
+ borderRadius: r(12),
150
+ backgroundColor: 'rgba(28,28,30,0.94)',
151
+ },
152
+ opt: {
153
+ flexDirection: 'row',
154
+ alignItems: 'center',
155
+ gap: r(8),
156
+ padding: r(10),
157
+ borderRadius: r(8),
158
+ },
159
+ optTxt: { color: DARK.white, fontSize: r(14) },
160
+ optTxtSel: { color: DARK.orange },
161
+ });
@@ -1 +1,2 @@
1
- export { SetUp, type FlashMode, type AspectRatio } from './SetUp';
1
+ export { SideRail } from './SideRail';
2
+ export type { FlashMode, AspectRatio } from './SideRail';
@@ -0,0 +1,27 @@
1
+ import { render } from '@testing-library/react-native';
2
+ import { SlideItem } from './SlideItem';
3
+ import type { CustomPhotoFile } from '../../utils';
4
+
5
+ const base = { id: '1', cameraType: 'back' as const, width: 1, height: 1 };
6
+ const img: CustomPhotoFile = {
7
+ ...base,
8
+ cameraMode: 'single',
9
+ mode: 'single',
10
+ mime: 'image/jpeg',
11
+ path: '/a.jpg',
12
+ uri: 'file:///a.jpg',
13
+ };
14
+ const vid: CustomPhotoFile = {
15
+ ...base,
16
+ cameraMode: 'video',
17
+ mode: 'video',
18
+ mime: 'video/mp4',
19
+ path: '/v.mp4',
20
+ uri: 'file:///v.mp4',
21
+ };
22
+
23
+ it('renders image vs video branch', () => {
24
+ expect(() => render(<SlideItem file={img} />)).not.toThrow();
25
+ const { getByTestId } = render(<SlideItem file={vid} />);
26
+ expect(getByTestId('video-player')).toBeTruthy();
27
+ });
@@ -1,20 +1,24 @@
1
1
  import { Image, StyleSheet, View } from 'react-native';
2
2
  import type { CustomPhotoFile } from '../../utils';
3
+ import { VideoPlayer } from '../VideoPlayer';
3
4
 
4
5
  export function SlideItem({ file }: { file: CustomPhotoFile }) {
5
6
  return (
6
7
  <View style={styles.root}>
7
- <Image
8
- source={{ uri: file.uri }}
9
- style={StyleSheet.absoluteFill}
10
- resizeMode="contain"
11
- />
8
+ {file.mime === 'video/mp4' ? (
9
+ <VideoPlayer uri={file.uri} />
10
+ ) : (
11
+ <Image
12
+ source={{ uri: file.uri }}
13
+ style={StyleSheet.absoluteFill}
14
+ resizeMode="contain"
15
+ />
16
+ )}
12
17
  </View>
13
18
  );
14
19
  }
15
20
 
16
21
  const styles = StyleSheet.create({
17
- // 相机 slide 固定黑底:与 SinglePre / PreView 一致,
18
- // 让照片在纯黑上凸显的 UX 惯例,不走 c.background token.
22
+ // 相机 slide 固定黑底:让图/视频在纯黑上凸显的 UX 惯例,不走 c.background token.
19
23
  root: { flex: 1, backgroundColor: '#000' },
20
24
  });
@@ -0,0 +1,8 @@
1
+ import { render, fireEvent } from '@testing-library/react-native';
2
+ import { VideoPlayer } from './VideoPlayer';
3
+
4
+ it('renders and toggles play on tap without crashing', () => {
5
+ const { getByTestId } = render(<VideoPlayer uri="file:///v.mp4" />);
6
+ expect(getByTestId('video-player')).toBeTruthy();
7
+ expect(() => fireEvent.press(getByTestId('video-player'))).not.toThrow();
8
+ });
@@ -0,0 +1,30 @@
1
+ import { useState } from 'react';
2
+ import { Pressable, StyleSheet } from 'react-native';
3
+ import { useVideoPlayer, VideoView } from 'react-native-video';
4
+
5
+ export function VideoPlayer({ uri }: { uri: string }) {
6
+ const player = useVideoPlayer({ uri }, (p) => {
7
+ p.loop = true;
8
+ });
9
+ const [playing, setPlaying] = useState(false);
10
+ return (
11
+ <Pressable
12
+ testID="video-player"
13
+ style={StyleSheet.absoluteFill}
14
+ onPress={() => {
15
+ if (playing) {
16
+ player.pause();
17
+ } else {
18
+ player.play();
19
+ }
20
+ setPlaying((p) => !p);
21
+ }}
22
+ >
23
+ <VideoView
24
+ player={player}
25
+ style={StyleSheet.absoluteFill}
26
+ resizeMode="contain"
27
+ />
28
+ </Pressable>
29
+ );
30
+ }
@@ -1,3 +1,2 @@
1
1
  export { Loading } from './Loading';
2
- export { PreviewThumbnail } from './PreviewThumbnail';
3
2
  export * from './Carousel';
@@ -1,3 +1,2 @@
1
1
  export { useCreation } from './useCreation';
2
- export { useConfirm } from './useConfirm';
3
2
  export { useCamera } from './useCamera';