Преглед изворни кода

feat: Establish initial mobile application with YOLO parsing, local detection history management, and core navigation.

Dr-Swopt пре 2 недеља
родитељ
комит
969ec40a51

+ 19 - 0
check_tflite.py

@@ -0,0 +1,19 @@
+import tensorflow as tf
+
+def check_tflite(model_path):
+    interpreter = tf.lite.Interpreter(model_path=model_path)
+    interpreter.allocate_tensors()
+
+    input_details = interpreter.get_input_details()
+    output_details = interpreter.get_output_details()
+
+    print("--- Input Details ---")
+    for detail in input_details:
+        print(f"Name: {detail['name']}, Shape: {detail['shape']}, Type: {detail['dtype']}, Quantization: {detail['quantization']}")
+
+    print("\n--- Output Details ---")
+    for detail in output_details:
+        print(f"Name: {detail['name']}, Shape: {detail['shape']}, Type: {detail['dtype']}, Quantization: {detail['quantization']}")
+
+if __name__ == "__main__":
+    check_tflite('mobile/assets/best.tflite')

+ 8 - 0
mobile/android/build.gradle

@@ -19,3 +19,11 @@ buildscript {
 }
 
 apply plugin: "com.facebook.react.rootproject"
+
+allprojects {
+    repositories {
+        maven {
+            url "$rootDir/../node_modules/@react-native-async-storage/async-storage/android/local_repo"
+        }
+    }
+}

+ 4 - 1
mobile/babel.config.js

@@ -1,4 +1,7 @@
 module.exports = {
   presets: ['module:@react-native/babel-preset'],
-  plugins: ['react-native-reanimated/plugin'],
+  plugins: [
+    ['react-native-worklets-core/plugin'],
+    ['react-native-reanimated/plugin']
+  ],
 };

+ 314 - 18
mobile/package-lock.json

@@ -8,14 +8,21 @@
       "name": "mobile",
       "version": "0.0.1",
       "dependencies": {
+        "@react-native-async-storage/async-storage": "^3.0.1",
         "@react-native/new-app-screen": "0.84.1",
+        "@react-navigation/bottom-tabs": "^7.15.5",
+        "@react-navigation/native": "^7.1.33",
+        "@react-navigation/native-stack": "^7.14.5",
         "lucide-react-native": "^0.577.0",
         "react": "19.2.3",
         "react-native": "0.84.1",
         "react-native-fast-tflite": "^2.0.0",
         "react-native-gesture-handler": "^2.30.0",
+        "react-native-image-picker": "^8.2.1",
         "react-native-reanimated": "^4.2.2",
         "react-native-safe-area-context": "^5.5.2",
+        "react-native-screens": "^4.24.0",
+        "react-native-svg": "^15.15.3",
         "react-native-vision-camera": "^4.7.3",
         "react-native-worklets": "^0.7.4",
         "react-native-worklets-core": "^1.6.3"
@@ -2723,6 +2730,19 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@react-native-async-storage/async-storage": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-3.0.1.tgz",
+      "integrity": "sha512-VHwHb19sMg4Xh3W5M6YmJ/HSm1uh8RYFa6Dozm9o/jVYTYUgz2BmDXqXF7sum3glQaR34/hlwVc94px1sSdC2A==",
+      "license": "MIT",
+      "dependencies": {
+        "idb": "8.0.3"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/@react-native-community/cli": {
       "version": "20.1.0",
       "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz",
@@ -3331,6 +3351,117 @@
         }
       }
     },
+    "node_modules/@react-navigation/bottom-tabs": {
+      "version": "7.15.5",
+      "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.5.tgz",
+      "integrity": "sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-navigation/elements": "^2.9.10",
+        "color": "^4.2.3",
+        "sf-symbols-typescript": "^2.1.0"
+      },
+      "peerDependencies": {
+        "@react-navigation/native": "^7.1.33",
+        "react": ">= 18.2.0",
+        "react-native": "*",
+        "react-native-safe-area-context": ">= 4.0.0",
+        "react-native-screens": ">= 4.0.0"
+      }
+    },
+    "node_modules/@react-navigation/core": {
+      "version": "7.16.1",
+      "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.16.1.tgz",
+      "integrity": "sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-navigation/routers": "^7.5.3",
+        "escape-string-regexp": "^4.0.0",
+        "fast-deep-equal": "^3.1.3",
+        "nanoid": "^3.3.11",
+        "query-string": "^7.1.3",
+        "react-is": "^19.1.0",
+        "use-latest-callback": "^0.2.4",
+        "use-sync-external-store": "^1.5.0"
+      },
+      "peerDependencies": {
+        "react": ">= 18.2.0"
+      }
+    },
+    "node_modules/@react-navigation/core/node_modules/react-is": {
+      "version": "19.2.4",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+      "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
+      "license": "MIT"
+    },
+    "node_modules/@react-navigation/elements": {
+      "version": "2.9.10",
+      "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.10.tgz",
+      "integrity": "sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg==",
+      "license": "MIT",
+      "dependencies": {
+        "color": "^4.2.3",
+        "use-latest-callback": "^0.2.4",
+        "use-sync-external-store": "^1.5.0"
+      },
+      "peerDependencies": {
+        "@react-native-masked-view/masked-view": ">= 0.2.0",
+        "@react-navigation/native": "^7.1.33",
+        "react": ">= 18.2.0",
+        "react-native": "*",
+        "react-native-safe-area-context": ">= 4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@react-native-masked-view/masked-view": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@react-navigation/native": {
+      "version": "7.1.33",
+      "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz",
+      "integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-navigation/core": "^7.16.1",
+        "escape-string-regexp": "^4.0.0",
+        "fast-deep-equal": "^3.1.3",
+        "nanoid": "^3.3.11",
+        "use-latest-callback": "^0.2.4"
+      },
+      "peerDependencies": {
+        "react": ">= 18.2.0",
+        "react-native": "*"
+      }
+    },
+    "node_modules/@react-navigation/native-stack": {
+      "version": "7.14.5",
+      "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.5.tgz",
+      "integrity": "sha512-NuyMf21kKk3jODvYgpcDA+HwyWr/KEj72ciqquyEupZlsmQ3WNUGgdaixEB3A19+iPOvHLQzDLcoTrrqZk8Leg==",
+      "license": "MIT",
+      "dependencies": {
+        "@react-navigation/elements": "^2.9.10",
+        "color": "^4.2.3",
+        "sf-symbols-typescript": "^2.1.0",
+        "warn-once": "^0.1.1"
+      },
+      "peerDependencies": {
+        "@react-navigation/native": "^7.1.33",
+        "react": ">= 18.2.0",
+        "react-native": "*",
+        "react-native-safe-area-context": ">= 4.0.0",
+        "react-native-screens": ">= 4.0.0"
+      }
+    },
+    "node_modules/@react-navigation/routers": {
+      "version": "7.5.3",
+      "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz",
+      "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11"
+      }
+    },
     "node_modules/@sideway/address": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -4443,8 +4574,7 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
       "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
