mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Mobile: Camera screen: Support scanning QR codes (#11245)
This commit is contained in:
parent
441021bb7e
commit
100f8a23f4
.eslintignore.gitignoreyarn.lock
packages
app-mobile
android
components
CameraView.tsx
CameraView
screens
testing
ios
jest.setup.jspackage.jsonweb
lib/models/settings
tools/cspell
@ -553,7 +553,16 @@ packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
@ -730,6 +739,7 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -530,7 +530,16 @@ packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
packages/app-mobile/components/CameraView/Camera/types.js
|
||||
packages/app-mobile/components/CameraView/CameraView.test.js
|
||||
packages/app-mobile/components/CameraView/CameraView.js
|
||||
packages/app-mobile/components/CameraView/ScannedBarcodes.js
|
||||
packages/app-mobile/components/CameraView/types.js
|
||||
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
|
||||
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
@ -707,6 +716,7 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/testing/TestProviderStack.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/index.web.js
|
||||
|
@ -84,9 +84,6 @@ android {
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
// https://github.com/react-native-community/react-native-camera/issues/2138
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
|
||||
// Needed to fix: The number of method references in a .dex file cannot exceed 64K
|
||||
multiDexEnabled true
|
||||
@ -122,12 +119,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// This removes proprietary bits to enable inclusion in F-Droid
|
||||
// https://gitlab.com/fdroid/rfp/-/issues/434#note_443458711
|
||||
implementation (project(':react-native-camera')){
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-vision'
|
||||
}
|
||||
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
package net.cozic.joplin
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
@ -18,5 +19,5 @@ class MainActivity : ReactActivity() {
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
package net.cozic.joplin
|
||||
import android.content.res.Configuration
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
|
||||
import android.app.Application
|
||||
import android.database.CursorWindow
|
||||
@ -18,7 +21,7 @@ import net.cozic.joplin.ssl.SslPackage
|
||||
import net.cozic.joplin.textinput.TextInputPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) {
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
@ -35,10 +38,10 @@ class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
})
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
|
||||
get() = ReactNativeHostWrapper.createReactHost(this.applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -59,5 +62,11 @@ class MainApplication : Application(), ReactApplication {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,11 @@ allprojects {
|
||||
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
|
||||
maven {
|
||||
// expo-camera bundles a custom com.google.android:cameraview
|
||||
url "$rootDir/../node_modules/expo-camera/android/maven"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,4 +3,6 @@ include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
|
||||
useExpoModules()
|
@ -1,247 +0,0 @@
|
||||
const { RNCamera } = require('react-native-camera');
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { View, TouchableOpacity, Text, Dimensions } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
class CameraView extends Component {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
const dimensions = Dimensions.get('window');
|
||||
|
||||
this.state = {
|
||||
snapping: false,
|
||||
ratios: [],
|
||||
screenWidth: dimensions.width,
|
||||
screenHeight: dimensions.height,
|
||||
};
|
||||
|
||||
this.back_onPress = this.back_onPress.bind(this);
|
||||
this.photo_onPress = this.photo_onPress.bind(this);
|
||||
this.reverse_onPress = this.reverse_onPress.bind(this);
|
||||
this.ratio_onPress = this.ratio_onPress.bind(this);
|
||||
this.onCameraReady = this.onCameraReady.bind(this);
|
||||
this.onLayout = this.onLayout.bind(this);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public onLayout(event: any) {
|
||||
this.setState({
|
||||
screenWidth: event.nativeEvent.layout.width,
|
||||
screenHeight: event.nativeEvent.layout.height,
|
||||
});
|
||||
}
|
||||
|
||||
private back_onPress() {
|
||||
if (this.props.onCancel) this.props.onCancel();
|
||||
}
|
||||
|
||||
private reverse_onPress() {
|
||||
if (this.props.cameraType === RNCamera.Constants.Type.back) {
|
||||
Setting.setValue('camera.type', RNCamera.Constants.Type.front);
|
||||
} else {
|
||||
Setting.setValue('camera.type', RNCamera.Constants.Type.back);
|
||||
}
|
||||
}
|
||||
|
||||
private ratio_onPress() {
|
||||
if (this.state.ratios.length <= 1) return;
|
||||
|
||||
let index = this.state.ratios.indexOf(this.props.cameraRatio);
|
||||
index++;
|
||||
if (index >= this.state.ratios.length) index = 0;
|
||||
Setting.setValue('camera.ratio', this.state.ratios[index]);
|
||||
}
|
||||
|
||||
private async photo_onPress() {
|
||||
if (!this.camera || !this.props.onPhoto) return;
|
||||
|
||||
this.setState({ snapping: true });
|
||||
|
||||
const result = await this.camera.takePictureAsync({
|
||||
quality: 0.8,
|
||||
exif: true,
|
||||
fixOrientation: true,
|
||||
});
|
||||
|
||||
this.setState({ snapping: false });
|
||||
|
||||
if (this.props.onPhoto) this.props.onPhoto(result);
|
||||
|
||||
}
|
||||
|
||||
public async onCameraReady() {
|
||||
if (this.supportsRatios()) {
|
||||
const ratios = await this.camera.getSupportedRatiosAsync();
|
||||
this.setState({ ratios: ratios });
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
public renderButton(onPress: Function, iconNameOrIcon: any, style: any) {
|
||||
let icon = null;
|
||||
|
||||
if (typeof iconNameOrIcon === 'string') {
|
||||
icon = (
|
||||
<Icon
|
||||
name={iconNameOrIcon}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
icon = iconNameOrIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={{ ...style }}>
|
||||
<View style={{ borderRadius: 32, width: 60, height: 60, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center', alignSelf: 'baseline' }}>
|
||||
{ icon }
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public fitRectIntoBounds(rect: any, bounds: any) {
|
||||
const rectRatio = rect.width / rect.height;
|
||||
const boundsRatio = bounds.width / bounds.height;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const newDimensions: any = {};
|
||||
|
||||
// Rect is more landscape than bounds - fit to width
|
||||
if (rectRatio > boundsRatio) {
|
||||
newDimensions.width = bounds.width;
|
||||
newDimensions.height = rect.height * (bounds.width / rect.width);
|
||||
} else { // Rect is more portrait than bounds - fit to height
|
||||
newDimensions.width = rect.width * (bounds.height / rect.height);
|
||||
newDimensions.height = bounds.height;
|
||||
}
|
||||
|
||||
return newDimensions;
|
||||
}
|
||||
|
||||
public cameraRect(ratio: string) {
|
||||
// To keep the calculations simpler, it's assumed that the phone is in
|
||||
// portrait orientation. Then at the end we swap the values if needed.
|
||||
const splitted = ratio.split(':');
|
||||
|
||||
const output = this.fitRectIntoBounds({
|
||||
width: Number(splitted[1]),
|
||||
height: Number(splitted[0]),
|
||||
}, {
|
||||
width: Math.min(this.state.screenWidth, this.state.screenHeight),
|
||||
height: Math.max(this.state.screenWidth, this.state.screenHeight),
|
||||
});
|
||||
|
||||
if (this.state.screenWidth > this.state.screenHeight) {
|
||||
const w = output.width;
|
||||
output.width = output.height;
|
||||
output.height = w;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public supportsRatios() {
|
||||
return shim.mobilePlatform() === 'android';
|
||||
}
|
||||
|
||||
public render() {
|
||||
const photoIcon = this.state.snapping ? 'checkmark' : 'camera';
|
||||
|
||||
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
|
||||
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
|
||||
|
||||
let cameraRatio = '4:3';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const cameraProps: any = {};
|
||||
|
||||
if (displayRatios) {
|
||||
cameraProps.ratio = this.props.cameraRatio;
|
||||
cameraRatio = this.props.cameraRatio;
|
||||
}
|
||||
|
||||
const cameraRect = this.cameraRect(cameraRatio);
|
||||
cameraRect.left = (this.state.screenWidth - cameraRect.width) / 2;
|
||||
cameraRect.top = (this.state.screenHeight - cameraRect.height) / 2;
|
||||
|
||||
return (
|
||||
<View style={{ ...this.props.style, position: 'relative' }} onLayout={this.onLayout}>
|
||||
<View style={{ position: 'absolute', backgroundColor: '#000000', width: '100%', height: '100%' }}/>
|
||||
<RNCamera
|
||||
style={({ position: 'absolute', ...cameraRect })}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
ref={(ref: any) => {
|
||||
this.camera = ref;
|
||||
}}
|
||||
type={this.props.cameraType}
|
||||
captureAudio={false}
|
||||
onCameraReady={this.onCameraReady}
|
||||
androidCameraPermissionOptions={{
|
||||
title: _('Permission to use camera'),
|
||||
message: _('Your permission to use your camera is required.'),
|
||||
buttonPositive: _('OK'),
|
||||
buttonNegative: _('Cancel'),
|
||||
}}
|
||||
|
||||
{ ...cameraProps }
|
||||
>
|
||||
<View style={{ flex: 1, justifyContent: 'space-between', flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1, justifyContent: 'flex-start' }}>
|
||||
<TouchableOpacity onPress={this.back_onPress}>
|
||||
<View style={{ marginLeft: 5, marginTop: 5, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={'arrow-back'}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'flex-end' }}>
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginBottom: 20 }}>
|
||||
{ reverseCameraButton }
|
||||
<TouchableOpacity onPress={this.photo_onPress} disabled={this.state.snapping}>
|
||||
<View style={{ flexDirection: 'row', borderRadius: 90, width: 90, height: 90, backgroundColor: '#ffffffaa', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={photoIcon}
|
||||
style={{
|
||||
fontSize: 60,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{ ratioButton }
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RNCamera>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mapStateToProps = (state: any) => {
|
||||
return {
|
||||
cameraRatio: state.settings['camera.ratio'],
|
||||
cameraType: state.settings['camera.type'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export default connect(mapStateToProps)(CameraView);
|
155
packages/app-mobile/components/CameraView/ActionButtons.tsx
Normal file
155
packages/app-mobile/components/CameraView/ActionButtons.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { View, StyleSheet, ViewStyle, Platform } from 'react-native';
|
||||
import IconButton from '../IconButton';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { ActivityIndicator } from 'react-native-paper';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
onCameraReverse: ()=> void;
|
||||
cameraDirection: CameraDirection;
|
||||
|
||||
onSetCameraRatio: ()=> void;
|
||||
cameraRatio: string;
|
||||
|
||||
onCancelPhoto: ()=> void;
|
||||
onTakePicture: ()=> void;
|
||||
takingPicture: boolean;
|
||||
|
||||
cameraReady: boolean;
|
||||
}
|
||||
|
||||
const useStyles = () => {
|
||||
return useMemo(() => {
|
||||
const buttonContainer: ViewStyle = {
|
||||
borderRadius: 32,
|
||||
minWidth: 60,
|
||||
minHeight: 60,
|
||||
borderColor: '#00000040',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: '#ffffff77',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-end',
|
||||
};
|
||||
const buttonRowContainer: ViewStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
left: 20,
|
||||
right: 20,
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
return StyleSheet.create({
|
||||
buttonRowContainerTop: {
|
||||
...buttonRowContainer,
|
||||
top: 20,
|
||||
},
|
||||
buttonRowContainerBottom: {
|
||||
...buttonRowContainer,
|
||||
bottom: 20,
|
||||
},
|
||||
buttonContainer,
|
||||
buttonContent: {
|
||||
color: 'black',
|
||||
fontSize: 40,
|
||||
},
|
||||
buttonPlaceHolder: {
|
||||
width: 60,
|
||||
},
|
||||
|
||||
qrCodeButtonDimmed: {
|
||||
...buttonContainer,
|
||||
backgroundColor: '#ffffff44',
|
||||
borderWidth: 0,
|
||||
},
|
||||
|
||||
takePhotoButtonContainer: {
|
||||
...buttonContainer,
|
||||
minWidth: 80,
|
||||
minHeight: 80,
|
||||
borderRadius: 80,
|
||||
},
|
||||
takePhotoButtonContent: {
|
||||
color: 'black',
|
||||
fontSize: 60,
|
||||
},
|
||||
|
||||
ratioButtonContainer: {
|
||||
...buttonContainer,
|
||||
aspectRatio: undefined,
|
||||
padding: 12,
|
||||
},
|
||||
ratioButtonContent: {
|
||||
fontSize: 20,
|
||||
color: 'black',
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const ActionButtons: React.FC<Props> = props => {
|
||||
const styles = useStyles();
|
||||
const reverseButton = (
|
||||
<IconButton
|
||||
iconName='ionicon camera-reverse'
|
||||
onPress={props.onCameraReverse}
|
||||
description={props.cameraDirection === CameraDirection.Front ? _('Switch to back-facing camera') : _('Switch to front-facing camera')}
|
||||
themeId={props.themeId}
|
||||
iconStyle={styles.buttonContent}
|
||||
containerStyle={styles.buttonContainer}
|
||||
/>
|
||||
);
|
||||
const takePhotoButton = (
|
||||
<IconButton
|
||||
iconName={props.takingPicture ? 'ionicon checkmark' : 'ionicon camera'}
|
||||
onPress={props.onTakePicture}
|
||||
description={props.takingPicture ? _('Processing photo...') : _('Take picture')}
|
||||
themeId={props.themeId}
|
||||
iconStyle={styles.takePhotoButtonContent}
|
||||
containerStyle={styles.takePhotoButtonContainer}
|
||||
/>
|
||||
);
|
||||
const ratioButton = (
|
||||
<IconButton
|
||||
themeId={props.themeId}
|
||||
iconName={`text ${props.cameraRatio}`}
|
||||
onPress={props.onSetCameraRatio}
|
||||
iconStyle={styles.ratioButtonContent}
|
||||
containerStyle={styles.ratioButtonContainer}
|
||||
description={_('Change ratio')}
|
||||
/>
|
||||
);
|
||||
|
||||
const cameraActions = (
|
||||
<View style={styles.buttonRowContainerBottom}>
|
||||
{reverseButton}
|
||||
{takePhotoButton}
|
||||
{
|
||||
// Changing ratio is only supported on Android:
|
||||
Platform.OS === 'android' ? ratioButton : <View style={styles.buttonPlaceHolder}/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
return <>
|
||||
<View style={styles.buttonRowContainerTop}>
|
||||
<IconButton
|
||||
themeId={props.themeId}
|
||||
iconName='ionicon arrow-back'
|
||||
containerStyle={styles.buttonContainer}
|
||||
iconStyle={styles.buttonContent}
|
||||
onPress={props.onCancelPhoto}
|
||||
description={_('Back')}
|
||||
/>
|
||||
</View>
|
||||
{props.cameraReady ? cameraActions : <ActivityIndicator/>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef, forwardRef, useCallback, useImperativeHandle } from 'react';
|
||||
import { CameraRef, Props } from './types';
|
||||
import { PrimaryButton } from '../../buttons';
|
||||
import { Surface, Text } from 'react-native-paper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
takePictureAsync: async () => {
|
||||
const path = `${shim.fsDriver().getCacheDirectoryPath()}/test-photo.svg`;
|
||||
await shim.fsDriver().writeFile(
|
||||
path,
|
||||
`<svg viewBox="0 0 232 78" width="232" height="78" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
|
||||
<text style="font-family: serif; font-size: 104px; fill: rgb(128, 51, 128);">Test!</text>
|
||||
</svg>`,
|
||||
'utf8',
|
||||
);
|
||||
return { uri: path, type: 'image/svg+xml' };
|
||||
},
|
||||
}), []);
|
||||
|
||||
const onCodeChange = useCallback((data: string) => {
|
||||
props.codeScanner.onBarcodeScanned?.({
|
||||
data,
|
||||
type: 'qr',
|
||||
});
|
||||
}, [props.codeScanner]);
|
||||
|
||||
return <Surface elevation={1}>
|
||||
<Text>Camera mock</Text>
|
||||
<PrimaryButton onPress={props.onPermissionRequestFailure}>Reject permission</PrimaryButton>
|
||||
<PrimaryButton onPress={props.onHasPermission}>Accept permission</PrimaryButton>
|
||||
<PrimaryButton onPress={props.onCameraReady}>On camera ready</PrimaryButton>
|
||||
<TextInput placeholder='QR code data' onChangeText={onCodeChange}/>
|
||||
</Surface>;
|
||||
};
|
||||
|
||||
export default forwardRef(Camera);
|
57
packages/app-mobile/components/CameraView/Camera/index.tsx
Normal file
57
packages/app-mobile/components/CameraView/Camera/index.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { BarcodeSettings, CameraRatio, CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import { ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { CameraRef, Props } from './types';
|
||||
|
||||
const barcodeScannerSettings: BarcodeSettings = {
|
||||
// Rocketbook pages use both QR and datamatrix
|
||||
barcodeTypes: ['qr', 'datamatrix'],
|
||||
};
|
||||
|
||||
const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
takePictureAsync: async () => {
|
||||
const result = await cameraRef.current.takePictureAsync();
|
||||
return {
|
||||
uri: result.uri,
|
||||
type: 'image/jpg',
|
||||
};
|
||||
},
|
||||
}), []);
|
||||
|
||||
const [hasPermission, requestPermission] = useCameraPermissions();
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
if (!hasPermission?.granted) {
|
||||
await requestPermission();
|
||||
}
|
||||
} finally {
|
||||
if (!!hasPermission && !hasPermission.canAskAgain) {
|
||||
props.onPermissionRequestFailure();
|
||||
}
|
||||
}
|
||||
}, [hasPermission, requestPermission, props.onPermissionRequestFailure]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPermission?.granted) {
|
||||
props.onHasPermission();
|
||||
}
|
||||
}, [hasPermission, props.onHasPermission]);
|
||||
|
||||
return <CameraView
|
||||
ref={cameraRef}
|
||||
style={props.style}
|
||||
facing={props.cameraType === CameraDirection.Front ? 'front' : 'back'}
|
||||
ratio={props.ratio as CameraRatio}
|
||||
onCameraReady={props.onCameraReady}
|
||||
animateShutter={false}
|
||||
barcodeScannerSettings={barcodeScannerSettings}
|
||||
onBarcodeScanned={props.codeScanner.onBarcodeScanned}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default forwardRef(Camera);
|
19
packages/app-mobile/components/CameraView/Camera/types.ts
Normal file
19
packages/app-mobile/components/CameraView/Camera/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { ViewStyle } from 'react-native';
|
||||
import { BarcodeScanner } from '../utils/useBarcodeScanner';
|
||||
import { CameraResult } from '../types';
|
||||
|
||||
export interface Props {
|
||||
style: ViewStyle;
|
||||
cameraType: CameraDirection;
|
||||
ratio: string|undefined;
|
||||
codeScanner: BarcodeScanner;
|
||||
|
||||
onCameraReady: ()=> void;
|
||||
onPermissionRequestFailure: ()=> void;
|
||||
onHasPermission: ()=> void;
|
||||
}
|
||||
|
||||
export interface CameraRef {
|
||||
takePictureAsync(): Promise<CameraResult>;
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import * as React from 'react';
|
||||
import CameraView from './CameraView';
|
||||
import { CameraResult } from './types';
|
||||
import { fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
|
||||
interface WrapperProps {
|
||||
onPhoto?: (result: CameraResult)=> void;
|
||||
onInsertBarcode?: (text: string)=> void;
|
||||
onCancel?: ()=> void;
|
||||
}
|
||||
|
||||
const emptyFn = ()=>{};
|
||||
const store = createMockReduxStore();
|
||||
const CameraViewWrapper: React.FC<WrapperProps> = props => {
|
||||
return <TestProviderStack store={store}>
|
||||
<CameraView
|
||||
style={{}}
|
||||
onPhoto={props.onPhoto ?? emptyFn}
|
||||
onInsertBarcode={props.onInsertBarcode ?? emptyFn}
|
||||
onCancel={props.onCancel ?? emptyFn}
|
||||
/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const rejectCameraPermission = () => {
|
||||
const rejectPermissionButton = screen.getByRole('button', { name: 'Reject permission' });
|
||||
fireEvent.press(rejectPermissionButton);
|
||||
};
|
||||
|
||||
const acceptCameraPermission = () => {
|
||||
const acceptPermissionButton = screen.getByRole('button', { name: 'Accept permission' });
|
||||
fireEvent.press(acceptPermissionButton);
|
||||
};
|
||||
|
||||
const startCamera = () => {
|
||||
const startCameraButton = screen.getByRole('button', { name: 'On camera ready' });
|
||||
fireEvent.press(startCameraButton);
|
||||
};
|
||||
|
||||
const setQrCodeData = (data: string) => {
|
||||
const qrCodeDataInput = screen.getByPlaceholderText('QR code data');
|
||||
fireEvent.changeText(qrCodeDataInput, data);
|
||||
};
|
||||
|
||||
describe('CameraView', () => {
|
||||
test('should hide permissions error if camera permission is granted', async () => {
|
||||
const view = render(<CameraViewWrapper/>);
|
||||
|
||||
const queryPermissionsError = () => screen.queryByText('Missing camera permission');
|
||||
|
||||
expect(queryPermissionsError()).toBeNull();
|
||||
rejectCameraPermission();
|
||||
expect(queryPermissionsError()).toBeVisible();
|
||||
acceptCameraPermission();
|
||||
expect(queryPermissionsError()).toBeNull();
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Back' })).toBeVisible();
|
||||
startCamera();
|
||||
expect(await screen.findByRole('button', { name: 'Take picture' })).toBeVisible();
|
||||
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
test('should allow inserting QR code text', async () => {
|
||||
const onInsertBarcode = jest.fn();
|
||||
const view = render(<CameraViewWrapper onInsertBarcode={onInsertBarcode}/>);
|
||||
acceptCameraPermission();
|
||||
startCamera();
|
||||
|
||||
const qrCodeData = 'Test!';
|
||||
setQrCodeData(qrCodeData);
|
||||
|
||||
const qrCodeButton = await screen.findByRole('button', { name: 'QR Code' });
|
||||
expect(qrCodeButton).toBeVisible();
|
||||
fireEvent.press(qrCodeButton);
|
||||
|
||||
const addToNoteButton = await screen.findByRole('button', { name: 'Add to note' });
|
||||
fireEvent.press(addToNoteButton);
|
||||
|
||||
expect(onInsertBarcode).toHaveBeenCalledTimes(1);
|
||||
expect(onInsertBarcode).toHaveBeenCalledWith(qrCodeData);
|
||||
|
||||
view.unmount();
|
||||
});
|
||||
});
|
216
packages/app-mobile/components/CameraView/CameraView.tsx
Normal file
216
packages/app-mobile/components/CameraView/CameraView.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, 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 BackButtonService from '../../services/BackButtonService';
|
||||
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';
|
||||
import { CameraResult } from './types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
cameraType: CameraDirection;
|
||||
cameraRatio: string;
|
||||
onPhoto: (data: CameraResult)=> void;
|
||||
onCancel: ()=> void;
|
||||
onInsertBarcode: (barcodeText: string)=> void;
|
||||
}
|
||||
|
||||
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 = ['1:1', '4:3', '16:9'];
|
||||
const iOSRatios: string[] = [];
|
||||
const useAvailableRatios = (): string[] => {
|
||||
return Platform.OS === 'android' ? androidRatios : iOSRatios;
|
||||
};
|
||||
|
||||
|
||||
const CameraViewComponent: React.FC<Props> = props => {
|
||||
const styles = useStyles(props);
|
||||
const cameraRef = useRef<CameraRef|null>(null);
|
||||
const [cameraReady, setCameraReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
props.onCancel();
|
||||
return true;
|
||||
};
|
||||
BackButtonService.addHandler(handler);
|
||||
return () => {
|
||||
BackButtonService.removeHandler(handler);
|
||||
};
|
||||
}, [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(() => {
|
||||
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>
|
||||
<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);
|
103
packages/app-mobile/components/CameraView/ScannedBarcodes.tsx
Normal file
103
packages/app-mobile/components/CameraView/ScannedBarcodes.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Platform, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { BarcodeScanner } from './utils/useBarcodeScanner';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import { Chip, Text } from 'react-native-paper';
|
||||
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
codeScanner: BarcodeScanner;
|
||||
onInsertCode: (codeText: string)=> void;
|
||||
}
|
||||
|
||||
const useStyles = () => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 10,
|
||||
},
|
||||
spacer: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
scannedCode: {
|
||||
padding: 20,
|
||||
fontFamily: Platform.select({
|
||||
android: 'monospace',
|
||||
ios: 'Courier New',
|
||||
default: undefined,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const ScannedBarcodes: React.FC<Props> = props => {
|
||||
const styles = useStyles();
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
|
||||
const [dismissedAtTime, setDismissedAtTime] = useState(0);
|
||||
const onHideCodeNotification = useCallback(() => {
|
||||
setDismissedAtTime(performance.now());
|
||||
}, []);
|
||||
|
||||
const onShowDialog = useCallback(() => {
|
||||
setDialogVisible(true);
|
||||
}, []);
|
||||
const onHideDialog = useCallback(() => {
|
||||
setDialogVisible(false);
|
||||
onHideCodeNotification();
|
||||
}, [onHideCodeNotification]);
|
||||
|
||||
const codeScanner = props.codeScanner;
|
||||
const scannedText = codeScanner.lastScan?.text;
|
||||
|
||||
const isLink = useMemo(() => {
|
||||
return scannedText && isCallbackUrl(scannedText);
|
||||
}, [scannedText]);
|
||||
const onFollowLink = useCallback(() => {
|
||||
setDialogVisible(false);
|
||||
const data = parseCallbackUrl(scannedText);
|
||||
if (data && data.params.id) {
|
||||
void CommandService.instance().execute('openItem', `:/${data.params.id}`);
|
||||
}
|
||||
}, [scannedText]);
|
||||
const onInsertText = useCallback(() => {
|
||||
setDialogVisible(false);
|
||||
props.onInsertCode(scannedText);
|
||||
}, [scannedText, props.onInsertCode]);
|
||||
|
||||
const codeChipHidden = !scannedText || dialogVisible || codeScanner.lastScan.timestamp < dismissedAtTime;
|
||||
const dialogOpenButton = <Chip icon='qrcode' onPress={onShowDialog} onClose={onHideCodeNotification}>
|
||||
{_('QR Code')}
|
||||
</Chip>;
|
||||
return <View style={styles.container}>
|
||||
{codeChipHidden ? null : dialogOpenButton}
|
||||
<DismissibleDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
themeId={props.themeId}
|
||||
size={DialogSize.Small}
|
||||
>
|
||||
<ScrollView>
|
||||
<Text variant='titleMedium' role='heading'>{_('Scanned code')}</Text>
|
||||
<Text
|
||||
style={styles.scannedCode}
|
||||
variant='labelLarge'
|
||||
selectable={true}
|
||||
>{scannedText}</Text>
|
||||
</ScrollView>
|
||||
<View style={styles.spacer}/>
|
||||
{isLink ? <LinkButton onPress={onFollowLink}>{_('Follow link')}</LinkButton> : null}
|
||||
<PrimaryButton onPress={onInsertText}>{_('Add to note')}</PrimaryButton>
|
||||
</DismissibleDialog>
|
||||
</View>;
|
||||
};
|
||||
|
||||
export default ScannedBarcodes;
|
5
packages/app-mobile/components/CameraView/types.ts
Normal file
5
packages/app-mobile/components/CameraView/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
export interface CameraResult {
|
||||
uri: string;
|
||||
type: string;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
export interface Rect {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const fitRectIntoBounds = (rect: Rect, bounds: Rect) => {
|
||||
const rectRatio = rect.width / rect.height;
|
||||
const boundsRatio = bounds.width / bounds.height;
|
||||
|
||||
const newDimensions: Rect = { width: 0, height: 0 };
|
||||
|
||||
// Rect is more landscape than bounds - fit to width
|
||||
if (rectRatio > boundsRatio) {
|
||||
newDimensions.width = bounds.width;
|
||||
newDimensions.height = rect.height * (bounds.width / rect.width);
|
||||
} else { // Rect is more portrait than bounds - fit to height
|
||||
newDimensions.width = rect.width * (bounds.height / rect.height);
|
||||
newDimensions.height = bounds.height;
|
||||
}
|
||||
|
||||
return newDimensions;
|
||||
};
|
||||
|
||||
export default fitRectIntoBounds;
|
@ -0,0 +1,42 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
|
||||
interface ScannedData {
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface BarcodeScanningResult {
|
||||
type: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface BarcodeScanner {
|
||||
enabled: boolean;
|
||||
onToggleEnabled: ()=> void;
|
||||
onBarcodeScanned: (scan: BarcodeScanningResult)=> void;
|
||||
lastScan: ScannedData|null;
|
||||
}
|
||||
|
||||
|
||||
const useBarcodeScanner = (): BarcodeScanner => {
|
||||
const [lastScan, setLastScan] = useState<ScannedData|null>(null);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
return useMemo(() => {
|
||||
return {
|
||||
enabled,
|
||||
onToggleEnabled: () => {
|
||||
setEnabled(enabled => !enabled);
|
||||
},
|
||||
onBarcodeScanned: enabled ? (scanningResult: BarcodeScanningResult) => {
|
||||
setLastScan({
|
||||
text: scanningResult.data ?? 'null',
|
||||
timestamp: performance.now(),
|
||||
});
|
||||
} : null,
|
||||
lastScan,
|
||||
};
|
||||
}, [lastScan, enabled]);
|
||||
};
|
||||
|
||||
export default useBarcodeScanner;
|
@ -3,10 +3,8 @@ import * as React from 'react';
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import NoteScreen from './Note';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
|
||||
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@ -14,7 +12,6 @@ import { AppState } from '../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import initializeCommandService from '../../utils/initializeCommandService';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
@ -29,6 +26,7 @@ import getWebViewWindowById from '../../utils/testing/getWebViewWindowById';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
|
||||
interface WrapperProps {
|
||||
}
|
||||
@ -36,13 +34,9 @@ interface WrapperProps {
|
||||
let store: Store<AppState>;
|
||||
|
||||
const WrappedNoteScreen: React.FC<WrapperProps> = _props => {
|
||||
return <MenuProvider>
|
||||
<PaperProvider>
|
||||
<Provider store={store}>
|
||||
<NoteScreen />
|
||||
</Provider>
|
||||
</PaperProvider>
|
||||
</MenuProvider>;
|
||||
return <TestProviderStack store={store}>
|
||||
<NoteScreen />
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const getNoteViewerDom = async () => {
|
||||
|
@ -38,7 +38,7 @@ import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib
|
||||
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../utils/ShareExtension.js';
|
||||
import CameraView from '../CameraView';
|
||||
import CameraView from '../CameraView/CameraView';
|
||||
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ImageEditor from '../NoteEditor/ImageEditor/ImageEditor';
|
||||
@ -63,6 +63,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler';
|
||||
import getImageDimensions from '../../utils/image/getImageDimensions';
|
||||
import resizeImage from '../../utils/image/resizeImage';
|
||||
import { CameraResult } from '../CameraView/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@ -680,6 +681,39 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
return await saveOriginalImage();
|
||||
}
|
||||
|
||||
private async insertText(text: string) {
|
||||
const newNote = { ...this.state.note };
|
||||
|
||||
if (this.state.mode === 'edit') {
|
||||
let newText = '';
|
||||
|
||||
if (this.selection) {
|
||||
newText = `\n${text}\n`;
|
||||
const prefix = newNote.body.substring(0, this.selection.start);
|
||||
const suffix = newNote.body.substring(this.selection.end);
|
||||
newNote.body = `${prefix}${newText}${suffix}`;
|
||||
} else {
|
||||
newText = `\n${text}`;
|
||||
newNote.body = `${newNote.body}\n${newText}`;
|
||||
}
|
||||
|
||||
if (this.useEditorBeta()) {
|
||||
// The beta editor needs to be explicitly informed of changes
|
||||
// to the note's body
|
||||
if (this.editorRef.current) {
|
||||
this.editorRef.current.insertText(newText);
|
||||
} else {
|
||||
logger.info(`Tried to insert text ${text} to the note when the editor is not visible -- updating the note body instead.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newNote.body += `\n${text}`;
|
||||
}
|
||||
|
||||
this.setState({ note: newNote });
|
||||
return newNote;
|
||||
}
|
||||
|
||||
public async attachFile(pickerResponse: Asset, fileType: string): Promise<ResourceEntity|null> {
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
@ -753,36 +787,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
resource = await Resource.save(resource, { isNew: true });
|
||||
|
||||
const resourceTag = Resource.markupTag(resource);
|
||||
|
||||
const newNote = { ...this.state.note };
|
||||
|
||||
if (this.state.mode === 'edit') {
|
||||
let newText = '';
|
||||
|
||||
if (this.selection) {
|
||||
newText = `\n${resourceTag}\n`;
|
||||
const prefix = newNote.body.substring(0, this.selection.start);
|
||||
const suffix = newNote.body.substring(this.selection.end);
|
||||
newNote.body = `${prefix}${newText}${suffix}`;
|
||||
} else {
|
||||
newText = `\n${resourceTag}`;
|
||||
newNote.body = `${newNote.body}\n${newText}`;
|
||||
}
|
||||
|
||||
if (this.useEditorBeta()) {
|
||||
// The beta editor needs to be explicitly informed of changes
|
||||
// to the note's body
|
||||
if (this.editorRef.current) {
|
||||
this.editorRef.current.insertText(newText);
|
||||
} else {
|
||||
logger.info(`Tried to attach resource ${resource.id} to the note when the editor is not visible -- updating the note body instead.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
}
|
||||
|
||||
this.setState({ note: newNote });
|
||||
const newNote = await this.insertText(resourceTag);
|
||||
|
||||
void this.refreshResource(resource, newNote.body);
|
||||
|
||||
@ -821,19 +826,20 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private cameraView_onPhoto(data: any) {
|
||||
private cameraView_onPhoto(data: CameraResult) {
|
||||
void this.attachFile(
|
||||
{
|
||||
uri: data.uri,
|
||||
type: 'image/jpg',
|
||||
},
|
||||
data,
|
||||
'image',
|
||||
);
|
||||
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
private cameraView_onInsertBarcode = (data: string) => {
|
||||
this.setState({ showCamera: false });
|
||||
void this.insertText(data);
|
||||
};
|
||||
|
||||
private cameraView_onCancel() {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
@ -1440,7 +1446,12 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
const isTodo = !!Number(note.is_todo);
|
||||
|
||||
if (this.state.showCamera) {
|
||||
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
|
||||
return <CameraView
|
||||
style={{ flex: 1 }}
|
||||
onPhoto={this.cameraView_onPhoto}
|
||||
onInsertBarcode={this.cameraView_onInsertBarcode}
|
||||
onCancel={this.cameraView_onCancel}
|
||||
/>;
|
||||
} else if (this.state.showImageEditor) {
|
||||
return <ImageEditor
|
||||
resourceFilename={this.state.imageEditorResourceFilepath}
|
||||
|
23
packages/app-mobile/components/testing/TestProviderStack.tsx
Normal file
23
packages/app-mobile/components/testing/TestProviderStack.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../../utils/types';
|
||||
|
||||
interface Props {
|
||||
store: Store<AppState>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TestProviderStack: React.FC<Props> = props => {
|
||||
return <Provider store={props.store}>
|
||||
<MenuProvider>
|
||||
<PaperProvider>
|
||||
{props.children}
|
||||
</PaperProvider>
|
||||
</MenuProvider>
|
||||
</Provider>;
|
||||
};
|
||||
|
||||
export default TestProviderStack;
|
@ -1,3 +1,4 @@
|
||||
#import <Expo/Expo.h>
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
@ -11,6 +11,7 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
46E31F54C547C341F605BB66 /* libPods-Joplin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */; };
|
||||
4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */; };
|
||||
4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; };
|
||||
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
@ -79,6 +80,7 @@
|
||||
AE82E4B02599FA3A0013551B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B61798F36B3BC123BF8EA4D9 /* libPods-Joplin-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
|
||||
F69B873C692CE22F1C4C9264 /* libPods-Joplin-JoplinTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin-JoplinTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -153,6 +155,14 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
336B5BD29D5BD1737E707672 /* Joplin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = Joplin;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -171,6 +181,7 @@
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
9CDB1D9DB6483D893504BFCB /* Pods */,
|
||||
E67B064D6F5F7E8B1080FDB0 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
@ -215,6 +226,14 @@
|
||||
path = ShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E67B064D6F5F7E8B1080FDB0 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
336B5BD29D5BD1737E707672 /* Joplin */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -223,6 +242,7 @@
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Joplin" */;
|
||||
buildPhases = (
|
||||
335ACF4DE85695BEBB18D8A3 /* [CP] Check Pods Manifest.lock */,
|
||||
EB61CD887618E406C80EBC43 /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
@ -404,6 +424,9 @@
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||
@ -428,6 +451,9 @@
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||
@ -455,6 +481,25 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
EB61CD887618E406C80EBC43 /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Joplin/expo-configure-project.sh\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -465,6 +510,7 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
4D122473270878D700DE23E8 /* wtf.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -519,6 +565,7 @@
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin;
|
||||
PRODUCT_NAME = Joplin;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Joplin-Bridging-Header.h";
|
||||
@ -549,6 +596,7 @@
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin;
|
||||
PRODUCT_NAME = Joplin;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Joplin-Bridging-Header.h";
|
||||
@ -743,6 +791,7 @@
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@ -780,6 +829,7 @@
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -1,7 +1,8 @@
|
||||
#import <RCTAppDelegate.h>
|
||||
#import <Expo/Expo.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <UserNotifications/UNUserNotificationCenter.h>
|
||||
|
||||
@interface AppDelegate : RCTAppDelegate
|
||||
@interface AppDelegate : EXAppDelegateWrapper
|
||||
|
||||
@end
|
||||
|
@ -1,3 +1,4 @@
|
||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
@ -23,6 +24,14 @@ if linkage != nil
|
||||
end
|
||||
|
||||
target 'Joplin' do
|
||||
use_expo_modules!
|
||||
post_integrate do |installer|
|
||||
begin
|
||||
expo_patch_react_imports!(installer)
|
||||
rescue => e
|
||||
Pod::UI.warn e
|
||||
end
|
||||
end
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(
|
||||
|
@ -1,6 +1,43 @@
|
||||
PODS:
|
||||
- boost (1.83.0)
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXConstants (16.0.2):
|
||||
- ExpoModulesCore
|
||||
- Expo (51.0.0):
|
||||
- ExpoModulesCore
|
||||
- ExpoAsset (10.0.10):
|
||||
- ExpoModulesCore
|
||||
- ExpoCamera (15.0.16):
|
||||
- ExpoModulesCore
|
||||
- ZXingObjC/OneD
|
||||
- ZXingObjC/PDF417
|
||||
- ExpoFileSystem (17.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoFont (12.0.10):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (1.12.9):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.01.01.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Codegen
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsinspector
|
||||
- React-NativeModulesApple
|
||||
- React-RCTAppDelegate
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- FBLazyVector (0.74.1)
|
||||
- fmt (9.1.0)
|
||||
- glog (0.3.5)
|
||||
@ -941,14 +978,6 @@ PODS:
|
||||
- React-debug
|
||||
- react-native-alarm-notification (3.1.0):
|
||||
- React
|
||||
- react-native-camera (4.2.1):
|
||||
- React-Core
|
||||
- react-native-camera/RCT (= 4.2.1)
|
||||
- react-native-camera/RN (= 4.2.1)
|
||||
- react-native-camera/RCT (4.2.1):
|
||||
- React-Core
|
||||
- react-native-camera/RN (4.2.1):
|
||||
- React-Core
|
||||
- react-native-document-picker (9.3.0):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
@ -1005,7 +1034,7 @@ PODS:
|
||||
- React
|
||||
- react-native-saf-x (3.1.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.10.7):
|
||||
- react-native-safe-area-context (4.10.8):
|
||||
- React-Core
|
||||
- react-native-slider (4.4.4):
|
||||
- DoubleConversion
|
||||
@ -1032,7 +1061,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.10.4):
|
||||
- react-native-webview (13.10.5):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@ -1288,7 +1317,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (8.0.1):
|
||||
- RNDateTimePicker (8.1.1):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.14.0):
|
||||
- React-Core
|
||||
@ -1337,10 +1366,22 @@ PODS:
|
||||
- SocketRocket (0.7.0)
|
||||
- SSZipArchive (2.4.3)
|
||||
- Yoga (0.0.0)
|
||||
- ZXingObjC/Core (3.6.9)
|
||||
- ZXingObjC/OneD (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
- ZXingObjC/PDF417 (3.6.9):
|
||||
- ZXingObjC/Core
|
||||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||
- Expo (from `../node_modules/expo`)
|
||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||
- ExpoCamera (from `../node_modules/expo-camera/ios`)
|
||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
@ -1374,7 +1415,6 @@ DEPENDENCIES:
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||
- "react-native-alarm-notification (from `../node_modules/@joplin/react-native-alarm-notification`)"
|
||||
- react-native-camera (from `../node_modules/react-native-camera`)
|
||||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
||||
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
|
||||
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
|
||||
@ -1432,12 +1472,27 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- ZXingObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
boost:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXConstants:
|
||||
:path: "../node_modules/expo-constants/ios"
|
||||
Expo:
|
||||
:path: "../node_modules/expo"
|
||||
ExpoAsset:
|
||||
:path: "../node_modules/expo-asset/ios"
|
||||
ExpoCamera:
|
||||
:path: "../node_modules/expo-camera/ios"
|
||||
ExpoFileSystem:
|
||||
:path: "../node_modules/expo-file-system/ios"
|
||||
ExpoFont:
|
||||
:path: "../node_modules/expo-font/ios"
|
||||
ExpoModulesCore:
|
||||
:path: "../node_modules/expo-modules-core"
|
||||
FBLazyVector:
|
||||
:path: "../node_modules/react-native/Libraries/FBLazyVector"
|
||||
fmt:
|
||||
@ -1501,8 +1556,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
react-native-alarm-notification:
|
||||
:path: "../node_modules/@joplin/react-native-alarm-notification"
|
||||
react-native-camera:
|
||||
:path: "../node_modules/react-native-camera"
|
||||
react-native-document-picker:
|
||||
:path: "../node_modules/react-native-document-picker"
|
||||
react-native-fingerprint-scanner:
|
||||
@ -1611,6 +1664,13 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXConstants: 409690fbfd5afea964e5e9d6c4eb2c2b59222c59
|
||||
Expo: 17eb02cbf2ac0db49a6bc0c80a6b635a900d4f60
|
||||
ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875
|
||||
ExpoCamera: 929be541d1c1319fcf32f9f5d9df8b97804346b5
|
||||
ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51
|
||||
ExpoFont: 00756e6c796d8f7ee8d211e29c8b619e75cbf238
|
||||
ExpoModulesCore: 070bbb7162641709919cbf50230f0b535b8190b1
|
||||
FBLazyVector: 898d14d17bf19e2435cafd9ea2a1033efe445709
|
||||
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
|
||||
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
|
||||
@ -1642,7 +1702,6 @@ SPEC CHECKSUMS:
|
||||
React-logger: 7e7403a2b14c97f847d90763af76b84b152b6fce
|
||||
React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33
|
||||
react-native-alarm-notification: 43183613222c563c071f2c726624f9f6f06e605d
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 5b97e24a7f1a1e4a50a72c540a043f32d29a70a2
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: fe0562c94eb0b6334f266aea717448dfd9b08cd0
|
||||
@ -1652,11 +1711,11 @@ SPEC CHECKSUMS:
|
||||
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: 7dfb7e614512c82dba2dea3401509e1c44f3d1f9
|
||||
react-native-safe-area-context: 422017db8bcabbada9ad607d010996c56713234c
|
||||
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
|
||||
react-native-slider: 03f213d3ffbf919b16298c7896c1b60101d8e137
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
react-native-webview: 596fb33d67a3cde5a74bf1f6b4c28d3543477fdd
|
||||
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
|
||||
React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec
|
||||
React-NativeModulesApple: df46ff3e3de5b842b30b4ca8a6caae6d7c8ab09f
|
||||
React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a
|
||||
@ -1683,7 +1742,7 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: b6a9b35a785ecbe12b4e7d6de5439d0aa4614146
|
||||
RNDateTimePicker: 430174392d275f0ffefb627d04f0f8677f667fed
|
||||
RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba
|
||||
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
@ -1697,7 +1756,8 @@ SPEC CHECKSUMS:
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
Yoga: 348f8b538c3ed4423eb58a8e5730feec50bce372
|
||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||
|
||||
PODFILE CHECKSUM: 0b954caebefad4e9dc123f5491a2649c02c896ea
|
||||
PODFILE CHECKSUM: 11a2f8ebab99f816b8905858bff8a86a196b1f7e
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
|
@ -4,14 +4,6 @@
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
@ -25,6 +17,8 @@
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>0A2A.1</string>
|
||||
<string>3B52.1</string>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
@ -33,8 +27,16 @@
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>85F4.1</string>
|
||||
<string>E174.1</string>
|
||||
<string>85F4.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
@ -55,6 +55,10 @@ jest.mock('./components/ExtendedWebView', () => {
|
||||
return require('./components/ExtendedWebView/index.jest.js');
|
||||
});
|
||||
|
||||
jest.mock('./components/CameraView/Camera', () => {
|
||||
return require('./components/CameraView/Camera/index.jest');
|
||||
});
|
||||
|
||||
jest.mock('@react-native-clipboard/clipboard', () => {
|
||||
return { default: { getString: jest.fn(), setString: jest.fn() } };
|
||||
});
|
||||
|
@ -39,6 +39,8 @@
|
||||
"crypto-browserify": "3.12.0",
|
||||
"deprecated-react-native-prop-types": "5.0.0",
|
||||
"events": "3.3.0",
|
||||
"expo": "51.0.0",
|
||||
"expo-camera": "15.0.16",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
"path-browserify": "1.0.1",
|
||||
@ -46,7 +48,6 @@
|
||||
"punycode": "2.3.1",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.14.0",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "9.3.0",
|
||||
@ -134,5 +135,13 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"expo": {
|
||||
"autolinking": {
|
||||
"exclude": [
|
||||
"expo-application",
|
||||
"expo-keep-awake"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ module.exports = {
|
||||
'react-native-zip-archive': emptyLibraryMock,
|
||||
'react-native-document-picker': emptyLibraryMock,
|
||||
'react-native-exit-app': emptyLibraryMock,
|
||||
'expo-camera': emptyLibraryMock,
|
||||
|
||||
// Workaround for applying serviceworker types to a single file.
|
||||
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.
|
||||
@ -100,6 +101,7 @@ module.exports = {
|
||||
'timers': require.resolve('timers-browserify'),
|
||||
'path': require.resolve('path-browserify'),
|
||||
'stream': require.resolve('stream-browserify'),
|
||||
'crypto': require.resolve('crypto-browserify'),
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -14,6 +14,11 @@ const customCssFilePath = (Setting: typeof SettingType, filename: string): strin
|
||||
return `${Setting.value('rootProfileDir')}/${filename}`;
|
||||
};
|
||||
|
||||
export enum CameraDirection {
|
||||
Back,
|
||||
Front,
|
||||
}
|
||||
|
||||
const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
const platform = shim.platformName();
|
||||
const mobilePlatform = shim.mobilePlatform();
|
||||
@ -1436,7 +1441,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
'welcome.wasBuilt': { value: false, type: SettingItemType.Bool, public: false },
|
||||
'welcome.enabled': { value: true, type: SettingItemType.Bool, public: false },
|
||||
|
||||
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
|
||||
'camera.type': { value: CameraDirection.Back, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
|
||||
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
|
||||
|
||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false },
|
||||
|
@ -136,3 +136,6 @@ Backblaze
|
||||
onnx
|
||||
onnxruntime
|
||||
treeitem
|
||||
qrcode
|
||||
Rocketbook
|
||||
datamatrix
|
||||
|
Loading…
Reference in New Issue
Block a user