feat: scanner shit

This commit is contained in:
2025-07-18 05:27:49 +03:00
parent 52529a7974
commit 1403c3115c
18 changed files with 296 additions and 110 deletions

View File

@@ -93,7 +93,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.4.3"
versionName "1.5.3"
}
signingConfigs {
debug {
@@ -154,7 +154,11 @@ dependencies {
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
implementation project(':react-native-tcp-socket')
implementation project(':react-native-keyevent')
implementation project(':react-native-bluetooth-classic')
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")

View File

@@ -1,7 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -6,7 +6,10 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>

View File

@@ -1,65 +1,88 @@
package com.anonymous.Assemblr
import expo.modules.splashscreen.SplashScreenManager
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import com.github.kevinejohn.keyevent.KeyEventModule
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
// setTheme(R.style.AppTheme);
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
SplashScreenManager.registerOnActivity(this)
// @generated end expo-splashscreen
super.onCreate(null)
}
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
// setTheme(R.style.AppTheme);
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
SplashScreenManager.registerOnActivity(this)
// @generated end expo-splashscreen
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
) {})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
KeyEventModule.getInstance().onKeyDownEvent(keyCode, event)
// super.onKeyDown(keyCode, event)
return true
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
KeyEventModule.getInstance().onKeyUpEvent(keyCode, event);
// super.onKeyUp(keyCode, event)
return true;
}
override fun onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent): Boolean {
KeyEventModule.getInstance().onKeyMultipleEvent(keyCode, repeatCount, event)
return true;
// return super.onKeyMultiple(keyCode, repeatCount, event)
}
}

View File

@@ -12,10 +12,12 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.github.kevinejohn.keyevent.KeyEventPackage
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import com.asterinet.react.tcpsocket.TcpSocketPackage;
import kjd.reactnative.bluetooth.RNBluetoothClassicPackage;
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
@@ -26,6 +28,8 @@ class MainApplication : Application(), ReactApplication {
packages.add(PdfToBitmapPackage())
packages.add(TcpSocketPackage())
packages.add(MyTcpSocketPackage())
packages.add(KeyEventPackage())
packages.add(RNBluetoothClassicPackage())
return packages
}
@@ -55,4 +59,5 @@ class MainApplication : Application(), ReactApplication {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

View File

@@ -37,5 +37,12 @@ expoAutolinking.useExpoVersionCatalog()
include ':app'
includeBuild(expoAutolinking.reactNativeGradlePlugin)
include ':react-native-tcp-socket'
project(':react-native-tcp-socket').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tcp-socket/android')
include ':react-native-keyevent'
project(':react-native-keyevent').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyevent/android')
include ':react-native-bluetooth-classic'
project(':react-native-bluetooth-classic').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bluetooth-classic/android')

View File

@@ -12,7 +12,7 @@
],
"name": "Assemblr",
"slug": "Assemblr",
"version": "1.4.3",
"version": "1.5.3",
"orientation": "portrait",
"icon": "./src/assets/icon.png",
"userInterfaceStyle": "light",

View File

@@ -46,9 +46,12 @@
"react-hook-form": "^7.57.0",
"react-native": "0.79.2",
"react-native-animatable": "^1.4.0",
"react-native-bluetooth-classic": "^1.73.0-rc.13",
"react-native-bluetooth-serial": "lucassouza16/react-native-bluetooth-serial",
"react-native-gesture-handler": "~2.24.0",
"react-native-image-pan-zoom": "^2.1.12",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-keyevent": "^0.3.2",
"react-native-modal": "^13.0.1",
"react-native-paper": "^5.12.5",
"react-native-picker-select": "^9.3.1",
@@ -60,6 +63,7 @@
"react-native-responsive-fontsize": "^0.5.1",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.10.0",
"react-native-serialport-bluetooth": "^0.4.11",
"react-native-svg": "15.11.2",
"react-native-tcp-socket": "^6.3.0",
"react-native-toast-message": "^2.2.1",

View File