-      "license": "ISC",
-      "peer": true
+      "license": "ISC"
     },
     "node_modules/brace-expansion": {
       "version": "5.0.4",
@@ -4815,6 +4945,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1",
+        "color-string": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=12.5.0"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4833,6 +4976,16 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "license": "MIT"
     },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
     "node_modules/colorette": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@@ -5040,7 +5193,6 @@
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
       "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "dependencies": {
         "boolbase": "^1.0.0",
         "css-what": "^6.1.0",
@@ -5057,7 +5209,6 @@
       "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
       "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "mdn-data": "2.0.14",
         "source-map": "^0.6.1"
@@ -5071,7 +5222,6 @@
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
       "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "engines": {
         "node": ">= 6"
       },
@@ -5174,6 +5324,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/decode-uri-component": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+      "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/dedent": {
       "version": "1.7.2",
       "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
@@ -5312,7 +5471,6 @@
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
       "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "domelementtype": "^2.3.0",
         "domhandler": "^5.0.2",
@@ -5332,15 +5490,13 @@
           "url": "https://github.com/sponsors/fb55"
         }
       ],
-      "license": "BSD-2-Clause",
-      "peer": true
+      "license": "BSD-2-Clause"
     },
     "node_modules/domhandler": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
       "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "dependencies": {
         "domelementtype": "^2.3.0"
       },
@@ -5356,7 +5512,6 @@
       "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
       "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "dependencies": {
         "dom-serializer": "^2.0.0",
         "domelementtype": "^2.3.0",
@@ -5426,7 +5581,6 @@
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
       "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "engines": {
         "node": ">=0.12"
       },
@@ -6288,7 +6442,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/fast-glob": {
@@ -6409,6 +6562,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/filter-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+      "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/finalhandler": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -7029,6 +7191,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/idb": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
+      "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==",
+      "license": "ISC"
+    },
     "node_modules/ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -8845,8 +9013,7 @@
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
       "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
-      "license": "CC0-1.0",
-      "peer": true
+      "license": "CC0-1.0"
     },
     "node_modules/media-typer": {
       "version": "0.3.0",
@@ -9372,6 +9539,24 @@
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
       "license": "MIT"
     },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
     "node_modules/natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -9471,7 +9656,6 @@
       "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
       "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
       "license": "BSD-2-Clause",
-      "peer": true,
       "dependencies": {
         "boolbase": "^1.0.0"
       },
@@ -10084,6 +10268,24 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/query-string": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
+      "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+      "license": "MIT",
+      "dependencies": {
+        "decode-uri-component": "^0.2.2",
+        "filter-obj": "^1.1.0",
+        "split-on-first": "^1.0.0",
+        "strict-uri-encode": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/queue": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -10179,6 +10381,18 @@
         }
       }
     },
+    "node_modules/react-freeze": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
+      "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": ">=17.0.0"
+      }
+    },
     "node_modules/react-is": {
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -10271,6 +10485,16 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-image-picker": {
+      "version": "8.2.1",
+      "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz",
+      "integrity": "sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-is-edge-to-edge": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -10318,12 +10542,25 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-screens": {
+      "version": "4.24.0",
+      "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.24.0.tgz",
+      "integrity": "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA==",
+      "license": "MIT",
+      "dependencies": {
+        "react-freeze": "^1.0.0",
+        "warn-once": "^0.1.0"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-svg": {
       "version": "15.15.3",
       "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz",
       "integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "css-select": "^5.1.0",
         "css-tree": "^1.1.3",
@@ -11071,6 +11308,15 @@
       "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
       "license": "ISC"
     },
+    "node_modules/sf-symbols-typescript": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
+      "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -11186,6 +11432,21 @@
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
       "license": "ISC"
     },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+      "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/simple-swizzle/node_modules/is-arrayish": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+      "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+      "license": "MIT"
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -11267,6 +11528,15 @@
         "source-map": "^0.6.0"
       }
     },
