2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16: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 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '11' java-version: '17'
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
@ -29,7 +29,7 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2.4.2 uses: gradle/gradle-build-action@v2.4.2
with: with:
gradle-version: 7.6 gradle-version: 8.5
- name: Collect Translation Files - name: Collect Translation Files
run: | run: |
cd lib/l10n 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 ## 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. Refer to the [build instructions](BUILDING.md) for information on how to build the app from source.
### 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
```

View File

@ -31,6 +31,16 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 34 compileSdkVersion 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
// If using Kotlin
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 - Enhanced home-screen display using grid-view
- Improvements for image uploading - Improvements for image uploading
- Provide "upload image" shortcut on Purchase Order detail view - 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/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/preferences.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"; import "package:inventree/l10.dart";
@ -26,197 +26,234 @@ class CameraBarcodeController extends InvenTreeBarcodeController {
class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
_CameraBarcodeControllerState() : super(); _CameraBarcodeControllerState() : super();
QRViewController? _controller;
bool flash_status = false; bool flash_status = false;
int scan_delay = 500;
bool single_scanning = false; bool single_scanning = false;
bool scanning_paused = false; bool scanning_paused = false;
String scanned_code = "";
@override
void initState() {
super.initState();
_loadSettings();
}
/*
* Load the barcode scanning settings
*/
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
bool _single = await InvenTreeSettingsManager() bool _single = await InvenTreeSettingsManager()
.getBool(INV_BARCODE_SCAN_SINGLE, false); .getBool(INV_BARCODE_SCAN_SINGLE, false);
int _delay = await InvenTreeSettingsManager()
.getValue(INV_BARCODE_SCAN_DELAY, 500) as int;
if (mounted) { if (mounted) {
setState(() { setState(() {
scan_delay = _delay;
single_scanning = _single; single_scanning = _single;
scanning_paused = false; 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 @override
Future<void> pauseScan() async { Future<void> pauseScan() async {
try { if (mounted) {
await _controller?.pauseCamera(); setState(() {
} on CameraException { scanning_paused = true;
// do nothing });
} }
} }
@override @override
Future<void> resumeScan() async { 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) { if (mounted) {
setState(() { setState(() {
scanning_paused = false; scanning_paused = false;
}); });
} }
}, }
child: Stack(
children: <Widget>[ /*
Column(children: [ * Callback function when a barcode is scanned
Expanded( */
child: QRView( void _onScanSuccess(Code? code) {
key: barcodeControllerKey,
onQRViewCreated: (QRViewController controller) { if (scanning_paused) {
_onViewCreated(context, controller); return;
}, }
overlay: QrScannerOverlayShape(
borderColor: String barcode_data = code?.text ?? "";
scanning_paused ? COLOR_WARNING : COLOR_ACTION,
borderRadius: 10, if (mounted) {
borderLength: 30, setState(() {
borderWidth: 10, scanned_code = barcode_data;
cutOutSize: 300, scanning_paused = barcode_data.isNotEmpty;
), });
)) }
]),
Center( if (barcode_data.isNotEmpty) {
child: Column(children: [ handleBarcodeData(barcode_data).then((_) {
Padding( 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( child: Text(
widget.handler.getOverlayText(context), widget.handler.getOverlayText(context),
style: TextStyle( style: TextStyle(
color: Colors.white,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, color: Colors.white), fontWeight: FontWeight.bold
), )
padding: EdgeInsets.all(25)), )
Padding( )
child: CircularProgressIndicator( )
value: scanning_paused ? 0 : null), );
padding: EdgeInsets.all(40), }
),
Spacer(), Widget bottomCenterOverlay() {
SizedBox(
child: Center( String info_text = scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause;
child: actionIcon,
), return SafeArea(
width: 100, child: Align(
height: 150, alignment: Alignment.bottomCenter,
), child: Padding(
Padding( padding: EdgeInsets.all(10),
child: Text(info_text, child: Text(
scanned_code.isNotEmpty ? scanned_code : info_text,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
)), fontSize: 16,
padding: EdgeInsets.all(25), 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, Future<void> processBarcode(String barcode,
{String url = "barcode/", {String url = "barcode/",
Map<String, dynamic> extra_data = const {}}) async { Map<String, dynamic> extra_data = const {}}) async {
debug("Scanned barcode data: '${barcode}'"); debug("Scanned barcode data: '${barcode}'");
barcode = barcode.trim(); barcode = barcode.trim();

View File

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

View File

@ -174,7 +174,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
ListTile( ListTile(
title: Text(L10().darkMode), title: Text(L10().darkMode),
subtitle: Text(L10().darkModeEnable), subtitle: Text(L10().darkModeEnable),
leading: Icon(TablerIcons.moon), leading: Icon(TablerIcons.sun_moon),
trailing: Switch( trailing: Switch(
value: darkMode, value: darkMode,
onChanged: (bool value) { onChanged: (bool value) {

View File

@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/preferences.dart"; import "package:inventree/preferences.dart";
@ -39,7 +40,10 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(L10().purchaseOrderSettings)), appBar: AppBar(
title: Text(L10().purchaseOrderSettings),
backgroundColor: COLOR_APP_BAR,
),
body: Container( body: Container(
child: ListView( child: ListView(
children: [ children: [

View File

@ -3,6 +3,7 @@ import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/preferences.dart"; import "package:inventree/preferences.dart";
@ -39,7 +40,10 @@ class _InvenTreeSalesOrderSettingsState extends State<InvenTreeSalesOrderSetting
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(L10().salesOrderSettings)), appBar: AppBar(
title: Text(L10().salesOrderSettings),
backgroundColor: COLOR_APP_BAR,
),
body: Container( body: Container(
child: ListView( child: ListView(
children: [ children: [

View File

@ -266,6 +266,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
key: _loginKey, key: _loginKey,
appBar: AppBar( appBar: AppBar(
title: Text(L10().profileSelect), title: Text(L10().profileSelect),
backgroundColor: COLOR_APP_BAR,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(TablerIcons.circle_plus), icon: Icon(TablerIcons.circle_plus),

View File

@ -208,11 +208,8 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
if (tiles.isEmpty) { if (tiles.isEmpty) {
tiles.add(ListTile( tiles.add(ListTile(
leading: Icon(TablerIcons.file_x, color: COLOR_WARNING),
title: Text(L10().attachmentNone), 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/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.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( tiles.add(
ListTile( ListTile(
title: Text(L10().settings), title: Text(L10().settings),

View File

@ -101,7 +101,7 @@ mixin BaseWidgetProperties {
}, },
), ),
IconButton( IconButton(
icon: Icon(Icons.barcode_reader, color: COLOR_ACTION), icon: Icon(TablerIcons.barcode, color: COLOR_ACTION),
iconSize: iconSize, iconSize: iconSize,
onPressed: () { onPressed: () {
if (InvenTreeAPI().checkConnection()) { if (InvenTreeAPI().checkConnection()) {

View File

@ -210,10 +210,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cli_util name: cli_util
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.5" version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -391,10 +391,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.0" version: "0.14.1"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -466,6 +466,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -510,10 +518,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" version: "4.3.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -750,10 +758,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.2" version: "8.1.1"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -858,14 +866,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: rxdart:
dependency: transitive dependency: transitive
description: description:

View File

@ -27,22 +27,22 @@ dependencies:
flutter_overlay_loader: ^2.0.0 # Overlay screen support flutter_overlay_loader: ^2.0.0 # Overlay screen support
flutter_speed_dial: ^6.2.0 # Speed dial / FAB implementation flutter_speed_dial: ^6.2.0 # Speed dial / FAB implementation
flutter_tabler_icons: ^1.35.0 flutter_tabler_icons: ^1.35.0
flutter_zxing: ^1.8.2 # Barcode scanning
http: ^1.2.2 http: ^1.2.2
image_picker: ^1.1.2 # Select or take photos image_picker: ^1.1.2 # Select or take photos
infinite_scroll_pagination: ^4.0.0 # Let the server do all the work! infinite_scroll_pagination: ^4.0.0 # Let the server do all the work!
intl: ^0.19.0 intl: ^0.19.0
one_context: ^4.0.0 # Dialogs without requiring context one_context: ^4.0.0 # Dialogs without requiring context
open_filex: ^4.5.0 # Open local files 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: ^1.9.0
path_provider: ^2.1.3 # Local file storage path_provider: ^2.1.3 # Local file storage
qr_code_scanner: ^1.0.1 # Barcode scanning
sembast: ^3.6.0 # NoSQL data storage sembast: ^3.6.0 # NoSQL data storage
sentry_flutter: 8.9.0 # Error reporting sentry_flutter: 8.9.0 # Error reporting
url_launcher: ^6.3.0 # Open link in system browser url_launcher: ^6.3.0 # Open link in system browser
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.11.0 flutter_launcher_icons: ^0.14.1
flutter_test: flutter_test:
sdk: flutter sdk: flutter
lint: ^2.1.2 lint: ^2.1.2