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 _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 pauseScan() async { - try { - await _controller?.pauseCamera(); - } on CameraException { - // do nothing - } - } - - @override - Future 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 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 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: [ + 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: [ - 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 processBarcode(String barcode, {String url = "barcode/", Map 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 { 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 { 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 { 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