+    "node_modules/split-on-first": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+      "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -11344,6 +11614,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/strict-uri-encode": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+      "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -12024,6 +12303,24 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/use-latest-callback": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
+      "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": ">=16.8"
+      }
+    },
+    "node_modules/use-sync-external-store": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+      "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -12084,8 +12381,7 @@
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
       "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/wcwidth": {
       "version": "1.0.1",

+ 7 - 0
mobile/package.json

@@ -10,14 +10,21 @@
     "test": "jest"
   },
   "dependencies": {
+    "@react-native-async-storage/async-storage": "^3.0.1",
     "@react-native/new-app-screen": "0.84.1",
+    "@react-navigation/bottom-tabs": "^7.15.5",
+    "@react-navigation/native": "^7.1.33",
+    "@react-navigation/native-stack": "^7.14.5",
     "lucide-react-native": "^0.577.0",
     "react": "19.2.3",
     "react-native": "0.84.1",
     "react-native-fast-tflite": "^2.0.0",
     "react-native-gesture-handler": "^2.30.0",
+    "react-native-image-picker": "^8.2.1",
     "react-native-reanimated": "^4.2.2",
     "react-native-safe-area-context": "^5.5.2",
+    "react-native-screens": "^4.24.0",
+    "react-native-svg": "^15.15.3",
     "react-native-vision-camera": "^4.7.3",
     "react-native-worklets": "^0.7.4",
     "react-native-worklets-core": "^1.6.3"

+ 6 - 121
mobile/src/App.tsx

@@ -1,126 +1,11 @@
-import React, { useEffect, useState, useRef, useMemo } from 'react';
-import { StyleSheet, View, Text, StatusBar, SafeAreaView } from 'react-native';
-import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
-import { useTensorflowModel } from 'react-native-fast-tflite';
-import { DetectionOverlay } from './components/DetectionOverlay';
-import { TallyDashboard } from './components/TallyDashboard';
-import { Colors } from './theme';
-
-const CLASS_NAMES = [
-  'Empty_Bunch',
-  'Underripe',
-  'Abnormal',
-  'Ripe',
-  'Unripe',
-  'Overripe'
-];
+import React from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { AppNavigator } from './navigation/AppNavigator';
 
 export default function App() {
-  const { hasPermission, requestPermission } = useCameraPermission();
-  const device = useCameraDevice('back');
-  const [detections, setDetections] = useState([]);
-  const [counts, setCounts] = useState({});
-
-  console.log('App State:', { hasPermission, device: !!device, deviceDetails: device?.name });
-
-  // Load the model
-  const model = useTensorflowModel(require('../assets/best.tflite'));
-  console.log('Model State:', model.state);
-
-  useEffect(() => {
-    if (!hasPermission) {
-      requestPermission();
-    }
-  }, [hasPermission]);
-
-  const frameProcessor = useFrameProcessor((frame) => {
-    'worklet';
-    if (model.state === 'loaded') {
-      // High-performance inference on frame
-      // Note: This is a conceptual implementation of the inference call
-      // The actual implementation depends on the model's input/output shape
-      try {
-        // High-performance inference on frame using a TypedArray (Uint8Array)
-        const result = model.model.run([new Uint8Array(frame.toArrayBuffer())]);
-        // Process results (bounding boxes, classes, confidence)
-        // This part would involve post-processing (NMS, scaling)
-      } catch (e) {
-        console.error('Inference error:', e);
-      }
-    }
-  }, [model]);
-
-  if (!hasPermission) return (
-    <View style={[styles.container, { backgroundColor: 'red' }]}>
-      <Text style={styles.text}>ERROR: No Camera Permission</Text>
-    </View>
-  );
-
-  if (!device) return (
-    <View style={[styles.container, { backgroundColor: 'blue' }]}>
-      <Text style={styles.text}>ERROR: No Camera Device Found</Text>
-    </View>
-  );
-
   return (
-    <View style={styles.container}>
-      <StatusBar barStyle="light-content" />
-      <Camera
-        style={StyleSheet.absoluteFill}
-        device={device}
-        isActive={true}
-        frameProcessor={frameProcessor}
-        fps={30}
-      />
-      
-      <SafeAreaView style={styles.overlay} pointerEvents="none">
-        <View style={[styles.header, { backgroundColor: 'rgba(255, 0, 0, 0.5)' }]}>
-          <Text style={styles.title}>Industrial Palm AI v4</Text>
-          <Text style={styles.status}>
-            {model.state === 'loaded' ? '● AI ACTIVE' : `○ ${model.state.toUpperCase()}`}
-          </Text>
-        </View>
-
-        <DetectionOverlay detections={detections} />
-        <TallyDashboard counts={counts} />
-      </SafeAreaView>
-
-      <View style={{ position: 'absolute', top: 100, left: 20, right: 20, backgroundColor: 'white', padding: 10 }}>
-        <Text style={{ color: 'black' }}>Debug: {device?.name || 'Unknown Device'} | {model.state}</Text>
-      </View>
-    </View>
+    <NavigationContainer>
+      <AppNavigator />
+    </NavigationContainer>
   );
 }
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    backgroundColor: '#000',
-  },
-  overlay: {
-    flex: 1,
-  },
-  header: {
-    padding: 20,
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-    backgroundColor: 'rgba(15, 23, 42, 0.4)',
-  },
-  title: {
-    color: '#FFF',
-    fontSize: 18,
-    fontWeight: 'bold',
-    letterSpacing: 1,
-  },
-  status: {
-    color: Colors.success,
-    fontSize: 12,
-    fontWeight: '800',
-  },
-  text: {
-    color: '#FFF',
-    textAlign: 'center',
-    marginTop: 100,
-  }
-});

+ 49 - 0
mobile/src/navigation/AppNavigator.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { DashboardScreen } from '../screens/DashboardScreen';
+import { ScannerScreen } from '../screens/ScannerScreen';
+import { HistoryScreen } from '../screens/HistoryScreen';
+import { Colors } from '../theme';
+
+const Stack = createNativeStackNavigator();
+
+export const AppNavigator = () => {
+  return (
+    <Stack.Navigator
+      initialRouteName="Dashboard"
+      screenOptions={{
+        headerStyle: {
+          backgroundColor: Colors.background,
+        },
+        headerTintColor: '#FFF',
+        headerTitleStyle: {
+          fontWeight: 'bold',
+        },
+        headerShadowVisible: false,
+      }}
+    >
+      <Stack.Screen 
+        name="Dashboard" 
+        component={DashboardScreen} 
+        options={{ headerShown: false }}
+      />
+      <Stack.Screen 
+        name="Scanner" 
+        component={ScannerScreen} 
+        options={{ 
+          title: 'Industrial Scanner',
+          headerTransparent: true,
+          headerTitleStyle: { color: '#FFF' }
+        }}
+      />
+      <Stack.Screen 
+        name="History" 
+        component={HistoryScreen} 
+        options={{ 
+          title: 'Field Journal',
+          headerLargeTitle: true,
+        }}
+      />
+    </Stack.Navigator>
+  );
+};

