@unif/react-native-camera 2.6.0 → 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.
- package/README.md +14 -0
- package/lib/module/camera/Camera.js +96 -27
- package/lib/module/camera/Camera.js.map +1 -1
- package/lib/module/camera/CaptureFlash.js +38 -0
- package/lib/module/camera/CaptureFlash.js.map +1 -0
- package/lib/module/camera/CaptureFlash.test.js +19 -0
- package/lib/module/camera/CaptureFlash.test.js.map +1 -0
- package/lib/module/camera/Container.js +166 -64
- package/lib/module/camera/Container.js.map +1 -1
- package/lib/module/camera/FocusIndicator.js +99 -23
- package/lib/module/camera/FocusIndicator.js.map +1 -1
- package/lib/module/camera/FocusIndicator.test.js +27 -0
- package/lib/module/camera/FocusIndicator.test.js.map +1 -0
- package/lib/module/camera/colors/dark.js +22 -0
- package/lib/module/camera/colors/dark.js.map +1 -0
- package/lib/module/camera/colors/dark.test.js +10 -0
- package/lib/module/camera/colors/dark.test.js.map +1 -0
- package/lib/module/camera/footer/ActionRow.js +65 -0
- package/lib/module/camera/footer/ActionRow.js.map +1 -0
- package/lib/module/camera/footer/ActionRow.test.js +53 -0
- package/lib/module/camera/footer/ActionRow.test.js.map +1 -0
- package/lib/module/camera/footer/FlipButton.js +31 -0
- package/lib/module/camera/footer/FlipButton.js.map +1 -0
- package/lib/module/camera/footer/FlipButton.test.js +16 -0
- package/lib/module/camera/footer/FlipButton.test.js.map +1 -0
- package/lib/module/camera/footer/ModeSwitcherPill.js +89 -0
- package/lib/module/camera/footer/ModeSwitcherPill.js.map +1 -0
- package/lib/module/camera/footer/ModeSwitcherPill.test.js +43 -0
- package/lib/module/camera/footer/ModeSwitcherPill.test.js.map +1 -0
- package/lib/module/camera/footer/RecordingTimer.js +66 -0
- package/lib/module/camera/footer/RecordingTimer.js.map +1 -0
- package/lib/module/camera/footer/RecordingTimer.test.js +20 -0
- package/lib/module/camera/footer/RecordingTimer.test.js.map +1 -0
- package/lib/module/camera/footer/Shutter.js +70 -0
- package/lib/module/camera/footer/Shutter.js.map +1 -0
- package/lib/module/camera/footer/Shutter.test.js +42 -0
- package/lib/module/camera/footer/Shutter.test.js.map +1 -0
- package/lib/module/camera/footer/ThumbnailStack.js +71 -0
- package/lib/module/camera/footer/ThumbnailStack.js.map +1 -0
- package/lib/module/camera/footer/ThumbnailStack.test.js +30 -0
- package/lib/module/camera/footer/ThumbnailStack.test.js.map +1 -0
- package/lib/module/camera/footer/ZoomChips.js +57 -0
- package/lib/module/camera/footer/ZoomChips.js.map +1 -0
- package/lib/module/camera/footer/ZoomChips.test.js +17 -0
- package/lib/module/camera/footer/ZoomChips.test.js.map +1 -0
- package/lib/module/camera/footer/index.js +7 -1
- package/lib/module/camera/footer/index.js.map +1 -1
- package/lib/module/camera/icons/VolumeIcon.js +44 -0
- package/lib/module/camera/icons/VolumeIcon.js.map +1 -0
- package/lib/module/camera/icons/VolumeIcon.test.js +18 -0
- package/lib/module/camera/icons/VolumeIcon.test.js.map +1 -0
- package/lib/module/camera/index.js +3 -1
- package/lib/module/camera/index.js.map +1 -1
- package/lib/module/camera/preview/PreviewBottomBar.js +69 -0
- package/lib/module/camera/preview/PreviewBottomBar.js.map +1 -0
- package/lib/module/camera/preview/PreviewBottomBar.test.js +46 -0
- package/lib/module/camera/preview/PreviewBottomBar.test.js.map +1 -0
- package/lib/module/camera/preview/PreviewOverlay.js +89 -0
- package/lib/module/camera/preview/PreviewOverlay.js.map +1 -0
- package/lib/module/camera/preview/PreviewOverlay.test.js +45 -0
- package/lib/module/camera/preview/PreviewOverlay.test.js.map +1 -0
- package/lib/module/camera/preview/PreviewTopBar.js +96 -0
- package/lib/module/camera/preview/PreviewTopBar.js.map +1 -0
- package/lib/module/camera/preview/PreviewTopBar.test.js +44 -0
- package/lib/module/camera/preview/PreviewTopBar.test.js.map +1 -0
- package/lib/module/camera/preview/groupTypes.js +11 -0
- package/lib/module/camera/preview/groupTypes.js.map +1 -0
- package/lib/module/camera/preview/groupTypes.test.js +25 -0
- package/lib/module/camera/preview/groupTypes.test.js.map +1 -0
- package/lib/module/camera/preview/index.js +3 -4
- package/lib/module/camera/preview/index.js.map +1 -1
- package/lib/module/camera/setup/SideRail.js +138 -0
- package/lib/module/camera/setup/SideRail.js.map +1 -0
- package/lib/module/camera/setup/SideRail.test.js +49 -0
- package/lib/module/camera/setup/SideRail.test.js.map +1 -0
- package/lib/module/camera/setup/index.js +1 -1
- package/lib/module/camera/setup/index.js.map +1 -1
- package/lib/module/components/Carousel/SlideItem.js +5 -3
- package/lib/module/components/Carousel/SlideItem.js.map +1 -1
- package/lib/module/components/Carousel/SlideItem.test.js +39 -0
- package/lib/module/components/Carousel/SlideItem.test.js.map +1 -0
- package/lib/module/components/VideoPlayer.js +34 -0
- package/lib/module/components/VideoPlayer.js.map +1 -0
- package/lib/module/components/VideoPlayer.test.js +15 -0
- package/lib/module/components/VideoPlayer.test.js.map +1 -0
- package/lib/module/components/index.js +0 -1
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/hooks/index.js +0 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/typescript/src/camera/Camera.d.ts +3 -0
- package/lib/typescript/src/camera/Camera.d.ts.map +1 -1
- package/lib/typescript/src/camera/CaptureFlash.d.ts +6 -0
- package/lib/typescript/src/camera/CaptureFlash.d.ts.map +1 -0
- package/lib/typescript/src/camera/CaptureFlash.test.d.ts +2 -0
- package/lib/typescript/src/camera/CaptureFlash.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/Container.d.ts.map +1 -1
- package/lib/typescript/src/camera/FocusIndicator.d.ts.map +1 -1
- package/lib/typescript/src/camera/FocusIndicator.test.d.ts +2 -0
- package/lib/typescript/src/camera/FocusIndicator.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/colors/dark.d.ts +18 -0
- package/lib/typescript/src/camera/colors/dark.d.ts.map +1 -0
- package/lib/typescript/src/camera/colors/dark.test.d.ts +2 -0
- package/lib/typescript/src/camera/colors/dark.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ActionRow.d.ts +14 -0
- package/lib/typescript/src/camera/footer/ActionRow.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ActionRow.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/ActionRow.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/FlipButton.d.ts +4 -0
- package/lib/typescript/src/camera/footer/FlipButton.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/FlipButton.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/FlipButton.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ModeSwitcherPill.d.ts +10 -0
- package/lib/typescript/src/camera/footer/ModeSwitcherPill.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ModeSwitcherPill.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/ModeSwitcherPill.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/RecordingTimer.d.ts +5 -0
- package/lib/typescript/src/camera/footer/RecordingTimer.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/RecordingTimer.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/RecordingTimer.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/Shutter.d.ts +9 -0
- package/lib/typescript/src/camera/footer/Shutter.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/Shutter.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/Shutter.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ThumbnailStack.d.ts +8 -0
- package/lib/typescript/src/camera/footer/ThumbnailStack.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ThumbnailStack.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/ThumbnailStack.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ZoomChips.d.ts +5 -0
- package/lib/typescript/src/camera/footer/ZoomChips.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/ZoomChips.test.d.ts +2 -0
- package/lib/typescript/src/camera/footer/ZoomChips.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/footer/index.d.ts +7 -1
- package/lib/typescript/src/camera/footer/index.d.ts.map +1 -1
- package/lib/typescript/src/camera/icons/VolumeIcon.d.ts +8 -0
- package/lib/typescript/src/camera/icons/VolumeIcon.d.ts.map +1 -0
- package/lib/typescript/src/camera/icons/VolumeIcon.test.d.ts +2 -0
- package/lib/typescript/src/camera/icons/VolumeIcon.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/index.d.ts +3 -1
- package/lib/typescript/src/camera/index.d.ts.map +1 -1
- package/lib/typescript/src/camera/preview/PreviewBottomBar.d.ts +12 -0
- package/lib/typescript/src/camera/preview/PreviewBottomBar.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/PreviewBottomBar.test.d.ts +2 -0
- package/lib/typescript/src/camera/preview/PreviewBottomBar.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/PreviewOverlay.d.ts +12 -0
- package/lib/typescript/src/camera/preview/PreviewOverlay.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/PreviewOverlay.test.d.ts +2 -0
- package/lib/typescript/src/camera/preview/PreviewOverlay.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/PreviewTopBar.d.ts +10 -0
- package/lib/typescript/src/camera/preview/PreviewTopBar.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/PreviewTopBar.test.d.ts +2 -0
- package/lib/typescript/src/camera/preview/PreviewTopBar.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/groupTypes.d.ts +4 -0
- package/lib/typescript/src/camera/preview/groupTypes.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/groupTypes.test.d.ts +2 -0
- package/lib/typescript/src/camera/preview/groupTypes.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/preview/index.d.ts +3 -4
- package/lib/typescript/src/camera/preview/index.d.ts.map +1 -1
- package/lib/typescript/src/camera/setup/SideRail.d.ts +15 -0
- package/lib/typescript/src/camera/setup/SideRail.d.ts.map +1 -0
- package/lib/typescript/src/camera/setup/SideRail.test.d.ts +2 -0
- package/lib/typescript/src/camera/setup/SideRail.test.d.ts.map +1 -0
- package/lib/typescript/src/camera/setup/index.d.ts +2 -1
- package/lib/typescript/src/camera/setup/index.d.ts.map +1 -1
- package/lib/typescript/src/components/Carousel/SlideItem.d.ts.map +1 -1
- package/lib/typescript/src/components/Carousel/SlideItem.test.d.ts +2 -0
- package/lib/typescript/src/components/Carousel/SlideItem.test.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts +4 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.test.d.ts +2 -0
- package/lib/typescript/src/components/VideoPlayer.test.d.ts.map +1 -0
- package/lib/typescript/src/components/index.d.ts +0 -1
- package/lib/typescript/src/components/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +0 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/camera/Camera.tsx +117 -26
- package/src/camera/CaptureFlash.test.tsx +11 -0
- package/src/camera/CaptureFlash.tsx +42 -0
- package/src/camera/Container.tsx +158 -64
- package/src/camera/FocusIndicator.test.tsx +17 -0
- package/src/camera/FocusIndicator.tsx +111 -27
- package/src/camera/colors/dark.test.ts +8 -0
- package/src/camera/colors/dark.ts +19 -0
- package/src/camera/footer/ActionRow.test.tsx +44 -0
- package/src/camera/footer/ActionRow.tsx +80 -0
- package/src/camera/footer/FlipButton.test.tsx +9 -0
- package/src/camera/footer/FlipButton.tsx +22 -0
- package/src/camera/footer/ModeSwitcherPill.test.tsx +29 -0
- package/src/camera/footer/ModeSwitcherPill.tsx +97 -0
- package/src/camera/footer/RecordingTimer.test.tsx +14 -0
- package/src/camera/footer/RecordingTimer.tsx +58 -0
- package/src/camera/footer/Shutter.test.tsx +26 -0
- package/src/camera/footer/Shutter.tsx +71 -0
- package/src/camera/footer/ThumbnailStack.test.tsx +19 -0
- package/src/camera/footer/ThumbnailStack.tsx +58 -0
- package/src/camera/footer/ZoomChips.test.tsx +9 -0
- package/src/camera/footer/ZoomChips.tsx +53 -0
- package/src/camera/footer/index.tsx +7 -1
- package/src/camera/icons/VolumeIcon.test.tsx +9 -0
- package/src/camera/icons/VolumeIcon.tsx +39 -0
- package/src/camera/index.tsx +11 -1
- package/src/camera/preview/PreviewBottomBar.test.tsx +43 -0
- package/src/camera/preview/PreviewBottomBar.tsx +79 -0
- package/src/camera/preview/PreviewOverlay.test.tsx +45 -0
- package/src/camera/preview/PreviewOverlay.tsx +99 -0
- package/src/camera/preview/PreviewTopBar.test.tsx +46 -0
- package/src/camera/preview/PreviewTopBar.tsx +91 -0
- package/src/camera/preview/groupTypes.test.ts +34 -0
- package/src/camera/preview/groupTypes.ts +15 -0
- package/src/camera/preview/index.tsx +3 -4
- package/src/camera/setup/SideRail.test.tsx +37 -0
- package/src/camera/setup/SideRail.tsx +161 -0
- package/src/camera/setup/index.tsx +2 -1
- package/src/components/Carousel/SlideItem.test.tsx +27 -0
- package/src/components/Carousel/SlideItem.tsx +11 -7
- package/src/components/VideoPlayer.test.tsx +8 -0
- package/src/components/VideoPlayer.tsx +30 -0
- package/src/components/index.tsx +0 -1
- package/src/hooks/index.ts +0 -1
- package/lib/module/camera/footer/Footer.js +0 -94
- package/lib/module/camera/footer/Footer.js.map +0 -1
- package/lib/module/camera/preview/PreView.js +0 -43
- package/lib/module/camera/preview/PreView.js.map +0 -1
- package/lib/module/camera/preview/PreViewContainer.js +0 -33
- package/lib/module/camera/preview/PreViewContainer.js.map +0 -1
- package/lib/module/camera/preview/PreviewFooter.js +0 -36
- package/lib/module/camera/preview/PreviewFooter.js.map +0 -1
- package/lib/module/camera/preview/SinglePre.js +0 -32
- package/lib/module/camera/preview/SinglePre.js.map +0 -1
- package/lib/module/camera/setup/SetUp.js +0 -74
- package/lib/module/camera/setup/SetUp.js.map +0 -1
- package/lib/module/components/PreviewThumbnail.js +0 -39
- package/lib/module/components/PreviewThumbnail.js.map +0 -1
- package/lib/module/hooks/useConfirm.js +0 -16
- package/lib/module/hooks/useConfirm.js.map +0 -1
- package/lib/typescript/src/camera/footer/Footer.d.ts +0 -14
- package/lib/typescript/src/camera/footer/Footer.d.ts.map +0 -1
- package/lib/typescript/src/camera/preview/PreView.d.ts +0 -7
- package/lib/typescript/src/camera/preview/PreView.d.ts.map +0 -1
- package/lib/typescript/src/camera/preview/PreViewContainer.d.ts +0 -9
- package/lib/typescript/src/camera/preview/PreViewContainer.d.ts.map +0 -1
- package/lib/typescript/src/camera/preview/PreviewFooter.d.ts +0 -7
- package/lib/typescript/src/camera/preview/PreviewFooter.d.ts.map +0 -1
- package/lib/typescript/src/camera/preview/SinglePre.d.ts +0 -7
- package/lib/typescript/src/camera/preview/SinglePre.d.ts.map +0 -1
- package/lib/typescript/src/camera/setup/SetUp.d.ts +0 -13
- package/lib/typescript/src/camera/setup/SetUp.d.ts.map +0 -1
- package/lib/typescript/src/components/PreviewThumbnail.d.ts +0 -9
- package/lib/typescript/src/components/PreviewThumbnail.d.ts.map +0 -1
- package/lib/typescript/src/hooks/useConfirm.d.ts +0 -2
- package/lib/typescript/src/hooks/useConfirm.d.ts.map +0 -1
- package/src/camera/footer/Footer.tsx +0 -113
- package/src/camera/preview/PreView.tsx +0 -32
- package/src/camera/preview/PreViewContainer.tsx +0 -30
- package/src/camera/preview/PreviewFooter.tsx +0 -38
- package/src/camera/preview/SinglePre.tsx +0 -30
- package/src/camera/setup/SetUp.tsx +0 -93
- package/src/components/PreviewThumbnail.tsx +0 -45
- package/src/hooks/useConfirm.tsx +0 -11
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StyleSheet, TouchableOpacity } from 'react-native';
|
|
2
|
+
import { Icon, r } from '@unif/react-native-design';
|
|
3
|
+
import { DARK } from '../colors/dark';
|
|
4
|
+
|
|
5
|
+
export function FlipButton({ onFlip }: { onFlip: () => void }) {
|
|
6
|
+
return (
|
|
7
|
+
<TouchableOpacity testID="flip-btn" onPress={onFlip} style={styles.btn}>
|
|
8
|
+
<Icon name="lens-flip" size={r(20)} color={DARK.white} />
|
|
9
|
+
</TouchableOpacity>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
btn: {
|
|
15
|
+
width: r(44),
|
|
16
|
+
height: r(44),
|
|
17
|
+
borderRadius: r(22),
|
|
18
|
+
backgroundColor: DARK.white12,
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
2
|
+
import { ModeSwitcherPill } from './ModeSwitcherPill';
|
|
3
|
+
|
|
4
|
+
const items = [
|
|
5
|
+
{ key: 'continuous', label: '连拍' },
|
|
6
|
+
{ key: 'single', label: '单拍' },
|
|
7
|
+
{ key: 'video', label: '视频' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
it('selects by tap', () => {
|
|
11
|
+
const onSelect = jest.fn();
|
|
12
|
+
const { getByTestId } = render(
|
|
13
|
+
<ModeSwitcherPill items={items} currentIndex={1} onSelect={onSelect} />
|
|
14
|
+
);
|
|
15
|
+
fireEvent.press(getByTestId('mode-pill-2'));
|
|
16
|
+
expect(onSelect).toHaveBeenCalledWith(2);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('single item shows label only (no pill)', () => {
|
|
20
|
+
const { queryByTestId, getByText } = render(
|
|
21
|
+
<ModeSwitcherPill
|
|
22
|
+
items={[{ key: 'single', label: '单拍' }]}
|
|
23
|
+
currentIndex={0}
|
|
24
|
+
onSelect={() => {}}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
expect(queryByTestId('mode-pill-0')).toBeNull();
|
|
28
|
+
expect(getByText('单拍')).toBeTruthy();
|
|
29
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
View,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import type { LayoutChangeEvent } from 'react-native';
|
|
10
|
+
import { r } from '@unif/react-native-design';
|
|
11
|
+
import { DARK } from '../colors/dark';
|
|
12
|
+
|
|
13
|
+
export type ModeItem = { key: string; label: string };
|
|
14
|
+
|
|
15
|
+
export function ModeSwitcherPill({
|
|
16
|
+
items,
|
|
17
|
+
currentIndex,
|
|
18
|
+
onSelect,
|
|
19
|
+
}: {
|
|
20
|
+
items: ModeItem[];
|
|
21
|
+
currentIndex: number;
|
|
22
|
+
onSelect: (i: number) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const [w, setW] = useState(0);
|
|
25
|
+
const slide = useRef(new Animated.Value(0)).current;
|
|
26
|
+
const itemW = items.length ? w / items.length : 0;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
Animated.timing(slide, {
|
|
30
|
+
toValue: currentIndex * itemW,
|
|
31
|
+
duration: 240,
|
|
32
|
+
useNativeDriver: true,
|
|
33
|
+
}).start();
|
|
34
|
+
}, [currentIndex, itemW, slide]);
|
|
35
|
+
|
|
36
|
+
if (items.length === 1) {
|
|
37
|
+
return <Text style={styles.singleLabel}>{items[0]!.label}</Text>;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
style={styles.wrap}
|
|
42
|
+
onLayout={(e: LayoutChangeEvent) => setW(e.nativeEvent.layout.width)}
|
|
43
|
+
>
|
|
44
|
+
{itemW > 0 && (
|
|
45
|
+
<Animated.View
|
|
46
|
+
style={[
|
|
47
|
+
styles.slider,
|
|
48
|
+
{ width: itemW, transform: [{ translateX: slide }] },
|
|
49
|
+
]}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
{items.map((it, i) => {
|
|
53
|
+
const sel = i === currentIndex;
|
|
54
|
+
return (
|
|
55
|
+
<TouchableOpacity
|
|
56
|
+
key={it.key}
|
|
57
|
+
testID={`mode-pill-${i}`}
|
|
58
|
+
style={styles.item}
|
|
59
|
+
onPress={() => onSelect(i)}
|
|
60
|
+
>
|
|
61
|
+
<Text style={[styles.txt, sel && styles.txtSel]}>{it.label}</Text>
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
wrap: { flexDirection: 'row', alignSelf: 'center', position: 'relative' },
|
|
71
|
+
slider: {
|
|
72
|
+
position: 'absolute',
|
|
73
|
+
top: r(4),
|
|
74
|
+
bottom: r(4),
|
|
75
|
+
left: 0,
|
|
76
|
+
borderRadius: r(999),
|
|
77
|
+
backgroundColor: DARK.orange16,
|
|
78
|
+
},
|
|
79
|
+
item: {
|
|
80
|
+
paddingVertical: r(8),
|
|
81
|
+
paddingHorizontal: r(22),
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
},
|
|
84
|
+
txt: {
|
|
85
|
+
color: DARK.white65,
|
|
86
|
+
fontSize: r(15),
|
|
87
|
+
fontWeight: '500',
|
|
88
|
+
letterSpacing: 1,
|
|
89
|
+
},
|
|
90
|
+
txtSel: { color: DARK.orange, fontWeight: '600' },
|
|
91
|
+
singleLabel: {
|
|
92
|
+
color: DARK.white65,
|
|
93
|
+
fontSize: r(14),
|
|
94
|
+
letterSpacing: 2,
|
|
95
|
+
alignSelf: 'center',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { render } from '@testing-library/react-native';
|
|
2
|
+
import { RecordingTimer, formatDuration } from './RecordingTimer';
|
|
3
|
+
|
|
4
|
+
it('formatDuration → MM:SS', () => {
|
|
5
|
+
expect(formatDuration(0)).toBe('00:00');
|
|
6
|
+
expect(formatDuration(65)).toBe('01:05');
|
|
7
|
+
expect(formatDuration(3661)).toBe('61:01');
|
|
8
|
+
expect(formatDuration(-5)).toBe('00:00');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders timer pill', () => {
|
|
12
|
+
const { getByTestId } = render(<RecordingTimer seconds={5} />);
|
|
13
|
+
expect(getByTestId('recording-timer')).toBeTruthy();
|
|
14
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { r } from '@unif/react-native-design';
|
|
4
|
+
import { DARK } from '../colors/dark';
|
|
5
|
+
|
|
6
|
+
export function formatDuration(totalSec: number): string {
|
|
7
|
+
const s = Math.max(0, Math.floor(totalSec));
|
|
8
|
+
const mm = String(Math.floor(s / 60)).padStart(2, '0');
|
|
9
|
+
const ss = String(s % 60).padStart(2, '0');
|
|
10
|
+
return `${mm}:${ss}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RecordingTimer({ seconds }: { seconds: number }) {
|
|
14
|
+
const blink = useRef(new Animated.Value(1)).current;
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const loop = Animated.loop(
|
|
17
|
+
Animated.sequence([
|
|
18
|
+
Animated.timing(blink, {
|
|
19
|
+
toValue: 0,
|
|
20
|
+
duration: 500,
|
|
21
|
+
useNativeDriver: true,
|
|
22
|
+
}),
|
|
23
|
+
Animated.timing(blink, {
|
|
24
|
+
toValue: 1,
|
|
25
|
+
duration: 500,
|
|
26
|
+
useNativeDriver: true,
|
|
27
|
+
}),
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
loop.start();
|
|
31
|
+
return () => loop.stop();
|
|
32
|
+
}, [blink]);
|
|
33
|
+
return (
|
|
34
|
+
<View style={styles.pill} testID="recording-timer">
|
|
35
|
+
<Animated.View style={[styles.dot, { opacity: blink }]} />
|
|
36
|
+
<Text style={styles.text}>{formatDuration(seconds)}</Text>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
pill: {
|
|
43
|
+
flexDirection: 'row',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
gap: r(8),
|
|
46
|
+
paddingVertical: r(6),
|
|
47
|
+
paddingHorizontal: r(14),
|
|
48
|
+
borderRadius: r(999),
|
|
49
|
+
backgroundColor: 'rgba(255,59,48,0.18)',
|
|
50
|
+
},
|
|
51
|
+
dot: {
|
|
52
|
+
width: r(8),
|
|
53
|
+
height: r(8),
|
|
54
|
+
borderRadius: r(4),
|
|
55
|
+
backgroundColor: DARK.recRed,
|
|
56
|
+
},
|
|
57
|
+
text: { color: DARK.white, fontSize: r(13), fontVariant: ['tabular-nums'] },
|
|
58
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
2
|
+
import { Shutter } from './Shutter';
|
|
3
|
+
|
|
4
|
+
it('fires onPress and renders for each mode', () => {
|
|
5
|
+
const onPress = jest.fn();
|
|
6
|
+
const { getByTestId, rerender } = render(
|
|
7
|
+
<Shutter mode="single" recording={false} onPress={onPress} />
|
|
8
|
+
);
|
|
9
|
+
fireEvent.press(getByTestId('shutter-btn'));
|
|
10
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
11
|
+
expect(() =>
|
|
12
|
+
rerender(<Shutter mode="video" recording={false} onPress={onPress} />)
|
|
13
|
+
).not.toThrow();
|
|
14
|
+
expect(() =>
|
|
15
|
+
rerender(<Shutter mode="video" recording={true} onPress={onPress} />)
|
|
16
|
+
).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('disabled blocks press', () => {
|
|
20
|
+
const onPress = jest.fn();
|
|
21
|
+
const { getByTestId } = render(
|
|
22
|
+
<Shutter mode="single" recording={false} disabled onPress={onPress} />
|
|
23
|
+
);
|
|
24
|
+
fireEvent.press(getByTestId('shutter-btn'));
|
|
25
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { Animated, Pressable, StyleSheet } from 'react-native';
|
|
3
|
+
import { r } from '@unif/react-native-design';
|
|
4
|
+
import { DARK } from '../colors/dark';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
mode: 'single' | 'continuous' | 'video';
|
|
8
|
+
recording: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Shutter({ mode, recording, disabled, onPress }: Props) {
|
|
14
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
15
|
+
const to = (v: number) =>
|
|
16
|
+
Animated.timing(scale, {
|
|
17
|
+
toValue: v,
|
|
18
|
+
duration: 100,
|
|
19
|
+
useNativeDriver: true,
|
|
20
|
+
}).start();
|
|
21
|
+
const isVideo = mode === 'video';
|
|
22
|
+
const inner = recording
|
|
23
|
+
? styles.innerRecording
|
|
24
|
+
: isVideo
|
|
25
|
+
? styles.innerVideo
|
|
26
|
+
: styles.innerPhoto;
|
|
27
|
+
return (
|
|
28
|
+
<Pressable
|
|
29
|
+
testID="shutter-btn"
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
onPress={onPress}
|
|
32
|
+
onPressIn={() => to(0.94)}
|
|
33
|
+
onPressOut={() => to(1)}
|
|
34
|
+
>
|
|
35
|
+
<Animated.View style={[styles.ring, { transform: [{ scale }] }]}>
|
|
36
|
+
<Animated.View style={[styles.innerBase, inner]} />
|
|
37
|
+
</Animated.View>
|
|
38
|
+
</Pressable>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
ring: {
|
|
44
|
+
width: r(72),
|
|
45
|
+
height: r(72),
|
|
46
|
+
borderRadius: r(36),
|
|
47
|
+
borderWidth: r(3),
|
|
48
|
+
borderColor: DARK.white95,
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
},
|
|
52
|
+
innerBase: {},
|
|
53
|
+
innerPhoto: {
|
|
54
|
+
width: r(58),
|
|
55
|
+
height: r(58),
|
|
56
|
+
borderRadius: r(29),
|
|
57
|
+
backgroundColor: DARK.white,
|
|
58
|
+
},
|
|
59
|
+
innerVideo: {
|
|
60
|
+
width: r(58),
|
|
61
|
+
height: r(58),
|
|
62
|
+
borderRadius: r(29),
|
|
63
|
+
backgroundColor: DARK.recRed,
|
|
64
|
+
},
|
|
65
|
+
innerRecording: {
|
|
66
|
+
width: r(24),
|
|
67
|
+
height: r(24),
|
|
68
|
+
borderRadius: r(4),
|
|
69
|
+
backgroundColor: DARK.recRed,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
2
|
+
import { ThumbnailStack } from './ThumbnailStack';
|
|
3
|
+
|
|
4
|
+
it('empty state, no badge, tappable', () => {
|
|
5
|
+
const onPress = jest.fn();
|
|
6
|
+
const { getByTestId, queryByTestId } = render(
|
|
7
|
+
<ThumbnailStack latestUri={undefined} count={0} onPress={onPress} />
|
|
8
|
+
);
|
|
9
|
+
fireEvent.press(getByTestId('thumbnail-stack'));
|
|
10
|
+
expect(onPress).toHaveBeenCalled();
|
|
11
|
+
expect(queryByTestId('thumb-badge')).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('shows badge when count > 1', () => {
|
|
15
|
+
const { getByTestId } = render(
|
|
16
|
+
<ThumbnailStack latestUri="file:///a.jpg" count={3} onPress={() => {}} />
|
|
17
|
+
);
|
|
18
|
+
expect(getByTestId('thumb-badge')).toBeTruthy();
|
|
19
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
2
|
+
import { r } from '@unif/react-native-design';
|
|
3
|
+
import { DARK } from '../colors/dark';
|
|
4
|
+
|
|
5
|
+
type Props = { latestUri?: string; count: number; onPress: () => void };
|
|
6
|
+
|
|
7
|
+
export function ThumbnailStack({ latestUri, count, onPress }: Props) {
|
|
8
|
+
return (
|
|
9
|
+
<TouchableOpacity
|
|
10
|
+
testID="thumbnail-stack"
|
|
11
|
+
onPress={onPress}
|
|
12
|
+
style={styles.wrap}
|
|
13
|
+
>
|
|
14
|
+
{latestUri ? (
|
|
15
|
+
<Image source={{ uri: latestUri }} style={styles.img} />
|
|
16
|
+
) : (
|
|
17
|
+
<View style={styles.empty} />
|
|
18
|
+
)}
|
|
19
|
+
{count > 1 && (
|
|
20
|
+
<View style={styles.badge} testID="thumb-badge">
|
|
21
|
+
<Text style={styles.badgeText}>{count}</Text>
|
|
22
|
+
</View>
|
|
23
|
+
)}
|
|
24
|
+
</TouchableOpacity>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const styles = StyleSheet.create({
|
|
29
|
+
wrap: { width: r(44), height: r(44) },
|
|
30
|
+
img: {
|
|
31
|
+
width: r(44),
|
|
32
|
+
height: r(44),
|
|
33
|
+
borderRadius: r(6),
|
|
34
|
+
borderWidth: 2,
|
|
35
|
+
borderColor: DARK.white,
|
|
36
|
+
},
|
|
37
|
+
empty: {
|
|
38
|
+
width: r(44),
|
|
39
|
+
height: r(44),
|
|
40
|
+
borderRadius: r(8),
|
|
41
|
+
borderWidth: 1.5,
|
|
42
|
+
borderColor: DARK.white40,
|
|
43
|
+
backgroundColor: DARK.white08,
|
|
44
|
+
},
|
|
45
|
+
badge: {
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
top: r(-6),
|
|
48
|
+
right: r(-6),
|
|
49
|
+
minWidth: r(20),
|
|
50
|
+
height: r(20),
|
|
51
|
+
borderRadius: r(999),
|
|
52
|
+
backgroundColor: DARK.orange,
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
justifyContent: 'center',
|
|
55
|
+
paddingHorizontal: r(4),
|
|
56
|
+
},
|
|
57
|
+
badgeText: { color: DARK.white, fontSize: r(11), fontWeight: '700' },
|
|
58
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
2
|
+
import { ZoomChips } from './ZoomChips';
|
|
3
|
+
|
|
4
|
+
it('renders 3 chips and selects', () => {
|
|
5
|
+
const onSelect = jest.fn();
|
|
6
|
+
const { getByTestId } = render(<ZoomChips zoom={1} onSelect={onSelect} />);
|
|
7
|
+
fireEvent.press(getByTestId('zoom-chip-2'));
|
|
8
|
+
expect(onSelect).toHaveBeenCalledWith(2);
|
|
9
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
2
|
+
import { r } from '@unif/react-native-design';
|
|
3
|
+
import { DARK } from '../colors/dark';
|
|
4
|
+
|
|
5
|
+
const STOPS = [0.5, 1, 2] as const;
|
|
6
|
+
|
|
7
|
+
export function ZoomChips({
|
|
8
|
+
zoom,
|
|
9
|
+
onSelect,
|
|
10
|
+
}: {
|
|
11
|
+
zoom: number;
|
|
12
|
+
onSelect: (z: number) => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.row}>
|
|
16
|
+
{STOPS.map((z) => {
|
|
17
|
+
const active = Math.abs(zoom - z) < 0.05;
|
|
18
|
+
return (
|
|
19
|
+
<TouchableOpacity
|
|
20
|
+
key={z}
|
|
21
|
+
testID={`zoom-chip-${z}`}
|
|
22
|
+
onPress={() => onSelect(z)}
|
|
23
|
+
style={[styles.chip, active && styles.chipActive]}
|
|
24
|
+
>
|
|
25
|
+
<Text style={[styles.txt, active && styles.txtActive]}>
|
|
26
|
+
{active ? `${z}x` : `${z}`}
|
|
27
|
+
</Text>
|
|
28
|
+
</TouchableOpacity>
|
|
29
|
+
);
|
|
30
|
+
})}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
row: {
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
gap: r(8),
|
|
39
|
+
padding: r(4),
|
|
40
|
+
borderRadius: r(999),
|
|
41
|
+
backgroundColor: DARK.black45,
|
|
42
|
+
},
|
|
43
|
+
chip: {
|
|
44
|
+
width: r(32),
|
|
45
|
+
height: r(32),
|
|
46
|
+
borderRadius: r(16),
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
},
|
|
50
|
+
chipActive: { backgroundColor: DARK.white95 },
|
|
51
|
+
txt: { color: DARK.white, fontSize: r(12), fontWeight: '500' },
|
|
52
|
+
txtActive: { color: DARK.orange, fontSize: r(11), fontWeight: '700' },
|
|
53
|
+
});
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ActionRow } from './ActionRow';
|
|
2
|
+
export { ModeSwitcherPill } from './ModeSwitcherPill';
|
|
3
|
+
export { Shutter } from './Shutter';
|
|
4
|
+
export { ZoomChips } from './ZoomChips';
|
|
5
|
+
export { RecordingTimer } from './RecordingTimer';
|
|
6
|
+
export { ThumbnailStack } from './ThumbnailStack';
|
|
7
|
+
export { FlipButton } from './FlipButton';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { render } from '@testing-library/react-native';
|
|
2
|
+
import { VolumeIcon } from './VolumeIcon';
|
|
3
|
+
|
|
4
|
+
it('renders without crashing (on/off)', () => {
|
|
5
|
+
expect(() => render(<VolumeIcon on size={20} color="#fff" />)).not.toThrow();
|
|
6
|
+
expect(() =>
|
|
7
|
+
render(<VolumeIcon on={false} size={20} color="#fff" />)
|
|
8
|
+
).not.toThrow();
|
|
9
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Svg, { Path, Line } from 'react-native-svg';
|
|
2
|
+
|
|
3
|
+
type Props = { on: boolean; size: number; color: string };
|
|
4
|
+
|
|
5
|
+
export function VolumeIcon({ on, size, color }: Props) {
|
|
6
|
+
return (
|
|
7
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
8
|
+
{/* 喇叭主体 */}
|
|
9
|
+
<Path
|
|
10
|
+
d="M4 9v6h4l5 4V5L8 9H4z"
|
|
11
|
+
fill={color}
|
|
12
|
+
stroke={color}
|
|
13
|
+
strokeWidth={1.6}
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
/>
|
|
16
|
+
{on ? (
|
|
17
|
+
// 声波两道
|
|
18
|
+
<Path
|
|
19
|
+
d="M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12"
|
|
20
|
+
stroke={color}
|
|
21
|
+
strokeWidth={1.6}
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
fill="none"
|
|
24
|
+
/>
|
|
25
|
+
) : (
|
|
26
|
+
// 关:斜杠
|
|
27
|
+
<Line
|
|
28
|
+
x1={16}
|
|
29
|
+
y1={8}
|
|
30
|
+
x2={21}
|
|
31
|
+
y2={16}
|
|
32
|
+
stroke={color}
|
|
33
|
+
strokeWidth={1.6}
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</Svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/camera/index.tsx
CHANGED
|
@@ -9,4 +9,14 @@ export {
|
|
|
9
9
|
capturePhotoToFile,
|
|
10
10
|
type CapturedPhotoRaw,
|
|
11
11
|
} from './capturePhotoHelper';
|
|
12
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
ActionRow,
|
|
14
|
+
ModeSwitcherPill,
|
|
15
|
+
Shutter,
|
|
16
|
+
ZoomChips,
|
|
17
|
+
RecordingTimer,
|
|
18
|
+
ThumbnailStack,
|
|
19
|
+
FlipButton,
|
|
20
|
+
} from './footer';
|
|
21
|
+
export { CaptureFlash } from './CaptureFlash';
|
|
22
|
+
export { SideRail, type FlashMode, type AspectRatio } from './setup';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
2
|
+
import { PreviewBottomBar } from './PreviewBottomBar';
|
|
3
|
+
|
|
4
|
+
it('confirm 变体: 重拍/保存', () => {
|
|
5
|
+
const onRetake = jest.fn();
|
|
6
|
+
const onSave = jest.fn();
|
|
7
|
+
const { getByTestId } = render(
|
|
8
|
+
<PreviewBottomBar
|
|
9
|
+
variant="confirm"
|
|
10
|
+
index={0}
|
|
11
|
+
total={1}
|
|
12
|
+
onRetake={onRetake}
|
|
13
|
+
onSave={onSave}
|
|
14
|
+
onBack={() => {}}
|
|
15
|
+
onDelete={() => {}}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
fireEvent.press(getByTestId('retake-btn'));
|
|
19
|
+
expect(onRetake).toHaveBeenCalled();
|
|
20
|
+
fireEvent.press(getByTestId('save-btn'));
|
|
21
|
+
expect(onSave).toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('gallery 变体: 第X/Y张 + 返回/删除', () => {
|
|
25
|
+
const onBack = jest.fn();
|
|
26
|
+
const onDelete = jest.fn();
|
|
27
|
+
const { getByTestId, getByText } = render(
|
|
28
|
+
<PreviewBottomBar
|
|
29
|
+
variant="gallery"
|
|
30
|
+
index={1}
|
|
31
|
+
total={3}
|
|
32
|
+
onRetake={() => {}}
|
|
33
|
+
onSave={() => {}}
|
|
34
|
+
onBack={onBack}
|
|
35
|
+
onDelete={onDelete}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
expect(getByText('第 2/3 张')).toBeTruthy();
|
|
39
|
+
fireEvent.press(getByTestId('back-btn'));
|
|
40
|
+
expect(onBack).toHaveBeenCalled();
|
|
41
|
+
fireEvent.press(getByTestId('delete-btn'));
|
|
42
|
+
expect(onDelete).toHaveBeenCalled();
|
|
43
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import { Button, r, useThemedStyles } from '@unif/react-native-design';
|
|
3
|
+
import type { ColorTokens } from '@unif/react-native-design';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
variant: 'confirm' | 'gallery';
|
|
7
|
+
index: number;
|
|
8
|
+
total: number;
|
|
9
|
+
onRetake: () => void;
|
|
10
|
+
onSave: () => void;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
onDelete: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function PreviewBottomBar({
|
|
16
|
+
variant,
|
|
17
|
+
index,
|
|
18
|
+
total,
|
|
19
|
+
onRetake,
|
|
20
|
+
onSave,
|
|
21
|
+
onBack,
|
|
22
|
+
onDelete,
|
|
23
|
+
}: Props) {
|
|
24
|
+
const styles = useThemedStyles(makeStyles);
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.root}>
|
|
27
|
+
{variant === 'gallery' && (
|
|
28
|
+
<Text style={styles.counter}>
|
|
29
|
+
第 {index + 1}/{total} 张
|
|
30
|
+
</Text>
|
|
31
|
+
)}
|
|
32
|
+
<View style={styles.btns}>
|
|
33
|
+
{variant === 'confirm' ? (
|
|
34
|
+
<>
|
|
35
|
+
<Button
|
|
36
|
+
variant="ghost"
|
|
37
|
+
label="重拍"
|
|
38
|
+
onPress={onRetake}
|
|
39
|
+
testID="retake-btn"
|
|
40
|
+
/>
|
|
41
|
+
<Button
|
|
42
|
+
variant="primary"
|
|
43
|
+
label="保存"
|
|
44
|
+
onPress={onSave}
|
|
45
|
+
testID="save-btn"
|
|
46
|
+
/>
|
|
47
|
+
</>
|
|
48
|
+
) : (
|
|
49
|
+
<>
|
|
50
|
+
<Button
|
|
51
|
+
variant="ghost"
|
|
52
|
+
label="返回"
|
|
53
|
+
onPress={onBack}
|
|
54
|
+
testID="back-btn"
|
|
55
|
+
/>
|
|
56
|
+
<Button
|
|
57
|
+
variant="danger"
|
|
58
|
+
label="删除"
|
|
59
|
+
onPress={onDelete}
|
|
60
|
+
testID="delete-btn"
|
|
61
|
+
/>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const makeStyles = (c: ColorTokens) =>
|
|
70
|
+
StyleSheet.create({
|
|
71
|
+
root: {
|
|
72
|
+
paddingHorizontal: r(16),
|
|
73
|
+
paddingBottom: r(26),
|
|
74
|
+
gap: r(12),
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
},
|
|
77
|
+
counter: { color: c.foreground, fontSize: r(15), fontWeight: '600' },
|
|
78
|
+
btns: { flexDirection: 'row', gap: r(12), justifyContent: 'center' },
|
|
79
|
+
});
|