You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Mobile: Camera screen: Support scanning QR codes (#11245)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user