+ 135 - 0
mobile/src/screens/DashboardScreen.tsx

@@ -0,0 +1,135 @@
+import React from 'react';
+import { StyleSheet, View, Text, TouchableOpacity, SafeAreaView, StatusBar, ScrollView } from 'react-native';
+import { Scan, Image as ImageIcon, History, ShieldAlert } from 'lucide-react-native';
+import { Colors } from '../theme';
+
+export const DashboardScreen = ({ navigation }: any) => {
+  return (
+    <SafeAreaView style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      
+      <ScrollView 
+        contentContainerStyle={styles.scrollContent}
+        showsVerticalScrollIndicator={false}
+      >
+        <View style={styles.header}>
+          <Text style={styles.title}>Palm Oil AI</Text>
+          <Text style={styles.subtitle}>Industrial Management Hub</Text>
+        </View>
+
+        <View style={styles.grid}>
+          <TouchableOpacity 
+            style={styles.card} 
+            onPress={() => navigation.navigate('Scanner')}
+          >
+            <View style={[styles.iconContainer, { backgroundColor: 'rgba(52, 199, 89, 0.1)' }]}>
+              <Scan color={Colors.success} size={32} />
+            </View>
+            <Text style={styles.cardTitle}>Live Field Scan</Text>
+            <Text style={styles.cardDesc}>Real-time ripeness detection & health alerts</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity 
+            style={styles.card} 
+            onPress={() => navigation.navigate('Scanner', { triggerUpload: true })}
+          >
+            <View style={[styles.iconContainer, { backgroundColor: 'rgba(0, 122, 255, 0.1)' }]}>
+              <ImageIcon color={Colors.info} size={32} />
+            </View>
+            <Text style={styles.cardTitle}>Analyze Gallery</Text>
+            <Text style={styles.cardDesc}>Upload & analyze harvested bunches from storage</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity 
+            style={styles.card} 
+            onPress={() => navigation.navigate('History')}
+          >
+            <View style={[styles.iconContainer, { backgroundColor: 'rgba(148, 163, 184, 0.1)' }]}>
+              <History color={Colors.textSecondary} size={32} />
+            </View>
+            <Text style={styles.cardTitle}>Detection History</Text>
+            <Text style={styles.cardDesc}>Review past logs and industrial field journal</Text>
+          </TouchableOpacity>
+
+          <View style={[styles.card, styles.alertCard]}>
+            <View style={[styles.iconContainer, { backgroundColor: 'rgba(255, 59, 48, 0.1)' }]}>
+              <ShieldAlert color={Colors.error} size={32} />
+            </View>
+            <Text style={styles.cardTitle}>System Health</Text>
+            <Text style={styles.cardDesc}>AI Inference: ACTIVE | Model: V11-INT8</Text>
+          </View>
+        </View>
+
+        <View style={styles.footer}>
+          <Text style={styles.versionText}>Industrial Suite v4.2.0-stable</Text>
+        </View>
+      </ScrollView>
+    </SafeAreaView>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: Colors.background,
+  },
+  scrollContent: {
+    paddingBottom: 32,
+  },
+  header: {
+    padding: 32,
+    paddingTop: 48,
+  },
+  title: {
+    color: '#FFF',
+    fontSize: 32,
+    fontWeight: 'bold',
+  },
+  subtitle: {
+    color: Colors.textSecondary,
+    fontSize: 16,
+    marginTop: 4,
+  },
+  grid: {
+    flex: 1,
+    padding: 24,
+    gap: 16,
+  },
+  card: {
+    backgroundColor: Colors.surface,
+    padding: 20,
+    borderRadius: 20,
+    borderWidth: 1,
+    borderColor: 'rgba(255,255,255,0.05)',
+  },
+  alertCard: {
+    borderColor: 'rgba(255, 59, 48, 0.2)',
+  },
+  iconContainer: {
+    width: 64,
+    height: 64,
+    borderRadius: 16,
+    justifyContent: 'center',
+    alignItems: 'center',
+    marginBottom: 16,
+  },
+  cardTitle: {
+    color: '#FFF',
+    fontSize: 18,
+    fontWeight: 'bold',
+  },
+  cardDesc: {
+    color: Colors.textSecondary,
+    fontSize: 14,
+    marginTop: 4,
+  },
+  footer: {
+    padding: 24,
+    alignItems: 'center',
+  },
+  versionText: {
+    color: 'rgba(255,255,255,0.3)',
+    fontSize: 12,
+    fontWeight: '500',
+  }
+});

+ 399 - 0
mobile/src/screens/HistoryScreen.tsx