@@ -1,15 +1,18 @@
import {StyleSheet} from 'react-native';
import {Provider} from "react-redux";
import {store} from "./redux/store";
import React from "react";
import React, {useEffect} from "react";
import CommonPage from "./screens/CommonPage/CommonPage";
import {BottomSheetModalProvider} from "@gorhom/bottom-sheet";
import {GestureHandlerRootView} from "react-native-gesture-handler";
import KeyEvent from "react-native-keyevent";
export default function App() {
return (
<Provider store={store}>
<>
</>
<GestureHandlerRootView style={{flex: 1}}>
<BottomSheetModalProvider>
<CommonPage/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,5 +1,5 @@
import {FC, useEffect, useRef} from "react";
import {GestureResponderEvent, StyleSheet, Text, TextInput, View} from "react-native";
import {StyleSheet, TextInput, View} from "react-native";
import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions";
import {RFPercentage} from "react-native-responsive-fontsize";
import Modal from "react-native-modal";
@@ -11,12 +11,14 @@ import {closeScanModal, setScannedData} from "../../features/scanModal/scanModal
const ScanModal: FC = () => {
const inputRef = useRef<TextInput | null>(null);
const scannerState = useSelector((state: RootState) => state.scanner);
const state = useSelector((state: RootState) => state.scanModal);
const getDefaultLabel = () => "Наведите сканер на штрихкод товара или заказа";
const dispatch = useDispatch();
useEffect(() => {
if (state.isVisible) inputRef.current?.focus();
}, [state.isVisible]);
return (
<Modal isVisible={state.isVisible}>
<View style={styles.container}>

View File

@@ -1,3 +1,5 @@
export const background = "#F5F5F5";
export const blue = "#2478F8";
export const gray = '#A5A5A5';
export const gray = '#A5A5A5';
export const red = "#e03131"
export const blueButton = '#2478F8'

View File

@@ -0,0 +1,30 @@
import {BluetoothDevice} from "react-native-bluetooth-classic";
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
export interface ScannerState {
isConnected: boolean;
deviceAddress?: string;
}
const initialState: ScannerState = {
isConnected: false,
deviceAddress: undefined
}
export const scannerSlice = createSlice({
name: "scannerState",
initialState,
reducers: {
setDevice(state, action: PayloadAction<{ deviceAddress: string }>) {
state.deviceAddress = action.payload.deviceAddress;
},
setIsConnected(state, action: PayloadAction<{ isConnected: boolean }>) {
state.isConnected = action.payload.isConnected
}
}
})
export const {setDevice, setIsConnected} = scannerSlice.actions
export default scannerSlice.reducer

View File

@@ -15,6 +15,7 @@ import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModa
import balanceReducer from 'features/balance/balanceSlice';
import animationsReducer from 'features/animations/animationsSlice';
import filterReducer from 'features/filterSlice/filterSlice';
import scannerReducer from 'features/scannerState/scannerState';
import {useDispatch} from "react-redux";
export const store = configureStore({
@@ -33,7 +34,8 @@ export const store = configureStore({
cancelAssemblyModal: cancelAssemblyModal,
balance: balanceReducer,
animations: animationsReducer,
filter:filterReducer
filter: filterReducer,
scanner: scannerReducer
},
});

View File

@@ -66,6 +66,7 @@ function CommonPage() {
})
}
const checkUpdates = async () => {
return
const currentVersion = Application.nativeApplicationVersion;
applicationApi.getVersion('assemblr').then(({latest_version}) => {

View File

@@ -11,7 +11,6 @@ import {useDispatch} from "react-redux";
import {login} from "../../features/auth/authSlice";
import Toast from "react-native-toast-message";
import * as SecureStore from 'expo-secure-store';
import KeyEvent from "react-native-keyevent";
function LoginScreen() {
const dispatch = useAppDispatch();

View File

@@ -70,7 +70,7 @@ function MainScreen() {
{
name: "Money",
component: moneyScreen,
icon: require('assets/icons/money.png'),
icon: require('assets/icons/scanner.png'),
hidden: false
},
{

View File

@@ -1,65 +1,157 @@
import {Text, View} from "react-native";
import {PermissionsAndroid, Platform, Text, TouchableOpacity, View} from "react-native";
import {useEffect, useState} from "react";
import {Permission} from "react-native/Libraries/PermissionsAndroid/PermissionsAndroid";
import RNBluetoothClassic, {BluetoothDevice} from "react-native-bluetooth-classic";
import {RFPercentage} from "react-native-responsive-fontsize";
import {responsiveWidth} from "react-native-responsive-dimensions";
import {blue, blueButton, red} from "../../css/colors";
import BasicButton from "../../components/BasicButton/BasicButton";
import {useScanningContext} from "../../providers/ScanProvider";
import printingApi from "../../api/printingApi";
import {Zpl} from "react-native-zpl-code";
import PrintingService from "../../utils/PrintingService";
import {useSelector} from "react-redux";
import {RootState, useAppDispatch} from "../../redux/store";
import {setDevice, setIsConnected} from "../../features/scannerState/scannerState";
import {closeScanModal, setScannedData} from "../../features/scanModal/scanModalSlice";
async function generateZplCodes(images: string[]) {
const zplCodes = [];
for (const image of images) {
const zplBuilder = new Zpl.Builder();
zplBuilder.setup({
size: {
heightDots: 320, // 40mm at 203 DPI
widthDots: 464, // 58mm at 203 DPI
},
labelHome: {
x: 0,
y: 0,
},
labelTop: 0,
labeShift: 0,
orientation: "NORMAL",
media: {
type: "MARK_SENSING",
dots: 0,
},
});
zplBuilder.image({
uri: `data:image/png;base64,${image}`,
x: 0,
y: 0,
width: 464,
height: 320,
dither: true,
});
const zplCode = await zplBuilder.build();
zplCodes.push(zplCode);
}
return zplCodes;
type BluetoothSettingsState = {
enabled: boolean;
devices: BluetoothDevice[]
}
const DEFAULT_BLUETOOTH_SETTINGS_STATE: BluetoothSettingsState = {
enabled: false,
devices: []
}
function MoneyScreen() {
const {scan} = useScanningContext();
const a = "123";
const onPress = () => {
printingApi.getLabel(391845).then(async result => {
PrintingService.getInstance().printPdf("", result).then(() => {
}
).catch(error => {
console.error("Error printing:", error);
});
});
const [state, setState] = useState<BluetoothSettingsState>(DEFAULT_BLUETOOTH_SETTINGS_STATE);
const scannerState = useSelector((state: RootState) => state.scanner);
const dispatch = useAppDispatch();
async function requestBluetoothPermissions() {
if (Platform.OS === 'android') {
const permissions: Permission[] = [];
if (Platform.Version >= 31) { // Android 12+
permissions.push(
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE,
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
console.log("android12")
} else { // Below Android 12
permissions.push(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
}
const granted = await PermissionsAndroid.requestMultiple(permissions);
const hasBluetoothPermission =
(Platform.Version >= 31 &&
granted['android.permission.BLUETOOTH_SCAN'] === PermissionsAndroid.RESULTS.GRANTED &&
granted['android.permission.BLUETOOTH_CONNECT'] === PermissionsAndroid.RESULTS.GRANTED) ||
(Platform.Version < 31 &&
granted['android.permission.ACCESS_FINE_LOCATION'] === PermissionsAndroid.RESULTS.GRANTED);
if (hasBluetoothPermission) {
console.log('Bluetooth permissions granted');
} else {
console.warn('Bluetooth permissions denied');
}
}
}
const initialize = async () => {
const isEnabled = await RNBluetoothClassic.isBluetoothEnabled();
const devices = isEnabled ? await RNBluetoothClassic.getBondedDevices() : []
setState(prevState => {
return {
...prevState,
enabled: isEnabled,
devices: devices
}
})
}
const setSelectedDevice = (device: BluetoothDevice) => {
dispatch(setDevice({deviceAddress: device.address}));
}
const onConnectPress = async () => {
if (!scannerState.deviceAddress) return;
if (scannerState.isConnected) {
console.log("connected")
await RNBluetoothClassic.disconnectFromDevice(scannerState.deviceAddress);
return
}
console.log("connecting")
const connected = await RNBluetoothClassic.connectToDevice(scannerState.deviceAddress, {
secureSocket: false,
charset: "UTF-8",
delimiter: "\r\r"
});
const isConnected = await connected.isConnected();
dispatch(setIsConnected({isConnected}));
if (isConnected) {
RNBluetoothClassic.onDeviceRead(connected.address, event => {
dispatch(setScannedData(event.data));
dispatch(closeScanModal())
})
}
}
useEffect(() => {
const interval = setInterval(initialize, 1000);
const {remove: removeOnConnectedListener} = RNBluetoothClassic.onDeviceConnected(event => {
console.log("Connected event")
dispatch(setIsConnected({isConnected: true}));
})
const {remove: removeOnDisconnectedListener} = RNBluetoothClassic.onDeviceDisconnected(event => {
dispatch(setIsConnected({isConnected: false}));
})
return () => {
clearInterval(interval);
removeOnConnectedListener();
removeOnDisconnectedListener()
}
}, []);
return (
<View>
<Text style={{fontSize: 36}}>Money</Text>
<BasicButton onPress={() => onPress()} label={"test"}/>
<View style={{padding: "5%"}}>
<Text>
Блютуз включен: {state.enabled ? "да" : "нет"}
</Text>
<View
pointerEvents={scannerState.isConnected ? "none" : "auto"}
style={{opacity: scannerState.isConnected ? 0.4 : 1}}
>
{state.devices.map(device => (
<TouchableOpacity onPress={() => setSelectedDevice(device)} key={device.address}>
<View
style={{
backgroundColor: scannerState.deviceAddress == device.address ? blue : "white",
padding: RFPercentage(2),
borderRadius: responsiveWidth(2.5),
marginBottom: 10,
}}>
<Text style={{fontSize: RFPercentage(2)}}>
{device.name}
</Text>
</View>
</TouchableOpacity>
))}
</View>
<BasicButton
onPress={onConnectPress}
label={scannerState.isConnected ? "Отключить" : "Подключить"}
style={{borderRadius: responsiveWidth(3), backgroundColor: scannerState.isConnected ? red : blueButton}}
/>
</View>
)
}