2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 04:56:48 +00:00

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
This commit is contained in:
Oliver 2024-12-06 00:08:04 +11:00 committed by GitHub
parent 4151aeb8e1
commit d4cff1a5b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 339 additions and 228 deletions

View File

@ -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

64
BUILDING.md Normal file
View File

@ -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
```

View File

@ -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.

View File

@ -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'
}

View File

@ -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

View File

@ -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

View File

@ -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
if (mounted) {
setState(() {
scanning_paused = true;
});
}
}
@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;
});
}
}
@override
Widget build(BuildContext context) {
Widget actionIcon =
Icon(TablerIcons.player_pause, color: COLOR_WARNING, size: 64);
if (scanning_paused) {
actionIcon =
Icon(TablerIcons.player_play, color: COLOR_ACTION, size: 64);
}
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();
},
)
],
),
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(
}
/*
* Callback function when a barcode is scanned
*/
void _onScanSuccess(Code? code) {
if (scanning_paused) {
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, 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,
fontWeight: FontWeight.bold
)
)
)
)
);
}
Widget bottomCenterOverlay() {
String info_text = scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause;
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,
)),
padding: EdgeInsets.all(25),
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(),
],
),
),
);
}
}

View File

@ -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();

View File

@ -274,6 +274,12 @@
"damaged": "Damaged",
"@damaged": {},
"colorScheme": "Color Scheme",
"@colorScheme": {},
"colorSchemeDetail": "Select color scheme",
"@colorSchemeDetail": {},
"darkMode": "Dark Mode",
"@darkMode": {},

View File

@ -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) {

View File

@ -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: [

View File

@ -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: [

View File

@ -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),

View File

@ -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),
),
));
}

View File

@ -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),

View File

@ -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()) {

View File

@ -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:

View File

@ -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