From d4cff1a5b9363d54c36eb123767eb81c40b82ca0 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Fri, 6 Dec 2024 00:08:04 +1100
Subject: [PATCH] Barcode scanner updates (#562)

* Add BUILDING.md

* Replace scaning library

- Out with qr_code_scanner
- In with flutter_zxing

* Update specs for jdk / kotlin / gradle

- NFI what this all means?

* Refactor barcode scanning widget

* Refactor barcode overlay

* Add handlers

* Update release notes

* Fix AppBar color

* Enhance attachment widget

* remove unused import

* Improved icon

* Select theme from main drawer
---
 .github/workflows/android.yaml                |   4 +-
 BUILDING.md                                   |  64 ++++
 README.md                                     |  39 +-
 android/app/build.gradle                      |  10 +
 .../gradle/wrapper/gradle-wrapper.properties  |   2 +-
 assets/release_notes.md                       |   3 +-
 lib/barcode/camera_controller.dart            | 355 ++++++++++--------
 lib/barcode/handler.dart                      |   1 +
 lib/l10n/app_en.arb                           |   6 +
 lib/settings/app_settings.dart                |   2 +-
 lib/settings/purchase_order_settings.dart     |   6 +-
 lib/settings/sales_order_settings.dart        |   6 +-
 lib/settings/select_server.dart               |   1 +
 lib/widget/attachment_widget.dart             |   5 +-
 lib/widget/drawer.dart                        |  23 ++
 lib/widget/refreshable_state.dart             |   2 +-
 pubspec.lock                                  |  32 +-
 pubspec.yaml                                  |   6 +-
 18 files changed, 339 insertions(+), 228 deletions(-)
 create mode 100644 BUILDING.md

diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml
index 16c06b74..2298bad1 100644
--- a/.github/workflows/android.yaml
+++ b/.github/workflows/android.yaml
@@ -19,7 +19,7 @@ jobs:
       uses: actions/setup-java@v3
       with:
         distribution: 'temurin'
-        java-version: '11'
+        java-version: '17'
     - name: Setup Flutter
       uses: subosito/flutter-action@v2
       with:
@@ -29,7 +29,7 @@ jobs:
     - name: Setup Gradle
       uses: gradle/gradle-build-action@v2.4.2
       with:
-        gradle-version: 7.6
+        gradle-version: 8.5
     - name: Collect Translation Files
       run: |
         cd lib/l10n
diff --git a/BUILDING.md b/BUILDING.md
new file mode 100644
index 00000000..9e1d3fea
--- /dev/null
+++ b/BUILDING.md
@@ -0,0 +1,64 @@
+## InvenTree App Development
+
+For developers looking to contribute to the project, we use Flutter for app development. The project has been tested in Android Studio (on both Windows and Mac) and also VSCode.
+
+## Prerequisites
+
+To build the app from source, you will need the following tools installed on your system:
+
+- Android Studio (with Flutter and Dart plugins)
+
+### iOS Development
+
+For iOS development, you will need a Mac system with XCode installed.
+
+### Java Version
+
+Some versions of Android Studio ship with a built-in version of the Java JDK. However, the InvenTree app requires [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to be installed.
+
+If you see any errors related to JDK version mismatch, download and install the correct version of the JDK (from the link above) and update your Android Studio settings to point to the correct JDK location:
+
+```bash
+flutter config --jdk-dir /path/to/jdk
+```
+
+## Invoke Tasks
+
+We use the [invoke](https://www.pyinvoke.org) to run some core tasks - you will need python and invoke installed on your local system.
+
+## Getting Started
+
+Initial project setup (after you have installed all required dev tools) is as follows:
+
+Generate initial translation files:
+
+```
+invoke translate
+```
+
+Install required flutter packages:
+```
+flutter pub get
+```
+
+You should now be ready to debug on a connected or emulated device!
+
+## Building Release Versions
+
+Building release versions for target platforms (either android or iOS) is simplified using invoke:
+
+### Android
+
+Build Android release:
+
+```
+invoke android
+```
+
+### iOS
+
+Build iOS release:
+
+```
+invoke ios
+```
diff --git a/README.md b/README.md
index 636f1007..c422c6ca 100644
--- a/README.md
+++ b/README.md
@@ -31,41 +31,4 @@ User documentation for the InvenTree mobile app can be found [within the InvenTr
 
 ## Developer Documentation
 
-For developers looking to contribute to the project, we use Flutter for app development. The project has been tested in Android Studio (on both Windows and Mac) and also VSCode.
-
-### Invoke Tasks
-
-We use the [invoke](https://www.pyinvoke.org) to run some core tasks - you will need python and invoke installed on your local system.
-
-### Getting Started
-
-Initial project setup (after you have installed all required dev tools) is as follows:
-
-Generate initial translation files:
-
-```
-invoke translate
-```
-
-Install required flutter packages:
-```
-flutter pub get
-```
-
-You should now be ready to debug on a connected or emulated device!
-
-### Building Release Versions
-
-Building release versions for target platforms (either android or iOS) is simplified using invoke:
-
-Build Android release:
-
-```
-invoke android
-```
-
-Build iOS release:
-
-```
-invoke ios
-```
+Refer to the [build instructions](BUILDING.md) for information on how to build the app from source.
diff --git a/android/app/build.gradle b/android/app/build.gradle
index a20b12b5..bdee7d8e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -31,6 +31,16 @@ if (keystorePropertiesFile.exists()) {
 android {
     compileSdkVersion 34
 
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+
+    // If using Kotlin
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_17
+    }
+
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
     }
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 2b22d057..829e1a5a 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
 networkTimeout=10000
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/assets/release_notes.md b/assets/release_notes.md
index 9329ce1c..8d3947bf 100644
--- a/assets/release_notes.md
+++ b/assets/release_notes.md
@@ -1,6 +1,7 @@
-### 0.17.0 - November 2024
+### 0.17.0 - December 2024
 ---
 
+- Improved barcode scanning with new scanning library
 - Enhanced home-screen display using grid-view
 - Improvements for image uploading
 - Provide "upload image" shortcut on Purchase Order detail view
diff --git a/lib/barcode/camera_controller.dart b/lib/barcode/camera_controller.dart
index 460aecb5..ea4cf633 100644
--- a/lib/barcode/camera_controller.dart
+++ b/lib/barcode/camera_controller.dart
@@ -1,10 +1,10 @@
-import "dart:io";
+import "dart:math";
 import "package:flutter/material.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
 import "package:inventree/app_colors.dart";
 import "package:inventree/preferences.dart";
 
-import "package:qr_code_scanner/qr_code_scanner.dart";
+import "package:flutter_zxing/flutter_zxing.dart";
 
 import "package:inventree/l10.dart";
 
@@ -26,197 +26,234 @@ class CameraBarcodeController extends InvenTreeBarcodeController {
 class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
   _CameraBarcodeControllerState() : super();
 
-  QRViewController? _controller;
-
   bool flash_status = false;
 
+  int scan_delay = 500;
   bool single_scanning = false;
   bool scanning_paused = false;
 
+  String scanned_code = "";
+
+  @override
+  void initState() {
+    super.initState();
+    _loadSettings();
+  }
+
+  /*
+   * Load the barcode scanning settings
+   */
   Future<void> _loadSettings() async {
     bool _single = await InvenTreeSettingsManager()
         .getBool(INV_BARCODE_SCAN_SINGLE, false);
 
+    int _delay = await InvenTreeSettingsManager()
+        .getValue(INV_BARCODE_SCAN_DELAY, 500) as int;
+
     if (mounted) {
       setState(() {
+        scan_delay = _delay;
         single_scanning = _single;
         scanning_paused = false;
       });
     }
   }
 
-  /* Callback function when the Barcode scanner view is initially created */
-  void _onViewCreated(BuildContext context, QRViewController controller) {
-    _controller = controller;
-
-    controller.scannedDataStream.listen((barcode) {
-      if (!scanning_paused) {
-        handleBarcodeData(barcode.code).then((value) => {
-              // If in single-scanning mode, pause after successful scan
-              if (single_scanning && mounted)
-                {
-                  setState(() {
-                    scanning_paused = true;
-                  })
-                }
-            });
-      }
-    });
-
-    _loadSettings();
-  }
-
-  // In order to get hot reload to work we need to pause the camera if the platform
-  // is android, or resume the camera if the platform is iOS.
-  @override
-  void reassemble() {
-    super.reassemble();
-
-    if (mounted) {
-      if (Platform.isAndroid) {
-        _controller!.pauseCamera();
-      }
-
-      _controller!.resumeCamera();
-    }
-  }
-
-  @override
-  void dispose() {
-    _controller?.dispose();
-    super.dispose();
-  }
-
   @override
   Future<void> pauseScan() async {
-    try {
-      await _controller?.pauseCamera();
-    } on CameraException {
-      // do nothing
-    }
-  }
-
-  @override
-  Future<void> resumeScan() async {
-    // Do not attempt to resume if the widget is not mounted
-    if (!mounted) {
-      return;
-    }
-
-    try {
-      await _controller?.resumeCamera();
-    } on CameraException {
-      // do nothing
-    }
-  }
-
-  // Toggle the status of the camera flash
-  Future<void> updateFlashStatus() async {
-    final bool? status = await _controller?.getFlashStatus();
-
     if (mounted) {
       setState(() {
-        flash_status = status != null && status;
+        scanning_paused = true;
       });
     }
   }
 
   @override
-  Widget build(BuildContext context) {
-    Widget actionIcon =
-        Icon(TablerIcons.player_pause, color: COLOR_WARNING, size: 64);
+  Future<void> resumeScan() async {
+    if (mounted) {
+      setState(() {
+        scanning_paused = false;
+      });
+    }
+  }
+
+  /*
+   * Callback function when a barcode is scanned
+   */
+  void _onScanSuccess(Code? code) {
 
     if (scanning_paused) {
-      actionIcon =
-          Icon(TablerIcons.player_play, color: COLOR_ACTION, size: 64);
+      return;
     }
 
+    String barcode_data = code?.text ?? "";
+
+    if (mounted) {
+      setState(() {
+        scanned_code = barcode_data;
+        scanning_paused = barcode_data.isNotEmpty;
+      });
+    }
+
+    if (barcode_data.isNotEmpty) {
+      handleBarcodeData(barcode_data).then((_) {
+        if (!single_scanning && mounted) {
+          // Resume next scan
+          setState(() {
+            scanning_paused = false;
+          });
+        }
+      });
+    }
+
+  }
+
+  /*
+   * Build the barcode scanner overlay
+   */
+  FixedScannerOverlay BarcodeOverlay(BuildContext context) {
+
+    // Note: Copied from reader_widget.dart:ReaderWidget.build
+    final Size size = MediaQuery.of(context).size;
+    final double cropSize = min(size.width, size.height) * 0.5;
+
+    return FixedScannerOverlay(
+      borderColor: scanning_paused ? COLOR_WARNING : COLOR_ACTION,
+      overlayColor: Colors.black45,
+      borderRadius: 1,
+      borderLength: 15,
+      borderWidth: 8,
+      cutOutSize: cropSize,
+    );
+  }
+
+  /*
+   * Build the barcode reader widget
+   */
+  Widget BarcodeReader(BuildContext context) {
+
+    return ReaderWidget(
+      onScan: _onScanSuccess,
+      isMultiScan: false,
+      tryHarder: true,
+      tryInverted: true,
+      tryRotate: true,
+      showGallery: false,
+      scanDelay: Duration(milliseconds: scan_delay),
+      resolution: ResolutionPreset.high,
+      lensDirection: CameraLensDirection.back,
+      flashOnIcon: const Icon(Icons.flash_on),
+      flashOffIcon: const Icon(Icons.flash_off),
+      toggleCameraIcon: const Icon(TablerIcons.camera_rotate),
+      actionButtonsBackgroundBorderRadius:
+      BorderRadius.circular(40),
+      scannerOverlay: BarcodeOverlay(context),
+      actionButtonsBackgroundColor: Colors.black.withOpacity(0.7),
+    );
+  }
+
+  Widget topCenterOverlay() {
+    return SafeArea(
+      child: Align(
+        alignment: Alignment.topCenter,
+        child: Padding(
+          padding: EdgeInsets.all(10),
+          child: Text(
+            widget.handler.getOverlayText(context),
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 16,
+              fontWeight: FontWeight.bold
+            )
+          )
+        )
+      )
+    );
+  }
+
+  Widget bottomCenterOverlay() {
+
     String info_text = scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause;
 
-    return Scaffold(
-        appBar: AppBar(
-          backgroundColor: COLOR_APP_BAR,
-          title: Text(L10().scanBarcode),
-          actions: [
-            IconButton(
-                icon: Icon(Icons.flip_camera_android),
-                onPressed: () {
-                  _controller?.flipCamera();
-                }),
-            IconButton(
-              icon: flash_status ? Icon(Icons.flash_off) : Icon(Icons.flash_on),
-              onPressed: () {
-                _controller?.toggleFlash();
-                updateFlashStatus();
-              },
+    return SafeArea(
+      child: Align(
+        alignment: Alignment.bottomCenter,
+        child: Padding(
+          padding: EdgeInsets.all(10),
+          child: Text(
+              scanned_code.isNotEmpty ? scanned_code : info_text,
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 16,
+                fontWeight: FontWeight.bold
+              )
+          ),
+        )
+      )
+    );
+  }
+
+
+  /*
+   *  Display an overlay at the bottom right of the screen
+   */
+  Widget bottomRightOverlay() {
+    return SafeArea(
+        child: Align(
+            alignment: Alignment.bottomRight,
+            child: Padding(
+                padding: EdgeInsets.all(10),
+                child: ClipRRect(
+                    borderRadius: BorderRadius.circular(40),
+                    child: ColoredBox(
+                        color: Colors.black45,
+                        child: Row(
+                            mainAxisSize: MainAxisSize.min,
+                            children: scanning_paused ? [] : [
+                              CircularProgressIndicator(
+                                  value: null
+                              )
+                              // actionIcon,
+                            ]
+                        )
+                    )
+                )
             )
+        )
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+
+    return Scaffold(
+      appBar: AppBar(
+        backgroundColor: COLOR_APP_BAR,
+        title: Text(L10().scanBarcode),
+      ),
+      body: GestureDetector(
+        onTap: () async {
+          setState(() {
+            scanning_paused = !scanning_paused;
+          });
+        },
+        child: Stack(
+          children: <Widget>[
+            Column(
+              children: [
+                Expanded(
+                    child: BarcodeReader(context)
+                ),
+              ],
+            ),
+            topCenterOverlay(),
+            bottomCenterOverlay(),
+            bottomRightOverlay(),
           ],
         ),
-        body: GestureDetector(
-            onTapDown: (details) async {
-              setState(() {
-                scanning_paused = !scanning_paused;
-              });
-            },
-            onLongPressEnd: (details) async {
-              if (mounted) {
-                setState(() {
-                  scanning_paused = false;
-                });
-              }
-            },
-            child: Stack(
-              children: <Widget>[
-                Column(children: [
-                  Expanded(
-                      child: QRView(
-                    key: barcodeControllerKey,
-                    onQRViewCreated: (QRViewController controller) {
-                      _onViewCreated(context, controller);
-                    },
-                    overlay: QrScannerOverlayShape(
-                      borderColor:
-                          scanning_paused ? COLOR_WARNING : COLOR_ACTION,
-                      borderRadius: 10,
-                      borderLength: 30,
-                      borderWidth: 10,
-                      cutOutSize: 300,
-                    ),
-                  ))
-                ]),
-                Center(
-                    child: Column(children: [
-                  Padding(
-                      child: Text(
-                        widget.handler.getOverlayText(context),
-                        style: TextStyle(
-                          fontSize: 16,
-                            fontWeight: FontWeight.bold, color: Colors.white),
-                      ),
-                      padding: EdgeInsets.all(25)),
-                  Padding(
-                    child: CircularProgressIndicator(
-                        value: scanning_paused ? 0 : null),
-                    padding: EdgeInsets.all(40),
-                  ),
-                  Spacer(),
-                  SizedBox(
-                    child: Center(
-                      child: actionIcon,
-                    ),
-                    width: 100,
-                    height: 150,
-                  ),
-                  Padding(
-                    child: Text(info_text,
-                        textAlign: TextAlign.center,
-                        style: TextStyle(
-                          color: Colors.white,
-                        )),
-                    padding: EdgeInsets.all(25),
-                  ),
-                ]))
-              ],
-            )));
+      ),
+    );
   }
+
 }
diff --git a/lib/barcode/handler.dart b/lib/barcode/handler.dart
index a5e1b8ac..20dc9e92 100644
--- a/lib/barcode/handler.dart
+++ b/lib/barcode/handler.dart
@@ -60,6 +60,7 @@ class BarcodeHandler {
   Future<void> processBarcode(String barcode,
       {String url = "barcode/",
       Map<String, dynamic> extra_data = const {}}) async {
+
     debug("Scanned barcode data: '${barcode}'");
 
     barcode = barcode.trim();
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 3db78e6c..cbaeb350 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -274,6 +274,12 @@
   "damaged": "Damaged",
   "@damaged": {},
 
+  "colorScheme": "Color Scheme",
+  "@colorScheme": {},
+
+  "colorSchemeDetail": "Select color scheme",
+  "@colorSchemeDetail": {},
+
   "darkMode": "Dark Mode",
   "@darkMode": {},
 
diff --git a/lib/settings/app_settings.dart b/lib/settings/app_settings.dart
index a2e82aad..7b6b08de 100644
--- a/lib/settings/app_settings.dart
+++ b/lib/settings/app_settings.dart
@@ -174,7 +174,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
             ListTile(
               title: Text(L10().darkMode),
               subtitle: Text(L10().darkModeEnable),
-              leading: Icon(TablerIcons.moon),
+              leading: Icon(TablerIcons.sun_moon),
               trailing: Switch(
                 value: darkMode,
                 onChanged: (bool value) {
diff --git a/lib/settings/purchase_order_settings.dart b/lib/settings/purchase_order_settings.dart
index d4211c29..5a3b430e 100644
--- a/lib/settings/purchase_order_settings.dart
+++ b/lib/settings/purchase_order_settings.dart
@@ -1,6 +1,7 @@
 
 import "package:flutter/material.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
+import "package:inventree/app_colors.dart";
 
 import "package:inventree/l10.dart";
 import "package:inventree/preferences.dart";
@@ -39,7 +40,10 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(title: Text(L10().purchaseOrderSettings)),
+        appBar: AppBar(
+          title: Text(L10().purchaseOrderSettings),
+          backgroundColor: COLOR_APP_BAR,
+        ),
         body: Container(
             child: ListView(
                 children: [
diff --git a/lib/settings/sales_order_settings.dart b/lib/settings/sales_order_settings.dart
index 62219b96..cbeacb66 100644
--- a/lib/settings/sales_order_settings.dart
+++ b/lib/settings/sales_order_settings.dart
@@ -3,6 +3,7 @@ import "package:flutter/material.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
 
 import "package:inventree/l10.dart";
+import "package:inventree/app_colors.dart";
 import "package:inventree/preferences.dart";
 
 
@@ -39,7 +40,10 @@ class _InvenTreeSalesOrderSettingsState extends State<InvenTreeSalesOrderSetting
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(title: Text(L10().salesOrderSettings)),
+        appBar: AppBar(
+          title: Text(L10().salesOrderSettings),
+          backgroundColor: COLOR_APP_BAR,
+        ),
         body: Container(
             child: ListView(
                 children: [
diff --git a/lib/settings/select_server.dart b/lib/settings/select_server.dart
index 5c941aa2..323ce8f6 100644
--- a/lib/settings/select_server.dart
+++ b/lib/settings/select_server.dart
@@ -266,6 +266,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
       key: _loginKey,
       appBar: AppBar(
         title: Text(L10().profileSelect),
+        backgroundColor: COLOR_APP_BAR,
         actions: [
           IconButton(
             icon: Icon(TablerIcons.circle_plus),
diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart
index e6cfcd75..ab9c61c2 100644
--- a/lib/widget/attachment_widget.dart
+++ b/lib/widget/attachment_widget.dart
@@ -208,11 +208,8 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
 
     if (tiles.isEmpty) {
       tiles.add(ListTile(
+        leading: Icon(TablerIcons.file_x, color: COLOR_WARNING),
         title: Text(L10().attachmentNone),
-        subtitle: Text(
-          L10().attachmentNoneDetail,
-          style: TextStyle(fontStyle: FontStyle.italic),
-        ),
       ));
     }
 
diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart
index 1fbbbe47..2288e340 100644
--- a/lib/widget/drawer.dart
+++ b/lib/widget/drawer.dart
@@ -1,3 +1,4 @@
+import "package:adaptive_theme/adaptive_theme.dart";
 import "package:flutter/material.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
 
@@ -185,6 +186,28 @@ class InvenTreeDrawer extends StatelessWidget {
       )
     );
 
+    tiles.add(Divider());
+
+    bool darkMode = AdaptiveTheme.of(context).mode.isDark;
+
+    tiles.add(
+      ListTile(
+        onTap: () {
+          AdaptiveTheme.of(context).toggleThemeMode();
+          _closeDrawer();
+        },
+        title: Text(L10().colorScheme),
+        subtitle: Text(L10().colorSchemeDetail),
+        leading: Icon(
+          TablerIcons.sun_moon,
+          color: COLOR_ACTION
+        ),
+        trailing: Icon(
+          darkMode ? TablerIcons.moon : TablerIcons.sun,
+        )
+      )
+    );
+
     tiles.add(
       ListTile(
         title: Text(L10().settings),
diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart
index d18d07cb..f20462cb 100644
--- a/lib/widget/refreshable_state.dart
+++ b/lib/widget/refreshable_state.dart
@@ -101,7 +101,7 @@ mixin BaseWidgetProperties {
         },
       ),
       IconButton(
-        icon: Icon(Icons.barcode_reader, color: COLOR_ACTION),
+        icon: Icon(TablerIcons.barcode, color: COLOR_ACTION),
         iconSize: iconSize,
         onPressed: () {
           if (InvenTreeAPI().checkConnection()) {
diff --git a/pubspec.lock b/pubspec.lock
index 6cd6d6f9..afda72c6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -210,10 +210,10 @@ packages:
     dependency: transitive
     description:
       name: cli_util
-      sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
+      sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
       url: "https://pub.dev"
     source: hosted
-    version: "0.3.5"
+    version: "0.4.2"
   clock:
     dependency: transitive
     description:
@@ -391,10 +391,10 @@ packages:
     dependency: "direct dev"
     description:
       name: flutter_launcher_icons
-      sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c
+      sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
       url: "https://pub.dev"
     source: hosted
-    version: "0.11.0"
+    version: "0.14.1"
   flutter_localizations:
     dependency: "direct main"
     description: flutter
@@ -466,6 +466,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_zxing:
+    dependency: "direct main"
+    description:
+      name: flutter_zxing
+      sha256: "5b2670f151a6d96643204ff3a781e073739c23a91ef5fc39742bf13fb8287b4c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.8.2"
   frontend_server_client:
     dependency: transitive
     description:
@@ -510,10 +518,10 @@ packages:
     dependency: transitive
     description:
       name: image
-      sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
+      sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
       url: "https://pub.dev"
     source: hosted
-    version: "3.3.0"
+    version: "4.3.0"
   image_picker:
     dependency: "direct main"
     description:
@@ -750,10 +758,10 @@ packages:
     dependency: "direct main"
     description:
       name: package_info_plus
-      sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
+      sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce
       url: "https://pub.dev"
     source: hosted
-    version: "8.0.2"
+    version: "8.1.1"
   package_info_plus_platform_interface:
     dependency: transitive
     description:
@@ -858,14 +866,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
-  qr_code_scanner:
-    dependency: "direct main"
-    description:
-      name: qr_code_scanner
-      sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
   rxdart:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 5cbee61c..496fd1c5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -27,22 +27,22 @@ dependencies:
   flutter_overlay_loader: ^2.0.0          # Overlay screen support
   flutter_speed_dial: ^6.2.0              # Speed dial / FAB implementation
   flutter_tabler_icons: ^1.35.0
+  flutter_zxing: ^1.8.2                   # Barcode scanning
   http: ^1.2.2
   image_picker: ^1.1.2                    # Select or take photos
   infinite_scroll_pagination: ^4.0.0      # Let the server do all the work!
   intl: ^0.19.0
   one_context: ^4.0.0                     # Dialogs without requiring context
   open_filex: ^4.5.0                      # Open local files
-  package_info_plus: ^8.0.2               # App information introspection
+  package_info_plus: ^8.1.1               # App information introspection
   path: ^1.9.0
   path_provider: ^2.1.3                   # Local file storage
-  qr_code_scanner: ^1.0.1                 # Barcode scanning
   sembast: ^3.6.0                         # NoSQL data storage
   sentry_flutter: 8.9.0                   # Error reporting
   url_launcher: ^6.3.0                    # Open link in system browser
 
 dev_dependencies:
-  flutter_launcher_icons: ^0.11.0
+  flutter_launcher_icons: ^0.14.1
   flutter_test:
     sdk: flutter
   lint: ^2.1.2