@@ -0,0 +1,399 @@
+import React, { useState, useCallback } from 'react';
+import { StyleSheet, View, Text, FlatList, TouchableOpacity, RefreshControl, Image, Alert } from 'react-native';
+import { useFocusEffect } from '@react-navigation/native';
+import { Trash2, Clock, CheckCircle, AlertTriangle, Square, CheckSquare, X, Trash } from 'lucide-react-native';
+import { getHistory, clearHistory, deleteRecords, DetectionRecord } from '../utils/storage';
+import { Colors } from '../theme';
+import { DetectionOverlay } from '../components/DetectionOverlay';
+
+export const HistoryScreen = () => {
+  const [history, setHistory] = useState<DetectionRecord[]>([]);
+  const [refreshing, setRefreshing] = useState(false);
+  const [expandedId, setExpandedId] = useState<string | null>(null);
+  const [isSelectMode, setIsSelectMode] = useState(false);
+  const [selectedIds, setSelectedIds] = useState<string[]>([]);
+
+  const fetchHistory = async () => {
+    const data = await getHistory();
+    setHistory(data);
+  };
+
+  useFocusEffect(
+    useCallback(() => {
+      fetchHistory();
+    }, [])
+  );
+
+  const onRefresh = async () => {
+    setRefreshing(true);
+    await fetchHistory();
+    setRefreshing(false);
+  };
+
+  const handleClearAll = () => {
+    Alert.alert(
+      "Delete All Logs",
+      "This action will permanently wipe your entire industrial field journal. Are you sure?",
+      [
+        { text: "Cancel", style: "cancel" },
+        { 
+          text: "Delete All", 
+          style: "destructive",
+          onPress: async () => {
+            await clearHistory();
+            setHistory([]);
+            setIsSelectMode(false);
+            setSelectedIds([]);
+          }
+        }
+      ]
+    );
+  };
+
+  const handleDeleteSelected = () => {
+    Alert.alert(
+      "Delete Selected",
+      `Are you sure you want to delete ${selectedIds.length} records?`,
+      [
+        { text: "Cancel", style: "cancel" },
+        { 
+          text: "Delete", 
+          style: "destructive",
+          onPress: async () => {
+            await deleteRecords(selectedIds);
+            setSelectedIds([]);
+            setIsSelectMode(false);
+            fetchHistory();
+          }
+        }
+      ]
+    );
+  };
+
+  const toggleSelect = (id: string) => {
+    if (selectedIds.includes(id)) {
+      setSelectedIds(selectedIds.filter((idx: string) => idx !== id));
+    } else {
+      setSelectedIds([...selectedIds, id]);
+    }
+  };
+
+  const toggleExpand = (id: string) => {
+    if (isSelectMode) {
+      toggleSelect(id);
+    } else {
+      setExpandedId(expandedId === id ? null : id);
+    }
+  };
+
+  const handleLongPress = (id: string) => {
+    if (!isSelectMode) {
+      setIsSelectMode(true);
+      setSelectedIds([id]);
+    }
+  };
+
+  const exitSelectionMode = () => {
+    setIsSelectMode(false);
+    setSelectedIds([]);
+  };
+
+  const renderItem = ({ item }: { item: DetectionRecord }) => {
+    const date = new Date(item.timestamp);
+    const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+    const dateStr = date.toLocaleDateString();
+    const isExpanded = expandedId === item.id;
+    const isSelected = selectedIds.includes(item.id);
+
+    return (
+      <TouchableOpacity 
+        activeOpacity={0.9} 
+        onPress={() => toggleExpand(item.id)}
+        onLongPress={() => handleLongPress(item.id)}
+        style={[
+          styles.card, 
+          item.isHealthAlert && styles.alertCard,
+          isSelected && styles.selectedCard
+        ]}
+      >
+        <View style={styles.cardHeader}>
+          <View style={styles.labelContainer}>
+            {isSelectMode ? (
+              isSelected ? (
+                <CheckSquare color={Colors.info} size={20} />
+              ) : (
+                <Square color={Colors.textSecondary} size={20} />
+              )
+            ) : item.isHealthAlert ? (
+              <AlertTriangle color={Colors.error} size={18} />
+            ) : (
+              <CheckCircle color={Colors.success} size={18} />
+            )}
+            <Text style={[styles.label, { color: isSelected ? Colors.info : item.isHealthAlert ? Colors.error : Colors.success }]}>
+              {item.label}
+            </Text>
+          </View>
+          <Text style={styles.confidence}>{(item.confidence * 100).toFixed(1)}% Conf.</Text>
+        </View>
+        
+        {isExpanded && item.imageUri && (
+          <View style={styles.expandedContent}>
+            <View style={styles.imageWrapper}>
+              <Image source={{ uri: item.imageUri }} style={styles.detailImage} resizeMode="cover" />
+              <DetectionOverlay detections={item.detections} />
+            </View>
+          </View>
+        )}
+
+        <View style={styles.cardBody}>
+          <View style={styles.tallyContainer}>
+            {Object.entries(item.counts).map(([label, count]) => (
+              <View key={label} style={styles.tallyItem}>
+                <Text style={styles.tallyLabel}>{label}:</Text>
+                <Text style={styles.tallyCount}>{count}</Text>
+              </View>
+            ))}
+          </View>
+        </View>
+
+        <View style={styles.cardFooter}>
+          <Clock color={Colors.textSecondary} size={14} />
+          <Text style={styles.footerText}>{dateStr} at {timeStr}</Text>
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  return (
+    <View style={styles.container}>
+      <View style={styles.header}>
+        <View>
+          <Text style={styles.title}>Field Journal</Text>
+          {isSelectMode && (
+            <Text style={styles.selectionCount}>{selectedIds.length} Selected</Text>
+          )}
+        </View>
+        
+        <View style={styles.headerActions}>
+          {history.length > 0 && (
+            isSelectMode ? (
+              <TouchableOpacity onPress={exitSelectionMode} style={styles.iconButton}>
+                <X color={Colors.textSecondary} size={24} />
+              </TouchableOpacity>
+            ) : (
+              <>
+                <TouchableOpacity onPress={handleClearAll} style={styles.iconButton}>
+                  <Trash2 color={Colors.textSecondary} size={22} />
+                </TouchableOpacity>
+                <TouchableOpacity onPress={() => setIsSelectMode(true)} style={styles.iconButton}>
+                  <CheckSquare color={Colors.textSecondary} size={22} />
+                </TouchableOpacity>
+              </>
+            )
+          )}
+        </View>
+      </View>
+
+      {history.length === 0 ? (
+        <View style={styles.emptyState}>
+          <Clock color={Colors.textSecondary} size={48} strokeWidth={1} />
+          <Text style={styles.emptyText}>No detections recorded yet.</Text>
+          <Text style={styles.emptySubtext}>Perform detections in the Scanner tab to see them here.</Text>
+        </View>
+      ) : (
+        <View style={{ flex: 1 }}>
+          <FlatList
+            data={history}
+            keyExtractor={(item) => item.id}
+            renderItem={renderItem}
+            contentContainerStyle={styles.listContent}
+            refreshControl={
+              <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.success} />
+            }
+          />
+          
+          {isSelectMode && selectedIds.length > 0 && (
+            <View style={styles.bottomActions}>
+              <TouchableOpacity 
+                style={styles.deleteSelectionButton} 
+                onPress={handleDeleteSelected}
+              >
+                <Trash color="#FFF" size={20} />
+                <Text style={styles.deleteButtonText}>Delete Selected ({selectedIds.length})</Text>
+              </TouchableOpacity>
+            </View>
+          )}
+        </View>
+      )}
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: Colors.background,
+  },
+  header: {
+    padding: 24,
+    paddingBottom: 16,
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  title: {
+    color: '#FFF',
+    fontSize: 28,
+    fontWeight: 'bold',
+  },
+  selectionCount: {
+    color: Colors.info,
+    fontSize: 14,
+    fontWeight: '500',
+    marginTop: 2,
+  },
+  headerActions: {
+    flexDirection: 'row',
+    gap: 8,
+  },
+  iconButton: {
+    padding: 8,
+    backgroundColor: 'rgba(255,255,255,0.05)',
+    borderRadius: 12,
+  },
+  clearButton: {
+    padding: 8,
+  },
+  listContent: {
+    padding: 16,
+    paddingTop: 0,
+  },
+  card: {
+    backgroundColor: Colors.surface,
+    borderRadius: 16,
+    padding: 16,
+    marginBottom: 16,
+    borderWidth: 1,
+    borderColor: 'rgba(255,255,255,0.05)',
+  },
+  selectedCard: {
+    borderColor: Colors.info,
+    borderWidth: 2,
+    backgroundColor: 'rgba(0, 122, 255, 0.05)',
+  },
+  alertCard: {
+    borderColor: 'rgba(255, 59, 48, 0.3)',
+    borderLeftWidth: 4,
+    borderLeftColor: Colors.error,
+  },
+  expandedContent: {
+    marginVertical: 12,
+    borderRadius: 12,
+    overflow: 'hidden',
+    backgroundColor: '#000',
+  },
+  imageWrapper: {
+    width: '100%',
+    aspectRatio: 1,
+    position: 'relative',
+  },
+  detailImage: {
+    width: '100%',
+    height: '100%',
+  },
+  cardHeader: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    marginBottom: 12,
+  },
+  labelContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+  },
+  label: {
+    fontSize: 18,
+    fontWeight: 'bold',
+  },
+  confidence: {
+    color: Colors.textSecondary,
+    fontSize: 14,
+  },
+  cardBody: {
+    paddingVertical: 12,
+    borderTopWidth: 1,
+    borderBottomWidth: 1,
+    borderColor: 'rgba(255,255,255,0.05)',
+  },
+  tallyContainer: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    gap: 12,
+  },
+  tallyItem: {
+    flexDirection: 'row',
+    gap: 4,
+  },
+  tallyLabel: {
+    color: Colors.textSecondary,
+    fontSize: 12,
+  },
+  tallyCount: {
+    color: '#FFF',
+    fontSize: 12,
+    fontWeight: 'bold',
+  },
+  cardFooter: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 6,
+    marginTop: 12,
+  },
+  footerText: {
+    color: Colors.textSecondary,
+    fontSize: 12,
+  },
+  emptyState: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    padding: 32,
+  },
+  emptyText: {
+    color: '#FFF',
+    fontSize: 18,
+    fontWeight: 'bold',
+    marginTop: 16,
+  },
+  emptySubtext: {
+    color: Colors.textSecondary,
+    textAlign: 'center',
+    marginTop: 8,
+  },
+  bottomActions: {
+    position: 'absolute',
+    bottom: 24,
+    left: 24,
+    right: 24,
+    backgroundColor: Colors.error,
+    borderRadius: 16,
+    elevation: 8,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 4 },
+    shadowOpacity: 0.3,
+    shadowRadius: 8,
+  },
+  deleteSelectionButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 16,
+    gap: 12,
+  },
+  deleteButtonText: {
+    color: '#FFF',
+    fontSize: 16,
+    fontWeight: 'bold',
+  },
+});

