diff --git a/android/build.gradle b/android/build.gradle index 0faeb93d..7d9f14ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.3.61' + ext.kotlin_version = '1.4.21' repositories { google() diff --git a/assets/release_notes.md b/assets/release_notes.md index aaeee437..25a12a4b 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,4 +1,4 @@ -## 0.1.3 - February 2021 +## 0.1.3 - March 2021 --- - Adds ability to toggle "star" status for Part @@ -6,6 +6,7 @@ - User permissions are now queried from the InvenTree server - Any "unauthorized" actions are now not displayed - Uses server-side pagination, providing a significant increase in UI performance +- Adds audio feedback for server errors and barcode scanning ## 0.1.2 - February 2021 --- diff --git a/lib/barcode.dart b/lib/barcode.dart index b43b2143..f97e2ebd 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -1,5 +1,6 @@ import 'package:InvenTree/widget/dialogs.dart'; import 'package:InvenTree/widget/snacks.dart'; +import 'package:audioplayers/audio_cache.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -38,6 +39,16 @@ class BarcodeHandler { QRViewController _controller; BuildContext _context; + void successTone() { + AudioCache player = AudioCache(); + player.play("sounds/barcode_scan.mp3"); + } + + void failureTone() { + AudioCache player = AudioCache(); + player.play("sounds/barcode_error.mp3"); + } + Future onBarcodeMatched(Map data) { // Called when the server "matches" a barcode // Override this function @@ -46,6 +57,9 @@ class BarcodeHandler { Future onBarcodeUnknown(Map data) { // Called when the server does not know about a barcode // Override this function + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeNoMatch, success: false, @@ -54,6 +68,9 @@ class BarcodeHandler { } Future onBarcodeUnhandled(Map data) { + + failureTone(); + // Called when the server returns an unhandled response showServerError(I18N.of(OneContext().context).responseUnknown, data.toString()); @@ -119,6 +136,8 @@ class BarcodeScanHandler extends BarcodeHandler { @override Future onBarcodeUnknown(Map data) { + failureTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeNoMatch, icon: FontAwesomeIcons.exclamationCircle, @@ -139,6 +158,9 @@ class BarcodeScanHandler extends BarcodeHandler { pk = data['stocklocation']['pk'] as int ?? null; if (pk != null) { + + successTone(); + InvenTreeStockLocation().get(_context, pk).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.of(_context).pop(); @@ -146,6 +168,9 @@ class BarcodeScanHandler extends BarcodeHandler { } }); } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).invalidStockLocation, success: false @@ -157,11 +182,17 @@ class BarcodeScanHandler extends BarcodeHandler { pk = data['stockitem']['pk'] as int ?? null; if (pk != null) { + + successTone(); + InvenTreeStockItem().get(_context, pk).then((var item) { Navigator.of(_context).pop(); Navigator.push(_context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); }); } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).invalidStockItem, success: false @@ -172,17 +203,26 @@ class BarcodeScanHandler extends BarcodeHandler { pk = data['part']['pk'] as int ?? null; if (pk != null) { + + successTone(); + InvenTreePart().get(_context, pk).then((var part) { Navigator.of(_context).pop(); Navigator.push(_context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); }); } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).invalidPart, success: false ); } } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeUnknown, success: false, @@ -220,7 +260,10 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { @override Future onBarcodeMatched(Map data) { - // If the barcode is known, we can't asisgn it to the stock item! + + failureTone(); + + // If the barcode is known, we can't assign it to the stock item! showSnackIcon( I18N.of(OneContext().context).barcodeInUse, icon: FontAwesomeIcons.qrcode, @@ -245,6 +288,8 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { ).then((result) { if (result) { + failureTone(); + // Close the barcode scanner _controller.dispose(); Navigator.of(_context).pop(); @@ -256,6 +301,8 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { ); } else { + successTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeNotAssigned, success: false, @@ -294,6 +341,9 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { final result = await item.transferStock(location); if (result) { + + successTone(); + // Close the scanner _controller.dispose(); Navigator.of(_context).pop(); @@ -303,12 +353,18 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { success: true, ); } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeScanIntoLocationFailure, success: false ); } } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).invalidStockLocation, success: false, @@ -341,26 +397,37 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { final InvenTreeStockItem item = await InvenTreeStockItem().get(_context, item_id); if (item == null) { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).invalidStockItem, success: false, ); } else if (item.locationId == location.pk) { - showSnackIcon( - I18N.of(OneContext().context).itemInLocation, - success: true - ); - } + failureTone(); - else { + showSnackIcon( + I18N + .of(OneContext().context) + .itemInLocation, + success: true + ); + } else { final result = await item.transferStock(location.pk); if (result) { + + successTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeScanIntoLocationSuccess, success: true ); } else { + + failureTone(); + showSnackIcon( I18N.of(OneContext().context).barcodeScanIntoLocationFailure, success: false @@ -368,6 +435,9 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { } } } else { + + failureTone(); + // Does not match a valid stock item! showSnackIcon( I18N.of(OneContext().context).invalidStockItem, diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 712866b1..fcc92345 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,5 +1,7 @@ import 'package:InvenTree/widget/snacks.dart'; +import 'package:audioplayers/audio_cache.dart'; +import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -113,7 +115,9 @@ Future showServerError(String title, String description) async { title = I18N.of(OneContext().context).serverError; } - // TODO - Play audio notification + // Play a sound + AudioCache player = AudioCache(); + player.play("sounds/server_error.mp3"); showSnackIcon( title, diff --git a/pubspec.lock b/pubspec.lock index f9b801d4..fb5bd960 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.0-nullsafety.1" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.4" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b8a69244..8fadc6d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: sembast: ^2.4.9 # NoSQL data storage one_context: ^0.5.0 # Dialogs without requiring context infinite_scroll_pagination: ^2.3.0 # Let the server do all the work! + audioplayers: path: dev_dependencies: