refactor(mobile): more look and feel work
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m17s
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m17s
This commit is contained in:
@@ -15,8 +15,8 @@
|
|||||||
"foregroundImage": "./assets/adaptive-icon-white.png",
|
"foregroundImage": "./assets/adaptive-icon-white.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"versionCode": 8,
|
"versionCode": 10,
|
||||||
"minSupportedVersionCode": 4,
|
"minSupportedVersionCode": 5,
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "net.alpla.lst.mobile"
|
"package": "net.alpla.lst.mobile"
|
||||||
},
|
},
|
||||||
@@ -43,7 +43,8 @@
|
|||||||
"imageWidth": 200
|
"imageWidth": 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-audio"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
|
|||||||
BIN
lstMobile/assets/sounds/bad.wav
Normal file
BIN
lstMobile/assets/sounds/bad.wav
Normal file
Binary file not shown.
BIN
lstMobile/assets/sounds/good.wav
Normal file
BIN
lstMobile/assets/sounds/good.wav
Normal file
Binary file not shown.
62
lstMobile/package-lock.json
generated
62
lstMobile/package-lock.json
generated
@@ -19,12 +19,16 @@
|
|||||||
"babel-preset-expo": "^55.0.18",
|
"babel-preset-expo": "^55.0.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"expo": "~55.0.15",
|
"expo": "~55.0.15",
|
||||||
"expo-application": "~55.0.14",
|
"expo-application": "~55.0.14",
|
||||||
|
"expo-audio": "~55.0.14",
|
||||||
|
"expo-av": "^16.0.8",
|
||||||
"expo-constants": "~55.0.14",
|
"expo-constants": "~55.0.14",
|
||||||
"expo-device": "~55.0.15",
|
"expo-device": "~55.0.15",
|
||||||
"expo-font": "~55.0.6",
|
"expo-font": "~55.0.6",
|
||||||
"expo-glass-effect": "~55.0.10",
|
"expo-glass-effect": "~55.0.10",
|
||||||
|
"expo-haptics": "~55.0.14",
|
||||||
"expo-image": "~55.0.8",
|
"expo-image": "~55.0.8",
|
||||||
"expo-linking": "~55.0.13",
|
"expo-linking": "~55.0.13",
|
||||||
"expo-router": "~55.0.12",
|
"expo-router": "~55.0.12",
|
||||||
@@ -5808,6 +5812,26 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-fns-tz": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dateformat": {
|
"node_modules/dateformat": {
|
||||||
"version": "4.6.3",
|
"version": "4.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||||
@@ -7586,6 +7610,35 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-audio": {
|
||||||
|
"version": "55.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-audio/-/expo-audio-55.0.14.tgz",
|
||||||
|
"integrity": "sha512-Biy6ffKXrnKHgcWSVWLKVdWLNhV/pj1JWJeotY6nDR6fVe8mjXQDCvi6EbaSFPdffVHym6UB2siKzWUNSnG+kQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"expo-asset": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-av": {
|
||||||
|
"version": "16.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-av/-/expo-av-16.0.8.tgz",
|
||||||
|
"integrity": "sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-web": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "55.0.14",
|
"version": "55.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.14.tgz",
|
||||||
@@ -7647,6 +7700,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-haptics": {
|
||||||
|
"version": "55.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-55.0.14.tgz",
|
||||||
|
"integrity": "sha512-KjDItBsA9mi1f5nRwf8g1wOdfEcLHwvEdt5Jl1sMCDETR/homcGOl+F3QIiPOl/PRlbGVieQsjTtF4DGtHOj6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-image": {
|
"node_modules/expo-image": {
|
||||||
"version": "55.0.8",
|
"version": "55.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-55.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-55.0.8.tgz",
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ",
|
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk",
|
||||||
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && cd .. && copy /Y android\\app\\build\\outputs\\apk\\release\\app-release.apk downloads\\mobile\\lst-mobile.apk",
|
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
|
||||||
|
"copy:apk": "cd android && copy /Y app\\build\\outputs\\apk\\release\\app-release.apk ..\\..\\downloads\\mobile\\lst-mobile.apk",
|
||||||
"update": "adb install android/app/build/outputs/apk/release/app-release.apk"
|
"update": "adb install android/app/build/outputs/apk/release/app-release.apk"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,12 +26,16 @@
|
|||||||
"babel-preset-expo": "^55.0.18",
|
"babel-preset-expo": "^55.0.18",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"expo": "~55.0.15",
|
"expo": "~55.0.15",
|
||||||
"expo-application": "~55.0.14",
|
"expo-application": "~55.0.14",
|
||||||
|
"expo-audio": "~55.0.14",
|
||||||
|
"expo-av": "^16.0.8",
|
||||||
"expo-constants": "~55.0.14",
|
"expo-constants": "~55.0.14",
|
||||||
"expo-device": "~55.0.15",
|
"expo-device": "~55.0.15",
|
||||||
"expo-font": "~55.0.6",
|
"expo-font": "~55.0.6",
|
||||||
"expo-glass-effect": "~55.0.10",
|
"expo-glass-effect": "~55.0.10",
|
||||||
|
"expo-haptics": "~55.0.14",
|
||||||
"expo-image": "~55.0.8",
|
"expo-image": "~55.0.8",
|
||||||
"expo-linking": "~55.0.13",
|
"expo-linking": "~55.0.13",
|
||||||
"expo-router": "~55.0.12",
|
"expo-router": "~55.0.12",
|
||||||
|
|||||||
@@ -138,18 +138,12 @@ class ZebraScannerModule(
|
|||||||
fun ensureProfile() {
|
fun ensureProfile() {
|
||||||
val profileName = "LST_MOBILE"
|
val profileName = "LST_MOBILE"
|
||||||
|
|
||||||
// Create profile (safe to call even if it exists)
|
|
||||||
sendCommand(
|
sendCommand(
|
||||||
"com.symbol.datawedge.api.CREATE_PROFILE",
|
"com.symbol.datawedge.api.CREATE_PROFILE",
|
||||||
profileName
|
profileName
|
||||||
)
|
)
|
||||||
|
|
||||||
Thread.sleep(500)
|
Thread.sleep(500)
|
||||||
// Configure profile
|
|
||||||
val profileConfig = Bundle().apply {
|
|
||||||
putString("PROFILE_NAME", profileName)
|
|
||||||
putString("PROFILE_ENABLED", "true")
|
|
||||||
putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST")
|
|
||||||
|
|
||||||
val barcodeConfig = Bundle().apply {
|
val barcodeConfig = Bundle().apply {
|
||||||
putString("PLUGIN_NAME", "BARCODE")
|
putString("PLUGIN_NAME", "BARCODE")
|
||||||
@@ -157,8 +151,19 @@ class ZebraScannerModule(
|
|||||||
|
|
||||||
val props = Bundle().apply {
|
val props = Bundle().apply {
|
||||||
putString("scanner_input_enabled", "true")
|
putString("scanner_input_enabled", "true")
|
||||||
|
|
||||||
|
// Auto-select internal scanner
|
||||||
putString("scanner_selection", "auto")
|
putString("scanner_selection", "auto")
|
||||||
putString("trigger_mode", "2") // 2 = HARD trigger only (recommended) wakes scanner up
|
putString("scanner_selection_by_identifier", "AUTO")
|
||||||
|
|
||||||
|
// Hardware trigger behavior
|
||||||
|
putString("hardware_trigger_enabled", "true")
|
||||||
|
putString("trigger_mode", "2") // 2 = HARD trigger
|
||||||
|
|
||||||
|
// Disable Zebra's loud initial decode feedback
|
||||||
|
putString("decode_audio_feedback_uri", "")
|
||||||
|
putString("decode_haptic_feedback", "false")
|
||||||
|
putString("decode_led_feedback", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
putBundle("PARAM_LIST", props)
|
putBundle("PARAM_LIST", props)
|
||||||
@@ -172,7 +177,7 @@ class ZebraScannerModule(
|
|||||||
putString("intent_output_enabled", "true")
|
putString("intent_output_enabled", "true")
|
||||||
putString("intent_action", scanAction)
|
putString("intent_action", scanAction)
|
||||||
putString("intent_delivery", "2") // broadcast
|
putString("intent_delivery", "2") // broadcast
|
||||||
putString("intent_use_content_provider", "false") // optional but helps
|
putString("intent_use_content_provider", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
putBundle("PARAM_LIST", props)
|
putBundle("PARAM_LIST", props)
|
||||||
@@ -189,6 +194,10 @@ class ZebraScannerModule(
|
|||||||
putBundle("PARAM_LIST", props)
|
putBundle("PARAM_LIST", props)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val profileConfig = Bundle().apply {
|
||||||
|
putString("PROFILE_NAME", profileName)
|
||||||
|
putString("PROFILE_ENABLED", "true")
|
||||||
|
putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST")
|
||||||
|
|
||||||
putParcelableArrayList(
|
putParcelableArrayList(
|
||||||
"PLUGIN_CONFIG",
|
"PLUGIN_CONFIG",
|
||||||
@@ -198,7 +207,6 @@ class ZebraScannerModule(
|
|||||||
|
|
||||||
sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig)
|
sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig)
|
||||||
|
|
||||||
// Associate with your app
|
|
||||||
val appConfig = Bundle().apply {
|
val appConfig = Bundle().apply {
|
||||||
putString("PACKAGE_NAME", reactContext.packageName)
|
putString("PACKAGE_NAME", reactContext.packageName)
|
||||||
putStringArray("ACTIVITY_LIST", arrayOf("*"))
|
putStringArray("ACTIVITY_LIST", arrayOf("*"))
|
||||||
@@ -211,6 +219,12 @@ class ZebraScannerModule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendCommand("com.symbol.datawedge.api.SET_CONFIG", associateConfig)
|
sendCommand("com.symbol.datawedge.api.SET_CONFIG", associateConfig)
|
||||||
|
|
||||||
|
// Runtime nudge: make sure scanner input is enabled for the active profile
|
||||||
|
sendCommand(
|
||||||
|
"com.symbol.datawedge.api.SCANNER_INPUT_PLUGIN",
|
||||||
|
"ENABLE_PLUGIN"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ export default function TabsLayout() {
|
|||||||
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
|
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* <Tabs.Screen
|
||||||
|
name="lanes"
|
||||||
|
options={{
|
||||||
|
title: "Lanes",
|
||||||
|
href:
|
||||||
|
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import axios from "axios";
|
||||||
import { Redirect, useRouter } from "expo-router";
|
import { Redirect, useRouter } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ActivityIndicator, Text, View } from "react-native";
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
@@ -11,6 +12,7 @@ export default function Index() {
|
|||||||
|
|
||||||
const hasHydrated = useAppStore((s) => s.hasHydrated);
|
const hasHydrated = useAppStore((s) => s.hasHydrated);
|
||||||
const serverPort = useAppStore((s) => s.serverPort);
|
const serverPort = useAppStore((s) => s.serverPort);
|
||||||
|
const serverIp = useAppStore((s) => s.serverIp);
|
||||||
const hasValidSetup = useAppStore((s) => s.hasValidSetup);
|
const hasValidSetup = useAppStore((s) => s.hasValidSetup);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,6 +33,23 @@ export default function Index() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checking for lst.
|
||||||
|
console.log(
|
||||||
|
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const res = await axios.get(
|
||||||
|
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
|
||||||
|
{
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error: ", error);
|
||||||
|
}
|
||||||
|
|
||||||
setMessage(<Text>Checking scanner mode...</Text>);
|
setMessage(<Text>Checking scanner mode...</Text>);
|
||||||
await devDelay(1500);
|
await devDelay(1500);
|
||||||
|
|
||||||
@@ -60,7 +79,7 @@ export default function Index() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
startup();
|
startup();
|
||||||
}, [hasHydrated, hasValidSetup, serverPort, router]);
|
}, [hasHydrated, hasValidSetup, serverPort, serverIp, router]);
|
||||||
|
|
||||||
if (ready) {
|
if (ready) {
|
||||||
return <Redirect href="/(tabs)/scanner" />;
|
return <Redirect href="/(tabs)/scanner" />;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { format } from "date-fns-tz";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
import { useAppStore } from "../hooks/useAppStore";
|
import { useAppStore } from "../hooks/useAppStore";
|
||||||
|
import { useScannerStore } from "../hooks/useScannerStore";
|
||||||
|
import { scannerFeedback } from "../lib/feedbackScan";
|
||||||
import { sendTcpMessage } from "../lib/tcpScan";
|
import { sendTcpMessage } from "../lib/tcpScan";
|
||||||
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
|
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
|
||||||
import { ScannedLabelBox } from "./ScannedLabels";
|
import { ScannedLabelBox } from "./ScannedLabels";
|
||||||
@@ -9,23 +12,32 @@ const STX = "\x02";
|
|||||||
const ETX = "\x03";
|
const ETX = "\x03";
|
||||||
|
|
||||||
export default function ProdScanner() {
|
export default function ProdScanner() {
|
||||||
const [lastScan, setLastScan] = useState<any>(null);
|
const lastScan = useScannerStore((s) => s.lastScan);
|
||||||
|
const setLastScan = useScannerStore((s) => s.setLastScan);
|
||||||
const [tagScans, setTagScans] = useState<any>([]);
|
const [tagScans, setTagScans] = useState<any>([]);
|
||||||
const scannerIdFromStore = useAppStore((s) => s.scannerId);
|
const scannerIdFromStore = useAppStore((s) => s.scannerId);
|
||||||
const serverIp = useAppStore((s) => s.serverIp);
|
const serverIp = useAppStore((s) => s.serverIp);
|
||||||
const serverPort = useAppStore((s) => s.serverPort);
|
const serverPort = useAppStore((s) => s.serverPort);
|
||||||
|
const [bgColor, setBGColor] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleScan = useCallback(
|
const handleScan = useCallback(
|
||||||
async (scan: ZebraScanResult) => {
|
async (scan: ZebraScanResult) => {
|
||||||
const scanned = scan.data;
|
let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`;
|
||||||
|
await scannerFeedback({
|
||||||
let commandToSend = `${STX}${scannerIdFromStore}@${scanned}${ETX}`;
|
type: "good",
|
||||||
|
sound: true,
|
||||||
|
vibrate: true,
|
||||||
|
led: true,
|
||||||
|
});
|
||||||
|
|
||||||
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
|
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
|
||||||
if (scan.data.startsWith("000")) {
|
if (scan.data.startsWith("000")) {
|
||||||
commandToSend = `${STX}${scannerIdFromStore}@]C1${scanned}${ETX}`;
|
commandToSend = `${STX}${scannerIdFromStore}@]C1${scan.data}${ETX}`;
|
||||||
setTagScans((prev: any) => [
|
setTagScans((prev: any) => [
|
||||||
parseInt(scanned.slice(10, -1) || "000", 10).toString(),
|
{
|
||||||
|
label: parseInt(scan.data.slice(10, -1) || "000", 10).toString(),
|
||||||
|
date: format(new Date(Date.now()), "HH:mm"),
|
||||||
|
},
|
||||||
...prev,
|
...prev,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -35,19 +47,44 @@ export default function ProdScanner() {
|
|||||||
setTagScans([]);
|
setTagScans([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const something = await sendTcpMessage(
|
const scanned = (await sendTcpMessage(
|
||||||
commandToSend,
|
commandToSend,
|
||||||
serverIp,
|
serverIp,
|
||||||
parseInt(serverPort || "0", 10),
|
parseInt(serverPort || "0", 10),
|
||||||
);
|
)) as any;
|
||||||
// Later this is where your TCP send goes.
|
// Later this is where your TCP send goes.
|
||||||
// const response = await sendTcpMessage(tcpMessage);
|
// const response = await sendTcpMessage(tcpMessage);
|
||||||
setLastScan(something.data[0]);
|
console.log(scanned);
|
||||||
|
await scannerFeedback({
|
||||||
|
type: scanned.data[0]?.type === "error" ? "bad" : "good",
|
||||||
|
sound: true,
|
||||||
|
vibrate: true,
|
||||||
|
led: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scanned.data[0]?.type !== "error") {
|
||||||
|
setBGColor("bg-green-500");
|
||||||
|
setTimeout(() => {
|
||||||
|
setBGColor(null);
|
||||||
|
}, 1 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanned.data[0]?.type === "error") {
|
||||||
|
setBGColor("bg-red-500");
|
||||||
|
setTimeout(() => {
|
||||||
|
setBGColor(null);
|
||||||
|
}, 1 * 1000);
|
||||||
|
}
|
||||||
|
setLastScan(scanned.data[0]);
|
||||||
//console.log("TCP response:", something);
|
//console.log("TCP response:", something);
|
||||||
},
|
},
|
||||||
[scannerIdFromStore, serverIp, serverPort],
|
[scannerIdFromStore, serverIp, serverPort, setLastScan],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clearScans = () => {
|
||||||
|
setTagScans([]);
|
||||||
|
};
|
||||||
|
|
||||||
console.log(lastScan);
|
console.log(lastScan);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,21 +102,17 @@ export default function ProdScanner() {
|
|||||||
};
|
};
|
||||||
}, [handleScan]);
|
}, [handleScan]);
|
||||||
return (
|
return (
|
||||||
<View>
|
<View className={`${bgColor ?? ""} flex-1 w-screen`}>
|
||||||
<View>
|
<View>
|
||||||
<View style={{ alignItems: "center", margin: 10 }}>
|
<View style={{ alignItems: "center", margin: 10 }}>
|
||||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
<Text style={{ fontSize: 15, fontWeight: "600" }}>
|
||||||
Scanner ID: {parseInt(scannerIdFromStore || "0", 10)}
|
Scanner ID: {parseInt(scannerIdFromStore || "0", 10)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{!lastScan ? (
|
{!lastScan ? (
|
||||||
<View
|
<View style={{ marginTop: 10, alignItems: "center" }}>
|
||||||
style={{
|
<Text className="text-xl font-bold">Ready to scan</Text>
|
||||||
marginTop: 10,
|
<Text>Waiting for first scan...</Text>
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className="text-xl font-bold">Waiting on scan....</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
@@ -88,34 +121,25 @@ export default function ProdScanner() {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<View style={{ marginTop: 10, alignItems: "center" }}>
|
||||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
||||||
{lastScan?.action}
|
{lastScan.action}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{lastScan?.type === "error" ? (
|
|
||||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
||||||
{lastScan?.message}
|
{lastScan.message}
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
marginTop: 15,
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
|
||||||
{lastScan?.prompt}
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
|
||||||
{lastScan?.message}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
<View className="flex-1 w-full px-4">
|
||||||
|
<ScannedLabelBox
|
||||||
|
labels={tagScans}
|
||||||
|
color={bgColor}
|
||||||
|
clearScan={clearScans}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScannedLabelBox labels={tagScans} />
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,55 @@
|
|||||||
import { ScrollView, Text, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
|
||||||
type ScannedLabel = {
|
type ScannedLabel = {
|
||||||
id: string;
|
label: string;
|
||||||
barcode: string;
|
date: Date;
|
||||||
createdAt: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScannedLabelBoxProps = {
|
type ScannedLabelBoxProps = {
|
||||||
labels: ScannedLabel[];
|
labels: ScannedLabel[];
|
||||||
|
color: string | null;
|
||||||
|
clearScan: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ScannedLabelBox({ labels }: ScannedLabelBoxProps) {
|
export function ScannedLabelBox({
|
||||||
|
labels,
|
||||||
|
color,
|
||||||
|
clearScan,
|
||||||
|
}: ScannedLabelBoxProps) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, marginTop: 30 }}>
|
<SafeAreaView className={`flex-1 w-full items-center ${color ?? ""}`}>
|
||||||
|
<View className="flex flex-col gap-2">
|
||||||
<Text style={{ fontSize: 18, fontWeight: "700", marginBottom: 8 }}>
|
<Text style={{ fontSize: 18, fontWeight: "700", marginBottom: 8 }}>
|
||||||
Current scanned labels
|
Current scanned labels
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ccc",
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 2,
|
|
||||||
margin: 2,
|
|
||||||
}}
|
|
||||||
contentContainerStyle={{ gap: 2 }}
|
|
||||||
>
|
|
||||||
{labels.length === 0 ? (
|
|
||||||
<Text style={{ color: "#777" }}>No labels scanned yet</Text>
|
|
||||||
) : (
|
|
||||||
labels.map((label) => (
|
|
||||||
<View
|
|
||||||
key={`${label}`}
|
|
||||||
style={{
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 8,
|
|
||||||
backgroundColor: "#f2f2f2",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 18, fontWeight: "700" }}>{label}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
))
|
|
||||||
|
<ScrollView className="w-full flex-1">
|
||||||
|
{labels.length === 0 ? (
|
||||||
|
<Text className="text-center">No labels scanned yet</Text>
|
||||||
|
) : (
|
||||||
|
<View className="flex items-center gap-2 w-full">
|
||||||
|
{labels.map((i, index) => (
|
||||||
|
<View
|
||||||
|
key={`${i.label}-${index}`}
|
||||||
|
className={`p-2 border rounded items-center ${color ?? ""}`}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: "700" }}>
|
||||||
|
{i.label} - {i.date.toString()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
{labels.length !== 0 && (
|
||||||
|
<Button onPress={clearScan} variant="secondary">
|
||||||
|
<Text>Clear Scans</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
106
lstMobile/src/components/ui/button.tsx
Normal file
106
lstMobile/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { TextClassContext } from '@/components/ui/text';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import { Platform, Pressable } from 'react-native';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
cn(
|
||||||
|
'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none',
|
||||||
|
Platform.select({
|
||||||
|
web: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: cn(
|
||||||
|
'bg-primary active:bg-primary/90 shadow-sm shadow-black/5',
|
||||||
|
Platform.select({ web: 'hover:bg-primary/90' })
|
||||||
|
),
|
||||||
|
destructive: cn(
|
||||||
|
'bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5',
|
||||||
|
Platform.select({
|
||||||
|
web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
outline: cn(
|
||||||
|
'border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5',
|
||||||
|
Platform.select({
|
||||||
|
web: 'hover:bg-accent dark:hover:bg-input/50',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
secondary: cn(
|
||||||
|
'bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5',
|
||||||
|
Platform.select({ web: 'hover:bg-secondary/80' })
|
||||||
|
),
|
||||||
|
ghost: cn(
|
||||||
|
'active:bg-accent dark:active:bg-accent/50',
|
||||||
|
Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' })
|
||||||
|
),
|
||||||
|
link: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: cn('h-10 px-4 py-2 sm:h-9', Platform.select({ web: 'has-[>svg]:px-3' })),
|
||||||
|
sm: cn('h-9 gap-1.5 rounded-md px-3 sm:h-8', Platform.select({ web: 'has-[>svg]:px-2.5' })),
|
||||||
|
lg: cn('h-11 rounded-md px-6 sm:h-10', Platform.select({ web: 'has-[>svg]:px-4' })),
|
||||||
|
icon: 'h-10 w-10 sm:h-9 sm:w-9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonTextVariants = cva(
|
||||||
|
cn(
|
||||||
|
'text-foreground text-sm font-medium',
|
||||||
|
Platform.select({ web: 'pointer-events-none transition-colors' })
|
||||||
|
),
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'text-primary-foreground',
|
||||||
|
destructive: 'text-white',
|
||||||
|
outline: cn(
|
||||||
|
'group-active:text-accent-foreground',
|
||||||
|
Platform.select({ web: 'group-hover:text-accent-foreground' })
|
||||||
|
),
|
||||||
|
secondary: 'text-secondary-foreground',
|
||||||
|
ghost: 'group-active:text-accent-foreground',
|
||||||
|
link: cn(
|
||||||
|
'text-primary group-active:underline',
|
||||||
|
Platform.select({ web: 'underline-offset-4 hover:underline group-hover:underline' })
|
||||||
|
),
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: '',
|
||||||
|
sm: '',
|
||||||
|
lg: '',
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ButtonProps = React.ComponentProps<typeof Pressable> & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
|
function Button({ className, variant, size, ...props }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
|
||||||
|
<Pressable
|
||||||
|
className={cn(props.disabled && 'opacity-50', buttonVariants({ variant, size }), className)}
|
||||||
|
role="button"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</TextClassContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonTextVariants, buttonVariants };
|
||||||
|
export type { ButtonProps };
|
||||||
@@ -33,10 +33,8 @@ type AppActions = {
|
|||||||
setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void;
|
setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void;
|
||||||
setAppVersion: (value?: string) => void;
|
setAppVersion: (value?: string) => void;
|
||||||
setHasHydrated: (value: boolean) => void;
|
setHasHydrated: (value: boolean) => void;
|
||||||
|
|
||||||
updateAppState: (updates: Partial<AppState>) => void;
|
updateAppState: (updates: Partial<AppState>) => void;
|
||||||
resetApp: () => void;
|
resetApp: () => void;
|
||||||
|
|
||||||
hasValidSetup: () => boolean;
|
hasValidSetup: () => boolean;
|
||||||
canEnterApp: () => boolean;
|
canEnterApp: () => boolean;
|
||||||
getServerUrl: () => string;
|
getServerUrl: () => string;
|
||||||
@@ -50,15 +48,11 @@ const defaultAppState: AppState = {
|
|||||||
scannerId: "0001",
|
scannerId: "0001",
|
||||||
stageId: undefined,
|
stageId: undefined,
|
||||||
deviceName: undefined,
|
deviceName: undefined,
|
||||||
|
|
||||||
setupCompleted: false,
|
setupCompleted: false,
|
||||||
isRegistered: false,
|
isRegistered: false,
|
||||||
|
|
||||||
lastValidationStatus: "idle",
|
lastValidationStatus: "idle",
|
||||||
lastValidationAt: undefined,
|
lastValidationAt: undefined,
|
||||||
|
|
||||||
appVersion: undefined,
|
appVersion: undefined,
|
||||||
|
|
||||||
hasHydrated: false,
|
hasHydrated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,28 +68,23 @@ export const useAppStore = create<AppStore>()(
|
|||||||
setDeviceName: (value) => set({ deviceName: value }),
|
setDeviceName: (value) => set({ deviceName: value }),
|
||||||
setSetupCompleted: (value) => set({ setupCompleted: value }),
|
setSetupCompleted: (value) => set({ setupCompleted: value }),
|
||||||
setIsRegistered: (value) => set({ isRegistered: value }),
|
setIsRegistered: (value) => set({ isRegistered: value }),
|
||||||
|
|
||||||
setValidationStatus: (status, validatedAt) =>
|
setValidationStatus: (status, validatedAt) =>
|
||||||
set({
|
set({
|
||||||
lastValidationStatus: status,
|
lastValidationStatus: status,
|
||||||
lastValidationAt: validatedAt,
|
lastValidationAt: validatedAt,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setAppVersion: (value) => set({ appVersion: value }),
|
setAppVersion: (value) => set({ appVersion: value }),
|
||||||
setHasHydrated: (value) => set({ hasHydrated: value }),
|
setHasHydrated: (value) => set({ hasHydrated: value }),
|
||||||
|
|
||||||
updateAppState: (updates) =>
|
updateAppState: (updates) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
...state,
|
...state,
|
||||||
...updates,
|
...updates,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
resetApp: () =>
|
resetApp: () =>
|
||||||
set({
|
set({
|
||||||
...defaultAppState,
|
...defaultAppState,
|
||||||
hasHydrated: true,
|
hasHydrated: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasValidSetup: () => {
|
hasValidSetup: () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
return Boolean(
|
return Boolean(
|
||||||
@@ -104,7 +93,6 @@ export const useAppStore = create<AppStore>()(
|
|||||||
state.setupCompleted,
|
state.setupCompleted,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
canEnterApp: () => {
|
canEnterApp: () => {
|
||||||
const state = get();
|
const state = get();
|
||||||
return Boolean(
|
return Boolean(
|
||||||
|
|||||||
31
lstMobile/src/hooks/useScannerStore.ts
Normal file
31
lstMobile/src/hooks/useScannerStore.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type LastScan = {
|
||||||
|
action?: string;
|
||||||
|
type?: "success" | "error" | string;
|
||||||
|
prompt?: string;
|
||||||
|
message?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScannerStore = {
|
||||||
|
lastScan: LastScan | null;
|
||||||
|
setLastScan: (scan: LastScan | null) => void;
|
||||||
|
clearLastScan: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScannerStore = create<ScannerStore>((set) => ({
|
||||||
|
lastScan: null,
|
||||||
|
|
||||||
|
setLastScan: (scan) =>
|
||||||
|
set({
|
||||||
|
lastScan: scan
|
||||||
|
? {
|
||||||
|
...scan,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearLastScan: () => set({ lastScan: null }),
|
||||||
|
}));
|
||||||
38
lstMobile/src/lib/feedbackScan.ts
Normal file
38
lstMobile/src/lib/feedbackScan.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { createAudioPlayer } from "expo-audio";
|
||||||
|
import * as Haptics from "expo-haptics";
|
||||||
|
|
||||||
|
export type ScanFeedback = {
|
||||||
|
type: "good" | "bad";
|
||||||
|
sound?: boolean;
|
||||||
|
vibrate?: boolean;
|
||||||
|
led?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goodSound = createAudioPlayer(require("../../assets/sounds/good.wav"));
|
||||||
|
const badSound = createAudioPlayer(require("../../assets/sounds/bad.wav"));
|
||||||
|
|
||||||
|
export async function scannerFeedback({
|
||||||
|
type,
|
||||||
|
sound = true,
|
||||||
|
vibrate = true,
|
||||||
|
led = true,
|
||||||
|
}: ScanFeedback) {
|
||||||
|
if (sound) {
|
||||||
|
const player = type === "good" ? goodSound : badSound;
|
||||||
|
player.seekTo(0);
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vibrate) {
|
||||||
|
await Haptics.notificationAsync(
|
||||||
|
type === "good"
|
||||||
|
? Haptics.NotificationFeedbackType.Success
|
||||||
|
: Haptics.NotificationFeedbackType.Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (led) {
|
||||||
|
// Zebra LED hook goes here
|
||||||
|
// More below 👇
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user