+ 231 - 0
mobile/src/screens/ScannerScreen.tsx

@@ -0,0 +1,231 @@
+import React, { useState, useEffect } from 'react';
+import { StyleSheet, View, Text, StatusBar, SafeAreaView, TouchableOpacity, Image } from 'react-native';
+import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
+import { useTensorflowModel } from 'react-native-fast-tflite';
+import { runOnJS } from 'react-native-reanimated';
+import { launchImageLibrary } from 'react-native-image-picker';
+import { parseYoloResults, calculateTally, BoundingBox } from '../utils/yoloParser';
+import { saveDetectionRecord } from '../utils/storage';
+import { DetectionOverlay } from '../components/DetectionOverlay';
+import { TallyDashboard } from '../components/TallyDashboard';
+import { Colors } from '../theme';
+import { Image as ImageIcon, Upload } from 'lucide-react-native';
+
+export const ScannerScreen = ({ route }: any) => {
+  const { hasPermission, requestPermission } = useCameraPermission();
+  const device = useCameraDevice('back');
+  const [detections, setDetections] = useState<BoundingBox[]>([]);
+  const [counts, setCounts] = useState<Record<string, number>>({});
+  const [cameraInitialized, setCameraInitialized] = useState(false);
+  const [lastSavedTime, setLastSavedTime] = useState(0);
+
+  // Load the model
+  const model = useTensorflowModel(require('../../assets/best.tflite'));
+
+  useEffect(() => {
+    if (!hasPermission) {
+      requestPermission();
+    }
+  }, [hasPermission]);
+
+  // Handle trigger from Dashboard
+  useEffect(() => {
+    if (route.params?.triggerUpload && model.state === 'loaded') {
+      handleGalleryUpload();
+    }
+  }, [route.params, model.state]);
+
+  const frameProcessor = useFrameProcessor((frame) => {
+    'worklet';
+    if (model.state === 'loaded') {
+      try {
+        // Quantized input for INT8 model
+        const result = model.model.runSync([new Int8Array(frame.toArrayBuffer())]);
+        const boxes = parseYoloResults(result[0], frame.width, frame.height);
+        runOnJS(setDetections)(boxes);
+        
+        const currentCounts = calculateTally(boxes);
+        runOnJS(setCounts)(currentCounts);
+
+        if (boxes.length > 0) {
+          runOnJS(handleAutoSave)(boxes, currentCounts);
+        }
+      } catch (e) {
+        console.error('AI Inference Error:', e);
+      }
+    }
+  }, [model]);
+
+  const handleAutoSave = (boxes: BoundingBox[], currentCounts: Record<string, number>) => {
+    const now = Date.now();
+    if (now - lastSavedTime > 5000) {
+      const topDet = boxes.reduce((prev, current) => (prev.confidence > current.confidence) ? prev : current);
+      saveDetectionRecord({
+        label: topDet.label,
+        confidence: topDet.confidence,
+        classId: topDet.classId,
+        detections: boxes,
+        counts: currentCounts
+      });
+      setLastSavedTime(now);
+    }
+  };
+
+  const handleGalleryUpload = async () => {
+    try {
+      const result = await launchImageLibrary({ 
+        mediaType: 'photo', 
+        includeBase64: true,
+        quality: 0.8
+      });
+      
+      if (result.assets && result.assets[0] && model.state === 'loaded') {
+        const asset = result.assets[0];
+        console.log('Gallery: Image selected', asset.uri);
+        
+        // Simulated Static Analysis
+        const simulatedBoxes: BoundingBox[] = [
+          {
+            id: `static_${Date.now()}`,
+            x: 100,
+            y: 150,
+            width: 300,
+            height: 300,
+            label: 'Ripe',
+            confidence: 0.965,
+            classId: 3
+          }
+        ];
+        
+        const simulatedCounts = calculateTally(simulatedBoxes);
+        setDetections(simulatedBoxes);
+        setCounts(simulatedCounts);
+        
+        saveDetectionRecord({
+          label: simulatedBoxes[0].label,
+          confidence: simulatedBoxes[0].confidence,
+          classId: simulatedBoxes[0].classId,
+          imageUri: asset.uri,
+          detections: simulatedBoxes,
+          counts: simulatedCounts
+        });
+
+        console.log('Gallery: Analysis complete (Simulated)');
+      }
+    } catch (error) {
+      console.error('Gallery: Error picking image', error);
+    }
+  };
+
+  if (!hasPermission) return (
+    <View style={[styles.container, { backgroundColor: Colors.error, justifyContent: 'center' }]}>
+      <Text style={styles.text}>ERROR: No Camera Permission</Text>
+    </View>
+  );
+
+  if (!device) return (
+    <View style={[styles.container, { backgroundColor: Colors.info, justifyContent: 'center' }]}>
+      <Text style={styles.text}>ERROR: No Camera Device Found</Text>
+    </View>
+  );
+
+  return (
+    <View style={styles.container}>
+      <StatusBar barStyle="light-content" />
+      <Camera
+        style={StyleSheet.absoluteFill}
+        device={device}
+        isActive={true}
+        frameProcessor={frameProcessor}
+        pixelFormat="yuv"
+        onInitialized={() => {
+          console.log('Camera: Initialized');
+          setCameraInitialized(true);
+        }}
+        onError={(error) => console.error('Camera: Error', error)}
+      />
+      
+      <SafeAreaView style={styles.overlay} pointerEvents="none">
+        <View style={[styles.header, { backgroundColor: 'rgba(15, 23, 42, 0.6)' }]}>
+          <Text style={styles.title}>Live Scanner</Text>
+          <Text style={styles.status}>
+            {model.state === 'loaded' ? '● AI ACTIVE' : `○ ${model.state.toUpperCase()}`}
+          </Text>
+        </View>
+
+        <DetectionOverlay detections={detections} />
+        <TallyDashboard counts={counts} />
+      </SafeAreaView>
+
+      <TouchableOpacity style={styles.galleryButton} onPress={handleGalleryUpload}>
+        <Upload color="#FFF" size={24} />
+      </TouchableOpacity>
+
+      <View style={styles.debugBox}>
+        <Text style={styles.debugText}>
+          Cam: {cameraInitialized ? 'READY' : 'STARTING...'} | 
+          Model: {model.state.toUpperCase()} | 
+          Dets: {detections.length}
+        </Text>
+      </View>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: Colors.background,
+  },
+  overlay: {
+    flex: 1,
+  },
+  header: {
+    padding: 16,
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+  },
+  title: {
+    color: '#FFF',
+    fontSize: 16,
+    fontWeight: 'bold',
+    letterSpacing: 0.5,
+  },
+  status: {
+    color: Colors.success,
+    fontSize: 11,
+    fontWeight: '800',
+  },
+  text: {
+    color: '#FFF',
+    textAlign: 'center',
+    fontSize: 18,
+    fontWeight: 'bold',
+  },
+  galleryButton: {
+    position: 'absolute',
+    bottom: 100,
+    right: 20,
+    backgroundColor: 'rgba(30, 41, 59, 0.8)',
+    padding: 16,
+    borderRadius: 30,
+    borderWidth: 1,
+    borderColor: 'rgba(255,255,255,0.2)',
+  },
+  debugBox: {
+    position: 'absolute', 
+    top: 60, 
+    left: 20, 
+    right: 20, 
+    backgroundColor: 'rgba(255,255,255,0.9)', 
+    padding: 8,
+    borderRadius: 8,
+  },
+  debugText: {
+    color: '#000',
+    fontSize: 12,
+    fontWeight: '600',
+    textAlign: 'center',
+  }
+});

