@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,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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
+
}
|
package/src/components/index.tsx
CHANGED
package/src/hooks/index.ts
CHANGED