Рефакторинг. Добавление иконок для пунктов меню. Изменение папки файлов приложения (с app на parus_pre_trip_inspections). Доработка работы встроенного сканера устройства.
This commit is contained in:
parent
7bc9ddb898
commit
5449f61c6e
@ -13,8 +13,9 @@ const AppMessagingProvider = require('./src/components/layout/AppMessagingProvid
|
||||
const AppNavigationProvider = require('./src/components/layout/AppNavigationProvider').AppNavigationProvider; //Провайдер навигации
|
||||
const AppModeProvider = require('./src/components/layout/AppModeProvider').AppModeProvider; //Провайдер режима работы
|
||||
const AppLocalDbProvider = require('./src/components/layout/AppLocalDbProvider').AppLocalDbProvider; //Провайдер локальной БД
|
||||
const AppAuthProvider = require('./src/components/layout/AppAuthProvider').AppAuthProvider; //Провайдер авторизации
|
||||
const AppServerProvider = require('./src/components/layout/AppServerProvider').AppServerProvider; //Провайдер сервера
|
||||
const AppPreTripInspectionsProvider = require('./src/components/layout/AppPreTripInspectionsProvider').AppPreTripInspectionsProvider; //Провайдер осмотров
|
||||
const { AppIdleProvider } = require('./src/components/layout/AppIdleProvider'); //Провайдер простоя
|
||||
const AppRoot = require('./src/components/layout/AppRoot'); //Корневой layout приложения
|
||||
|
||||
//-----------
|
||||
@ -28,13 +29,15 @@ function App() {
|
||||
<AppMessagingProvider>
|
||||
<AppNavigationProvider>
|
||||
<AppLocalDbProvider>
|
||||
<AppModeProvider>
|
||||
<AppServerProvider>
|
||||
<AppPreTripInspectionsProvider>
|
||||
<AppRoot />
|
||||
</AppPreTripInspectionsProvider>
|
||||
</AppServerProvider>
|
||||
</AppModeProvider>
|
||||
<AppAuthProvider>
|
||||
<AppModeProvider>
|
||||
<AppServerProvider>
|
||||
<AppIdleProvider>
|
||||
<AppRoot />
|
||||
</AppIdleProvider>
|
||||
</AppServerProvider>
|
||||
</AppModeProvider>
|
||||
</AppAuthProvider>
|
||||
</AppLocalDbProvider>
|
||||
</AppNavigationProvider>
|
||||
</AppMessagingProvider>
|
||||
|
||||
@ -77,9 +77,9 @@ android {
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "com.app"
|
||||
namespace "com.parus_pre_trip_inspections"
|
||||
defaultConfig {
|
||||
applicationId "com.app"
|
||||
applicationId "com.parus_pre_trip_inspections"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
@ -117,3 +117,19 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
// Обход: перед native clean нужны сгенерированные codegen-папки автолинкованных библиотек
|
||||
afterEvaluate {
|
||||
def codegenDeps = []
|
||||
rootProject.subprojects.each { sub ->
|
||||
def codegenTask = sub.tasks.findByName("generateCodegenArtifactsFromSchema")
|
||||
if (codegenTask != null) {
|
||||
codegenDeps.add(codegenTask)
|
||||
}
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.startsWith("externalNativeBuild") && task.name.contains("Clean")) {
|
||||
codegenDeps.each { task.dependsOn(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||
android:supportsRtl="true">
|
||||
android:supportsRtl="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
package com.app
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "Pre-Trip_Inspections"
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Нативный модуль встроенного сканера: доставка штрихкода в JS через событие по мосту.
|
||||
*/
|
||||
|
||||
package com.parus_pre_trip_inspections
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
import com.facebook.react.bridge.Arguments
|
||||
|
||||
class HardwareScannerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
|
||||
init {
|
||||
sharedReactContext = reactApplicationContext
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MODULE_NAME = "HardwareScannerModule"
|
||||
private const val LOG_TAG = "PTI_HwScan"
|
||||
const val EVENT_HARDWARE_SCANNER_DATA = "HardwareScannerData"
|
||||
private const val EVENT_HARDWARE_SCANNER_TRIGGER = "HardwareScannerTriggerPressed"
|
||||
private const val TRIGGER_DEBOUNCE_MS = 600L
|
||||
|
||||
@Volatile
|
||||
var sharedReactContext: ReactApplicationContext? = null
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
private var lastTriggerEmitTime = 0L
|
||||
|
||||
fun emitBarcodeToJs(barcode: String?) {
|
||||
if (barcode.isNullOrEmpty()) return
|
||||
val ctx = sharedReactContext ?: return
|
||||
val payload = barcode
|
||||
try {
|
||||
if (!ctx.hasActiveCatalystInstance()) return
|
||||
ctx.runOnUiQueueThread {
|
||||
try {
|
||||
if (!ctx.hasActiveCatalystInstance()) return@runOnUiQueueThread
|
||||
val map = Arguments.createMap()
|
||||
map.putString("barcode", payload)
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
?.emit(EVENT_HARDWARE_SCANNER_DATA, map)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
//Эмит триггера только с UiQueueThread
|
||||
fun emitTriggerFromActivity(appContext: android.content.Context?): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastTriggerEmitTime < TRIGGER_DEBOUNCE_MS) return false
|
||||
lastTriggerEmitTime = now
|
||||
val ctx = sharedReactContext ?: return true
|
||||
try {
|
||||
if (!ctx.hasActiveCatalystInstance()) return true
|
||||
ctx.runOnUiQueueThread {
|
||||
try {
|
||||
if (!ctx.hasActiveCatalystInstance()) return@runOnUiQueueThread
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
?.emit(EVENT_HARDWARE_SCANNER_TRIGGER, null)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String = MODULE_NAME
|
||||
|
||||
@ReactMethod
|
||||
fun setCameraViewsNotFocusable() {
|
||||
try {
|
||||
val activity = reactApplicationContext.currentActivity ?: return
|
||||
val content = activity.window?.decorView?.findViewById<View>(android.R.id.content) ?: return
|
||||
setNotFocusableRecursive(content)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun setNotFocusableRecursive(view: View) {
|
||||
val name = view.javaClass.simpleName
|
||||
if (name.contains("Camera", ignoreCase = true) ||
|
||||
name.contains("Preview", ignoreCase = true) ||
|
||||
name.contains("TextureView", ignoreCase = true) ||
|
||||
name.contains("SurfaceView", ignoreCase = true)
|
||||
) {
|
||||
view.setFocusable(false)
|
||||
view.setFocusableInTouchMode(false)
|
||||
}
|
||||
if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
setNotFocusableRecursive(view.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Регистрация нативного модуля встроенного сканера
|
||||
*/
|
||||
|
||||
package com.parus_pre_trip_inspections
|
||||
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
//Пакет нативного модуля встроенного сканера
|
||||
class HardwareScannerPackage : ReactPackage {
|
||||
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||
return listOf(HardwareScannerModule(reactContext))
|
||||
}
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Главная Activity: встроенный сканер — сессия по триггеру, ENTER в буфер как \n, отправка по таймауту неактивности
|
||||
*/
|
||||
|
||||
package com.parus_pre_trip_inspections
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.KeyCharacterMap
|
||||
import android.view.KeyEvent
|
||||
import android.view.Window
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
private val scannerBuffer = StringBuilder()
|
||||
private var scannerSessionActive = false
|
||||
private var lastScannerKeyTime = 0L
|
||||
private var lastBarcodeSentTime = 0L
|
||||
private var scannerDeviceId: Int? = null
|
||||
private var dispatchFromWindowCallback = false
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private fun normalizeBarcodeForSend(raw: String): String = raw.trim()
|
||||
|
||||
override fun onCreate(savedInstanceState: android.os.Bundle?) {
|
||||
setDefaultUncaughtExceptionHandler()
|
||||
super.onCreate(savedInstanceState)
|
||||
installScannerKeyInterceptor()
|
||||
}
|
||||
|
||||
private fun installScannerKeyInterceptor() {
|
||||
val w = window ?: return
|
||||
val orig = w.callback ?: return
|
||||
w.callback = object : Window.Callback by orig {
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
if (handleScannerKeyEvent(event)) return true
|
||||
dispatchFromWindowCallback = true
|
||||
return try {
|
||||
orig.dispatchKeyEvent(event)
|
||||
} finally {
|
||||
dispatchFromWindowCallback = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultUncaughtExceptionHandler() {
|
||||
val prev = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
prev?.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
private val sessionIdleRunnable = Runnable {
|
||||
if (!scannerSessionActive) return@Runnable
|
||||
if (scannerBuffer.isEmpty()) {
|
||||
scannerSessionActive = false
|
||||
return@Runnable
|
||||
}
|
||||
val barcode = normalizeBarcodeForSend(scannerBuffer.toString())
|
||||
scannerBuffer.setLength(0)
|
||||
scannerSessionActive = false
|
||||
lastBarcodeSentTime = System.currentTimeMillis()
|
||||
sendBarcodeToReactNative(barcode)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOG_TAG = "PTI_HwScan"
|
||||
private const val SCANNER_SESSION_TIMEOUT_MS = 2000L
|
||||
private const val SESSION_IDLE_SEND_MS = 1000L
|
||||
private const val POST_BARCODE_SILENT_MS = 800L
|
||||
|
||||
private val TRIGGER_KEY_CODES = setOf(
|
||||
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||
139, 140, 141, 142, 143, 144,
|
||||
KeyEvent.KEYCODE_ASSIST
|
||||
)
|
||||
}
|
||||
|
||||
private fun isTriggerKey(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (event.getUnicodeChar(event.metaState) != 0) return false
|
||||
return keyCode in TRIGGER_KEY_CODES
|
||||
}
|
||||
|
||||
private fun isPrintableBarcodeChar(code: Int): Boolean =
|
||||
(code in 32..126) || (code in 160..255) || (code in 0x400..0x4FF) || (code in 0x370..0x3FF)
|
||||
|
||||
override fun getMainComponentName(): String = "Pre-Trip_Inspections"
|
||||
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
//Если событие пришло через установленный Window.Callback, здесь не повторяем обработку.
|
||||
if (dispatchFromWindowCallback) return super.dispatchKeyEvent(event)
|
||||
if (handleScannerKeyEvent(event)) return true
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private fun handleScannerKeyEvent(event: KeyEvent): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
val keyCode = event.keyCode
|
||||
|
||||
if (lastBarcodeSentTime != 0L && (now - lastBarcodeSentTime) < POST_BARCODE_SILENT_MS) return true
|
||||
|
||||
if (scannerSessionActive) {
|
||||
if (event.action != KeyEvent.ACTION_DOWN) return true
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
mainHandler.removeCallbacks(sessionIdleRunnable)
|
||||
scannerSessionActive = false
|
||||
scannerBuffer.setLength(0)
|
||||
return false
|
||||
}
|
||||
if (lastScannerKeyTime != 0L && now - lastScannerKeyTime > SCANNER_SESSION_TIMEOUT_MS) {
|
||||
mainHandler.removeCallbacks(sessionIdleRunnable)
|
||||
if (scannerBuffer.isNotEmpty()) {
|
||||
val barcode = normalizeBarcodeForSend(scannerBuffer.toString())
|
||||
scannerBuffer.setLength(0)
|
||||
scannerSessionActive = false
|
||||
lastBarcodeSentTime = now
|
||||
sendBarcodeToReactNative(barcode)
|
||||
} else scannerSessionActive = false
|
||||
return true
|
||||
}
|
||||
lastScannerKeyTime = now
|
||||
when {
|
||||
isTriggerKey(keyCode, event) -> { }
|
||||
keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
||||
scannerBuffer.append('\n')
|
||||
mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
|
||||
}
|
||||
else -> {
|
||||
val appended = appendKeyToBuffer(event)
|
||||
if (appended) mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val fromScanner = when {
|
||||
event.deviceId == -1 -> false
|
||||
scannerDeviceId != null -> event.deviceId == scannerDeviceId
|
||||
isTriggerKey(keyCode, event) -> true
|
||||
else -> false
|
||||
}
|
||||
if (!fromScanner) {
|
||||
mainHandler.removeCallbacks(sessionIdleRunnable)
|
||||
if (scannerSessionActive) {
|
||||
scannerSessionActive = false
|
||||
scannerBuffer.setLength(0)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (event.action != KeyEvent.ACTION_DOWN) return true
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) return false
|
||||
|
||||
mainHandler.removeCallbacks(sessionIdleRunnable)
|
||||
scannerSessionActive = true
|
||||
scannerBuffer.setLength(0)
|
||||
lastScannerKeyTime = now
|
||||
if (scannerDeviceId == null && event.deviceId > 0) scannerDeviceId = event.deviceId
|
||||
if (isTriggerKey(keyCode, event)) {
|
||||
HardwareScannerModule.emitTriggerFromActivity(applicationContext)
|
||||
return true
|
||||
}
|
||||
val appended = appendKeyToBuffer(event)
|
||||
if (appended) mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun appendKeyToBuffer(event: KeyEvent): Boolean {
|
||||
val keyCode = event.keyCode
|
||||
if (keyCode in TRIGGER_KEY_CODES && event.getUnicodeChar(event.metaState) == 0) return false
|
||||
var unicode = event.getUnicodeChar(event.metaState)
|
||||
if (unicode == 0 && event.action == KeyEvent.ACTION_DOWN) {
|
||||
try {
|
||||
val map = KeyCharacterMap.load(event.deviceId)
|
||||
unicode = map.get(keyCode, event.metaState)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
if (unicode != 0 && Character.isBmpCodePoint(unicode)) {
|
||||
scannerBuffer.append(unicode.toChar())
|
||||
return true
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_UNKNOWN || keyCode == 0) {
|
||||
val label = event.displayLabel
|
||||
val labelCode = label.code
|
||||
if (labelCode != 0 && isPrintableBarcodeChar(labelCode)) {
|
||||
scannerBuffer.append(label)
|
||||
return true
|
||||
}
|
||||
}
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_4,
|
||||
KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7, KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9 ->
|
||||
scannerBuffer.append('0' + (keyCode - KeyEvent.KEYCODE_0))
|
||||
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_NUMPAD_3,
|
||||
KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_NUMPAD_6, KeyEvent.KEYCODE_NUMPAD_7,
|
||||
KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_NUMPAD_9 ->
|
||||
scannerBuffer.append('0' + (keyCode - KeyEvent.KEYCODE_NUMPAD_0))
|
||||
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT -> scannerBuffer.append('-')
|
||||
KeyEvent.KEYCODE_SLASH -> scannerBuffer.append('/')
|
||||
KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_NUMPAD_DOT ->
|
||||
if (event.getUnicodeChar(event.metaState) != 0) scannerBuffer.append('.')
|
||||
in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z -> scannerBuffer.append('A' + (keyCode - KeyEvent.KEYCODE_A))
|
||||
KeyEvent.KEYCODE_SPACE -> scannerBuffer.append(' ')
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun sendBarcodeToReactNative(barcode: String) {
|
||||
HardwareScannerModule.emitBarcodeToJs(barcode)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.app
|
||||
package com.parus_pre_trip_inspections
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
add(HardwareScannerPackage())
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -11,10 +11,6 @@ const { AppRegistry } = require("react-native"); //Регистрация кор
|
||||
const App = require("./App"); //Корневой компонент приложения
|
||||
const { name: appName } = require("./app.json"); //Имя приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Регистрация корневого компонента
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
||||
|
||||
7
rn/app/react-native.config.js
Normal file
7
rn/app/react-native.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
project: {
|
||||
android: {
|
||||
packageName: 'com.parus_pre_trip_inspections',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -159,7 +159,7 @@ function readPng(filePath) {
|
||||
const inflated = zlib.inflateSync(concatenated, { chunkSize: rawSize + 1024 });
|
||||
if (inflated.length < rawSize) throw new Error('PNG decompressed size too small');
|
||||
|
||||
// Снятие фильтров строк и приведение к RGBA
|
||||
//Снятие фильтров строк и приведение к RGBA
|
||||
const rgba = new Uint8Array(width * height * 4);
|
||||
const stride = width * bytesPerPixel;
|
||||
let prev = null;
|
||||
@ -251,7 +251,7 @@ function writePngBuffer(width, height, rgba) {
|
||||
const rawRows = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = Buffer.alloc(1 + stride);
|
||||
row[0] = 0; // фильтр None
|
||||
row[0] = 0; //фильтр None
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
row[1 + x * 4] = rgba[i];
|
||||
@ -278,11 +278,11 @@ function writePngBuffer(width, height, rgba) {
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // глубина цвета
|
||||
ihdr[9] = 6; // тип цвета RGBA
|
||||
ihdr[10] = 0; // сжатие deflate
|
||||
ihdr[11] = 0; // фильтр адаптивный
|
||||
ihdr[12] = 0; // без чересстрочности
|
||||
ihdr[8] = 8; //глубина цвета
|
||||
ihdr[9] = 6; //тип цвета RGBA
|
||||
ihdr[10] = 0; //сжатие deflate
|
||||
ihdr[11] = 0; //фильтр адаптивный
|
||||
ihdr[12] = 0; //без чересстрочности
|
||||
|
||||
const out = [PNG_SIGNATURE];
|
||||
writeChunk(out, 'IHDR', ihdr);
|
||||
|
||||
@ -24,7 +24,7 @@ function getAppButtonPressableStyle(stylesRef, disabled, style) {
|
||||
}
|
||||
|
||||
//Общая кнопка приложения
|
||||
function AppButton({ title, onPress, disabled = false, style, textStyle }) {
|
||||
function AppButton({ title, onPress, disabled = false, style, textStyle, focusable }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
if (!disabled && typeof onPress === 'function') onPress();
|
||||
}, [disabled, onPress]);
|
||||
@ -32,7 +32,7 @@ function AppButton({ title, onPress, disabled = false, style, textStyle }) {
|
||||
const getPressableStyle = React.useMemo(() => getAppButtonPressableStyle(styles, disabled, style), [disabled, style]);
|
||||
|
||||
return (
|
||||
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle}>
|
||||
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle} focusable={focusable}>
|
||||
<View style={styles.content}>
|
||||
<AppText style={[styles.text, textStyle]}>{title}</AppText>
|
||||
</View>
|
||||
|
||||
@ -32,21 +32,31 @@ const AppInput = React.forwardRef(function AppInput(
|
||||
style,
|
||||
inputStyle,
|
||||
labelStyle,
|
||||
onFocus: onFocusProp,
|
||||
onBlur: onBlurProp,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
//Обработчик фокуса
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
//Обработчик фокуса (внутренний + опциональный внешний)
|
||||
const handleFocus = React.useCallback(
|
||||
e => {
|
||||
setIsFocused(true);
|
||||
if (typeof onFocusProp === 'function') onFocusProp(e);
|
||||
},
|
||||
[onFocusProp]
|
||||
);
|
||||
|
||||
//Обработчик потери фокуса
|
||||
const handleBlur = React.useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
//Обработчик потери фокуса (внутренний + опциональный внешний)
|
||||
const handleBlur = React.useCallback(
|
||||
e => {
|
||||
setIsFocused(false);
|
||||
if (typeof onBlurProp === 'function') onBlurProp(e);
|
||||
},
|
||||
[onBlurProp]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
|
||||
@ -23,6 +23,28 @@ function getMessageButtonPressableStyle(stylesRef, buttonStyle) {
|
||||
};
|
||||
}
|
||||
|
||||
//Формирование массива кнопок сообщения
|
||||
function getMessageButtonElements(buttons, handleClose) {
|
||||
if (!Array.isArray(buttons) || buttons.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const elements = [];
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const btn = buttons[i];
|
||||
elements.push(
|
||||
<AppMessageButton
|
||||
key={btn.id || btn.title}
|
||||
title={btn.title}
|
||||
onPress={btn.onPress}
|
||||
onDismiss={handleClose}
|
||||
buttonStyle={btn.buttonStyle}
|
||||
textStyle={btn.textStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
//Кнопка сообщения
|
||||
function AppMessageButton({ title, onPress, onDismiss, buttonStyle, textStyle }) {
|
||||
//Обработчик нажатия - вызывает onPress и закрывает диалог
|
||||
@ -95,20 +117,7 @@ function AppMessage({
|
||||
<View style={[styles.content, contentStyle]}>
|
||||
<Text style={[styles.message, messageStyle]}>{message}</Text>
|
||||
</View>
|
||||
{hasButtons ? (
|
||||
<View style={styles.buttonsRow}>
|
||||
{buttons.map(btn => (
|
||||
<AppMessageButton
|
||||
key={btn.id || btn.title}
|
||||
title={btn.title}
|
||||
onPress={btn.onPress}
|
||||
onDismiss={handleClose}
|
||||
buttonStyle={btn.buttonStyle}
|
||||
textStyle={btn.textStyle}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
{hasButtons ? <View style={styles.buttonsRow}>{getMessageButtonElements(buttons, handleClose)}</View> : null}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
@ -48,8 +48,6 @@ function CopyButton({ value, onCopy, onError, disabled = false, style }) {
|
||||
onCopy(value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка копирования в буфер:', error);
|
||||
|
||||
if (typeof onError === 'function') {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
@ -8,44 +8,100 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { Modal, View, TextInput, Pressable } = require('react-native'); //Базовые компоненты
|
||||
const { Modal, View, TextInput, Pressable, Platform, StyleSheet, BackHandler } = require('react-native'); //Базовые компоненты
|
||||
const AppText = require('./AppText'); //Общий текстовый компонент
|
||||
const AppButton = require('./AppButton'); //Кнопка
|
||||
const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
|
||||
|
||||
const OVERLAY_Z = 9999;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Модальное окно с полем ввода
|
||||
function InputDialog({
|
||||
visible,
|
||||
title = 'Ввод данных',
|
||||
label,
|
||||
value = '',
|
||||
placeholder,
|
||||
keyboardType = 'default',
|
||||
autoCapitalize = 'none',
|
||||
confirmText = 'Сохранить',
|
||||
cancelText = 'Отмена',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
validator,
|
||||
errorMessage
|
||||
}) {
|
||||
function InputDialog(
|
||||
{
|
||||
visible,
|
||||
title = 'Ввод данных',
|
||||
label,
|
||||
value = '',
|
||||
placeholder,
|
||||
keyboardType = 'default',
|
||||
autoCapitalize = 'none',
|
||||
confirmText = 'Сохранить',
|
||||
cancelText = 'Отмена',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
validator,
|
||||
errorMessage
|
||||
},
|
||||
ref
|
||||
) {
|
||||
//Локальное значение для редактирования
|
||||
const [inputValue, setInputValue] = React.useState(value);
|
||||
const inputRef = React.useRef(null);
|
||||
//На Android поле ввода изначально не фокусируемо, чтобы нативный слой видел «нет фокуса» и перехватывал сканер с первого символа (как на остальных экранах)
|
||||
const [inputFocusable, setInputFocusable] = React.useState(Platform.OS !== 'android');
|
||||
|
||||
const focusFirstEditableInput = React.useCallback(() => {
|
||||
if (inputRef.current && typeof inputRef.current.focus === 'function') {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
function exposeScannerApi() {
|
||||
return {
|
||||
setValueFromScanner(val) {
|
||||
setInputValue(val != null ? String(val) : '');
|
||||
if (Platform.OS === 'android') setInputFocusable(true);
|
||||
focusFirstEditableInput();
|
||||
}
|
||||
};
|
||||
},
|
||||
[focusFirstEditableInput]
|
||||
);
|
||||
|
||||
//После включения фокусируемости на Android — ставим фокус на поле (после setValueFromScanner)
|
||||
React.useEffect(
|
||||
function focusWhenFocusable() {
|
||||
if (Platform.OS !== 'android' || !inputFocusable) return;
|
||||
focusFirstEditableInput();
|
||||
},
|
||||
[inputFocusable, focusFirstEditableInput]
|
||||
);
|
||||
const [error, setError] = React.useState('');
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
//Сброс значения при открытии диалога
|
||||
//Сброс значения и фокусируемости только при открытии диалога
|
||||
const prevVisibleRef = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
const justOpened = visible && !prevVisibleRef.current;
|
||||
prevVisibleRef.current = visible;
|
||||
if (justOpened) {
|
||||
setInputValue(value);
|
||||
setError('');
|
||||
if (Platform.OS === 'android') setInputFocusable(false);
|
||||
}
|
||||
}, [visible, value]);
|
||||
|
||||
//Кнопка «Назад» на Android при overlay (без Modal) закрывает диалог
|
||||
React.useEffect(
|
||||
function backHandler() {
|
||||
if (Platform.OS !== 'android' || !visible) return;
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', function onBack() {
|
||||
handleCancel();
|
||||
return true;
|
||||
});
|
||||
return function remove() {
|
||||
sub.remove();
|
||||
};
|
||||
},
|
||||
[visible, handleCancel]
|
||||
);
|
||||
|
||||
//Обработчик фокуса
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
@ -97,54 +153,88 @@ function InputDialog({
|
||||
handleCancel();
|
||||
}, [handleCancel]);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent={true} animationType="fade" statusBarTranslucent={true} onRequestClose={handleRequestClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<AppText style={styles.title}>{title}</AppText>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleCancel} style={styles.closeButton}>
|
||||
<AppText style={styles.closeButtonText}>×</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
const preventKeyActions = Platform.OS === 'android';
|
||||
const closePressableProps = preventKeyActions ? { focusable: false } : {};
|
||||
const cancelButtonFocusable = preventKeyActions ? false : undefined;
|
||||
const confirmButtonFocusable = preventKeyActions ? false : undefined;
|
||||
|
||||
<View style={styles.content}>
|
||||
{label ? (
|
||||
<AppText style={styles.label} variant="caption" weight="medium">
|
||||
{label}
|
||||
</AppText>
|
||||
) : null}
|
||||
const dialogContent = (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<AppText style={styles.title}>{title}</AppText>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Закрыть"
|
||||
onPress={handleCancel}
|
||||
style={styles.closeButton}
|
||||
{...closePressableProps}
|
||||
>
|
||||
<AppText style={styles.closeButtonText}>×</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
|
||||
value={inputValue}
|
||||
onChangeText={handleChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.placeholder.color}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
autoFocus={true}
|
||||
selectTextOnFocus={true}
|
||||
accessible={true}
|
||||
accessibilityLabel={label || placeholder}
|
||||
accessibilityRole="text"
|
||||
/>
|
||||
<View style={styles.content}>
|
||||
{label ? (
|
||||
<AppText style={styles.label} variant="caption" weight="medium">
|
||||
{label}
|
||||
</AppText>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<AppText style={styles.errorText} variant="caption">
|
||||
{error}
|
||||
</AppText>
|
||||
) : null}
|
||||
</View>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
|
||||
value={inputValue}
|
||||
onChangeText={handleChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.placeholder.color}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
multiline={true}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
autoFocus={Platform.OS !== 'android'}
|
||||
focusable={Platform.OS !== 'android' || inputFocusable}
|
||||
selectTextOnFocus={true}
|
||||
accessible={true}
|
||||
accessibilityLabel={label || placeholder}
|
||||
accessibilityRole="text"
|
||||
/>
|
||||
|
||||
<View style={styles.buttonsRow}>
|
||||
<AppButton title={cancelText} onPress={handleCancel} style={styles.cancelButton} textStyle={styles.cancelButtonText} />
|
||||
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} />
|
||||
</View>
|
||||
{error ? (
|
||||
<AppText style={styles.errorText} variant="caption">
|
||||
{error}
|
||||
</AppText>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonsRow}>
|
||||
<AppButton
|
||||
title={cancelText}
|
||||
onPress={handleCancel}
|
||||
style={styles.cancelButton}
|
||||
textStyle={styles.cancelButtonText}
|
||||
focusable={cancelButtonFocusable}
|
||||
/>
|
||||
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} focusable={confirmButtonFocusable} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<View style={[StyleSheet.absoluteFill, { zIndex: OVERLAY_Z, elevation: OVERLAY_Z }]} pointerEvents="auto">
|
||||
<View style={styles.backdrop} pointerEvents="auto">
|
||||
{dialogContent}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal transparent={true} animationType="fade" statusBarTranslucent={true} visible={true} onRequestClose={handleRequestClose}>
|
||||
<View style={styles.backdrop}>{dialogContent}</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -153,4 +243,4 @@ function InputDialog({
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = InputDialog;
|
||||
module.exports = React.forwardRef(InputDialog);
|
||||
|
||||
@ -31,6 +31,8 @@ const PasswordInput = React.forwardRef(function PasswordInput(
|
||||
style,
|
||||
inputStyle,
|
||||
labelStyle,
|
||||
onFocus: onFocusProp,
|
||||
onBlur: onBlurProp,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
@ -50,15 +52,23 @@ const PasswordInput = React.forwardRef(function PasswordInput(
|
||||
[ref]
|
||||
);
|
||||
|
||||
//Обработчик фокуса
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
//Обработчик фокуса (внутренний + опциональный внешний)
|
||||
const handleFocus = React.useCallback(
|
||||
e => {
|
||||
setIsFocused(true);
|
||||
if (typeof onFocusProp === 'function') onFocusProp(e);
|
||||
},
|
||||
[onFocusProp]
|
||||
);
|
||||
|
||||
//Обработчик потери фокуса
|
||||
const handleBlur = React.useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
//Обработчик потери фокуса (внутренний + опциональный внешний)
|
||||
const handleBlur = React.useCallback(
|
||||
e => {
|
||||
setIsFocused(false);
|
||||
if (typeof onBlurProp === 'function') onBlurProp(e);
|
||||
},
|
||||
[onBlurProp]
|
||||
);
|
||||
|
||||
//Обработчик переключения видимости пароля
|
||||
const handleTogglePassword = React.useCallback(() => {
|
||||
|
||||
63
rn/app/src/components/layout/AppIdleProvider.js
Normal file
63
rn/app/src/components/layout/AppIdleProvider.js
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Провайдер отслеживания простоя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const useIdleTimeout = require('../../hooks/useIdleTimeout'); //Хук простоя
|
||||
const styles = require('../../styles/layout/AppIdleProvider.styles'); //Стили обёртки
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Контекст простоя (reportActivity для сброса таймера)
|
||||
const AppIdleContext = React.createContext(null);
|
||||
|
||||
//Провайдер простоя: оборачивает приложение и при отсутствии активности выполняет выход
|
||||
function AppIdleProvider({ children }) {
|
||||
const { reportActivity } = useIdleTimeout();
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
reportActivity
|
||||
}),
|
||||
[reportActivity]
|
||||
);
|
||||
|
||||
//Обёртка по touch сбрасывает таймер простоя
|
||||
const handleTouch = React.useCallback(() => {
|
||||
reportActivity();
|
||||
}, [reportActivity]);
|
||||
|
||||
return (
|
||||
<AppIdleContext.Provider value={value}>
|
||||
<View style={styles.touchArea} onTouchStart={handleTouch} onTouchEnd={handleTouch} collapsable={false}>
|
||||
{children}
|
||||
</View>
|
||||
</AppIdleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
//Хук контекста простоя
|
||||
function useAppIdleContext() {
|
||||
const context = React.useContext(AppIdleContext);
|
||||
if (context == null) {
|
||||
throw new Error('useAppIdleContext должен использоваться внутри AppIdleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
AppIdleProvider,
|
||||
useAppIdleContext
|
||||
};
|
||||
@ -25,16 +25,12 @@ function AppLocalDbProvider({ children }) {
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
isDbReady: api.isDbReady,
|
||||
inspections: api.inspections,
|
||||
error: api.error,
|
||||
loadInspections: api.loadInspections,
|
||||
saveInspection: api.saveInspection,
|
||||
getSetting: api.getSetting,
|
||||
setSetting: api.setSetting,
|
||||
deleteSetting: api.deleteSetting,
|
||||
getAllSettings: api.getAllSettings,
|
||||
clearSettings: api.clearSettings,
|
||||
clearInspections: api.clearInspections,
|
||||
vacuum: api.vacuum,
|
||||
checkTableExists: api.checkTableExists,
|
||||
setAuthSession: api.setAuthSession,
|
||||
@ -43,16 +39,12 @@ function AppLocalDbProvider({ children }) {
|
||||
}),
|
||||
[
|
||||
api.isDbReady,
|
||||
api.inspections,
|
||||
api.error,
|
||||
api.loadInspections,
|
||||
api.saveInspection,
|
||||
api.getSetting,
|
||||
api.setSetting,
|
||||
api.deleteSetting,
|
||||
api.getAllSettings,
|
||||
api.clearSettings,
|
||||
api.clearInspections,
|
||||
api.vacuum,
|
||||
api.checkTableExists,
|
||||
api.setAuthSession,
|
||||
|
||||
@ -11,6 +11,7 @@ const React = require('react'); //React и хуки
|
||||
const { useColorScheme, View } = require('react-native'); //Определение темы устройства и разметка
|
||||
const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
|
||||
const AppShell = require('./AppShell'); //Оболочка приложения
|
||||
const HardwareScannerProvider = require('./HardwareScannerProvider').HardwareScannerProvider; //Встроенный сканер
|
||||
const styles = require('../../styles/layout/AppRoot.styles'); //Стили корневого layout
|
||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
||||
@ -54,7 +55,9 @@ function AppRoot() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<View style={styles.container}>
|
||||
<AppShell isDarkMode={isDarkMode} />
|
||||
<HardwareScannerProvider>
|
||||
<AppShell isDarkMode={isDarkMode} />
|
||||
</HardwareScannerProvider>
|
||||
</View>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
|
||||
@ -11,6 +11,7 @@ const React = require('react'); //React
|
||||
const { StatusBar, Platform } = require('react-native'); //Базовые компоненты
|
||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
||||
const { useAppIdleContext } = require('./AppIdleProvider'); //Контекст простоя
|
||||
const MainScreen = require('../../screens/MainScreen'); //Главный экран
|
||||
const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
|
||||
const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации
|
||||
@ -27,6 +28,12 @@ const styles = require('../../styles/layout/AppShell.styles'); //Стили об
|
||||
function AppShell({ isDarkMode }) {
|
||||
const { currentScreen, SCREENS } = useAppNavigationContext();
|
||||
const { isStartupSessionCheckInProgress, isLogoutInProgress } = useAppAuthContext();
|
||||
const { reportActivity } = useAppIdleContext();
|
||||
|
||||
//Сброс таймера простоя при смене экрана (навигация считается активностью)
|
||||
React.useEffect(() => {
|
||||
reportActivity();
|
||||
}, [currentScreen, reportActivity]);
|
||||
|
||||
const renderScreen = React.useCallback(() => {
|
||||
switch (currentScreen) {
|
||||
|
||||
97
rn/app/src/components/layout/HardwareScannerProvider.js
Normal file
97
rn/app/src/components/layout/HardwareScannerProvider.js
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Провайдер встроенного сканера: маршрутизация данных по текущему экрану
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||
const HardwareScannerBridge = require('../../services/HardwareScannerBridge'); //Мост встроенного сканера
|
||||
|
||||
//---------
|
||||
//Контекст
|
||||
//---------
|
||||
|
||||
const HardwareScannerContext = React.createContext(null);
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Вызов обработчика для текущего экрана при получении данных от встроенного сканера
|
||||
function dispatchHardwareScan(currentScreen, handlersRef, barcode) {
|
||||
const handlers = handlersRef.current;
|
||||
const hasHandler = handlers && typeof handlers[currentScreen] === 'function';
|
||||
if (!handlers || !hasHandler) return;
|
||||
try {
|
||||
handlers[currentScreen](barcode);
|
||||
} catch (_err) {}
|
||||
}
|
||||
|
||||
//Провайдер встроенного сканера
|
||||
function HardwareScannerProvider({ children }) {
|
||||
const { currentScreen, SCREENS } = useAppNavigationContext();
|
||||
const handlersRef = React.useRef(Object.create(null));
|
||||
const currentScreenRef = React.useRef(currentScreen);
|
||||
const unsubscribeRef = React.useRef(null);
|
||||
|
||||
currentScreenRef.current = currentScreen;
|
||||
|
||||
const registerHandler = React.useCallback(function registerHandler(screen, handler) {
|
||||
if (!screen || typeof handler !== 'function') return;
|
||||
handlersRef.current[screen] = handler;
|
||||
}, []);
|
||||
|
||||
const unregisterHandler = React.useCallback(function unregisterHandler(screen) {
|
||||
if (!screen) return;
|
||||
delete handlersRef.current[screen];
|
||||
}, []);
|
||||
|
||||
React.useEffect(function subscribeToHardwareScanner() {
|
||||
const unsubscribe = HardwareScannerBridge.startHardwareListener(function onData(barcode) {
|
||||
const screen = currentScreenRef.current;
|
||||
dispatchHardwareScan(screen, handlersRef, barcode);
|
||||
});
|
||||
unsubscribeRef.current = unsubscribe;
|
||||
return function cleanup() {
|
||||
if (typeof unsubscribeRef.current === 'function') {
|
||||
unsubscribeRef.current();
|
||||
}
|
||||
unsubscribeRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo(
|
||||
function getValue() {
|
||||
return {
|
||||
registerHandler,
|
||||
unregisterHandler,
|
||||
SCREENS
|
||||
};
|
||||
},
|
||||
[registerHandler, unregisterHandler, SCREENS]
|
||||
);
|
||||
|
||||
return <HardwareScannerContext.Provider value={value}>{children}</HardwareScannerContext.Provider>;
|
||||
}
|
||||
|
||||
//Хук доступа к контексту встроенного сканера
|
||||
function useHardwareScannerContext() {
|
||||
const ctx = React.useContext(HardwareScannerContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useHardwareScannerContext должен использоваться внутри HardwareScannerProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
HardwareScannerProvider,
|
||||
useHardwareScannerContext
|
||||
};
|
||||
136
rn/app/src/components/menu/MenuItemIcon.js
Normal file
136
rn/app/src/components/menu/MenuItemIcon.js
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Компонент иконки пункта бокового меню
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const AppText = require('../common/AppText'); //Текст приложения
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема
|
||||
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGIN, MENU_ITEM_ID_LOGOUT } = require('../../config/menuItemIds'); //ID пунктов меню
|
||||
const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
const styles = require('../../styles/menu/MenuItemIcon.styles'); //Стили иконки
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Количество зубцов шестерёнки и угол шага (градусы)
|
||||
const SETTINGS_TEETH_COUNT = 6;
|
||||
const SETTINGS_TEETH_ANGLE_STEP = 360 / SETTINGS_TEETH_COUNT;
|
||||
|
||||
//Формирование массива элементов зубцов шестерёнки
|
||||
function getTeethViews(teeth, iconColor) {
|
||||
const views = [];
|
||||
for (let index = 0; index < teeth.length; index++) {
|
||||
const angle = teeth[index];
|
||||
views.push(
|
||||
<View
|
||||
key={`tooth-${index}`}
|
||||
style={[
|
||||
styles.settingsTooth,
|
||||
{ backgroundColor: iconColor },
|
||||
{
|
||||
transform: [{ rotate: `${angle}deg` }, { translateY: -responsiveSize(8) }]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
//Иконка «Настройки» (шестерёнка)
|
||||
function SettingsIcon({ color }) {
|
||||
const iconColor = color ?? APP_COLORS.textPrimary;
|
||||
|
||||
const teeth = React.useMemo(() => {
|
||||
return Array.from({ length: SETTINGS_TEETH_COUNT }, (_, i) => i * SETTINGS_TEETH_ANGLE_STEP);
|
||||
}, []);
|
||||
|
||||
const toothViews = React.useMemo(() => getTeethViews(teeth, iconColor), [teeth, iconColor]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.settingsCircle, { borderColor: iconColor }]} />
|
||||
{toothViews}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Иконка «О приложении» (информация)
|
||||
function AboutIcon({ color }) {
|
||||
const iconColor = color ?? APP_COLORS.textPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.aboutCircle, { borderColor: iconColor }]}>
|
||||
<AppText style={[styles.aboutText, { color: iconColor }]}>i</AppText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Иконка «Вход»
|
||||
function LoginIcon({ color }) {
|
||||
const iconColor = color ?? APP_COLORS.textPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.loginFrame, { borderColor: iconColor }]}>
|
||||
<View style={[styles.loginArrow, { borderLeftColor: iconColor }]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Иконка «Выход»
|
||||
function LogoutIcon({ color }) {
|
||||
const iconColor = color ?? APP_COLORS.textPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.logoutFrame, { borderColor: iconColor }]}>
|
||||
<View style={[styles.logoutArrow, { borderRightColor: iconColor }]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Рендер иконки по имени пункта меню
|
||||
function renderIconByName(name, color) {
|
||||
switch (name) {
|
||||
case MENU_ITEM_ID_SETTINGS:
|
||||
return <SettingsIcon color={color} />;
|
||||
case MENU_ITEM_ID_ABOUT:
|
||||
return <AboutIcon color={color} />;
|
||||
case MENU_ITEM_ID_LOGIN:
|
||||
return <LoginIcon color={color} />;
|
||||
case MENU_ITEM_ID_LOGOUT:
|
||||
return <LogoutIcon color={color} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Компонент иконки пункта меню
|
||||
function MenuItemIcon({ name, color }) {
|
||||
const iconElement = React.useMemo(() => renderIconByName(name, color), [name, color]);
|
||||
|
||||
if (iconElement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return iconElement;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = MenuItemIcon;
|
||||
module.exports.renderIconByName = renderIconByName;
|
||||
@ -10,25 +10,57 @@
|
||||
const React = require('react'); //React
|
||||
const { ScrollView, View } = require('react-native'); //Базовые компоненты
|
||||
const MenuItem = require('./MenuItem'); //Элемент меню
|
||||
const MenuItemIcon = require('./MenuItemIcon'); //Иконка пункта меню
|
||||
const EmptyMenu = require('./EmptyMenu'); //Пустое меню
|
||||
const MenuDivider = require('./MenuDivider'); //Разделитель меню
|
||||
const { MENU_ITEM_ID_DIVIDER } = require('../../config/menuItemIds'); //ID разделителя
|
||||
const styles = require('../../styles/menu/MenuList.styles'); //Стили списка меню
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Иконка для пункта меню
|
||||
function resolveItemIcon(item) {
|
||||
if (item.icon != null) {
|
||||
return item.icon;
|
||||
}
|
||||
if (item.id != null && item.id !== MENU_ITEM_ID_DIVIDER) {
|
||||
return <MenuItemIcon name={item.id} color={item.iconColor} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//Формирование массива элементов списка меню
|
||||
function getMenuListElements(items, onItemPress) {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const elements = [];
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index];
|
||||
if (item.type === 'divider') {
|
||||
elements.push(<MenuDivider key={item.id || `menu-divider-${index}`} />);
|
||||
} else {
|
||||
elements.push(<MenuItemRow key={item.id || `menu-item-${index}`} item={item} index={index} items={items} onItemPress={onItemPress} />);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
//Строка меню с элементом
|
||||
function MenuItemRow({ item, index, items, onItemPress }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
onItemPress(item);
|
||||
}, [item, onItemPress]);
|
||||
|
||||
const icon = React.useMemo(() => resolveItemIcon(item), [item]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<MenuItem
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
icon={icon}
|
||||
onPress={handlePress}
|
||||
isDestructive={item.isDestructive}
|
||||
disabled={item.disabled}
|
||||
@ -54,21 +86,15 @@ function MenuList({ items = [], onClose, style }) {
|
||||
[onClose]
|
||||
);
|
||||
|
||||
const listElements = React.useMemo(() => getMenuListElements(items, handleItemPress), [items, handleItemPress]);
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return <EmptyMenu />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={[styles.scrollView, style]} showsVerticalScrollIndicator={false} bounces={false}>
|
||||
{items.map((item, index) => {
|
||||
//Элемент-разделитель
|
||||
if (item.type === 'divider') {
|
||||
return <MenuDivider key={item.id || `menu-divider-${index}`} />;
|
||||
}
|
||||
|
||||
//Обычный элемент меню
|
||||
return <MenuItemRow key={item.id || `menu-item-${index}`} item={item} index={index} items={items} onItemPress={handleItemPress} />;
|
||||
})}
|
||||
{listElements}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const { View, Platform } = require('react-native'); //Базовые компоненты
|
||||
const { Camera, useCameraDevice, useCodeScanner, useCameraPermission } = require('react-native-vision-camera'); //Камера и сканер кодов
|
||||
const AppText = require('../common/AppText'); //Общий текст
|
||||
const { DEFAULT_CODE_TYPES } = require('../../config/scannerConfig'); //Конфиг сканера
|
||||
@ -68,10 +68,12 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
||||
}
|
||||
}, [hasPermission, requestPermission]);
|
||||
|
||||
const noFocusProps = Platform.OS === 'android' ? { focusable: false } : {};
|
||||
|
||||
//Нет устройства камеры
|
||||
if (device == null) {
|
||||
return (
|
||||
<View style={styles.fallbackContainer}>
|
||||
<View style={styles.fallbackContainer} {...noFocusProps}>
|
||||
<AppText style={styles.fallbackText} variant="body">
|
||||
{NO_CAMERA_MESSAGE}
|
||||
</AppText>
|
||||
@ -82,7 +84,7 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
||||
//Нет разрешения на камеру
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<View style={styles.fallbackContainer}>
|
||||
<View style={styles.fallbackContainer} {...noFocusProps}>
|
||||
<AppText style={styles.fallbackText} variant="body">
|
||||
{PERMISSION_DENIED_MESSAGE}
|
||||
</AppText>
|
||||
@ -90,8 +92,10 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
||||
);
|
||||
}
|
||||
|
||||
const containerProps = Platform.OS === 'android' ? { focusable: false, importantForAccessibility: 'no-hide-descendants' } : {};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.container} {...containerProps}>
|
||||
<Camera style={styles.camera} device={device} isActive={isActive} codeScanner={codeScanner} />
|
||||
</View>
|
||||
);
|
||||
|
||||
34
rn/app/src/components/scanner/CameraPausedOverlay.js
Normal file
34
rn/app/src/components/scanner/CameraPausedOverlay.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Оверлей области камеры на паузе
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const AppText = require('../common/AppText'); //Текст
|
||||
const styles = require('../../styles/scanner/CameraPausedOverlay.styles'); //Стили оверлея
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Оверлей паузы камеры: серая область с прозрачностью и текстом-подсказкой
|
||||
function CameraPausedOverlay({ message }) {
|
||||
return (
|
||||
<View style={styles.container} pointerEvents="box-none">
|
||||
<AppText style={styles.message} variant="body" numberOfLines={3}>
|
||||
{message}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CameraPausedOverlay;
|
||||
@ -8,10 +8,11 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { Modal, View, Pressable } = require('react-native'); //Базовые компоненты
|
||||
const { Modal, View, Pressable, Platform } = require('react-native'); //Базовые компоненты
|
||||
const AppText = require('../common/AppText'); //Общий текстовый компонент
|
||||
const AppButton = require('../common/AppButton'); //Кнопка
|
||||
const { SCAN_RESULT_MODAL_TITLE, SCAN_RESULT_CLOSE_BUTTON } = require('../../config/messages'); //Сообщения
|
||||
const { noop } = require('../../utils/noop'); //Пустая функция
|
||||
const styles = require('../../styles/scanner/ScanResultModal.styles'); //Стили модального окна
|
||||
|
||||
//-----------
|
||||
@ -37,42 +38,89 @@ function handleClose(onRequestClose) {
|
||||
}
|
||||
}
|
||||
|
||||
//Пропсы для элементов, закрывающих модальное окно: на Android отключаем фокус, чтобы Enter от встроенного сканера не вызывал закрытие
|
||||
function getClosePressableProps(preventKeyClose) {
|
||||
return preventKeyClose ? { focusable: false } : {};
|
||||
}
|
||||
|
||||
//Общий контент окна результата
|
||||
function ResultContent({ displayType, displayValue, onClosePress, styles: s, preventKeyClose }) {
|
||||
const backdropProps = getClosePressableProps(preventKeyClose);
|
||||
const closeButtonProps = getClosePressableProps(preventKeyClose);
|
||||
const closeButtonFocusable = preventKeyClose ? false : undefined;
|
||||
|
||||
return (
|
||||
<Pressable style={s.backdrop} onPress={onClosePress} {...backdropProps}>
|
||||
<Pressable style={s.container} onPress={noop} focusable={false}>
|
||||
<View style={s.header}>
|
||||
<AppText style={s.title} numberOfLines={1}>
|
||||
{SCAN_RESULT_MODAL_TITLE}
|
||||
</AppText>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Закрыть"
|
||||
onPress={onClosePress}
|
||||
style={s.closeButton}
|
||||
{...closeButtonProps}
|
||||
>
|
||||
<AppText style={s.closeButtonText}>×</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={s.content}>
|
||||
<AppText style={s.typeLabel} variant="caption" weight="medium">
|
||||
Тип: {displayType}
|
||||
</AppText>
|
||||
<View style={s.valueBlock}>
|
||||
<AppText style={s.valueText} selectable={true}>
|
||||
{displayValue}
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={s.buttonsRow}>
|
||||
<AppButton title={SCAN_RESULT_CLOSE_BUTTON} onPress={onClosePress} focusable={closeButtonFocusable} />
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//Модальное окно результата сканирования
|
||||
function ScanResultModal({ visible, codeType, value, onRequestClose }) {
|
||||
const handleClosePress = React.useCallback(() => {
|
||||
handleClose(onRequestClose);
|
||||
}, [onRequestClose]);
|
||||
|
||||
const modalOnRequestClose = React.useMemo(() => (Platform.OS === 'android' ? noop : handleClosePress), [handleClosePress]);
|
||||
|
||||
const displayType = formatCodeType(codeType);
|
||||
const displayValue = value != null && value !== '' ? String(value) : '—';
|
||||
|
||||
return (
|
||||
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={handleClosePress}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<AppText style={styles.title} numberOfLines={1}>
|
||||
{SCAN_RESULT_MODAL_TITLE}
|
||||
</AppText>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleClosePress} style={styles.closeButton}>
|
||||
<AppText style={styles.closeButtonText}>×</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<AppText style={styles.typeLabel} variant="caption" weight="medium">
|
||||
Тип: {displayType}
|
||||
</AppText>
|
||||
<View style={styles.valueBlock}>
|
||||
<AppText style={styles.valueText} selectable={true}>
|
||||
{displayValue}
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={styles.buttonsRow}>
|
||||
<AppButton title={SCAN_RESULT_CLOSE_BUTTON} onPress={handleClosePress} />
|
||||
</View>
|
||||
</View>
|
||||
const preventKeyClose = Platform.OS === 'android';
|
||||
|
||||
const content = (
|
||||
<ResultContent
|
||||
displayType={displayType}
|
||||
displayValue={displayValue}
|
||||
onClosePress={handleClosePress}
|
||||
styles={styles}
|
||||
preventKeyClose={preventKeyClose}
|
||||
/>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<View style={styles.overlay} focusable={false} importantForAccessibility="no-hide-descendants" collapsable={false}>
|
||||
<View style={styles.overlayContent} pointerEvents="box-none">
|
||||
{content}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={modalOnRequestClose} statusBarTranslucent={true}>
|
||||
<View style={styles.modalRoot}>{content}</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { View, Modal, Pressable } = require('react-native'); //Базовые компоненты
|
||||
const { View, Modal, Pressable, Platform } = require('react-native'); //Базовые компоненты
|
||||
const BarcodeScanner = require('./BarcodeScanner'); //Сканер
|
||||
const ScannerPlaceholder = require('./ScannerPlaceholder'); //Заглушка с кнопкой
|
||||
const CameraPausedOverlay = require('./CameraPausedOverlay'); //Оверлей «камера на паузе»
|
||||
const AppText = require('../common/AppText'); //Текст
|
||||
const { SCANNER_CAMERA_TAP_TO_RESUME } = require('../../config/messages'); //Сообщения
|
||||
const styles = require('../../styles/scanner/ScannerArea.styles'); //Стили области
|
||||
|
||||
//-----------
|
||||
@ -29,7 +31,16 @@ function handleScanResult(result, onScanResult, closeModal) {
|
||||
}
|
||||
|
||||
//Область сканера: при включённой настройке — активный сканер (если открыт), иначе заглушка и модальный сканер по кнопке
|
||||
function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResult }) {
|
||||
function ScannerArea({
|
||||
alwaysShowScanner = false,
|
||||
scannerOpen = true,
|
||||
cameraActive = true,
|
||||
onScanResult,
|
||||
onResumeCameraPress,
|
||||
placeholderHint,
|
||||
placeholderSecondaryLabel,
|
||||
placeholderSecondaryOnPress
|
||||
}) {
|
||||
const [scannerModalVisible, setScannerModalVisible] = React.useState(false);
|
||||
|
||||
const handleScanFromArea = React.useCallback(
|
||||
@ -56,9 +67,33 @@ function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResu
|
||||
|
||||
const renderScannerContent = () => {
|
||||
if (alwaysShowScanner && scannerOpen) {
|
||||
return <BarcodeScanner isActive={true} onScan={handleScanFromArea} />;
|
||||
const wrapperProps = Platform.OS === 'android' ? { focusable: false, importantForAccessibility: 'no-hide-descendants' } : {};
|
||||
const showPausedOverlay = !cameraActive && typeof onResumeCameraPress === 'function';
|
||||
return (
|
||||
<View style={styles.scannerWrapper} {...wrapperProps}>
|
||||
<BarcodeScanner isActive={cameraActive} onScan={handleScanFromArea} />
|
||||
{showPausedOverlay ? (
|
||||
<Pressable
|
||||
style={styles.cameraPausedOverlayTouchable}
|
||||
onPress={onResumeCameraPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Включить камеру"
|
||||
{...(Platform.OS === 'android' ? { focusable: false } : {})}
|
||||
>
|
||||
<CameraPausedOverlay message={SCANNER_CAMERA_TAP_TO_RESUME} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return <ScannerPlaceholder onScanPress={handleOpenScanner} />;
|
||||
return (
|
||||
<ScannerPlaceholder
|
||||
onScanPress={handleOpenScanner}
|
||||
hintText={placeholderHint}
|
||||
secondaryButtonTitle={placeholderSecondaryLabel}
|
||||
secondaryButtonOnPress={placeholderSecondaryOnPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const AppButton = require('../common/AppButton'); //Кнопка
|
||||
const AppText = require('../common/AppText'); //Текст
|
||||
const { SCAN_BUTTON_TITLE } = require('../../config/messages'); //Сообщения
|
||||
const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Стили заглушки
|
||||
|
||||
@ -17,17 +18,31 @@ const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Ст
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Заглушка: затемнённая область и кнопка открытия сканера
|
||||
function ScannerPlaceholder({ onScanPress }) {
|
||||
//Заглушка: затемнённая область и кнопка открытия сканера; опционально подсказка и вторая кнопка (режим встроенного сканера)
|
||||
function ScannerPlaceholder({ onScanPress, hintText, secondaryButtonTitle, secondaryButtonOnPress }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
if (typeof onScanPress === 'function') {
|
||||
onScanPress();
|
||||
}
|
||||
}, [onScanPress]);
|
||||
|
||||
const handleSecondary = React.useCallback(() => {
|
||||
if (typeof secondaryButtonOnPress === 'function') {
|
||||
secondaryButtonOnPress();
|
||||
}
|
||||
}, [secondaryButtonOnPress]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{hintText ? (
|
||||
<AppText style={styles.hint} variant="body" numberOfLines={2}>
|
||||
{hintText}
|
||||
</AppText>
|
||||
) : null}
|
||||
<AppButton title={SCAN_BUTTON_TITLE} onPress={handlePress} style={styles.button} />
|
||||
{secondaryButtonTitle && typeof secondaryButtonOnPress === 'function' ? (
|
||||
<AppButton title={secondaryButtonTitle} onPress={handleSecondary} style={styles.secondaryButton} />
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ const UI = {
|
||||
|
||||
//Высоты элементов
|
||||
BUTTON_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
|
||||
INPUT_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
|
||||
INPUT_HEIGHT: responsiveSize(56),
|
||||
HEADER_HEIGHT: responsiveSize(isTablet() ? 80 : Platform.OS === 'ios' ? 70 : 56)
|
||||
};
|
||||
|
||||
@ -43,7 +43,7 @@ const COMPATIBILITY = {
|
||||
if (Platform.OS !== 'android') return true;
|
||||
|
||||
const majorVersion = parseInt(Platform.Version, 10);
|
||||
return majorVersion >= 24; // Android 7.0 = API 24
|
||||
return majorVersion >= 24; //Android 7.0 = API 24
|
||||
},
|
||||
|
||||
//Проверяем версию iOS
|
||||
@ -51,7 +51,7 @@ const COMPATIBILITY = {
|
||||
if (Platform.OS !== 'ios') return true;
|
||||
|
||||
const majorVersion = parseInt(Platform.Version, 10);
|
||||
return majorVersion >= 11; // iOS 11.0
|
||||
return majorVersion >= 11; //iOS 11.0
|
||||
},
|
||||
|
||||
//Общая проверка совместимости
|
||||
|
||||
@ -13,16 +13,18 @@ const AUTH_SETTINGS_KEYS = {
|
||||
LAST_CONNECTED_SERVER_URL: 'auth_last_connected_server_url',
|
||||
HIDE_SERVER_URL: 'auth_hide_server_url',
|
||||
IDLE_TIMEOUT: 'auth_idle_timeout',
|
||||
SERVER_REQUEST_TIMEOUT: 'auth_server_request_timeout',
|
||||
DEVICE_ID: 'auth_device_id',
|
||||
DEVICE_SECRET_KEY: 'auth_device_secret_key',
|
||||
SAVED_LOGIN: 'auth_saved_login',
|
||||
SAVED_PASSWORD: 'auth_saved_password',
|
||||
SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled',
|
||||
ALWAYS_SHOW_SCANNER: 'main_always_show_scanner'
|
||||
ALWAYS_SHOW_SCANNER: 'main_always_show_scanner',
|
||||
MAIN_SCANNER_PRIORITY: 'main_scanner_priority'
|
||||
};
|
||||
|
||||
//Значение времени простоя по умолчанию (минуты)
|
||||
const DEFAULT_IDLE_TIMEOUT = 30;
|
||||
//Значение времени ожидания ответа от сервера по умолчанию (секунды)
|
||||
const DEFAULT_SERVER_REQUEST_TIMEOUT = 60;
|
||||
|
||||
//Ключи настроек подключения
|
||||
const CONNECTION_SETTINGS_KEYS = [
|
||||
@ -42,5 +44,5 @@ const CONNECTION_SETTINGS_KEYS = [
|
||||
module.exports = {
|
||||
AUTH_SETTINGS_KEYS,
|
||||
CONNECTION_SETTINGS_KEYS,
|
||||
DEFAULT_IDLE_TIMEOUT
|
||||
DEFAULT_SERVER_REQUEST_TIMEOUT
|
||||
};
|
||||
|
||||
27
rn/app/src/config/authFormFieldsConfig.js
Normal file
27
rn/app/src/config/authFormFieldsConfig.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация полей формы аутентификации
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Порядок полей формы аутентификации
|
||||
const AUTH_FORM_FIELD_ORDER = ['server', 'login', 'password'];
|
||||
|
||||
//Идентификаторы полей
|
||||
const AUTH_FORM_FIELD_ID = {
|
||||
SERVER: 'server',
|
||||
LOGIN: 'login',
|
||||
PASSWORD: 'password'
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
AUTH_FORM_FIELD_ORDER,
|
||||
AUTH_FORM_FIELD_ID
|
||||
};
|
||||
19
rn/app/src/config/inputDeviceConfig.js
Normal file
19
rn/app/src/config/inputDeviceConfig.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация совместной работы устройств ввода (клавиатура, встроенный сканер)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Режим подстановки данных сканера в поле: всегда замена текущего значения
|
||||
const SCAN_INPUT_MODE_REPLACE = 'replace';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
SCAN_INPUT_MODE_REPLACE
|
||||
};
|
||||
35
rn/app/src/config/menuItemIds.js
Normal file
35
rn/app/src/config/menuItemIds.js
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Идентификаторы пунктов бокового меню
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Пункт меню «Настройки»
|
||||
const MENU_ITEM_ID_SETTINGS = 'settings';
|
||||
|
||||
//Пункт меню «О приложении»
|
||||
const MENU_ITEM_ID_ABOUT = 'about';
|
||||
|
||||
//Пункт меню «Вход»
|
||||
const MENU_ITEM_ID_LOGIN = 'login';
|
||||
|
||||
//Пункт меню «Выход»
|
||||
const MENU_ITEM_ID_LOGOUT = 'logout';
|
||||
|
||||
//Разделитель в меню (не пункт, служебный тип)
|
||||
const MENU_ITEM_ID_DIVIDER = 'divider';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
MENU_ITEM_ID_SETTINGS,
|
||||
MENU_ITEM_ID_ABOUT,
|
||||
MENU_ITEM_ID_LOGIN,
|
||||
MENU_ITEM_ID_LOGOUT,
|
||||
MENU_ITEM_ID_DIVIDER
|
||||
};
|
||||
@ -13,6 +13,9 @@ const CONNECTION_LOST_MESSAGE = 'Нет связи с сервером. Прил
|
||||
//Заголовок сообщения при переходе в режим офлайн
|
||||
const OFFLINE_MODE_TITLE = 'Режим офлайн';
|
||||
|
||||
//Сообщение при переходе в офлайн по таймауту простоя
|
||||
const IDLE_TIMEOUT_OFFLINE_MESSAGE = 'Сессия закрыта по таймауту простоя. Приложение переведено в режим офлайн.';
|
||||
|
||||
//Сообщение при проверке соединения при старте приложения
|
||||
const STARTUP_CHECK_CONNECTION_MESSAGE = 'Проверка соединения...';
|
||||
|
||||
@ -35,7 +38,10 @@ const MENU_ITEM_LOGOUT = 'Выход';
|
||||
const AUTH_SCREEN_TITLE = 'Вход в приложение';
|
||||
const AUTH_BUTTON_LOGIN = 'Войти';
|
||||
const AUTH_BUTTON_LOADING = 'Вход...';
|
||||
const AUTH_BUTTON_LOGOUT = 'Выйти';
|
||||
const LOGOUT_IN_PROGRESS_MESSAGE = 'Выход...';
|
||||
const LOGOUT_CONFIRM_TITLE = 'Подтверждение выхода';
|
||||
const LOGOUT_CONFIRM_MESSAGE = 'Вы уверены, что хотите выйти?';
|
||||
|
||||
//Диалог подтверждения при смене сервера (относительно последнего успешного подключения)
|
||||
const AUTH_SERVER_CHANGE_CONFIRM_TITLE = 'Подтверждение входа';
|
||||
@ -52,10 +58,16 @@ const SCREEN_TITLE_SETTINGS = 'Настройки';
|
||||
|
||||
//Сканер на главном экране
|
||||
const SCANNER_SETTING_LABEL = 'Всегда отображать сканер на главном экране';
|
||||
const SCANNER_PRIORITY_LABEL = 'Приоритет на главном экране';
|
||||
const SCANNER_PRIORITY_CAMERA = 'Камера';
|
||||
const SCANNER_PRIORITY_HARDWARE = 'Встроенный сканер';
|
||||
const SCAN_BUTTON_TITLE = 'Сканировать';
|
||||
const SCAN_RESULT_MODAL_TITLE = 'Результат сканирования';
|
||||
const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
|
||||
|
||||
//Оверлей паузы камеры на главном экране: подсказка включить камеру по тапу
|
||||
const SCANNER_CAMERA_TAP_TO_RESUME = 'Нажмите на область камеры для её включения';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
@ -63,6 +75,7 @@ const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
|
||||
module.exports = {
|
||||
CONNECTION_LOST_MESSAGE,
|
||||
OFFLINE_MODE_TITLE,
|
||||
IDLE_TIMEOUT_OFFLINE_MESSAGE,
|
||||
STARTUP_CHECK_CONNECTION_MESSAGE,
|
||||
APP_ABOUT_TITLE,
|
||||
SIDE_MENU_TITLE,
|
||||
@ -74,7 +87,10 @@ module.exports = {
|
||||
AUTH_SCREEN_TITLE,
|
||||
AUTH_BUTTON_LOGIN,
|
||||
AUTH_BUTTON_LOADING,
|
||||
AUTH_BUTTON_LOGOUT,
|
||||
LOGOUT_IN_PROGRESS_MESSAGE,
|
||||
LOGOUT_CONFIRM_TITLE,
|
||||
LOGOUT_CONFIRM_MESSAGE,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||
@ -83,7 +99,11 @@ module.exports = {
|
||||
SETTINGS_RESET_SUCCESS_MESSAGE,
|
||||
SCREEN_TITLE_SETTINGS,
|
||||
SCANNER_SETTING_LABEL,
|
||||
SCANNER_PRIORITY_LABEL,
|
||||
SCANNER_PRIORITY_CAMERA,
|
||||
SCANNER_PRIORITY_HARDWARE,
|
||||
SCAN_BUTTON_TITLE,
|
||||
SCAN_RESULT_MODAL_TITLE,
|
||||
SCAN_RESULT_CLOSE_BUTTON
|
||||
SCAN_RESULT_CLOSE_BUTTON,
|
||||
SCANNER_CAMERA_TAP_TO_RESUME
|
||||
};
|
||||
|
||||
@ -10,8 +10,24 @@
|
||||
//Типы кодов для распознавания: QR и распространённые штрихкоды
|
||||
const DEFAULT_CODE_TYPES = ['qr', 'code-128', 'code-39', 'ean-13', 'ean-8', 'upc-a', 'upc-e', 'pdf-417', 'aztec', 'data-matrix'];
|
||||
|
||||
//Встроенный сканер устройства: имя события от нативного модуля (Android)
|
||||
const HARDWARE_SCANNER_EVENT_NAME = 'HardwareScannerData';
|
||||
//Событие нажатия кнопки встроенного сканера (142/141) — для временного скрытия камеры и перехода в режим «ввод от сканера»
|
||||
const HARDWARE_SCANNER_TRIGGER_EVENT_NAME = 'HardwareScannerTriggerPressed';
|
||||
|
||||
//Диапазоны символов, допустимых в данных штрихкодов/QR: ASCII printable + Latin-1
|
||||
const HARDWARE_SCANNER_CHAR_ASCII_MIN = 32;
|
||||
const HARDWARE_SCANNER_CHAR_ASCII_MAX = 126;
|
||||
const HARDWARE_SCANNER_CHAR_LATIN1_MIN = 160;
|
||||
const HARDWARE_SCANNER_CHAR_LATIN1_MAX = 255;
|
||||
|
||||
//Длительность (мс) скрытия камеры после нажатия кнопки встроенного сканера, чтобы прошивка успела включить лазер
|
||||
const HARDWARE_SCANNER_CAMERA_HIDE_MS = 3500;
|
||||
|
||||
//Определение, должен ли отображаться сканер на главном экране
|
||||
function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInProgress }) {
|
||||
function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInProgress, isOfflineMessageVisible, isAboutMessageVisible }) {
|
||||
if (isOfflineMessageVisible) return false;
|
||||
if (isAboutMessageVisible) return false;
|
||||
return Boolean(alwaysShowScanner && !hasScanResult && !isStartupCheckInProgress);
|
||||
}
|
||||
|
||||
@ -21,5 +37,12 @@ function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInP
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_CODE_TYPES,
|
||||
HARDWARE_SCANNER_EVENT_NAME,
|
||||
HARDWARE_SCANNER_TRIGGER_EVENT_NAME,
|
||||
HARDWARE_SCANNER_CAMERA_HIDE_MS,
|
||||
HARDWARE_SCANNER_CHAR_ASCII_MIN,
|
||||
HARDWARE_SCANNER_CHAR_ASCII_MAX,
|
||||
HARDWARE_SCANNER_CHAR_LATIN1_MIN,
|
||||
HARDWARE_SCANNER_CHAR_LATIN1_MAX,
|
||||
shouldShowScanner
|
||||
};
|
||||
|
||||
@ -44,7 +44,6 @@ class SQLiteDatabase {
|
||||
this.isInitialized = true;
|
||||
return this.db;
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации базы данных:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -59,15 +58,8 @@ class SQLiteDatabase {
|
||||
//Выполняем SQL запросы последовательно
|
||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_APP_SETTINGS);
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_INSPECTIONS);
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_AUTH_SESSION);
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS);
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_CREATED);
|
||||
} catch (error) {
|
||||
console.error('Ошибка настройки базы данных:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -82,7 +74,6 @@ class SQLiteDatabase {
|
||||
const result = await this.db.executeAsync(sql, params);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Ошибка выполнения SQL запроса:', error, 'SQL:', sql, 'Params:', params);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -97,7 +88,6 @@ class SQLiteDatabase {
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения настройки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -109,7 +99,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.SETTINGS_SET, [key, stringValue]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настройки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -120,7 +109,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.SETTINGS_DELETE, [key]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления настройки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -144,7 +132,6 @@ class SQLiteDatabase {
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения всех настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -155,126 +142,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.SETTINGS_CLEAR_ALL, []);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Сохранение осмотра
|
||||
async saveInspection(inspection) {
|
||||
try {
|
||||
const { id, title, status, data } = inspection;
|
||||
const dataString = data ? JSON.stringify(data) : null;
|
||||
|
||||
await this.executeQuery(this.sqlQueries.INSPECTIONS_UPSERT, [id, title, status, id, dataString]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения осмотра:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Получение всех осмотров
|
||||
async getInspections() {
|
||||
try {
|
||||
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_ALL, []);
|
||||
const inspections = [];
|
||||
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
for (let i = 0; i < result.rows.length; i++) {
|
||||
const row = result.rows.item(i);
|
||||
const inspection = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
};
|
||||
|
||||
if (row.data) {
|
||||
try {
|
||||
inspection.data = JSON.parse(row.data);
|
||||
} catch {
|
||||
inspection.data = row.data;
|
||||
}
|
||||
}
|
||||
|
||||
inspections.push(inspection);
|
||||
}
|
||||
}
|
||||
|
||||
return inspections;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения осмотров:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Получение осмотра по ID
|
||||
async getInspectionById(id) {
|
||||
try {
|
||||
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_BY_ID, [id]);
|
||||
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
const row = result.rows.item(0);
|
||||
const inspection = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
status: row.status,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
};
|
||||
|
||||
if (row.data) {
|
||||
try {
|
||||
inspection.data = JSON.parse(row.data);
|
||||
} catch {
|
||||
inspection.data = row.data;
|
||||
}
|
||||
}
|
||||
|
||||
return inspection;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения осмотра по ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Удаление осмотра
|
||||
async deleteInspection(id) {
|
||||
try {
|
||||
await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE, [id]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления осмотра:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Удаление всех осмотров
|
||||
async clearInspections() {
|
||||
try {
|
||||
await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE_ALL, []);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки осмотров:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Получение количества осмотров
|
||||
async getInspectionsCount() {
|
||||
try {
|
||||
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_COUNT, []);
|
||||
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
return result.rows.item(0).count;
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения количества осмотров:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -285,7 +152,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.UTILITY_VACUUM, []);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка оптимизации базы данных:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -316,7 +182,6 @@ class SQLiteDatabase {
|
||||
]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -343,7 +208,6 @@ class SQLiteDatabase {
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -354,7 +218,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.AUTH_SESSION_CLEAR, []);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -365,7 +228,6 @@ class SQLiteDatabase {
|
||||
const result = await this.executeQuery(this.sqlQueries.UTILITY_CHECK_TABLE, [tableName]);
|
||||
return result.rows && result.rows.length > 0 && result.rows.item(0).exists === 1;
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки существования таблицы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -376,7 +238,6 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.UTILITY_DROP_TABLE, [tableName]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления таблицы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -385,13 +246,12 @@ class SQLiteDatabase {
|
||||
async close() {
|
||||
try {
|
||||
if (this.db) {
|
||||
// В react-native-quick-sqlite нет явного метода close
|
||||
// База данных закрывается автоматически при уничтожении объекта
|
||||
//В react-native-quick-sqlite нет явного метода close
|
||||
//База данных закрывается автоматически при уничтожении объекта
|
||||
this.db = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка закрытия базы данных:', error);
|
||||
this.db = null;
|
||||
this.isInitialized = false;
|
||||
throw error;
|
||||
|
||||
@ -9,13 +9,8 @@
|
||||
|
||||
//Таблицы
|
||||
const CREATE_TABLE_APP_SETTINGS = require('./settings/create_table_app_settings.sql');
|
||||
const CREATE_TABLE_INSPECTIONS = require('./inspections/create_table_inspections.sql');
|
||||
const CREATE_TABLE_AUTH_SESSION = require('./auth/create_table_auth_session.sql');
|
||||
|
||||
//Индексы
|
||||
const CREATE_INDEX_INSPECTIONS_STATUS = require('./inspections/create_index_inspections_status.sql');
|
||||
const CREATE_INDEX_INSPECTIONS_CREATED = require('./inspections/create_index_inspections_created.sql');
|
||||
|
||||
//Настройки
|
||||
const SETTINGS_GET = require('./settings/get_setting.sql');
|
||||
const SETTINGS_SET = require('./settings/set_setting.sql');
|
||||
@ -23,15 +18,6 @@ const SETTINGS_DELETE = require('./settings/delete_setting.sql');
|
||||
const SETTINGS_CLEAR_ALL = require('./settings/clear_all_settings.sql');
|
||||
const SETTINGS_GET_ALL = require('./settings/get_all_settings.sql');
|
||||
|
||||
//Осмотры
|
||||
const INSPECTIONS_INSERT = require('./inspections/insert_inspection.sql');
|
||||
const INSPECTIONS_UPSERT = require('./inspections/upsert_inspection.sql');
|
||||
const INSPECTIONS_GET_ALL = require('./inspections/get_all_inspections.sql');
|
||||
const INSPECTIONS_GET_BY_ID = require('./inspections/get_inspection_by_id.sql');
|
||||
const INSPECTIONS_DELETE = require('./inspections/delete_inspection.sql');
|
||||
const INSPECTIONS_DELETE_ALL = require('./inspections/delete_all_inspections.sql');
|
||||
const INSPECTIONS_COUNT = require('./inspections/count_inspections.sql');
|
||||
|
||||
//Авторизация
|
||||
const AUTH_SESSION_SET = require('./auth/set_auth_session.sql');
|
||||
const AUTH_SESSION_GET = require('./auth/get_auth_session.sql');
|
||||
@ -49,22 +35,12 @@ const UTILITY_VACUUM = require('./utility/vacuum.sql');
|
||||
//Сбор всех SQL запросов в один объект
|
||||
const SQLQueries = {
|
||||
CREATE_TABLE_APP_SETTINGS,
|
||||
CREATE_TABLE_INSPECTIONS,
|
||||
CREATE_TABLE_AUTH_SESSION,
|
||||
CREATE_INDEX_INSPECTIONS_STATUS,
|
||||
CREATE_INDEX_INSPECTIONS_CREATED,
|
||||
SETTINGS_GET,
|
||||
SETTINGS_SET,
|
||||
SETTINGS_DELETE,
|
||||
SETTINGS_CLEAR_ALL,
|
||||
SETTINGS_GET_ALL,
|
||||
INSPECTIONS_INSERT,
|
||||
INSPECTIONS_UPSERT,
|
||||
INSPECTIONS_GET_ALL,
|
||||
INSPECTIONS_GET_BY_ID,
|
||||
INSPECTIONS_DELETE,
|
||||
INSPECTIONS_DELETE_ALL,
|
||||
INSPECTIONS_COUNT,
|
||||
AUTH_SESSION_SET,
|
||||
AUTH_SESSION_GET,
|
||||
AUTH_SESSION_CLEAR,
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: подсчет количества осмотров
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_COUNT = `
|
||||
-- Подсчет количества осмотров
|
||||
SELECT COUNT(*) as count FROM inspections;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_COUNT;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: создание индекса по дате создания
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const CREATE_INDEX_INSPECTIONS_CREATED = `
|
||||
-- Индекс для сортировки по дате создания
|
||||
CREATE INDEX IF NOT EXISTS idx_inspections_created ON inspections(created_at DESC);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CREATE_INDEX_INSPECTIONS_CREATED;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: создание индекса по статусу
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const CREATE_INDEX_INSPECTIONS_STATUS = `
|
||||
-- Индекс для быстрого поиска осмотров по статусу
|
||||
CREATE INDEX IF NOT EXISTS idx_inspections_status ON inspections(status);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CREATE_INDEX_INSPECTIONS_STATUS;
|
||||
@ -1,26 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: создание таблицы осмотров
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const CREATE_TABLE_INSPECTIONS = `
|
||||
-- Таблица для предрейсовых осмотров
|
||||
CREATE TABLE IF NOT EXISTS inspections (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
data TEXT
|
||||
);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CREATE_TABLE_INSPECTIONS;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: удаление всех осмотров
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_DELETE_ALL = `
|
||||
-- Удаление всех осмотров
|
||||
DELETE FROM inspections;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_DELETE_ALL;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: удаление осмотра
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_DELETE = `
|
||||
-- Удаление осмотра по ID
|
||||
DELETE FROM inspections WHERE id = ?;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_DELETE;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: получение всех осмотров
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_GET_ALL = `
|
||||
-- Получение всех осмотров
|
||||
SELECT * FROM inspections ORDER BY created_at DESC;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_GET_ALL;
|
||||
@ -1,19 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: получение осмотра по ID
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_GET_BY_ID = `
|
||||
-- Получение осмотра по ID
|
||||
SELECT * FROM inspections WHERE id = ?;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_GET_BY_ID;
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: вставка нового осмотра
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_INSERT = `
|
||||
-- Вставка нового осмотра
|
||||
INSERT INTO inspections (id, title, status, created_at, data)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_INSERT;
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: вставка или обновление осмотра
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const INSPECTIONS_UPSERT = `
|
||||
-- Вставка или обновление осмотра
|
||||
INSERT OR REPLACE INTO inspections (id, title, status, created_at, updated_at, data)
|
||||
VALUES (?, ?, ?, COALESCE((SELECT created_at FROM inspections WHERE id = ?), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP, ?);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = INSPECTIONS_UPSERT;
|
||||
@ -52,7 +52,6 @@ function useAppMode() {
|
||||
}
|
||||
//При отсутствии сохранённого режима остаётся NOT_CONNECTED
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки режима:', error);
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
loadingRef.current = false;
|
||||
@ -71,9 +70,7 @@ function useAppMode() {
|
||||
const saveMode = async () => {
|
||||
try {
|
||||
await setSetting(STORAGE_KEY, mode);
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения режима:', error);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
saveMode();
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
@ -60,9 +61,7 @@ function useAppServer() {
|
||||
let baseURL = '';
|
||||
try {
|
||||
baseURL = (await getSetting('app_server_url')) || '';
|
||||
} catch (e) {
|
||||
console.warn('Не удалось прочитать настройки сервера:', e);
|
||||
}
|
||||
} catch (_e) {}
|
||||
|
||||
if (!baseURL) {
|
||||
return makeRespErr({
|
||||
@ -77,6 +76,14 @@ function useAppServer() {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
|
||||
let timeoutId = null;
|
||||
if (timeoutMs > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
let response = null;
|
||||
let responseJSON = null;
|
||||
|
||||
@ -97,6 +104,9 @@ function useAppServer() {
|
||||
message: `${ERR_NETWORK}: ${e.message || 'неопределённая ошибка'}`
|
||||
});
|
||||
} finally {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
|
||||
|
||||
@ -9,10 +9,11 @@
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации
|
||||
const { ACTION_CODES, RESPONSE_STATES, ERROR_MESSAGES } = require('../config/authApi'); //API авторизации
|
||||
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage'); //Шифрование
|
||||
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId'); //Идентификатор устройства
|
||||
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -79,41 +80,55 @@ function useAuth() {
|
||||
}, [getSetting, setSetting]);
|
||||
|
||||
//Выполнение запроса к серверу
|
||||
const executeRequest = React.useCallback(async (serverUrl, payload) => {
|
||||
//Отменяем предыдущий запрос если есть
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
|
||||
const executeRequest = React.useCallback(
|
||||
async (serverUrl, payload) => {
|
||||
//Отменяем предыдущий запрос если есть
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (fetchError) {
|
||||
if (fetchError.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
||||
} finally {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
let timeoutId = null;
|
||||
try {
|
||||
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
|
||||
if (timeoutMs > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (fetchError) {
|
||||
if (fetchError.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
||||
} finally {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
},
|
||||
[getSetting]
|
||||
);
|
||||
|
||||
//Сохранение credentials после успешной аутентификации
|
||||
const saveCredentials = React.useCallback(
|
||||
@ -131,7 +146,6 @@ function useAuth() {
|
||||
|
||||
return true;
|
||||
} catch (saveError) {
|
||||
console.error('Ошибка сохранения credentials:', saveError);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -182,7 +196,6 @@ function useAuth() {
|
||||
savePasswordEnabled: true
|
||||
};
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки credentials:', loadError);
|
||||
return null;
|
||||
}
|
||||
}, [getSetting, getDeviceId, getSecretKey]);
|
||||
@ -195,7 +208,6 @@ function useAuth() {
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
|
||||
return true;
|
||||
} catch (clearError) {
|
||||
console.error('Ошибка очистки credentials:', clearError);
|
||||
return false;
|
||||
}
|
||||
}, [setSetting]);
|
||||
@ -229,8 +241,8 @@ function useAuth() {
|
||||
requestPayload.XREQUEST.XPAYLOAD.SCOMPANY = company;
|
||||
}
|
||||
|
||||
//Добавляем таймаут (используем значение по умолчанию если не указан)
|
||||
const timeoutValue = timeout && timeout > 0 ? timeout : DEFAULT_IDLE_TIMEOUT;
|
||||
//Таймаут сессии на сервере (минуты)
|
||||
const timeoutValue = timeout != null && Number(timeout) > 0 ? Number(timeout) : 0;
|
||||
requestPayload.XREQUEST.XPAYLOAD.NTIMEOUT = timeoutValue;
|
||||
|
||||
//Выполняем запрос
|
||||
@ -405,16 +417,18 @@ function useAuth() {
|
||||
//Выход из системы
|
||||
const logout = React.useCallback(
|
||||
async (options = {}) => {
|
||||
const { skipServerRequest = false } = options;
|
||||
const { skipServerRequest = false, keepSessionLocally = false } = options;
|
||||
|
||||
setIsLoading(true);
|
||||
setIsLogoutInProgress(true);
|
||||
if (!keepSessionLocally) {
|
||||
setIsLoading(true);
|
||||
setIsLogoutInProgress(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const currentSession = session || (await getAuthSession());
|
||||
|
||||
//Запрос на сервер только при онлайн и если не отключено опцией
|
||||
//Запрос на сервер при наличии сессии и если не отключено опцией
|
||||
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
|
||||
const requestPayload = {
|
||||
XREQUEST: {
|
||||
@ -429,14 +443,14 @@ function useAuth() {
|
||||
|
||||
try {
|
||||
await executeRequest(currentSession.serverUrl, requestPayload);
|
||||
} catch (logoutError) {
|
||||
console.warn('Ошибка при выходе из системы:', logoutError);
|
||||
}
|
||||
} catch (_logoutError) {}
|
||||
}
|
||||
|
||||
await clearAuthSession();
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
if (!keepSessionLocally) {
|
||||
await clearAuthSession();
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (logoutError) {
|
||||
@ -444,8 +458,10 @@ function useAuth() {
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLogoutInProgress(false);
|
||||
if (!keepSessionLocally) {
|
||||
setIsLoading(false);
|
||||
setIsLogoutInProgress(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[session, getAuthSession, executeRequest, clearAuthSession]
|
||||
@ -580,8 +596,7 @@ function useAuth() {
|
||||
setIsAuthenticated(true);
|
||||
sessionRestoredFromStorageRef.current = true;
|
||||
}
|
||||
} catch (initError) {
|
||||
console.error('Ошибка инициализации авторизации:', initError);
|
||||
} catch (_initError) {
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
|
||||
121
rn/app/src/hooks/useIdleTimeout.js
Normal file
121
rn/app/src/hooks/useIdleTimeout.js
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Хук отслеживания простоя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { AppState } = require('react-native'); //Состояние приложения (active/background)
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
|
||||
const { IDLE_TIMEOUT_OFFLINE_MESSAGE, OFFLINE_MODE_TITLE } = require('../config/messages'); //Сообщения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
const IDLE_CHECK_INTERVAL_MS = 15000; //Проверка каждые 15 секунд
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Хук отслеживания простоя: при превышении лимита — запрос logout на сервер, переход в офлайн и сообщение
|
||||
function useIdleTimeout() {
|
||||
const { getSetting } = useAppLocalDbContext();
|
||||
const { isAuthenticated, logout } = useAppAuthContext();
|
||||
const { APP_MODE, mode, setOffline } = useAppModeContext();
|
||||
const { showInfo } = useAppMessagingContext();
|
||||
|
||||
const lastActivityAtRef = React.useRef(Date.now());
|
||||
const intervalIdRef = React.useRef(null);
|
||||
const appStateRef = React.useRef(AppState.currentState);
|
||||
|
||||
//Сброс таймера простоя при любой активности
|
||||
const reportActivity = React.useCallback(() => {
|
||||
lastActivityAtRef.current = Date.now();
|
||||
}, []);
|
||||
|
||||
//Проверка простоя
|
||||
const checkIdleAndLogout = React.useCallback(async () => {
|
||||
if (!isAuthenticated || appStateRef.current !== 'active' || mode === APP_MODE.OFFLINE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let idleMinutes = 0;
|
||||
try {
|
||||
const raw = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
const trimmed = raw != null ? String(raw).trim() : '';
|
||||
if (trimmed === '') {
|
||||
return;
|
||||
}
|
||||
idleMinutes = parseInt(trimmed, 10);
|
||||
if (isNaN(idleMinutes) || idleMinutes <= 0) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idleLimitMs = idleMinutes * 60 * 1000;
|
||||
const now = Date.now();
|
||||
if (now - lastActivityAtRef.current >= idleLimitMs) {
|
||||
try {
|
||||
await logout({ skipServerRequest: false, keepSessionLocally: true });
|
||||
setOffline();
|
||||
showInfo(IDLE_TIMEOUT_OFFLINE_MESSAGE, { title: OFFLINE_MODE_TITLE });
|
||||
} catch (logoutErr) {}
|
||||
}
|
||||
}, [isAuthenticated, getSetting, logout, mode, APP_MODE.OFFLINE, setOffline, showInfo]);
|
||||
|
||||
//Запуск периодической проверки при авторизации; сброс времени активности при входе
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
if (intervalIdRef.current != null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastActivityAtRef.current = Date.now();
|
||||
|
||||
const intervalId = setInterval(checkIdleAndLogout, IDLE_CHECK_INTERVAL_MS);
|
||||
intervalIdRef.current = intervalId;
|
||||
|
||||
return function cleanup() {
|
||||
if (intervalIdRef.current != null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isAuthenticated, checkIdleAndLogout]);
|
||||
|
||||
//Учёт перехода приложения в фон/на передний план (не считать простой в фоне)
|
||||
React.useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', nextAppState => {
|
||||
appStateRef.current = nextAppState;
|
||||
if (nextAppState === 'active') {
|
||||
lastActivityAtRef.current = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
return function cleanup() {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { reportActivity };
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = useIdleTimeout;
|
||||
@ -17,7 +17,6 @@ const SQLiteDatabase = require('../database/SQLiteDatabase'); //Модуль SQL
|
||||
function useLocalDb() {
|
||||
//Состояние хука
|
||||
const [isDbReady, setIsDbReady] = React.useState(false);
|
||||
const [inspections, setInspections] = React.useState([]);
|
||||
const [dbError, setDbError] = React.useState(null);
|
||||
|
||||
//Отслеживания статуса инициализации
|
||||
@ -33,12 +32,6 @@ function useLocalDb() {
|
||||
if (initializingRef.current || SQLiteDatabase.isInitialized) {
|
||||
if (mounted && SQLiteDatabase.isInitialized) {
|
||||
setIsDbReady(true);
|
||||
try {
|
||||
const loadedInspections = await SQLiteDatabase.getInspections();
|
||||
setInspections(loadedInspections);
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки осмотров при переинициализации:', loadError);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -49,12 +42,9 @@ function useLocalDb() {
|
||||
await SQLiteDatabase.initialize();
|
||||
if (mounted) {
|
||||
setIsDbReady(true);
|
||||
const loadedInspections = await SQLiteDatabase.getInspections();
|
||||
setInspections(loadedInspections);
|
||||
setDbError(null);
|
||||
}
|
||||
} catch (initError) {
|
||||
console.error('Ошибка инициализации базы данных:', initError);
|
||||
} catch (_initError) {
|
||||
if (mounted) {
|
||||
setIsDbReady(false);
|
||||
setDbError('Не удалось инициализировать базу данных');
|
||||
@ -71,48 +61,6 @@ function useLocalDb() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
//Загрузка списка осмотров
|
||||
const loadInspections = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const loadedInspections = await SQLiteDatabase.getInspections();
|
||||
setInspections(loadedInspections);
|
||||
setDbError(null);
|
||||
return loadedInspections;
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки осмотров:', loadError);
|
||||
setDbError('Не удалось загрузить осмотры');
|
||||
return [];
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Сохранение осмотра
|
||||
const saveInspection = React.useCallback(
|
||||
async inspection => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return inspection;
|
||||
}
|
||||
|
||||
try {
|
||||
await SQLiteDatabase.saveInspection(inspection);
|
||||
const updatedInspections = await SQLiteDatabase.getInspections();
|
||||
setInspections(updatedInspections);
|
||||
setDbError(null);
|
||||
return inspection;
|
||||
} catch (saveError) {
|
||||
console.error('Ошибка сохранения осмотра:', saveError);
|
||||
setDbError('Не удалось сохранить осмотр');
|
||||
return inspection;
|
||||
}
|
||||
},
|
||||
[isDbReady]
|
||||
);
|
||||
|
||||
//Получение настройки
|
||||
const getSetting = React.useCallback(
|
||||
async key => {
|
||||
@ -122,8 +70,7 @@ function useLocalDb() {
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.getSetting(key);
|
||||
} catch (getSettingError) {
|
||||
console.error('Ошибка получения настройки:', getSettingError);
|
||||
} catch (_getSettingError) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@ -133,15 +80,11 @@ function useLocalDb() {
|
||||
//Сохранение настройки
|
||||
const setSetting = React.useCallback(
|
||||
async (key, value) => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.setSetting(key, value);
|
||||
} catch (setSettingError) {
|
||||
console.error('Ошибка сохранения настройки:', setSettingError);
|
||||
} catch (_setSettingError) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -151,15 +94,11 @@ function useLocalDb() {
|
||||
//Удаление настройки
|
||||
const deleteSetting = React.useCallback(
|
||||
async key => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.deleteSetting(key);
|
||||
} catch (deleteSettingError) {
|
||||
console.error('Ошибка удаления настройки:', deleteSettingError);
|
||||
} catch (_deleteSettingError) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -168,66 +107,33 @@ function useLocalDb() {
|
||||
|
||||
//Получение всех настроек
|
||||
const getAllSettings = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return {};
|
||||
}
|
||||
if (!isDbReady) return {};
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.getAllSettings();
|
||||
} catch (getAllSettingsError) {
|
||||
console.error('Ошибка получения всех настроек:', getAllSettingsError);
|
||||
} catch (_getAllSettingsError) {
|
||||
return {};
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Очистка всех настроек
|
||||
const clearSettings = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.clearSettings();
|
||||
} catch (clearSettingsError) {
|
||||
console.error('Ошибка очистки настроек:', clearSettingsError);
|
||||
return false;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Очистка всех осмотров
|
||||
const clearInspections = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await SQLiteDatabase.clearInspections();
|
||||
if (result) {
|
||||
setInspections([]);
|
||||
setDbError(null);
|
||||
}
|
||||
return result;
|
||||
} catch (clearInspectionsError) {
|
||||
console.error('Ошибка очистки осмотров:', clearInspectionsError);
|
||||
setDbError('Не удалось очистить осмотры');
|
||||
} catch (_clearSettingsError) {
|
||||
return false;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Оптимизация базы данных
|
||||
const vacuum = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.vacuum();
|
||||
} catch (vacuumError) {
|
||||
console.error('Ошибка оптимизации базы данных:', vacuumError);
|
||||
} catch (_vacuumError) {
|
||||
return false;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
@ -235,15 +141,11 @@ function useLocalDb() {
|
||||
//Проверка существования таблицы
|
||||
const checkTableExists = React.useCallback(
|
||||
async tableName => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.checkTableExists(tableName);
|
||||
} catch (checkTableError) {
|
||||
console.error('Ошибка проверки существования таблицы:', checkTableError);
|
||||
} catch (_checkTableError) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -253,15 +155,11 @@ function useLocalDb() {
|
||||
//Сохранение сессии авторизации
|
||||
const setAuthSession = React.useCallback(
|
||||
async session => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.setAuthSession(session);
|
||||
} catch (setAuthSessionError) {
|
||||
console.error('Ошибка сохранения сессии авторизации:', setAuthSessionError);
|
||||
} catch (_setAuthSessionError) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -270,46 +168,34 @@ function useLocalDb() {
|
||||
|
||||
//Получение сессии авторизации
|
||||
const getAuthSession = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return null;
|
||||
}
|
||||
if (!isDbReady) return null;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.getAuthSession();
|
||||
} catch (getAuthSessionError) {
|
||||
console.error('Ошибка получения сессии авторизации:', getAuthSessionError);
|
||||
} catch (_getAuthSessionError) {
|
||||
return null;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Очистка сессии авторизации
|
||||
const clearAuthSession = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
if (!isDbReady) return false;
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.clearAuthSession();
|
||||
} catch (clearAuthSessionError) {
|
||||
console.error('Ошибка очистки сессии авторизации:', clearAuthSessionError);
|
||||
} catch (_clearAuthSessionError) {
|
||||
return false;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
return {
|
||||
isDbReady,
|
||||
inspections,
|
||||
error: dbError,
|
||||
loadInspections,
|
||||
saveInspection,
|
||||
getSetting,
|
||||
setSetting,
|
||||
deleteSetting,
|
||||
getAllSettings,
|
||||
clearSettings,
|
||||
clearInspections,
|
||||
vacuum,
|
||||
checkTableExists,
|
||||
setAuthSession,
|
||||
|
||||
76
rn/app/src/hooks/useScanInputHandler.js
Normal file
76
rn/app/src/hooks/useScanInputHandler.js
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Универсальный хук обработки данных встроенного сканера для экранов с полями ввода
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { resolveScanTarget, getNextInOrder } = require('../utils/scanInputTargetResolver'); //Резолвер цели для сканера
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Хук формирования обработчика данных от встроенного сканера
|
||||
function useScanInputHandler(options) {
|
||||
const { allowedFieldIds, focusedFieldRef, expectedNextTargetRef, setFieldValue, focusField, parseSegments, onSubmitForm } = options;
|
||||
|
||||
const handleHardwareScan = React.useCallback(
|
||||
barcode => {
|
||||
const segments = parseSegments(barcode);
|
||||
if (segments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = Array.isArray(allowedFieldIds) ? allowedFieldIds : [];
|
||||
if (allowed.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focused = focusedFieldRef.current;
|
||||
let targetField = resolveScanTarget(focused, allowed);
|
||||
if (targetField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastTargetField = targetField;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const value = segments[i];
|
||||
setFieldValue(targetField, value);
|
||||
lastTargetField = targetField;
|
||||
const nextField = getNextInOrder(targetField, allowed);
|
||||
if (nextField && i < segments.length - 1) {
|
||||
expectedNextTargetRef.current = nextField;
|
||||
focusedFieldRef.current = nextField;
|
||||
targetField = nextField;
|
||||
} else if (!nextField) {
|
||||
focusedFieldRef.current = null;
|
||||
expectedNextTargetRef.current = null;
|
||||
if (typeof onSubmitForm === 'function') {
|
||||
onSubmitForm();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
expectedNextTargetRef.current = nextField;
|
||||
focusedFieldRef.current = nextField;
|
||||
focusField(nextField);
|
||||
return;
|
||||
}
|
||||
}
|
||||
focusedFieldRef.current = lastTargetField;
|
||||
focusField(lastTargetField);
|
||||
},
|
||||
[allowedFieldIds, focusedFieldRef, expectedNextTargetRef, setFieldValue, focusField, parseSegments, onSubmitForm]
|
||||
);
|
||||
|
||||
return handleHardwareScan;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = useScanInputHandler;
|
||||
@ -23,12 +23,17 @@ const OrganizationSelectDialog = require('../components/auth/OrganizationSelectD
|
||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
||||
const { isServerUrlFieldVisible } = require('../utils/loginFormUtils'); //Утилиты формы входа
|
||||
const { parseScannedSegmentsForAuth } = require('../utils/authScannerUtils'); //Разбор данных сканера по Enter для экрана входа
|
||||
const { getAllowedAuthFields } = require('../utils/authFormFieldsOrder'); //Порядок и доступность полей формы для сканера
|
||||
const useScanInputHandler = require('../hooks/useScanInputHandler'); //Универсальный обработчик данных сканера для полей ввода
|
||||
const { normalizeServerUrl, validateServerUrl } = require('../utils/validation'); //Валидация
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации
|
||||
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGOUT, MENU_ITEM_ID_DIVIDER } = require('../config/menuItemIds'); //ID пунктов меню
|
||||
const { getAuthFormStore, setAuthFormStore, clearAuthFormStore } = require('../utils/authFormStore'); //Хранилище формы входа
|
||||
const {
|
||||
APP_ABOUT_TITLE,
|
||||
@ -36,14 +41,22 @@ const {
|
||||
ORGANIZATION_SELECT_DIALOG_TITLE,
|
||||
MENU_ITEM_SETTINGS,
|
||||
MENU_ITEM_ABOUT,
|
||||
MENU_ITEM_LOGOUT,
|
||||
AUTH_SCREEN_TITLE,
|
||||
AUTH_BUTTON_LOGIN,
|
||||
AUTH_BUTTON_LOADING,
|
||||
AUTH_BUTTON_LOGOUT,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
||||
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||
AUTH_SERVER_CHANGE_CANCEL_BUTTON
|
||||
AUTH_SERVER_CHANGE_CANCEL_BUTTON,
|
||||
LOGOUT_CONFIRM_TITLE,
|
||||
LOGOUT_CONFIRM_MESSAGE
|
||||
} = require('../config/messages'); //Сообщения
|
||||
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||
const { createLogoutHandler } = require('../utils/logoutFlow'); //Универсальный поток выхода
|
||||
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
|
||||
const { noopCatch } = require('../utils/noop'); //Пустой обработчик для .catch()
|
||||
const styles = require('../styles/screens/AuthScreen.styles'); //Стили экрана
|
||||
|
||||
//-----------
|
||||
@ -53,12 +66,13 @@ const styles = require('../styles/screens/AuthScreen.styles'); //Стили эк
|
||||
//Экран аутентификации
|
||||
function AuthScreen() {
|
||||
const { showError, showInfo } = useAppMessagingContext();
|
||||
const { APP_MODE, mode, setOnline } = useAppModeContext();
|
||||
const { navigate, goBack, canGoBack, reset, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
|
||||
const { getSetting, isDbReady, clearInspections } = useAppLocalDbContext();
|
||||
const { APP_MODE, mode, setOnline, setNotConnected } = useAppModeContext();
|
||||
const { navigate, goBack, canGoBack, reset, setInitialScreen, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
|
||||
const { getSetting, isDbReady } = useAppLocalDbContext();
|
||||
const {
|
||||
session,
|
||||
login,
|
||||
logout,
|
||||
selectCompany,
|
||||
isLoading,
|
||||
getSavedCredentials,
|
||||
@ -115,6 +129,42 @@ function AuthScreen() {
|
||||
const loginInputRef = React.useRef(null);
|
||||
const passwordInputRef = React.useRef(null);
|
||||
|
||||
//Поле ввода, на котором сейчас фокус (null — фокус сброшен)
|
||||
const focusedFieldRef = React.useRef(null);
|
||||
//После программного перехода фокуса сканером — следующее поле (для случая сброса фокуса устройством)
|
||||
const expectedNextTargetRef = React.useRef(null);
|
||||
|
||||
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
|
||||
|
||||
//Первый видимый элемент ввода на экране аутентификации (для подстановки при отсутствии фокуса)
|
||||
const firstVisibleAuthField = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings) ? 'server' : 'login';
|
||||
|
||||
//Список доступных полей для сканера (видимые и редактируемые)
|
||||
const allowedAuthFieldIds = React.useMemo(
|
||||
() =>
|
||||
getAllowedAuthFields({
|
||||
serverVisible: firstVisibleAuthField === 'server',
|
||||
serverEditable: !(isLoading || isFromMenu),
|
||||
loginEditable: !(isLoading || isFromMenu),
|
||||
passwordEditable: !isLoading
|
||||
}),
|
||||
[firstVisibleAuthField, isLoading, isFromMenu]
|
||||
);
|
||||
|
||||
//Обработчики фокуса полей
|
||||
const handleServerFocus = React.useCallback(() => {
|
||||
focusedFieldRef.current = 'server';
|
||||
}, []);
|
||||
const handleLoginFocus = React.useCallback(() => {
|
||||
focusedFieldRef.current = 'login';
|
||||
}, []);
|
||||
const handlePasswordFocus = React.useCallback(() => {
|
||||
focusedFieldRef.current = 'password';
|
||||
}, []);
|
||||
const handleFieldBlur = React.useCallback(() => {
|
||||
focusedFieldRef.current = null;
|
||||
}, []);
|
||||
|
||||
//Актуализация ref и store при изменении полей; при размонтировании — сохранение в store
|
||||
React.useEffect(() => {
|
||||
const next = { serverUrl, username, password, savePassword, showPassword };
|
||||
@ -183,7 +233,7 @@ function AuthScreen() {
|
||||
setUsername(val.trim());
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(noopCatch);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
@ -240,7 +290,6 @@ function AuthScreen() {
|
||||
|
||||
initialLoadRef.current = true;
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки credentials из меню:', loadError);
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
@ -264,9 +313,7 @@ function AuthScreen() {
|
||||
if (savedLogin && typeof savedLogin === 'string') {
|
||||
setUsername(savedLogin.trim());
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Резервная загрузка логина:', e);
|
||||
}
|
||||
} catch (_e) {}
|
||||
};
|
||||
|
||||
loadSavedLoginOnly();
|
||||
@ -288,9 +335,7 @@ function AuthScreen() {
|
||||
}
|
||||
setSavedServerUrlFromSettings(trimmedUrl);
|
||||
setHideServerUrl(savedHide === 'true' || savedHide === true);
|
||||
} catch (e) {
|
||||
console.warn('Загрузка адреса сервера и настройки видимости:', e);
|
||||
}
|
||||
} catch (_e) {}
|
||||
};
|
||||
|
||||
loadServerAndVisibility();
|
||||
@ -338,8 +383,7 @@ function AuthScreen() {
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
}
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки настроек авторизации:', loadError);
|
||||
} catch (_loadError) {
|
||||
} finally {
|
||||
setIsSettingsLoaded(true);
|
||||
}
|
||||
@ -372,45 +416,77 @@ function AuthScreen() {
|
||||
}, [hideServerUrl, serverUrl, username, password, showError]);
|
||||
|
||||
//Выполнение входа
|
||||
const performLogin = React.useCallback(async () => {
|
||||
let idleTimeout = null;
|
||||
try {
|
||||
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
} catch (settingError) {
|
||||
console.warn('Ошибка получения таймаута:', settingError);
|
||||
}
|
||||
const performLogin = React.useCallback(
|
||||
async formOverride => {
|
||||
const data = formOverride != null ? formOverride : { serverUrl, username, password, savePassword };
|
||||
const s = (data.serverUrl != null ? String(data.serverUrl) : '').trim();
|
||||
const u = (data.username != null ? String(data.username) : '').trim();
|
||||
const p = (data.password != null ? String(data.password) : '').trim();
|
||||
const save = Boolean(data.savePassword);
|
||||
|
||||
const result = await login({
|
||||
serverUrl: serverUrl.trim(),
|
||||
user: username.trim(),
|
||||
password: password.trim(),
|
||||
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
|
||||
savePassword
|
||||
});
|
||||
let idleTimeout = null;
|
||||
try {
|
||||
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
} catch (_settingError) {}
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Ошибка входа');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.needSelectCompany) {
|
||||
setOrganizations(result.organizations);
|
||||
setPendingLoginData({
|
||||
serverUrl: result.serverUrl,
|
||||
sessionId: result.sessionId,
|
||||
user: result.user,
|
||||
savePassword: result.savePassword,
|
||||
loginCredentials: result.loginCredentials
|
||||
const result = await login({
|
||||
serverUrl: s,
|
||||
user: u,
|
||||
password: p,
|
||||
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
|
||||
savePassword: save
|
||||
});
|
||||
setShowOrgDialog(true);
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Ошибка входа');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.needSelectCompany) {
|
||||
setOrganizations(result.organizations);
|
||||
setPendingLoginData({
|
||||
serverUrl: result.serverUrl,
|
||||
sessionId: result.sessionId,
|
||||
user: result.user,
|
||||
savePassword: result.savePassword,
|
||||
loginCredentials: result.loginCredentials
|
||||
});
|
||||
setShowOrgDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setOnline();
|
||||
clearAuthFormData();
|
||||
clearAuthFormStore();
|
||||
reset();
|
||||
},
|
||||
[login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, reset]
|
||||
);
|
||||
|
||||
//Вход с данными из ref (после подстановки пароля встроенным сканером, чтобы не терять значение до применения setState)
|
||||
const performLoginWithFormRef = React.useCallback(() => {
|
||||
const form = formDataRef.current;
|
||||
if (!form) return;
|
||||
const s = (form.serverUrl != null ? String(form.serverUrl) : '').trim();
|
||||
const u = (form.username != null ? String(form.username) : '').trim();
|
||||
const p = (form.password != null ? String(form.password) : '').trim();
|
||||
if (!hideServerUrl || !form.serverUrl) {
|
||||
const urlResult = validateServerUrl(s, { emptyMessage: 'Укажите адрес сервера' });
|
||||
if (urlResult !== true) {
|
||||
showError(urlResult);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!u) {
|
||||
showError('Укажите логин');
|
||||
return;
|
||||
}
|
||||
|
||||
setOnline();
|
||||
clearAuthFormData();
|
||||
clearAuthFormStore();
|
||||
reset();
|
||||
}, [login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, reset]);
|
||||
if (!p) {
|
||||
showError('Укажите пароль');
|
||||
return;
|
||||
}
|
||||
performLogin(form);
|
||||
}, [hideServerUrl, showError, performLogin]);
|
||||
|
||||
//Обработчик входа (очистка локальных данных только при смене сервера относительно последнего успешного подключения)
|
||||
const handleLogin = React.useCallback(async () => {
|
||||
@ -440,7 +516,6 @@ function AuthScreen() {
|
||||
title: AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||
onPress: async () => {
|
||||
await clearAuthSession();
|
||||
await clearInspections();
|
||||
await performLogin();
|
||||
}
|
||||
}
|
||||
@ -449,7 +524,7 @@ function AuthScreen() {
|
||||
} else {
|
||||
await performLogin();
|
||||
}
|
||||
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, clearInspections, performLogin]);
|
||||
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, performLogin]);
|
||||
|
||||
//Обработчик выбора организации
|
||||
const handleSelectOrganization = React.useCallback(
|
||||
@ -536,6 +611,60 @@ function AuthScreen() {
|
||||
handleLogin();
|
||||
}, [handleLogin]);
|
||||
|
||||
//Подстановка значения в поле формы по идентификатору (state, ref, store)
|
||||
const setAuthFieldValue = React.useCallback((fieldId, value) => {
|
||||
const v = value != null ? String(value) : '';
|
||||
if (fieldId === 'server') {
|
||||
formDataRef.current.serverUrl = v;
|
||||
setAuthFormStore({ serverUrl: v });
|
||||
setServerUrl(v);
|
||||
} else if (fieldId === 'login') {
|
||||
formDataRef.current.username = v;
|
||||
setAuthFormStore({ username: v });
|
||||
setUsername(v);
|
||||
} else if (fieldId === 'password') {
|
||||
formDataRef.current.password = v;
|
||||
setAuthFormStore({ password: v });
|
||||
setPassword(v);
|
||||
}
|
||||
}, []);
|
||||
|
||||
//Перенос фокуса на поле формы по идентификатору
|
||||
const focusAuthField = React.useCallback(fieldId => {
|
||||
if (fieldId === 'server' && serverInputRef.current) {
|
||||
serverInputRef.current.focus();
|
||||
} else if (fieldId === 'login' && loginInputRef.current) {
|
||||
loginInputRef.current.focus();
|
||||
} else if (fieldId === 'password' && passwordInputRef.current) {
|
||||
passwordInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
//Обработчик встроенного сканера: подстановка в фокусное или первое доступное поле; при Enter — следующее поле или «Войти»
|
||||
const handleHardwareScan = useScanInputHandler({
|
||||
allowedFieldIds: allowedAuthFieldIds,
|
||||
focusedFieldRef,
|
||||
expectedNextTargetRef,
|
||||
setFieldValue: setAuthFieldValue,
|
||||
focusField: focusAuthField,
|
||||
parseSegments: parseScannedSegmentsForAuth,
|
||||
onSubmitForm: performLoginWithFormRef
|
||||
});
|
||||
|
||||
const authScanHandlerRef = React.useRef(handleHardwareScan);
|
||||
authScanHandlerRef.current = handleHardwareScan;
|
||||
const stableAuthScanHandler = React.useCallback(barcode => {
|
||||
if (typeof authScanHandlerRef.current === 'function') authScanHandlerRef.current(barcode);
|
||||
}, []);
|
||||
|
||||
//Регистрация обработчика встроенного сканера на экране AUTH
|
||||
React.useEffect(() => {
|
||||
registerHandler(scannerScreens.AUTH, stableAuthScanHandler);
|
||||
return function cleanup() {
|
||||
unregisterHandler(scannerScreens.AUTH);
|
||||
};
|
||||
}, [registerHandler, unregisterHandler, scannerScreens.AUTH, stableAuthScanHandler]);
|
||||
|
||||
//Обработчик открытия меню
|
||||
const handleMenuOpen = React.useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
@ -564,24 +693,67 @@ function AuthScreen() {
|
||||
});
|
||||
}, [showInfo, mode, serverUrl, isDbReady]);
|
||||
|
||||
//Обработчик выхода с экрана аутентификации: сброс стека, нельзя вернуться назад
|
||||
const logoutHandlerFromAuth = React.useMemo(() => {
|
||||
if (!isFromMenu) {
|
||||
return null;
|
||||
}
|
||||
return createLogoutHandler({
|
||||
logout,
|
||||
mode,
|
||||
setNotConnected,
|
||||
setInitialScreen,
|
||||
SCREENS,
|
||||
showError,
|
||||
showInfo,
|
||||
logoutConfirmTitle: LOGOUT_CONFIRM_TITLE,
|
||||
logoutConfirmMessage: LOGOUT_CONFIRM_MESSAGE,
|
||||
logoutButtonTitle: AUTH_BUTTON_LOGOUT,
|
||||
getConfirmButtonOptions,
|
||||
dialogButtonTypeError: DIALOG_BUTTON_TYPE.ERROR,
|
||||
dialogCancelButton: DIALOG_CANCEL_BUTTON
|
||||
});
|
||||
}, [isFromMenu, logout, mode, setNotConnected, setInitialScreen, SCREENS, showError, showInfo]);
|
||||
|
||||
const handleLogoutFromAuth = React.useCallback(() => {
|
||||
if (logoutHandlerFromAuth) {
|
||||
logoutHandlerFromAuth.handleLogout();
|
||||
}
|
||||
}, [logoutHandlerFromAuth]);
|
||||
|
||||
//Пункты бокового меню
|
||||
const menuItems = React.useMemo(() => {
|
||||
return [
|
||||
const items = [
|
||||
{
|
||||
id: 'settings',
|
||||
id: MENU_ITEM_ID_SETTINGS,
|
||||
title: MENU_ITEM_SETTINGS,
|
||||
onPress: handleOpenSettings
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
id: MENU_ITEM_ID_ABOUT,
|
||||
title: MENU_ITEM_ABOUT,
|
||||
onPress: handleShowAbout
|
||||
}
|
||||
];
|
||||
}, [handleOpenSettings, handleShowAbout]);
|
||||
|
||||
//Поле сервера показываем, если настройка выключена или в настройках ещё не сохранён адрес
|
||||
const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings);
|
||||
if (isFromMenu) {
|
||||
items.push({
|
||||
id: MENU_ITEM_ID_DIVIDER,
|
||||
type: 'divider'
|
||||
});
|
||||
items.push({
|
||||
id: MENU_ITEM_ID_LOGOUT,
|
||||
title: MENU_ITEM_LOGOUT,
|
||||
onPress: handleLogoutFromAuth,
|
||||
textStyle: { color: APP_COLORS.error },
|
||||
iconColor: APP_COLORS.error
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [handleOpenSettings, handleShowAbout, handleLogoutFromAuth, isFromMenu]);
|
||||
|
||||
const shouldShowServerUrl = firstVisibleAuthField === 'server';
|
||||
|
||||
return (
|
||||
<AdaptiveView padding={false}>
|
||||
@ -622,6 +794,8 @@ function AuthScreen() {
|
||||
blurOnSubmit={false}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={handleServerSubmitEditing}
|
||||
onFocus={handleServerFocus}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@ -638,6 +812,8 @@ function AuthScreen() {
|
||||
blurOnSubmit={false}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={handleLoginSubmitEditing}
|
||||
onFocus={handleLoginFocus}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
@ -653,6 +829,8 @@ function AuthScreen() {
|
||||
blurOnSubmit={true}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handlePasswordSubmitEditing}
|
||||
onFocus={handlePasswordFocus}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
|
||||
<View style={styles.switchContainer}>
|
||||
@ -666,6 +844,16 @@ function AuthScreen() {
|
||||
style={styles.loginButton}
|
||||
textStyle={styles.loginButtonText}
|
||||
/>
|
||||
|
||||
{isFromMenu ? (
|
||||
<AppButton
|
||||
title={AUTH_BUTTON_LOGOUT}
|
||||
onPress={handleLogoutFromAuth}
|
||||
disabled={isLoading}
|
||||
style={styles.logoutButton}
|
||||
textStyle={styles.logoutButtonText}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@ -12,6 +12,7 @@ const { View } = require('react-native'); //Базовые компоненты
|
||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||
const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
|
||||
@ -20,10 +21,25 @@ const ScannerArea = require('../components/scanner/ScannerArea'); //Област
|
||||
const ScanResultModal = require('../components/scanner/ScanResultModal'); //Модальное окно результата сканирования
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
|
||||
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
||||
const { APP_ABOUT_TITLE, SIDE_MENU_TITLE, MENU_ITEM_SETTINGS, MENU_ITEM_ABOUT, MENU_ITEM_LOGIN, MENU_ITEM_LOGOUT } = require('../config/messages'); //Сообщения
|
||||
const {
|
||||
APP_ABOUT_TITLE,
|
||||
SIDE_MENU_TITLE,
|
||||
MENU_ITEM_SETTINGS,
|
||||
MENU_ITEM_ABOUT,
|
||||
MENU_ITEM_LOGIN,
|
||||
MENU_ITEM_LOGOUT,
|
||||
OFFLINE_MODE_TITLE,
|
||||
LOGOUT_CONFIRM_TITLE,
|
||||
LOGOUT_CONFIRM_MESSAGE,
|
||||
AUTH_BUTTON_LOGOUT
|
||||
} = require('../config/messages'); //Сообщения
|
||||
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||
const { createLogoutHandler } = require('../utils/logoutFlow'); //Универсальный поток выхода
|
||||
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
|
||||
const { shouldShowScanner } = require('../config/scannerConfig'); //Логика видимости сканера
|
||||
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGIN, MENU_ITEM_ID_LOGOUT, MENU_ITEM_ID_DIVIDER } = require('../config/menuItemIds'); //ID пунктов меню
|
||||
const { shouldShowScanner, HARDWARE_SCANNER_TRIGGER_EVENT_NAME, HARDWARE_SCANNER_CAMERA_HIDE_MS } = require('../config/scannerConfig'); //Логика и константы сканера
|
||||
const { DeviceEventEmitter } = require('react-native'); //События нативного модуля
|
||||
const HardwareScannerBridge = require('../services/HardwareScannerBridge'); //Отключение фокуса у камеры при отображении
|
||||
const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана
|
||||
|
||||
//-----------
|
||||
@ -32,7 +48,7 @@ const styles = require('../styles/screens/MainScreen.styles'); //Стили эк
|
||||
|
||||
//Главный экран приложения
|
||||
function MainScreen() {
|
||||
const { showInfo, showError } = useAppMessagingContext();
|
||||
const { showInfo, showError, state: messagingState } = useAppMessagingContext();
|
||||
const { mode, setNotConnected } = useAppModeContext();
|
||||
const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext();
|
||||
const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
|
||||
@ -41,7 +57,60 @@ function MainScreen() {
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
const [serverUrl, setServerUrl] = React.useState('');
|
||||
const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false);
|
||||
const [mainScannerPriority, setMainScannerPriority] = React.useState('camera');
|
||||
const [cameraResumedByTap, setCameraResumedByTap] = React.useState(false);
|
||||
const [scanResult, setScanResult] = React.useState(null);
|
||||
const [cameraHiddenForHardwareScan, setCameraHiddenForHardwareScan] = React.useState(false);
|
||||
|
||||
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
|
||||
|
||||
//Обработчик встроенного сканера: данные как есть в результат сканирования (все символы)
|
||||
const handleHardwareScan = React.useCallback(barcode => {
|
||||
if (barcode == null) return;
|
||||
const value = typeof barcode === 'string' ? barcode : String(barcode);
|
||||
setScanResult({ type: 'unknown', value: value });
|
||||
setCameraHiddenForHardwareScan(false);
|
||||
}, []);
|
||||
|
||||
const mainScanHandlerRef = React.useRef(handleHardwareScan);
|
||||
mainScanHandlerRef.current = handleHardwareScan;
|
||||
const stableMainScanHandler = React.useCallback(barcode => {
|
||||
if (typeof mainScanHandlerRef.current === 'function') mainScanHandlerRef.current(barcode);
|
||||
}, []);
|
||||
|
||||
//Регистрация обработчика встроенного сканера
|
||||
React.useEffect(() => {
|
||||
registerHandler(scannerScreens.MAIN, stableMainScanHandler);
|
||||
return function cleanup() {
|
||||
unregisterHandler(scannerScreens.MAIN);
|
||||
};
|
||||
}, [registerHandler, unregisterHandler, scannerScreens.MAIN, stableMainScanHandler]);
|
||||
|
||||
//При нажатии кнопки встроенного сканера (142/141): скрываем камеру (отложенно, вне контекста события)
|
||||
React.useEffect(function subscribeHardwareScannerTrigger() {
|
||||
let restoreTimeoutId = null;
|
||||
const sub = DeviceEventEmitter.addListener(HARDWARE_SCANNER_TRIGGER_EVENT_NAME, function onTrigger() {
|
||||
setTimeout(function applyTriggerState() {
|
||||
setCameraResumedByTap(false);
|
||||
setCameraHiddenForHardwareScan(true);
|
||||
if (restoreTimeoutId != null) clearTimeout(restoreTimeoutId);
|
||||
restoreTimeoutId = setTimeout(function restoreCamera() {
|
||||
restoreTimeoutId = null;
|
||||
setCameraHiddenForHardwareScan(false);
|
||||
}, HARDWARE_SCANNER_CAMERA_HIDE_MS);
|
||||
}, 0);
|
||||
});
|
||||
return function cleanup() {
|
||||
sub.remove();
|
||||
if (restoreTimeoutId != null) clearTimeout(restoreTimeoutId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
//При отображении области сканера вызываем setCameraViewsNotFocusable
|
||||
React.useEffect(() => {
|
||||
if (!isScannerOpen) return;
|
||||
HardwareScannerBridge.setCameraViewsNotFocusable();
|
||||
}, [isScannerOpen]);
|
||||
|
||||
//Загрузка настроек главного экрана при готовности БД
|
||||
React.useEffect(() => {
|
||||
@ -58,9 +127,10 @@ function MainScreen() {
|
||||
|
||||
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
||||
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки настроек главного экрана:', loadError);
|
||||
}
|
||||
|
||||
const savedPriority = await getSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY);
|
||||
setMainScannerPriority(savedPriority === 'hardware' ? 'hardware' : 'camera');
|
||||
} catch (loadError) {}
|
||||
};
|
||||
|
||||
loadMainScreenSettings();
|
||||
@ -99,38 +169,37 @@ function MainScreen() {
|
||||
navigate(SCREENS.SETTINGS);
|
||||
}, [navigate, SCREENS.SETTINGS]);
|
||||
|
||||
//Выполнение выхода (для диалога подтверждения)
|
||||
const performLogout = React.useCallback(async () => {
|
||||
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
|
||||
|
||||
if (result.success) {
|
||||
setNotConnected();
|
||||
setInitialScreen(SCREENS.AUTH);
|
||||
} else {
|
||||
showError(result.error || 'Ошибка выхода');
|
||||
}
|
||||
}, [logout, mode, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]);
|
||||
|
||||
//Обработчик выхода из приложения
|
||||
const handleLogout = React.useCallback(() => {
|
||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Выйти', performLogout);
|
||||
|
||||
showInfo('Вы уверены, что хотите выйти?', {
|
||||
title: 'Подтверждение выхода',
|
||||
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
|
||||
});
|
||||
}, [showInfo, performLogout]);
|
||||
//Универсальный обработчик выхода
|
||||
const { handleLogout } = React.useMemo(
|
||||
() =>
|
||||
createLogoutHandler({
|
||||
logout,
|
||||
mode,
|
||||
setNotConnected,
|
||||
setInitialScreen,
|
||||
SCREENS,
|
||||
showError,
|
||||
showInfo,
|
||||
logoutConfirmTitle: LOGOUT_CONFIRM_TITLE,
|
||||
logoutConfirmMessage: LOGOUT_CONFIRM_MESSAGE,
|
||||
logoutButtonTitle: AUTH_BUTTON_LOGOUT,
|
||||
getConfirmButtonOptions,
|
||||
dialogButtonTypeError: DIALOG_BUTTON_TYPE.ERROR,
|
||||
dialogCancelButton: DIALOG_CANCEL_BUTTON
|
||||
}),
|
||||
[logout, mode, setNotConnected, setInitialScreen, SCREENS, showError, showInfo]
|
||||
);
|
||||
|
||||
//Пункты бокового меню
|
||||
const menuItems = React.useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
id: 'settings',
|
||||
id: MENU_ITEM_ID_SETTINGS,
|
||||
title: MENU_ITEM_SETTINGS,
|
||||
onPress: handleOpenSettings
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
id: MENU_ITEM_ID_ABOUT,
|
||||
title: MENU_ITEM_ABOUT,
|
||||
onPress: handleShowAbout
|
||||
}
|
||||
@ -138,14 +207,14 @@ function MainScreen() {
|
||||
|
||||
//Добавляем разделитель перед кнопками авторизации
|
||||
items.push({
|
||||
id: 'divider',
|
||||
id: MENU_ITEM_ID_DIVIDER,
|
||||
type: 'divider'
|
||||
});
|
||||
|
||||
//Кнопка "Вход" для оффлайн режима
|
||||
if (mode === 'OFFLINE') {
|
||||
items.push({
|
||||
id: 'login',
|
||||
id: MENU_ITEM_ID_LOGIN,
|
||||
title: MENU_ITEM_LOGIN,
|
||||
onPress: handleLogin
|
||||
});
|
||||
@ -154,10 +223,11 @@ function MainScreen() {
|
||||
//Кнопка "Выход" для онлайн/оффлайн режима (когда есть сессия)
|
||||
if ((mode === 'ONLINE' || mode === 'OFFLINE') && isAuthenticated) {
|
||||
items.push({
|
||||
id: 'logout',
|
||||
id: MENU_ITEM_ID_LOGOUT,
|
||||
title: MENU_ITEM_LOGOUT,
|
||||
onPress: handleLogout,
|
||||
textStyle: { color: APP_COLORS.error }
|
||||
textStyle: { color: APP_COLORS.error },
|
||||
iconColor: APP_COLORS.error
|
||||
});
|
||||
}
|
||||
|
||||
@ -175,18 +245,52 @@ function MainScreen() {
|
||||
}, []);
|
||||
|
||||
//Видимость сканера
|
||||
const isOfflineMessageVisible = Boolean(messagingState.visible && messagingState.title === OFFLINE_MODE_TITLE);
|
||||
const isAboutMessageVisible = Boolean(messagingState.visible && messagingState.title === APP_ABOUT_TITLE);
|
||||
const isScannerOpen = shouldShowScanner({
|
||||
alwaysShowScanner,
|
||||
hasScanResult: scanResult != null,
|
||||
isStartupCheckInProgress: isStartupSessionCheckInProgress
|
||||
isStartupCheckInProgress: isStartupSessionCheckInProgress,
|
||||
isOfflineMessageVisible,
|
||||
isAboutMessageVisible
|
||||
});
|
||||
const cameraShown = isScannerOpen && !cameraHiddenForHardwareScan;
|
||||
const cameraActiveOnMain = !(alwaysShowScanner && mainScannerPriority === 'hardware') || cameraResumedByTap;
|
||||
|
||||
//Когда камера на главном экране видима
|
||||
React.useEffect(
|
||||
function cameraVisibleOnMain() {
|
||||
if (!cameraShown) return;
|
||||
const t1 = setTimeout(function run1() {
|
||||
HardwareScannerBridge.setCameraViewsNotFocusable();
|
||||
}, 100);
|
||||
const t2 = setTimeout(function run2() {
|
||||
HardwareScannerBridge.setCameraViewsNotFocusable();
|
||||
}, 400);
|
||||
return function cleanup() {
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
};
|
||||
},
|
||||
[cameraShown]
|
||||
);
|
||||
|
||||
const handleResumeCameraByTap = React.useCallback(function resumeCameraByTap() {
|
||||
setCameraResumedByTap(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<AppHeader onMenuPress={handleMenuOpen} />
|
||||
|
||||
<View style={styles.content}>
|
||||
<ScannerArea alwaysShowScanner={alwaysShowScanner} scannerOpen={isScannerOpen} onScanResult={handleScanResult} />
|
||||
<ScannerArea
|
||||
alwaysShowScanner={alwaysShowScanner}
|
||||
scannerOpen={cameraShown}
|
||||
cameraActive={cameraActiveOnMain}
|
||||
onScanResult={handleScanResult}
|
||||
onResumeCameraPress={mainScannerPriority === 'hardware' ? handleResumeCameraByTap : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<SideMenu
|
||||
|
||||
@ -19,18 +19,22 @@ const AppHeader = require('../components/layout/AppHeader'); //Заголово
|
||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
|
||||
const { AUTH_SETTINGS_KEYS, DEFAULT_SERVER_REQUEST_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
|
||||
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||
const { getAppInfo, getModeLabel } = require('../utils/appInfo'); //Информация о приложении и режиме
|
||||
const { validateServerUrlAllowEmpty, validateIdleTimeout } = require('../utils/validation'); //Валидация
|
||||
const { validateServerUrlAllowEmpty, validateIdleTimeoutAllowEmpty, validateServerRequestTimeout } = require('../utils/validation'); //Валидация
|
||||
const {
|
||||
APP_ABOUT_TITLE,
|
||||
SETTINGS_SERVER_SAVED_MESSAGE,
|
||||
SETTINGS_RESET_SUCCESS_MESSAGE,
|
||||
MENU_ITEM_ABOUT,
|
||||
SCANNER_SETTING_LABEL
|
||||
SCANNER_SETTING_LABEL,
|
||||
SCANNER_PRIORITY_LABEL,
|
||||
SCANNER_PRIORITY_CAMERA,
|
||||
SCANNER_PRIORITY_HARDWARE
|
||||
} = require('../config/messages'); //Сообщения
|
||||
const styles = require('../styles/screens/SettingsScreen.styles'); //Стили экрана
|
||||
|
||||
@ -42,7 +46,7 @@ function SettingsScreen() {
|
||||
const { showInfo, showError, showSuccess } = useAppMessagingContext();
|
||||
const { APP_MODE, mode, setNotConnected } = useAppModeContext();
|
||||
const { goBack, canGoBack } = useAppNavigationContext();
|
||||
const { getSetting, setSetting, clearSettings, clearInspections, vacuum, isDbReady } = useAppLocalDbContext();
|
||||
const { getSetting, setSetting, clearSettings, vacuum, isDbReady } = useAppLocalDbContext();
|
||||
const { session, isAuthenticated, getDeviceId, setLastSavedServerUrlFromSettings } = useAppAuthContext();
|
||||
|
||||
const [serverUrl, setServerUrl] = React.useState('');
|
||||
@ -52,11 +56,47 @@ function SettingsScreen() {
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
|
||||
const [isIdleTimeoutDialogVisible, setIsIdleTimeoutDialogVisible] = React.useState(false);
|
||||
const [serverRequestTimeout, setServerRequestTimeout] = React.useState('');
|
||||
const [isServerRequestTimeoutDialogVisible, setIsServerRequestTimeoutDialogVisible] = React.useState(false);
|
||||
const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false);
|
||||
const [mainScannerPriority, setMainScannerPriority] = React.useState('camera');
|
||||
|
||||
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
|
||||
const serverUrlDialogRef = React.useRef(null);
|
||||
const idleTimeoutDialogRef = React.useRef(null);
|
||||
const serverRequestTimeoutDialogRef = React.useRef(null);
|
||||
|
||||
//Предотвращение повторной загрузки настроек
|
||||
const settingsLoadedRef = React.useRef(false);
|
||||
|
||||
//Обработчик встроенного сканера: только при открытом диалоге ввода — замена значения в поле (данные как есть); иначе ничего не делать
|
||||
const handleHardwareScan = React.useCallback(
|
||||
barcode => {
|
||||
if (isServerUrlDialogVisible && serverUrlDialogRef.current) {
|
||||
serverUrlDialogRef.current.setValueFromScanner(barcode);
|
||||
} else if (isIdleTimeoutDialogVisible && idleTimeoutDialogRef.current) {
|
||||
idleTimeoutDialogRef.current.setValueFromScanner(barcode);
|
||||
} else if (isServerRequestTimeoutDialogVisible && serverRequestTimeoutDialogRef.current) {
|
||||
serverRequestTimeoutDialogRef.current.setValueFromScanner(barcode);
|
||||
}
|
||||
},
|
||||
[isServerUrlDialogVisible, isIdleTimeoutDialogVisible, isServerRequestTimeoutDialogVisible]
|
||||
);
|
||||
|
||||
const settingsScanHandlerRef = React.useRef(handleHardwareScan);
|
||||
settingsScanHandlerRef.current = handleHardwareScan;
|
||||
const stableSettingsScanHandler = React.useCallback(barcode => {
|
||||
if (typeof settingsScanHandlerRef.current === 'function') settingsScanHandlerRef.current(barcode);
|
||||
}, []);
|
||||
|
||||
//Регистрация обработчика встроенного сканера на экране настроек
|
||||
React.useEffect(() => {
|
||||
registerHandler(scannerScreens.SETTINGS, stableSettingsScanHandler);
|
||||
return function cleanup() {
|
||||
unregisterHandler(scannerScreens.SETTINGS);
|
||||
};
|
||||
}, [registerHandler, unregisterHandler, scannerScreens.SETTINGS, stableSettingsScanHandler]);
|
||||
|
||||
//Загрузка сохраненных настроек при готовности БД
|
||||
React.useEffect(() => {
|
||||
//Выходим, если БД не готова или уже загрузили настройки
|
||||
@ -78,13 +118,16 @@ function SettingsScreen() {
|
||||
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
||||
|
||||
const savedIdleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
if (savedIdleTimeout) {
|
||||
setIdleTimeout(savedIdleTimeout);
|
||||
setIdleTimeout(savedIdleTimeout != null ? String(savedIdleTimeout).trim() : '');
|
||||
|
||||
const savedServerRequestTimeout = await getSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT);
|
||||
const serverRequestTimeoutValue = savedServerRequestTimeout != null ? String(savedServerRequestTimeout).trim() : '';
|
||||
if (serverRequestTimeoutValue) {
|
||||
setServerRequestTimeout(serverRequestTimeoutValue);
|
||||
} else {
|
||||
//Устанавливаем значение по умолчанию
|
||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
||||
setIdleTimeout(defaultValue);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
const defaultValue = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||
setServerRequestTimeout(defaultValue);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultValue);
|
||||
}
|
||||
|
||||
//Получаем или генерируем идентификатор устройства
|
||||
@ -93,8 +136,10 @@ function SettingsScreen() {
|
||||
|
||||
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
||||
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
|
||||
|
||||
const savedPriority = await getSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY);
|
||||
setMainScannerPriority(savedPriority === 'hardware' ? 'hardware' : 'camera');
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки настроек:', error);
|
||||
showError('Не удалось загрузить настройки');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -125,6 +170,12 @@ function SettingsScreen() {
|
||||
[]
|
||||
);
|
||||
|
||||
//Стиль поля времени ожидания сервера при нажатии
|
||||
const getServerRequestTimeoutFieldPressableStyle = React.useCallback(
|
||||
({ pressed }) => [styles.serverUrlField, pressed && styles.serverUrlFieldPressed],
|
||||
[]
|
||||
);
|
||||
|
||||
//Открытие диалога ввода URL сервера (только в режиме "Не подключено")
|
||||
const handleOpenServerUrlDialog = React.useCallback(() => {
|
||||
if (!isServerUrlEditable) {
|
||||
@ -159,7 +210,6 @@ function SettingsScreen() {
|
||||
showError('Не удалось сохранить настройки');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настроек:', error);
|
||||
showError('Не удалось сохранить настройки');
|
||||
}
|
||||
|
||||
@ -181,13 +231,41 @@ function SettingsScreen() {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настройки сканера:', error);
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
},
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Выбор приоритета на главном экране: камера или встроенный сканер
|
||||
const handleSelectScannerPriority = React.useCallback(
|
||||
async value => {
|
||||
if (value !== 'camera' && value !== 'hardware') return;
|
||||
try {
|
||||
const success = await setSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY, value);
|
||||
if (success) {
|
||||
setMainScannerPriority(value);
|
||||
showSuccess('Настройка сохранена');
|
||||
} else {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
},
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Выбор приоритета: камера
|
||||
const handleSelectCameraPriority = React.useCallback(() => {
|
||||
handleSelectScannerPriority('camera');
|
||||
}, [handleSelectScannerPriority]);
|
||||
|
||||
//Выбор приоритета: встроенный сканер
|
||||
const handleSelectHardwarePriority = React.useCallback(() => {
|
||||
handleSelectScannerPriority('hardware');
|
||||
}, [handleSelectScannerPriority]);
|
||||
|
||||
//Переключатель скрытия URL сервера в окне логина
|
||||
const handleToggleHideServerUrl = React.useCallback(
|
||||
async value => {
|
||||
@ -201,7 +279,6 @@ function SettingsScreen() {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настройки:', error);
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
},
|
||||
@ -218,6 +295,16 @@ function SettingsScreen() {
|
||||
setIsIdleTimeoutDialogVisible(false);
|
||||
}, []);
|
||||
|
||||
//Открытие диалога ввода времени ожидания ответа от сервера
|
||||
const handleOpenServerRequestTimeoutDialog = React.useCallback(() => {
|
||||
setIsServerRequestTimeoutDialogVisible(true);
|
||||
}, []);
|
||||
|
||||
//Закрытие диалога ввода времени ожидания ответа от сервера
|
||||
const handleCloseServerRequestTimeoutDialog = React.useCallback(() => {
|
||||
setIsServerRequestTimeoutDialogVisible(false);
|
||||
}, []);
|
||||
|
||||
//Сохранение времени простоя
|
||||
const handleSaveIdleTimeout = React.useCallback(
|
||||
async value => {
|
||||
@ -235,7 +322,6 @@ function SettingsScreen() {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения времени простоя:', error);
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
|
||||
@ -244,22 +330,37 @@ function SettingsScreen() {
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Выполнение очистки кэша (для диалога подтверждения)
|
||||
const performClearCache = React.useCallback(async () => {
|
||||
try {
|
||||
const success = await clearInspections();
|
||||
if (success) {
|
||||
showSuccess('Кэш успешно очищен');
|
||||
} else {
|
||||
showError('Не удалось очистить кэш');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки кэша:', error);
|
||||
showError('Не удалось очистить кэш');
|
||||
}
|
||||
}, [showSuccess, showError, clearInspections]);
|
||||
//Сохранение времени ожидания ответа от сервера
|
||||
const handleSaveServerRequestTimeout = React.useCallback(
|
||||
async value => {
|
||||
setIsServerRequestTimeoutDialogVisible(false);
|
||||
setIsLoading(true);
|
||||
|
||||
//Очистка кэша (осмотров)
|
||||
try {
|
||||
const trimmedValue = value != null ? String(value).trim() : '';
|
||||
const success = await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, trimmedValue);
|
||||
|
||||
if (success) {
|
||||
setServerRequestTimeout(trimmedValue);
|
||||
showSuccess('Время ожидания сервера сохранено');
|
||||
} else {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Выполнение очистки кэша
|
||||
const performClearCache = React.useCallback(async () => {
|
||||
showSuccess('Кэш успешно очищен');
|
||||
}, [showSuccess]);
|
||||
|
||||
//Очистка кэша — диалог подтверждения, по подтверждению ничего не выполняется
|
||||
const handleClearCache = React.useCallback(() => {
|
||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Очистить', performClearCache);
|
||||
|
||||
@ -273,29 +374,32 @@ function SettingsScreen() {
|
||||
//Подключён (онлайн/офлайн): сбрасываем только непричастные к подключению настройки; не подключён: полный сброс
|
||||
const performResetSettings = React.useCallback(async () => {
|
||||
try {
|
||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
||||
|
||||
if (mode === APP_MODE.NOT_CONNECTED) {
|
||||
const success = await clearSettings();
|
||||
if (success) {
|
||||
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
|
||||
setServerUrl('');
|
||||
setHideServerUrl(false);
|
||||
setAlwaysShowScanner(false);
|
||||
setIdleTimeout(defaultValue);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
setMainScannerPriority('camera');
|
||||
setIdleTimeout('');
|
||||
setServerRequestTimeout(defaultServerTimeout);
|
||||
setNotConnected();
|
||||
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
||||
} else {
|
||||
showError('Не удалось сбросить настройки');
|
||||
}
|
||||
} else {
|
||||
//Подключён (онлайн или офлайн): сбрасываем только время простоя
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
setIdleTimeout(defaultValue);
|
||||
//Подключён (онлайн или офлайн): сбрасываем только время простоя и таймаут сервера
|
||||
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, '');
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
|
||||
setIdleTimeout('');
|
||||
setServerRequestTimeout(defaultServerTimeout);
|
||||
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сброса настроек:', error);
|
||||
showError('Не удалось сбросить настройки');
|
||||
}
|
||||
}, [
|
||||
@ -304,6 +408,7 @@ function SettingsScreen() {
|
||||
setServerUrl,
|
||||
setHideServerUrl,
|
||||
setIdleTimeout,
|
||||
setServerRequestTimeout,
|
||||
setNotConnected,
|
||||
clearSettings,
|
||||
setSetting,
|
||||
@ -332,7 +437,6 @@ function SettingsScreen() {
|
||||
showError('Не удалось оптимизировать базу данных');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка оптимизации БД:', error);
|
||||
showError('Не удалось оптимизировать базу данных');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -443,20 +547,46 @@ function SettingsScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Главный экран
|
||||
</AppText>
|
||||
{mode === APP_MODE.ONLINE || mode === APP_MODE.OFFLINE ? (
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Главный экран
|
||||
</AppText>
|
||||
|
||||
<View style={styles.switchRow}>
|
||||
<AppSwitch
|
||||
label={SCANNER_SETTING_LABEL}
|
||||
value={alwaysShowScanner}
|
||||
onValueChange={handleToggleAlwaysShowScanner}
|
||||
disabled={isLoading || !isDbReady}
|
||||
/>
|
||||
<View style={styles.switchRow}>
|
||||
<AppSwitch
|
||||
label={SCANNER_SETTING_LABEL}
|
||||
value={alwaysShowScanner}
|
||||
onValueChange={handleToggleAlwaysShowScanner}
|
||||
disabled={isLoading || !isDbReady}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{alwaysShowScanner ? (
|
||||
<View style={styles.prioritySection}>
|
||||
<AppText style={styles.fieldLabel} variant="caption" weight="medium">
|
||||
{SCANNER_PRIORITY_LABEL}
|
||||
</AppText>
|
||||
<Pressable
|
||||
style={[styles.priorityRow, mainScannerPriority === 'camera' && styles.priorityRowSelected]}
|
||||
onPress={handleSelectCameraPriority}
|
||||
disabled={isLoading || !isDbReady}
|
||||
>
|
||||
<AppText style={styles.priorityRowText}>{SCANNER_PRIORITY_CAMERA}</AppText>
|
||||
{mainScannerPriority === 'camera' ? <AppText style={styles.priorityCheck}>✓</AppText> : null}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.priorityRow, mainScannerPriority === 'hardware' && styles.priorityRowSelected]}
|
||||
onPress={handleSelectHardwarePriority}
|
||||
disabled={isLoading || !isDbReady}
|
||||
>
|
||||
<AppText style={styles.priorityRowText}>{SCANNER_PRIORITY_HARDWARE}</AppText>
|
||||
{mainScannerPriority === 'hardware' ? <AppText style={styles.priorityCheck}>✓</AppText> : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
@ -468,8 +598,22 @@ function SettingsScreen() {
|
||||
</AppText>
|
||||
|
||||
<Pressable style={getIdleTimeoutFieldPressableStyle} onPress={handleOpenIdleTimeoutDialog} disabled={isLoading || !isDbReady}>
|
||||
<AppText style={[styles.serverUrlText, !idleTimeout && styles.serverUrlPlaceholder]} numberOfLines={1}>
|
||||
{idleTimeout || 'Не задано'}
|
||||
</AppText>
|
||||
</Pressable>
|
||||
|
||||
<AppText style={[styles.fieldLabel, styles.fieldLabelMarginTop]} variant="caption" weight="medium">
|
||||
Максимальное время ожидания ответа от сервера (секунд)
|
||||
</AppText>
|
||||
|
||||
<Pressable
|
||||
style={getServerRequestTimeoutFieldPressableStyle}
|
||||
onPress={handleOpenServerRequestTimeoutDialog}
|
||||
disabled={isLoading || !isDbReady}
|
||||
>
|
||||
<AppText style={styles.serverUrlText} numberOfLines={1}>
|
||||
{idleTimeout || String(DEFAULT_IDLE_TIMEOUT)}
|
||||
{serverRequestTimeout || String(DEFAULT_SERVER_REQUEST_TIMEOUT)}
|
||||
</AppText>
|
||||
</Pressable>
|
||||
|
||||
@ -562,6 +706,7 @@ function SettingsScreen() {
|
||||
</ScrollView>
|
||||
|
||||
<InputDialog
|
||||
ref={serverUrlDialogRef}
|
||||
visible={isServerUrlDialogVisible}
|
||||
title="Адрес сервера"
|
||||
label="URL сервера приложений"
|
||||
@ -576,17 +721,33 @@ function SettingsScreen() {
|
||||
/>
|
||||
|
||||
<InputDialog
|
||||
ref={idleTimeoutDialogRef}
|
||||
visible={isIdleTimeoutDialogVisible}
|
||||
title="Время простоя"
|
||||
label="Максимальное время простоя (минут)"
|
||||
value={idleTimeout}
|
||||
placeholder="Например: 30"
|
||||
placeholder="Например: 30 или пусто"
|
||||
keyboardType="numeric"
|
||||
confirmText="Сохранить"
|
||||
cancelText="Отмена"
|
||||
onConfirm={handleSaveIdleTimeout}
|
||||
onCancel={handleCloseIdleTimeoutDialog}
|
||||
validator={validateIdleTimeout}
|
||||
validator={validateIdleTimeoutAllowEmpty}
|
||||
/>
|
||||
|
||||
<InputDialog
|
||||
ref={serverRequestTimeoutDialogRef}
|
||||
visible={isServerRequestTimeoutDialogVisible}
|
||||
title="Время ожидания сервера"
|
||||
label="Максимальное время ожидания ответа от сервера (секунд)"
|
||||
value={serverRequestTimeout}
|
||||
placeholder="Например: 60"
|
||||
keyboardType="numeric"
|
||||
confirmText="Сохранить"
|
||||
cancelText="Отмена"
|
||||
onConfirm={handleSaveServerRequestTimeout}
|
||||
onCancel={handleCloseServerRequestTimeoutDialog}
|
||||
validator={validateServerRequestTimeout}
|
||||
/>
|
||||
</AdaptiveView>
|
||||
);
|
||||
|
||||
78
rn/app/src/services/HardwareScannerBridge.js
Normal file
78
rn/app/src/services/HardwareScannerBridge.js
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Мост встроенного сканера
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { NativeModules, Platform, DeviceEventEmitter } = require('react-native');
|
||||
const { HARDWARE_SCANNER_EVENT_NAME } = require('../config/scannerConfig');
|
||||
|
||||
//---------
|
||||
//Состояние
|
||||
//---------
|
||||
|
||||
let hardwareCallback = null;
|
||||
let isListening = false;
|
||||
|
||||
const { HardwareScannerModule } = NativeModules;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Доставка данных в колбэк в следующем тике (данные как есть, без обрезки)
|
||||
function deliverBarcode(barcode) {
|
||||
if (!isListening || !hardwareCallback) return;
|
||||
const s = typeof barcode === 'string' ? barcode : '';
|
||||
if (s.length === 0) return;
|
||||
setTimeout(function deliverNextTick() {
|
||||
if (!isListening || !hardwareCallback) return;
|
||||
try {
|
||||
hardwareCallback(s);
|
||||
} catch (_err) {}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
//Запуск прослушивания встроенного сканера (событие по мосту); возвращает функцию отписки
|
||||
function startHardwareListener(onData) {
|
||||
if (Platform.OS !== 'android') return function noopUnsubscribe() {};
|
||||
if (!HardwareScannerModule) return function noopUnsubscribe() {};
|
||||
if (typeof onData !== 'function') return function noopUnsubscribe() {};
|
||||
if (isListening && hardwareCallback) return function noopUnsubscribe() {};
|
||||
|
||||
hardwareCallback = onData;
|
||||
isListening = true;
|
||||
|
||||
const subscription = DeviceEventEmitter.addListener(HARDWARE_SCANNER_EVENT_NAME, function onBarcodeEvent(event) {
|
||||
const barcode = event && event.barcode != null ? String(event.barcode) : '';
|
||||
deliverBarcode(barcode);
|
||||
});
|
||||
|
||||
return function unsubscribe() {
|
||||
isListening = false;
|
||||
if (subscription && typeof subscription.remove === 'function') subscription.remove();
|
||||
hardwareCallback = null;
|
||||
};
|
||||
}
|
||||
|
||||
//Отключить фокус у видов камеры в иерархии, чтобы встроенный сканер получал клавиши при отображении камеры (только Android)
|
||||
function setCameraViewsNotFocusable() {
|
||||
if (Platform.OS !== 'android' || !HardwareScannerModule) return;
|
||||
try {
|
||||
if (typeof HardwareScannerModule.setCameraViewsNotFocusable === 'function') {
|
||||
HardwareScannerModule.setCameraViewsNotFocusable();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
startHardwareListener,
|
||||
setCameraViewsNotFocusable
|
||||
};
|
||||
@ -83,14 +83,17 @@ const styles = StyleSheet.create({
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
input: {
|
||||
height: UI.INPUT_HEIGHT,
|
||||
minHeight: UI.INPUT_HEIGHT,
|
||||
borderWidth: 1,
|
||||
borderColor: APP_COLORS.borderMedium,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
paddingHorizontal: responsiveSpacing(3),
|
||||
paddingVertical: responsiveSpacing(2.5),
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary,
|
||||
backgroundColor: APP_COLORS.surface
|
||||
backgroundColor: APP_COLORS.surface,
|
||||
includeFontPadding: false,
|
||||
textAlignVertical: 'center'
|
||||
},
|
||||
inputFocused: {
|
||||
borderColor: APP_COLORS.primary,
|
||||
|
||||
26
rn/app/src/styles/layout/AppIdleProvider.styles.js
Normal file
26
rn/app/src/styles/layout/AppIdleProvider.styles.js
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили обёртки провайдера простоя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
touchArea: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
112
rn/app/src/styles/menu/MenuItemIcon.styles.js
Normal file
112
rn/app/src/styles/menu/MenuItemIcon.styles.js
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили компонента иконки пункта меню
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Базовый размер контейнера иконки (должен совпадать с menuItemIcon в MenuItem.styles)
|
||||
const ICON_SIZE = responsiveSize(24);
|
||||
|
||||
//Стили компонента иконки пункта меню
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: ICON_SIZE,
|
||||
height: ICON_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
//Иконка «Настройки» (шестерёнка): центральный круг
|
||||
settingsCircle: {
|
||||
position: 'absolute',
|
||||
width: responsiveSize(10),
|
||||
height: responsiveSize(10),
|
||||
borderRadius: responsiveSize(5),
|
||||
borderWidth: responsiveSize(2),
|
||||
borderColor: APP_COLORS.textPrimary
|
||||
},
|
||||
settingsTooth: {
|
||||
position: 'absolute',
|
||||
width: responsiveSize(2),
|
||||
height: responsiveSize(6),
|
||||
backgroundColor: APP_COLORS.textPrimary,
|
||||
borderRadius: 1
|
||||
},
|
||||
|
||||
//Иконка «О приложении» (информация): круг с буквой i
|
||||
aboutCircle: {
|
||||
width: responsiveSize(18),
|
||||
height: responsiveSize(18),
|
||||
borderRadius: responsiveSize(9),
|
||||
borderWidth: responsiveSize(2),
|
||||
borderColor: APP_COLORS.textPrimary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
aboutText: {
|
||||
fontSize: responsiveSize(12),
|
||||
fontWeight: '700',
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
|
||||
//Иконка «Вход»: рамка и стрелка вправо
|
||||
loginFrame: {
|
||||
width: responsiveSize(14),
|
||||
height: responsiveSize(14),
|
||||
borderWidth: responsiveSize(2),
|
||||
borderColor: APP_COLORS.textPrimary,
|
||||
borderRadius: responsiveSize(2),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
loginArrow: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTopWidth: responsiveSize(4),
|
||||
borderBottomWidth: responsiveSize(4),
|
||||
borderLeftWidth: responsiveSize(5),
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
borderLeftColor: APP_COLORS.textPrimary,
|
||||
marginLeft: responsiveSize(2)
|
||||
},
|
||||
|
||||
//Иконка «Выход»: рамка и стрелка влево
|
||||
logoutFrame: {
|
||||
width: responsiveSize(14),
|
||||
height: responsiveSize(14),
|
||||
borderWidth: responsiveSize(2),
|
||||
borderColor: APP_COLORS.textPrimary,
|
||||
borderRadius: responsiveSize(2),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logoutArrow: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTopWidth: responsiveSize(4),
|
||||
borderBottomWidth: responsiveSize(4),
|
||||
borderRightWidth: responsiveSize(5),
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
borderRightColor: APP_COLORS.textPrimary,
|
||||
marginRight: responsiveSize(2)
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
48
rn/app/src/styles/scanner/CameraPausedOverlay.styles.js
Normal file
48
rn/app/src/styles/scanner/CameraPausedOverlay.styles.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили оверлея «камера на паузе» (серая область с подсказкой)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема
|
||||
const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
const { UI } = require('../../config/appConfig'); //Конфигурация UI
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Фон оверлея: серый с прозрачностью (область камеры на паузе)
|
||||
const OVERLAY_BACKGROUND = 'rgba(96, 96, 96, 0.65)';
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Стили оверлея паузы камеры
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: OVERLAY_BACKGROUND,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: responsiveSpacing(4),
|
||||
borderRadius: UI.BORDER_RADIUS
|
||||
},
|
||||
message: {
|
||||
color: APP_COLORS.textInverse,
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
textAlign: 'center',
|
||||
maxWidth: '100%'
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -7,7 +7,7 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet, Platform } = require('react-native'); //StyleSheet React Native
|
||||
const { StyleSheet, Platform, Dimensions } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
const { UI } = require('../../config/appConfig'); //Конфигурация UI
|
||||
const { responsiveSpacing, widthPercentage } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
@ -16,18 +16,50 @@ const { responsiveSpacing, widthPercentage } = require('../../utils/responsive')
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const windowSize = Dimensions.get('window');
|
||||
const resultCardWidth = widthPercentage(92);
|
||||
|
||||
//Стили модального окна результата сканирования
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
modalRoot: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: APP_COLORS.overlay
|
||||
},
|
||||
overlayContent: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1001
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: windowSize.width,
|
||||
height: windowSize.height,
|
||||
backgroundColor: APP_COLORS.overlay,
|
||||
zIndex: 1000
|
||||
},
|
||||
backdrop: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: UI.PADDING
|
||||
},
|
||||
container: {
|
||||
width: '100%',
|
||||
maxWidth: widthPercentage(90),
|
||||
width: resultCardWidth,
|
||||
minWidth: resultCardWidth,
|
||||
maxWidth: resultCardWidth,
|
||||
backgroundColor: APP_COLORS.surface,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
...Platform.select({
|
||||
@ -83,6 +115,7 @@ const styles = StyleSheet.create({
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
valueBlock: {
|
||||
width: '100%',
|
||||
paddingVertical: responsiveSpacing(2),
|
||||
paddingHorizontal: responsiveSpacing(3),
|
||||
backgroundColor: APP_COLORS.surfaceAlt,
|
||||
@ -90,6 +123,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: responsiveSpacing(4)
|
||||
},
|
||||
valueText: {
|
||||
width: '100%',
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
|
||||
@ -23,6 +23,14 @@ const styles = StyleSheet.create({
|
||||
minHeight: responsiveSpacing(30),
|
||||
marginBottom: responsiveSpacing(2)
|
||||
},
|
||||
scannerWrapper: {
|
||||
flex: 1,
|
||||
width: '100%'
|
||||
},
|
||||
//Область тапа для включения камеры (оверлей поверх сканера на паузе)
|
||||
cameraPausedOverlayTouchable: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
},
|
||||
modalContainer: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
@ -28,6 +28,15 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
button: {
|
||||
minWidth: responsiveSpacing(40)
|
||||
},
|
||||
hint: {
|
||||
textAlign: 'center',
|
||||
color: APP_COLORS.textSecondary,
|
||||
marginBottom: responsiveSpacing(3)
|
||||
},
|
||||
secondaryButton: {
|
||||
marginTop: responsiveSpacing(2),
|
||||
minWidth: responsiveSpacing(40)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -56,6 +56,16 @@ const styles = StyleSheet.create({
|
||||
color: APP_COLORS.white,
|
||||
fontWeight: '600'
|
||||
},
|
||||
logoutButton: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: APP_COLORS.error,
|
||||
marginTop: responsiveSpacing(3)
|
||||
},
|
||||
logoutButtonText: {
|
||||
color: APP_COLORS.error,
|
||||
fontWeight: '600'
|
||||
},
|
||||
hint: {
|
||||
textAlign: 'center',
|
||||
marginTop: responsiveSpacing(4),
|
||||
|
||||
@ -88,6 +88,33 @@ const styles = StyleSheet.create({
|
||||
switchRow: {
|
||||
marginTop: responsiveSpacing(3)
|
||||
},
|
||||
prioritySection: {
|
||||
marginTop: responsiveSpacing(4)
|
||||
},
|
||||
priorityRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: responsiveSpacing(2.5),
|
||||
paddingHorizontal: responsiveSpacing(2),
|
||||
marginTop: responsiveSpacing(2),
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
borderWidth: 1,
|
||||
borderColor: APP_COLORS.borderSubtle
|
||||
},
|
||||
priorityRowSelected: {
|
||||
borderColor: APP_COLORS.primary,
|
||||
backgroundColor: APP_COLORS.primaryExtraLight
|
||||
},
|
||||
priorityRowText: {
|
||||
flex: 1,
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
priorityCheck: {
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.primary
|
||||
},
|
||||
actionButton: {
|
||||
marginTop: responsiveSpacing(3)
|
||||
},
|
||||
|
||||
53
rn/app/src/utils/authFormFieldsOrder.js
Normal file
53
rn/app/src/utils/authFormFieldsOrder.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Порядок и доступность полей формы аутентификации для подстановки данных сканера
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { AUTH_FORM_FIELD_ORDER } = require('../config/authFormFieldsConfig'); //Порядок полей формы
|
||||
const { resolveScanTarget, getNextInOrder } = require('./scanInputTargetResolver'); //Резолвер цели для сканера
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Проверка доступности поля по опциям (видимость и редактируемость)
|
||||
function isFieldAllowed(fieldId, options) {
|
||||
if (!options || typeof options !== 'object') return false;
|
||||
const { serverVisible, serverEditable, loginEditable, passwordEditable } = options;
|
||||
if (fieldId === 'server') return Boolean(serverVisible && serverEditable);
|
||||
if (fieldId === 'login') return Boolean(loginEditable);
|
||||
if (fieldId === 'password') return Boolean(passwordEditable);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Возвращает список идентификаторов полей формы, доступных для ввода (видимых и не заблокированных)
|
||||
function getAllowedAuthFields(options) {
|
||||
if (!options || typeof options !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return AUTH_FORM_FIELD_ORDER.filter(fieldId => isFieldAllowed(fieldId, options));
|
||||
}
|
||||
|
||||
//Возвращает идентификатор следующего поля после текущего в списке доступных, либо null (после последнего — отправка формы)
|
||||
function getNextAuthField(currentFieldId, allowedFields) {
|
||||
return getNextInOrder(currentFieldId, allowedFields);
|
||||
}
|
||||
|
||||
//Определяет поле для первого сегмента сканера
|
||||
function getScannerInitialTarget(focusedFieldId, allowedFields) {
|
||||
return resolveScanTarget(focusedFieldId, allowedFields);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
getAllowedAuthFields,
|
||||
getNextAuthField,
|
||||
getScannerInitialTarget
|
||||
};
|
||||
31
rn/app/src/utils/authScannerUtils.js
Normal file
31
rn/app/src/utils/authScannerUtils.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Утилиты разбора данных от встроенного сканера на экране аутентификации
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Разделители строк (Enter) в данных штрихкода (CRLF и LF)
|
||||
const LINE_BREAK_REGEX = /\r?\n/;
|
||||
|
||||
//Разбивает отсканированную строку на сегменты по Enter (данные как есть, без trim)
|
||||
function parseScannedSegmentsForAuth(rawString) {
|
||||
if (rawString == null || typeof rawString !== 'string') {
|
||||
return [];
|
||||
}
|
||||
if (rawString === '') {
|
||||
return [];
|
||||
}
|
||||
return rawString.split(LINE_BREAK_REGEX);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
parseScannedSegmentsForAuth,
|
||||
LINE_BREAK_REGEX
|
||||
};
|
||||
@ -107,8 +107,7 @@ const getAndroidId = async () => {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Не удалось получить Android ID:', error);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -134,8 +133,7 @@ const getIosId = async () => {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Не удалось получить iOS ID:', error);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
61
rn/app/src/utils/logoutFlow.js
Normal file
61
rn/app/src/utils/logoutFlow.js
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Универсальный поток выхода из приложения
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обработчики: выполнение выхода и показ диалога подтверждения
|
||||
function createLogoutHandler(deps) {
|
||||
const {
|
||||
logout,
|
||||
mode,
|
||||
setNotConnected,
|
||||
setInitialScreen,
|
||||
SCREENS,
|
||||
showError,
|
||||
showInfo,
|
||||
logoutConfirmTitle,
|
||||
logoutConfirmMessage,
|
||||
logoutButtonTitle,
|
||||
getConfirmButtonOptions,
|
||||
dialogButtonTypeError,
|
||||
dialogCancelButton
|
||||
} = deps;
|
||||
|
||||
const performLogout = async () => {
|
||||
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
|
||||
|
||||
if (result.success) {
|
||||
setNotConnected();
|
||||
setInitialScreen(SCREENS.AUTH);
|
||||
} else {
|
||||
showError(result.error || 'Ошибка выхода');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
const confirmButton = getConfirmButtonOptions(dialogButtonTypeError, logoutButtonTitle, performLogout);
|
||||
|
||||
showInfo(logoutConfirmMessage, {
|
||||
title: logoutConfirmTitle,
|
||||
buttons: [dialogCancelButton, confirmButton]
|
||||
});
|
||||
};
|
||||
|
||||
return { performLogout, handleLogout };
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
createLogoutHandler
|
||||
};
|
||||
23
rn/app/src/utils/noop.js
Normal file
23
rn/app/src/utils/noop.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Пустые функции для использования в обработчиках и .catch()
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Функции
|
||||
//---------
|
||||
|
||||
//Пустая функция (например для onPress, чтобы не передавать клик дальше)
|
||||
function noop() {}
|
||||
|
||||
//Пустой обработчик для .catch() — подавляет ошибку без побочных эффектов
|
||||
function noopCatch() {}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
noop,
|
||||
noopCatch
|
||||
};
|
||||
@ -19,7 +19,7 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
//Проверка на старые устройства Android
|
||||
const isLegacyAndroid = () => {
|
||||
return Platform.OS === 'android' && Platform.Version < 26; // Android 8.0 = API 26
|
||||
return Platform.OS === 'android' && Platform.Version < 26; //Android 8.0 = API 26
|
||||
};
|
||||
|
||||
//Нормализация размера для разных плотностей пикселей
|
||||
@ -99,7 +99,7 @@ const isLargeScreen = () => {
|
||||
|
||||
//Адаптивный отступ
|
||||
const responsiveSpacing = (size = 1) => {
|
||||
const baseSpacing = 4; // 4px базовый отступ
|
||||
const baseSpacing = 4; //4px базовый отступ
|
||||
return responsiveSize(baseSpacing * size);
|
||||
};
|
||||
|
||||
|
||||
39
rn/app/src/utils/scanInputTargetResolver.js
Normal file
39
rn/app/src/utils/scanInputTargetResolver.js
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Разрешение цели для данных встроенного сканера
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Определяет поле ввода для подстановки: сфокусированное (если в списке доступных), иначе первое доступное
|
||||
function resolveScanTarget(focusedFieldId, allowedFieldIds) {
|
||||
if (!allowedFieldIds || !Array.isArray(allowedFieldIds) || allowedFieldIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (focusedFieldId != null && allowedFieldIds.indexOf(focusedFieldId) >= 0) {
|
||||
return focusedFieldId;
|
||||
}
|
||||
return allowedFieldIds[0];
|
||||
}
|
||||
|
||||
//Возвращает идентификатор следующего поля в порядке списка после текущего, либо null
|
||||
function getNextInOrder(currentFieldId, allowedFieldIds) {
|
||||
if (!allowedFieldIds || !Array.isArray(allowedFieldIds) || allowedFieldIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const idx = allowedFieldIds.indexOf(currentFieldId);
|
||||
if (idx < 0) return null;
|
||||
const nextIdx = idx + 1;
|
||||
return nextIdx < allowedFieldIds.length ? allowedFieldIds[nextIdx] : null;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
resolveScanTarget,
|
||||
getNextInOrder
|
||||
};
|
||||
@ -112,7 +112,6 @@ const encryptData = (data, secretKey, salt = '') => {
|
||||
}
|
||||
|
||||
if (!secretKey) {
|
||||
console.error('Ошибка шифрования: секретный ключ не предоставлен');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -121,7 +120,6 @@ const encryptData = (data, secretKey, salt = '') => {
|
||||
const encrypted = xorEncrypt(data, keyHash);
|
||||
return ENCRYPTED_PREFIX + encrypted;
|
||||
} catch (error) {
|
||||
console.error('Ошибка шифрования:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@ -133,7 +131,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
|
||||
}
|
||||
|
||||
if (!secretKey) {
|
||||
console.error('Ошибка расшифровки: секретный ключ не предоставлен');
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -148,7 +145,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
|
||||
const keyHash = generateKeyHash(secretKey, salt);
|
||||
return xorDecrypt(encryptedHex, keyHash);
|
||||
} catch (error) {
|
||||
console.error('Ошибка расшифровки:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
40
rn/app/src/utils/serverRequestTimeout.js
Normal file
40
rn/app/src/utils/serverRequestTimeout.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Утилита таймаута запросов к серверу
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Возвращает максимальное время ожидания ответа от сервера в миллисекундах (Promise)
|
||||
async function getServerRequestTimeoutMs(getSetting) {
|
||||
try {
|
||||
const raw = await getSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT);
|
||||
const trimmed = raw != null ? String(raw).trim() : '';
|
||||
if (trimmed === '') {
|
||||
return 0;
|
||||
}
|
||||
const sec = parseInt(trimmed, 10);
|
||||
if (isNaN(sec) || sec <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return sec * 1000;
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
getServerRequestTimeoutMs
|
||||
};
|
||||
@ -19,7 +19,10 @@ const VALIDATION_MESSAGES = {
|
||||
SERVER_URL_INVALID_SHORT: 'Некорректный формат URL',
|
||||
IDLE_TIMEOUT_EMPTY: 'Введите время простоя',
|
||||
IDLE_TIMEOUT_MIN: 'Введите положительное число (минимум 1 минута)',
|
||||
IDLE_TIMEOUT_MAX: 'Максимальное значение: 1440 минут (24 часа)'
|
||||
IDLE_TIMEOUT_MAX: 'Максимальное значение: 1440 минут (24 часа)',
|
||||
SERVER_REQUEST_TIMEOUT_EMPTY: 'Введите время ожидания ответа от сервера',
|
||||
SERVER_REQUEST_TIMEOUT_MIN: 'Значение должно быть больше 1 (минимум 1 секунда)',
|
||||
SERVER_REQUEST_TIMEOUT_MAX: 'Максимальное значение: 600 секунд (10 минут)'
|
||||
};
|
||||
|
||||
//-----------
|
||||
@ -87,6 +90,42 @@ function validateIdleTimeout(value, options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Валидация времени простоя с допуском пустого или нуля
|
||||
function validateIdleTimeoutAllowEmpty(value, options) {
|
||||
const trimmed = value != null ? String(value).trim() : '';
|
||||
if (trimmed === '') {
|
||||
return true;
|
||||
}
|
||||
const num = parseInt(trimmed, 10);
|
||||
if (num === 0) {
|
||||
return true;
|
||||
}
|
||||
return validateIdleTimeout(value, options);
|
||||
}
|
||||
|
||||
//Валидация времени ожидания ответа от сервера (секунды)
|
||||
function validateServerRequestTimeout(value, options) {
|
||||
const opts = options || {};
|
||||
const min = opts.min !== undefined ? opts.min : 1;
|
||||
const max = opts.max !== undefined ? opts.max : 600;
|
||||
const emptyMessage = opts.emptyMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_EMPTY;
|
||||
const minMessage = opts.minMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_MIN;
|
||||
const maxMessage = opts.maxMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_MAX;
|
||||
|
||||
const trimmed = value != null ? String(value).trim() : '';
|
||||
if (trimmed === '') {
|
||||
return emptyMessage;
|
||||
}
|
||||
const num = parseInt(trimmed, 10);
|
||||
if (isNaN(num) || num < min) {
|
||||
return minMessage;
|
||||
}
|
||||
if (num > max) {
|
||||
return maxMessage;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
@ -96,5 +135,7 @@ module.exports = {
|
||||
normalizeServerUrl,
|
||||
validateServerUrl,
|
||||
validateServerUrlAllowEmpty,
|
||||
validateIdleTimeout
|
||||
validateIdleTimeout,
|
||||
validateIdleTimeoutAllowEmpty,
|
||||
validateServerRequestTimeout
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user