+ 80 - 0
mobile/src/utils/storage.ts

@@ -0,0 +1,80 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { BoundingBox } from './yoloParser';
+
+export interface DetectionRecord {
+  id: string;
+  timestamp: string;
+  label: string;
+  confidence: number;
+  classId: number;
+  isHealthAlert: boolean;
+  imageUri?: string;
+  detections: BoundingBox[];
+  counts: Record<string, number>;
+}
+
+const STORAGE_KEY = 'palm_history';
+
+/**
+ * Saves a new detection record to local storage.
+ */
+export const saveDetectionRecord = async (record: Omit<DetectionRecord, 'id' | 'timestamp' | 'isHealthAlert'>) => {
+  try {
+    const existing = await AsyncStorage.getItem(STORAGE_KEY);
+    const history: DetectionRecord[] = existing ? JSON.parse(existing) : [];
+    
+    const newRecord: DetectionRecord = {
+      ...record,
+      id: Date.now().toString(),
+      timestamp: new Date().toISOString(),
+      isHealthAlert: record.detections.some(d => d.classId === 0 || d.classId === 2)
+    };
+    
+    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([newRecord, ...history]));
+    console.log('Storage: Record saved successfully');
+  } catch (error) {
+    console.error('Storage: Error saving record', error);
+  }
+};
+
+/**
+ * Retrieves all detection records from local storage.
+ */
+export const getHistory = async (): Promise<DetectionRecord[]> => {
+  try {
+    const existing = await AsyncStorage.getItem(STORAGE_KEY);
+    return existing ? JSON.parse(existing) : [];
+  } catch (error) {
+    console.error('Storage: Error fetching history', error);
+    return [];
+  }
+};
+
+/**
+ * Clears all detection records from local storage.
+ */
+export const clearHistory = async () => {
+  try {
+    await AsyncStorage.removeItem(STORAGE_KEY);
+    console.log('Storage: History cleared');
+  } catch (error) {
+    console.error('Storage: Error clearing history', error);
+  }
+};
+/**
+ * Deletes specific records from local storage.
+ */
+export const deleteRecords = async (ids: string[]) => {
+  try {
+    const existing = await AsyncStorage.getItem(STORAGE_KEY);
+    if (!existing) return;
+    
+    const history: DetectionRecord[] = JSON.parse(existing);
+    const updated = history.filter(record => !ids.includes(record.id));
+    
+    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
+    console.log(`Storage: ${ids.length} records deleted`);
+  } catch (error) {
+    console.error('Storage: Error deleting records', error);
+  }
+};

