feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,201 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { Text, StyleSheet, Linking, View, Platform, useWindowDimensions } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ViewStyle } from 'react-native';
|
||||
import { AppState } from '../../utils/types';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import { themeStyle } from '../global-style';
|
||||
import fitRectIntoBounds from './utils/fitRectIntoBounds';
|
||||
import useBarcodeScanner from './utils/useBarcodeScanner';
|
||||
import ScannedBarcodes from './ScannedBarcodes';
|
||||
import { CameraRef } from './Camera/types';
|
||||
import Camera from './Camera/index';
|
||||
import { CameraViewProps } from './types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import useBackHandler from '../../utils/hooks/useBackHandler';
|
||||
|
||||
const logger = Logger.create('CameraView');
|
||||
|
||||
interface UseStyleProps {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
cameraRatio: string;
|
||||
}
|
||||
|
||||
const useStyles = ({ themeId, style, cameraRatio }: UseStyleProps) => {
|
||||
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
||||
const outputPositioning = useMemo((): ViewStyle => {
|
||||
const ratioMatch = cameraRatio?.match(/^(\d+):(\d+)$/);
|
||||
if (!ratioMatch) {
|
||||
return { left: 0, top: 0 };
|
||||
}
|
||||
const output = fitRectIntoBounds({
|
||||
width: Number(ratioMatch[2]),
|
||||
height: Number(ratioMatch[1]),
|
||||
}, {
|
||||
width: Math.min(screenWidth, screenHeight),
|
||||
height: Math.max(screenWidth, screenHeight),
|
||||
});
|
||||
|
||||
if (screenWidth > screenHeight) {
|
||||
const w = output.width;
|
||||
output.width = output.height;
|
||||
output.height = w;
|
||||
}
|
||||
|
||||
return {
|
||||
left: (screenWidth - output.width) / 2,
|
||||
top: (screenHeight - output.height) / 2,
|
||||
width: output.width,
|
||||
height: output.height,
|
||||
flexBasis: output.height,
|
||||
flexGrow: 0,
|
||||
alignContent: 'center',
|
||||
};
|
||||
}, [cameraRatio, screenWidth, screenHeight]);
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#000',
|
||||
...style,
|
||||
},
|
||||
camera: {
|
||||
position: 'relative',
|
||||
...outputPositioning,
|
||||
...style,
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
maxWidth: 600,
|
||||
padding: 28,
|
||||
borderRadius: 28,
|
||||
},
|
||||
});
|
||||
}, [themeId, style, outputPositioning]);
|
||||
};
|
||||
|
||||
const androidRatios = ['4:3', '16:9'];
|
||||
const iOSRatios: string[] = [];
|
||||
const useAvailableRatios = (): string[] => {
|
||||
return Platform.OS === 'android' ? androidRatios : iOSRatios;
|
||||
};
|
||||
|
||||
|
||||
const CameraViewComponent: React.FC<CameraViewProps> = props => {
|
||||
const styles = useStyles(props);
|
||||
const cameraRef = useRef<CameraRef|null>(null);
|
||||
const [cameraReady, setCameraReady] = useState(false);
|
||||
|
||||
useBackHandler(props.onCancel);
|
||||
|
||||
const onCameraReverse = useCallback(() => {
|
||||
const newDirection = props.cameraType === CameraDirection.Front ? CameraDirection.Back : CameraDirection.Front;
|
||||
Setting.setValue('camera.type', newDirection);
|
||||
}, [props.cameraType]);
|
||||
|
||||
const availableRatios = useAvailableRatios();
|
||||
const onNextCameraRatio = useCallback(async () => {
|
||||
const ratioIndex = Math.max(0, availableRatios.indexOf(props.cameraRatio));
|
||||
|
||||
Setting.setValue('camera.ratio', availableRatios[(ratioIndex + 1) % availableRatios.length]);
|
||||
}, [props.cameraRatio, availableRatios]);
|
||||
|
||||
const codeScanner = useBarcodeScanner();
|
||||
|
||||
const onCameraReady = useCallback(() => {
|
||||
logger.debug('Camera ready');
|
||||
setCameraReady(true);
|
||||
}, []);
|
||||
|
||||
const [takingPicture, setTakingPicture] = useState(false);
|
||||
const takingPictureRef = useRef(takingPicture);
|
||||
takingPictureRef.current = takingPicture;
|
||||
const onTakePicture = useCallback(async () => {
|
||||
if (takingPictureRef.current) return;
|
||||
setTakingPicture(true);
|
||||
try {
|
||||
const picture = await cameraRef.current.takePictureAsync();
|
||||
if (picture) {
|
||||
props.onPhoto(picture);
|
||||
}
|
||||
} finally {
|
||||
setTakingPicture(false);
|
||||
}
|
||||
}, [props.onPhoto]);
|
||||
|
||||
const [permissionRequestFailed, setPermissionRequestFailed] = useState(false);
|
||||
const onPermissionRequestFailure = useCallback(() => {
|
||||
setPermissionRequestFailed(true);
|
||||
}, []);
|
||||
const onHasPermission = useCallback(() => {
|
||||
setPermissionRequestFailed(false);
|
||||
}, []);
|
||||
|
||||
let overlay;
|
||||
if (permissionRequestFailed) {
|
||||
overlay = <View style={styles.errorContainer}>
|
||||
<Text>{_('Missing camera permission')}</Text>
|
||||
<LinkButton onPress={() => Linking.openSettings()}>{_('Open settings')}</LinkButton>
|
||||
{props.onCancel && <PrimaryButton onPress={props.onCancel}>{_('Go back')}</PrimaryButton>}
|
||||
</View>;
|
||||
} else {
|
||||
overlay = <>
|
||||
<ActionButtons
|
||||
themeId={props.themeId}
|
||||
onCameraReverse={onCameraReverse}
|
||||
cameraDirection={props.cameraType}
|
||||
|
||||
cameraRatio={props.cameraRatio}
|
||||
onSetCameraRatio={onNextCameraRatio}
|
||||
|
||||
onTakePicture={onTakePicture}
|
||||
takingPicture={takingPicture}
|
||||
onCancelPhoto={props.onCancel}
|
||||
|
||||
cameraReady={cameraReady}
|
||||
/>
|
||||
<ScannedBarcodes
|
||||
themeId={props.themeId}
|
||||
codeScanner={codeScanner}
|
||||
onInsertCode={props.onInsertBarcode}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
style={styles.camera}
|
||||
cameraType={props.cameraType}
|
||||
ratio={availableRatios.includes(props.cameraRatio) ? props.cameraRatio : undefined}
|
||||
onCameraReady={onCameraReady}
|
||||
codeScanner={codeScanner}
|
||||
onPermissionRequestFailure={onPermissionRequestFailure}
|
||||
onHasPermission={onHasPermission}
|
||||
/>
|
||||
{overlay}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
cameraRatio: state.settings['camera.ratio'],
|
||||
cameraType: state.settings['camera.type'],
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(CameraViewComponent);
|
||||
在新工单中引用
屏蔽一个用户