From bb6155c9692220542a52664848abf0b9eee91a43 Mon Sep 17 00:00:00 2001 From: Blake Matthes Date: Tue, 28 Apr 2026 19:49:07 -0500 Subject: [PATCH] refactor(mobile): more look and feel work --- lstMobile/app.json | 9 +- lstMobile/assets/sounds/bad.wav | Bin 0 -> 63128 bytes lstMobile/assets/sounds/good.wav | Bin 0 -> 11968 bytes lstMobile/package-lock.json | 62 ++++++++++++ lstMobile/package.json | 9 +- lstMobile/plugins/withZebraScanner.js | 40 +++++--- lstMobile/src/app/(tabs)/_layout.tsx | 8 ++ lstMobile/src/app/index.tsx | 21 +++- lstMobile/src/components/ProdScanner.tsx | 108 +++++++++++++-------- lstMobile/src/components/ScannedLabels.tsx | 73 +++++++------- lstMobile/src/components/ui/button.tsx | 106 ++++++++++++++++++++ lstMobile/src/hooks/useAppStore.ts | 12 --- lstMobile/src/hooks/useScannerStore.ts | 31 ++++++ lstMobile/src/lib/feedbackScan.ts | 38 ++++++++ 14 files changed, 409 insertions(+), 108 deletions(-) create mode 100644 lstMobile/assets/sounds/bad.wav create mode 100644 lstMobile/assets/sounds/good.wav create mode 100644 lstMobile/src/components/ui/button.tsx create mode 100644 lstMobile/src/hooks/useScannerStore.ts create mode 100644 lstMobile/src/lib/feedbackScan.ts diff --git a/lstMobile/app.json b/lstMobile/app.json index 5f55696..6ffdbfc 100644 --- a/lstMobile/app.json +++ b/lstMobile/app.json @@ -15,8 +15,8 @@ "foregroundImage": "./assets/adaptive-icon-white.png", "backgroundColor": "#ffffff" }, - "versionCode": 8, - "minSupportedVersionCode": 4, + "versionCode": 10, + "minSupportedVersionCode": 5, "predictiveBackGestureEnabled": false, "package": "net.alpla.lst.mobile" }, @@ -40,10 +40,11 @@ "image": "./assets/splash.png", "backgroundColor": "#000000" }, - "imageWidth": 200 + "imageWidth": 200 } } - ] + ], + "expo-audio" ], "experiments": { "typedRoutes": true, diff --git a/lstMobile/assets/sounds/bad.wav b/lstMobile/assets/sounds/bad.wav new file mode 100644 index 0000000000000000000000000000000000000000..3a8570798d029df296b643759f5ac36a31c76dc3 GIT binary patch literal 63128 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 E4|kIwBLDyZ literal 0 HcmV?d00001 diff --git a/lstMobile/assets/sounds/good.wav b/lstMobile/assets/sounds/good.wav new file mode 100644 index 0000000000000000000000000000000000000000..24a13d40e4907753f41626b309e06c826ad0f4c8 GIT binary patch literal 11968 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≶nr&j8Vj;~5`AcBn}QET0|k{2K2#3q2HAmz>V`h$uZ)&Jx}&|Crb~g* z=y0XE*6XqGr{Qo39Ev8awCF?KTKH#hsKAHXFM-MzIW+G>6$ph7>A~o+1R0DgZ76yU zAe6sA6$Gqkinrgiou}_m8X|-Cp(?nlm0UShX#Z)5Q>f~`Ug0I zg>JIXXRtbzM?bbzkH}SfWCfMC!6D0|PeYGoJ*gD|p=dI0<$J7| z^8R67G9m4s>yS1sNo(Gg+)pp$4yI7~?3lZ5Pv{Fu+-CKX(ljX`l)l07p=q@pgbYT7 zrWLH#&idL|H!odVGA|wH`dd5X`vO8kQnIM>jHG1jny1}z!!vLwy4GnnjM22hoyL|x zPQbBN!52h!_BBak-;Tkd7L!taAF6kg^VvSPL3bb#BFiHkfU1 zOWPSemO}<}?>zcY>%eH6zydML8?wAMkY5Uwx3dNg&2}cIJbEmy4m}q4)Dbd+R_J0k z)enLDM+OTmfxD(hkzL_%lS|nH7k#L9gr-iSh0xd81cXW(Mcbsv@=!|nP}&_@0(wDc z2}09a%wU2ZtC+#i0)d@@eQ;<%=s64OInQnC2tE6?BOTeKVn^yNnwmWwwy`8-D z-z6dxPGA=PED)LvN}_+rKI9&ymmRXaMIS1WL*?JHJl>jCLFM&t2M(p(_{@7lOAt5| zD&Hn>s6*3A3pqMmLXQP@zE79(K2(lnSCc3H6E*^5d1O{3CBHmD)5<-$rFRZn0N(yn z(5dxErDx%+Vg^gvrhRf_)7d#VT*ek)9e!u|kd%sT6MYF=K=7elN${a)hDJ~ieE%Zf z{IlH6niCCnrt2MA0_2r9`V0L}Tkgl>JX(S*uyeoAHsu?8bfyRLeW=_paA;8Z3Nta;77>b{u zf;lv;{#tT8gTtkR=+*A)TX-8dTEU{xla<9v&4v zR%lw0tiYj3=d?Vh>4)TzUS_lev1yGeFGxzR3tHaThvKu06sWx9topag%U(SrXbIq_ zu>~+V6ir&8X$@OIXj+RsR%n~_sslnLCM6!4SNZxrG_Udoga(zjKjpRmnKVq>alypsq1fA+Rx|h zE+c~l4lVXr5uyF@?KhS*-!?(BH~jzFrr<-N_1FS@Gi+F2I5w>c4n_LEko}ds^)&*A z%l&nno0qLk<^_jqZK+YQZ3=5uNJ#zm5 z)ME?q{RL~4u5ok2rsUhWI+YK7D3+AEEg+9jnqkX%2^4Fn2SU~8v!EqFJB_u<&=P<{ zVvm*2U|0lK^K7K)K7=gK^*EGw!`t1~*tF&m8hR{SL{kJA%ry;7t3FDuW=GsszX{!C zY+A`nXbI}FywoKn!CIBy0(5tBCcBN_W36(nk(AI9@G7_hYyofx|7!~%^jPkrdTJ^_ zDE|Fun-ciYfKbkH|B^gQujfu=QRTUgL#>_k_duwwrFk+w>z8YvB7-UVP`n?pZ7TMm zf-ElugywxHvR)lR;jp(sCD3DGL3@k0r!hWbUDJvVF27bql@D27k>&NI-15(J)hV*P z2Zw?KfKZ!aPf7}%ia*|Cxymdd$@>Ddrr3~CM7g`;86EE z1BZI}(EOfS?6G{Q%lx~rrxNzm&=SbC>~ZdMO1Wlf*QD6kWAV4{(gG}1@S$*j(T4_= z_rH=xY1>@CY^3dIvU3UlQ20<}cA}dMM{#3O*Ry_Vi>4J_I+|8q%V=7| z;_uKB*jso$?5TPG;I3y+k}ny#X4cJyxoO!_x6CSSShz_Ei$9Y74L*V9w95A95gMA- zuvW=_y&I5?&&^NQBy;^(0f%ZsSL0v1xTpH{5&O4r-4l~iv2FU_NeTPO zHnh}-iU@UOS%=)fbV7o)DzpUX4x_^jdurSQVjmh7f2e38Xq#{Y0w0PTjidxW*aYnl z_SE`YKygnMAXKoY(rR3Uz(1h(0XqwNEC&w#y{FoDWc?Q;85?e5?~9!Vz4M%2~3Z|tw1ZV@OOF<8v9~W`km$TNy)Er3;jgwXB}O+Mjr}4 zf~FPi8xSg3{KHzs@DBoqtLU+8r1i{mXxLNrq}!FPOZNE!LS5ugiz@FHq!V&LC@y@` zumyw+hP#SwdNjXQ#ha9_!Qt9sS%Do9Dj)h#MITD&LzA7^`ec804&UbMTup7mTsx1& zUkV&*MIQ=>8q=eO!d{fsIX&Bcop|yn9MV`Sz@HO0Hg&izxN`dKs@6c7KhuWuqox6R>3WSZOpHp zk?9&^Re-~lc_GWAe+G`=gKCk_?jffg;3~yUN_I0r9}1TYb`CujFAq$I8;dU3l+a_L zX}#{j&I-5aFS&>Qz4O68z~`WU_?KNicDQ;j+!8=FPoW8a2^=NG9t-za@p1&$OHINK zCQp!t;UAL7bcIa`w**+h(c!{d&J2w`wdNB2357(ui@YB@As3hqYgMjSHj={@@OziH z{0^qU;nv@j${ettOv)BuHJb_75q)UUnIPS*L%VU#BhT1RgTwW`+-MJnTVy)m2<%+v z9}1Tv*qe=Gy30Q}AdMkj=3=1uLd65JASA0FdpC9AV}`L+qS zLty88hl?EyyS%?9Pc5ufO|b02JM&^HTur&4^3?(R=m~d%z4n8}(AfJLBnPck#Fnz`LP_X zU4T&Pr)}~7;g*2F&S>_l!Y+>uAzV!rmfEB;U6L%tC0RtMTJkl0T5_ZIcy31qgyMn` zT1=TX!u?gha4{G*C2V+To2)^&C7^q9!S7G1{7yO*e5k8drsn9x;GNNHpjQu@67q}* znpV3OGMH|1hxJl^^%PtTq9q6!Y!w=rBGdhX+QgLs_h!3_eW({aRXAKjOOP#0khld1 z4Lw%&IKj#w+bFJ{`0hWN=emnQ+?m8p39Wod@!ADyxh`xw-z@<}gz7+e*m7{% zCwYvm^CQ9 znwq&K$TNbrDJ->G+`+cf3C_1omhW&w|3E+}IIkNu7r}A^{}7s1+y@*8c?MfoiYt+J z0))n!*17rnP^_Nd(7GKAS)Ojfdwfkes|eS!xJHTWT)684hoXnQ5trJSxWjD|R?kMb zVFf}79E$8X25%S;3g-)ln@4C?xa-4PSx9$o8ZHr+WXST?*_t3Jedc)6imS!VxW%~a zAIJynig393dn{QEHW-uk&S6uMcuT;zB@iH#(AdX56waj1J1-2ar3-n6@ZCNZG_6pz z!hI+Mp;RWgCCE)o=jT>sGo^6b#7K9z7(6Rj8DtMKJ=!MdbCT~LuwkwQ?$BC-CCeN# zSpJqkw(6F!DTVt`v|G5x!s-+{TySV;n|w>K@DKM2w|r-1um2<2o5i*Xs7-KD)eBxV z++!`EiJ=pcvdkwR{X>TghHI>Cb`(p-6I|v}xFwMM4z^r=ub$uIJyvigg+3HLG!Pmx z7>_{%vPytZ0z$d8c+(mXn(ssTDQ=?)em(jJEFH`Jw7SL~|75t4b9l~MalMF(NT9IB zHSjH64?mPA^L(8w!>l{HxQ0H(>h%mA+66lzr{Dx1@>~6mhO88gZ+9DLqutnSVA9RE z<}$d&UugVZ(0yR(GBWQIerMm<= 50000 ? null : "/(tabs)/logs", }} /> + {/* = 50000 ? null : "/(tabs)/logs", + }} + /> */} ); } diff --git a/lstMobile/src/app/index.tsx b/lstMobile/src/app/index.tsx index 331da3d..8a53119 100644 --- a/lstMobile/src/app/index.tsx +++ b/lstMobile/src/app/index.tsx @@ -1,3 +1,4 @@ +import axios from "axios"; import { Redirect, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { ActivityIndicator, Text, View } from "react-native"; @@ -11,6 +12,7 @@ export default function Index() { const hasHydrated = useAppStore((s) => s.hasHydrated); const serverPort = useAppStore((s) => s.serverPort); + const serverIp = useAppStore((s) => s.serverIp); const hasValidSetup = useAppStore((s) => s.hasValidSetup); useEffect(() => { @@ -31,6 +33,23 @@ export default function Index() { 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(Checking scanner mode...); await devDelay(1500); @@ -60,7 +79,7 @@ export default function Index() { }; startup(); - }, [hasHydrated, hasValidSetup, serverPort, router]); + }, [hasHydrated, hasValidSetup, serverPort, serverIp, router]); if (ready) { return ; diff --git a/lstMobile/src/components/ProdScanner.tsx b/lstMobile/src/components/ProdScanner.tsx index 3ed76b0..6330b7f 100644 --- a/lstMobile/src/components/ProdScanner.tsx +++ b/lstMobile/src/components/ProdScanner.tsx @@ -1,6 +1,9 @@ +import { format } from "date-fns-tz"; import { useCallback, useEffect, useState } from "react"; import { Text, View } from "react-native"; import { useAppStore } from "../hooks/useAppStore"; +import { useScannerStore } from "../hooks/useScannerStore"; +import { scannerFeedback } from "../lib/feedbackScan"; import { sendTcpMessage } from "../lib/tcpScan"; import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner"; import { ScannedLabelBox } from "./ScannedLabels"; @@ -9,23 +12,32 @@ const STX = "\x02"; const ETX = "\x03"; export default function ProdScanner() { - const [lastScan, setLastScan] = useState(null); + const lastScan = useScannerStore((s) => s.lastScan); + const setLastScan = useScannerStore((s) => s.setLastScan); const [tagScans, setTagScans] = useState([]); const scannerIdFromStore = useAppStore((s) => s.scannerId); const serverIp = useAppStore((s) => s.serverIp); const serverPort = useAppStore((s) => s.serverPort); + const [bgColor, setBGColor] = useState(null); const handleScan = useCallback( async (scan: ZebraScanResult) => { - const scanned = scan.data; - - let commandToSend = `${STX}${scannerIdFromStore}@${scanned}${ETX}`; + let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`; + await scannerFeedback({ + type: "good", + sound: true, + vibrate: true, + led: true, + }); // if we are sscc we need to scan like this .... 98@]C100090087710038712256 if (scan.data.startsWith("000")) { - commandToSend = `${STX}${scannerIdFromStore}@]C1${scanned}${ETX}`; + commandToSend = `${STX}${scannerIdFromStore}@]C1${scan.data}${ETX}`; 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, ]); } @@ -35,19 +47,44 @@ export default function ProdScanner() { setTagScans([]); } - const something = await sendTcpMessage( + const scanned = (await sendTcpMessage( commandToSend, serverIp, parseInt(serverPort || "0", 10), - ); + )) as any; // Later this is where your TCP send goes. // 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); }, - [scannerIdFromStore, serverIp, serverPort], + [scannerIdFromStore, serverIp, serverPort, setLastScan], ); + const clearScans = () => { + setTagScans([]); + }; + console.log(lastScan); useEffect(() => { @@ -65,21 +102,17 @@ export default function ProdScanner() { }; }, [handleScan]); return ( - + - + Scanner ID: {parseInt(scannerIdFromStore || "0", 10)} {!lastScan ? ( - - Waiting on scan.... + + Ready to scan + Waiting for first scan... ) : ( - - {lastScan?.action} - - - {lastScan?.type === "error" ? ( + - {lastScan?.message} + {lastScan.action} - ) : ( - - - {lastScan?.prompt} - - - {lastScan?.message} - - - )} + + + {lastScan.message} + + )} - - + + + ); } diff --git a/lstMobile/src/components/ScannedLabels.tsx b/lstMobile/src/components/ScannedLabels.tsx index 5708ab4..ba0b82c 100644 --- a/lstMobile/src/components/ScannedLabels.tsx +++ b/lstMobile/src/components/ScannedLabels.tsx @@ -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 = { - id: string; - barcode: string; - createdAt: string; + label: string; + date: Date; }; type ScannedLabelBoxProps = { labels: ScannedLabel[]; + color: string | null; + clearScan: () => void; }; -export function ScannedLabelBox({ labels }: ScannedLabelBoxProps) { +export function ScannedLabelBox({ + labels, + color, + clearScan, +}: ScannedLabelBoxProps) { return ( - - - Current scanned labels - + + + + Current scanned labels + + - + {labels.length === 0 ? ( - No labels scanned yet + No labels scanned yet ) : ( - labels.map((label) => ( - - {label} - - )) + + {labels.map((i, index) => ( + + + {i.label} - {i.date.toString()} + + + ))} + )} - + {labels.length !== 0 && ( + + )} + ); } diff --git a/lstMobile/src/components/ui/button.tsx b/lstMobile/src/components/ui/button.tsx new file mode 100644 index 0000000..9d11ba1 --- /dev/null +++ b/lstMobile/src/components/ui/button.tsx @@ -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 & VariantProps; + +function Button({ className, variant, size, ...props }: ButtonProps) { + return ( + + + + ); +} + +export { Button, buttonTextVariants, buttonVariants }; +export type { ButtonProps }; diff --git a/lstMobile/src/hooks/useAppStore.ts b/lstMobile/src/hooks/useAppStore.ts index 9262430..e8cbfcf 100644 --- a/lstMobile/src/hooks/useAppStore.ts +++ b/lstMobile/src/hooks/useAppStore.ts @@ -33,10 +33,8 @@ type AppActions = { setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void; setAppVersion: (value?: string) => void; setHasHydrated: (value: boolean) => void; - updateAppState: (updates: Partial) => void; resetApp: () => void; - hasValidSetup: () => boolean; canEnterApp: () => boolean; getServerUrl: () => string; @@ -50,15 +48,11 @@ const defaultAppState: AppState = { scannerId: "0001", stageId: undefined, deviceName: undefined, - setupCompleted: false, isRegistered: false, - lastValidationStatus: "idle", lastValidationAt: undefined, - appVersion: undefined, - hasHydrated: false, }; @@ -74,28 +68,23 @@ export const useAppStore = create()( setDeviceName: (value) => set({ deviceName: value }), setSetupCompleted: (value) => set({ setupCompleted: value }), setIsRegistered: (value) => set({ isRegistered: value }), - setValidationStatus: (status, validatedAt) => set({ lastValidationStatus: status, lastValidationAt: validatedAt, }), - setAppVersion: (value) => set({ appVersion: value }), setHasHydrated: (value) => set({ hasHydrated: value }), - updateAppState: (updates) => set((state) => ({ ...state, ...updates, })), - resetApp: () => set({ ...defaultAppState, hasHydrated: true, }), - hasValidSetup: () => { const state = get(); return Boolean( @@ -104,7 +93,6 @@ export const useAppStore = create()( state.setupCompleted, ); }, - canEnterApp: () => { const state = get(); return Boolean( diff --git a/lstMobile/src/hooks/useScannerStore.ts b/lstMobile/src/hooks/useScannerStore.ts new file mode 100644 index 0000000..91f6427 --- /dev/null +++ b/lstMobile/src/hooks/useScannerStore.ts @@ -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((set) => ({ + lastScan: null, + + setLastScan: (scan) => + set({ + lastScan: scan + ? { + ...scan, + timestamp: Date.now(), + } + : null, + }), + + clearLastScan: () => set({ lastScan: null }), +})); diff --git a/lstMobile/src/lib/feedbackScan.ts b/lstMobile/src/lib/feedbackScan.ts new file mode 100644 index 0000000..6bc5bc1 --- /dev/null +++ b/lstMobile/src/lib/feedbackScan.ts @@ -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 👇 + } +}