+ 104 - 0
mobile/src/utils/yoloParser.ts

@@ -0,0 +1,104 @@
+export interface BoundingBox {
+  id: string;
+  x: number;
+  y: number;
+  width: number;
+  height: number;
+  label: string;
+  confidence: number;
+  classId: number;
+}
+
+const CLASS_NAMES = [
+  'Empty_Bunch',
+  'Underripe',
+  'Abnormal',
+  'Ripe',
+  'Unripe',
+  'Overripe'
+];
+
+/**
+ * Parses YOLOv8/v11 output tensor into BoundingBox objects.
+ * Format: [x1, y1, x2, y2, score, classId] 
+ * Quantization: scale=0.019916336983442307, zeroPoint=-124
+ */
+/**
+ * Normalizes a raw pixel buffer to 0.0-1.0 range for Float32 models.
+ */
+export function normalizeTensor(buffer: ArrayBuffer, width: number, height: number): Float32Array {
+  'worklet';
+  const data = new Uint8Array(buffer);
+  const normalized = new Float32Array(width * height * 3);
+  
+  for (let i = 0; i < data.length; i++) {
+    normalized[i] = data[i] / 255.0;
+  }
+  return normalized;
+}
+
+export function parseYoloResults(
+  tensor: Int8Array | Uint8Array | Float32Array | any, 
+  frameWidth: number, 
+  frameHeight: number
+): BoundingBox[] {
+  'worklet';
+  
+  // Detection parameters from INT8 model
+  const scale = 0.019916336983442307;
+  const zeroPoint = -124;
+  const numDetections = 300;
+  const numElements = 6;
+  const detections: BoundingBox[] = [];
+
+  const data = tensor;
+  if (!data || data.length === 0) return [];
+
+  for (let i = 0; i < numDetections; i++) {
+    const base = i * numElements;
+    if (base + 5 >= data.length) break;
+
+    // Handle Float32 vs Quantized Int8
+    const getVal = (idx: number) => {
+      const val = data[idx];
+      if (data instanceof Float32Array) return val;
+      return (val - zeroPoint) * scale;
+    };
+
+    const x1 = getVal(base + 0);
+    const y1 = getVal(base + 1);
+    const x2 = getVal(base + 2);
+    const y2 = getVal(base + 3);
+    const score = getVal(base + 4);
+    const classId = Math.round(getVal(base + 5));
+
+    if (score > 0.45 && classId >= 0 && classId < CLASS_NAMES.length) {
+      const normalizedX1 = x1 / 640;
+      const normalizedY1 = y1 / 640;
+      const normalizedX2 = x2 / 640;
+      const normalizedY2 = y2 / 640;
+
+      detections.push({
+        id: `det_${i}_${Math.random().toString(36).substr(2, 9)}`,
+        x: Math.max(0, normalizedX1 * frameWidth),
+        y: Math.max(0, normalizedY1 * frameHeight),
+        width: Math.max(0, (normalizedX2 - normalizedX1) * frameWidth),
+        height: Math.max(0, (normalizedY2 - normalizedY1) * frameHeight),
+        label: CLASS_NAMES[classId],
+        confidence: score,
+        classId: classId
+      });
+    }
+  }
+
+  return detections;
+}
+
+export function calculateTally(detections: BoundingBox[]) {
+  'worklet';
+  const counts: { [key: string]: number } = {};
+  for (const det of detections) {
+    counts[det.label] = (counts[det.label] || 0) + 1;
+  }
+  return counts;
+}