diff --git a/android/app/build.gradle b/android/app/build.gradle index 90858c7a..b4181680 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,8 +22,8 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') @@ -32,7 +32,11 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 29 + compileSdkVersion 30 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } packagingOptions { exclude 'META-INF/proguard/androidx-annotations.pro' @@ -45,7 +49,7 @@ android { defaultConfig { applicationId "inventree.inventree_app" minSdkVersion 25 - targetSdkVersion 29 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -78,7 +82,8 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - androidTestImplementation 'com.android.support:multidex:1.0.0' + androidTestImplementation 'com.android.support:multidex:2.0.1' + implementation "androidx.core:core:1.5.0-rc01" implementation 'androidx.appcompat:appcompat:1.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/build.gradle b/android/build.gradle index 7d9f14ca..13b686e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '1.5.10' repositories { google() @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,12 +25,7 @@ subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" project.evaluationDependsOn(':app') project.configurations.all { - resolutionStrategy.eachDependency { details -> - if (details.requested.group == 'androidx.core' && - !details.requested.name.contains('androidx')) { - details.useVersion "1.0.2" - } - } + } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 1e122a70..ca2bbd5d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip \ No newline at end of file diff --git a/assets/release_notes.md b/assets/release_notes.md index b5545c17..f8d1bc81 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,14 @@ ## InvenTree App Release Notes --- +### 0.2.6 - July 2021 +--- + +- Major code update with "null safety" features +- Handle case of improperly formatted hostname +- Multiple API bug fixes (mostly null references) +- Updated translations + ### 0.2.5 - June 2021 --- diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 45b3742b..4b84d6e6 100644 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -1,13 +1,13 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=C:\flutter" +export "FLUTTER_ROOT=c:\flutter" export "FLUTTER_APPLICATION_PATH=C:\inventree-app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib\main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build\ios" -export "FLUTTER_BUILD_NAME=0.1.5" -export "FLUTTER_BUILD_NUMBER=9" +export "FLUTTER_BUILD_NAME=0.2.6" +export "FLUTTER_BUILD_NUMBER=14" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=false" export "TREE_SHAKE_ICONS=false" diff --git a/lib/api.dart b/lib/api.dart index 1d062680..6c335740 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:InvenTree/inventree/sentry.dart'; import 'package:InvenTree/user_profile.dart'; import 'package:InvenTree/widget/snacks.dart'; import 'package:flutter/cupertino.dart'; @@ -11,6 +12,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:InvenTree/widget/dialogs.dart'; import 'package:InvenTree/l10.dart'; +import 'package:InvenTree/inventree/sentry.dart'; import 'package:http/http.dart' as http; import 'package:one_context/one_context.dart'; @@ -22,24 +24,31 @@ import 'package:one_context/one_context.dart'; */ class InvenTreeFileService extends FileService { - HttpClient _client; + HttpClient? _client = null; - InvenTreeFileService({HttpClient client, bool strictHttps = false}) { + InvenTreeFileService({HttpClient? client, bool strictHttps = false}) { _client = client ?? HttpClient(); - _client.badCertificateCallback = (cert, host, port) { - print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST"); - return !strictHttps; - }; + + if (_client != null) { + _client?.badCertificateCallback = (cert, host, port) { + print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST"); + return !strictHttps; + }; + } } @override Future get(String url, - {Map headers = const {}}) async { + {Map? headers}) async { final Uri resolved = Uri.base.resolve(url); - final HttpClientRequest req = await _client.getUrl(resolved); - headers?.forEach((key, value) { - req.headers.add(key, value); - }); + final HttpClientRequest req = await _client!.getUrl(resolved); + + if (headers != null) { + headers.forEach((key, value) { + req.headers.add(key, value); + }); + } + final HttpClientResponse httpResponse = await req.close(); final http.StreamedResponse _response = http.StreamedResponse( httpResponse.timeout(Duration(seconds: 60)), httpResponse.statusCode, @@ -101,7 +110,7 @@ class InvenTreeAPI { String makeUrl(String endpoint) => _makeUrl(endpoint); - UserProfile profile; + UserProfile? profile = null; Map roles = {}; @@ -171,15 +180,19 @@ class InvenTreeAPI { * - Request user token from the server * - Request user roles from the server */ - Future _connect(BuildContext context) async { + Future _connect() async { if (profile == null) return false; var ctx = OneContext().context; - String address = profile.server.trim(); - String username = profile.username.trim(); - String password = profile.password.trim(); + String address = profile?.server ?? ""; + String username = profile?.username ?? ""; + String password = profile?.password ?? ""; + + address = address.trim(); + username = username.trim(); + password = password.trim(); if (address.isEmpty || username.isEmpty || password.isEmpty) { showSnackIcon( @@ -202,7 +215,8 @@ class InvenTreeAPI { print("Connecting to ${apiUrl} -> username=${username}"); - HttpClientResponse response; + HttpClientResponse? response; + dynamic data; response = await getResponse(""); @@ -237,7 +251,7 @@ class InvenTreeAPI { instance = data['instance'] ?? ''; // Default API version is 1 if not provided - _apiVersion = data['apiVersion'] as int ?? 1; + _apiVersion = (data['apiVersion'] ?? 1) as int; if (_apiVersion < _minApiVersion) { @@ -315,7 +329,7 @@ class InvenTreeAPI { } - bool disconnectFromServer() { + void disconnectFromServer() { print("InvenTreeAPI().disconnectFromServer()"); _connected = false; @@ -324,7 +338,7 @@ class InvenTreeAPI { profile = null; } - Future connectToServer(BuildContext context) async { + Future connectToServer() async { // Ensure server is first disconnected disconnectFromServer(); @@ -345,7 +359,7 @@ class InvenTreeAPI { _connecting = true; - _connected = await _connect(context); + _connected = await _connect(); print("_connect() returned result: ${_connected}"); @@ -411,7 +425,7 @@ class InvenTreeAPI { // Perform a PATCH request - Future patch(String url, {Map body, int expectedStatusCode=200}) async { + Future patch(String url, {Map body = const {}, int expectedStatusCode=200}) async { var _url = makeApiUrl(url); var _body = Map(); @@ -433,7 +447,7 @@ class InvenTreeAPI { // Open a connection to the server HttpClientRequest request = await client.patchUrl(uri) .timeout(Duration(seconds: 10)) - .catchError((error) { + .catchError((error, stackTrace) { print("PATCH request return error"); print("URL: ${uri}"); print("Error: ${error.toString()}"); @@ -446,12 +460,14 @@ class InvenTreeAPI { error.toString(), ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, error.toString() ); + + sentryReportError(error, stackTrace); } return null; @@ -462,7 +478,7 @@ class InvenTreeAPI { return null; } - var data = json.encode(body); + var data = json.encode(_body); // Set headers request.headers.set('Accept', 'application/json'); @@ -474,7 +490,7 @@ class InvenTreeAPI { HttpClientResponse response = await request.close() .timeout(Duration(seconds: 30)) - .catchError((error) { + .catchError((error, stackTrace) { print("PATCH request returned error"); print("URL: ${_url}"); print("Error: ${error.toString()}"); @@ -487,29 +503,44 @@ class InvenTreeAPI { error.toString() ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, error.toString() ); + + sentryReportError(error, stackTrace); } return null; }); - if (response == null) { - print("null response from PATCH ${_url}"); - return null; - } + var responseData = await responseToJson(response); if (response.statusCode != expectedStatusCode) { showStatusCodeError(response.statusCode); + + print("PATCH to ${_url} returned status code ${response.statusCode}"); + print("Data:"); + print(responseData); + + // Server error + if (response.statusCode >= 500) { + sentryReportMessage( + "Server error on PATCH request", + context: { + "url": _url, + "statusCode": "${response.statusCode}", + "response": responseData.toString(), + "request": body.toString(), + } + ); + } + return null; } - var responseData = await responseToJson(response); - return responseData; } @@ -517,7 +548,8 @@ class InvenTreeAPI { * Upload a file to the given URL */ Future uploadFile(String url, File f, - {String name = "attachment", String method="POST", Map fields}) async { + {String name = "attachment", String method="POST", Map? fields}) async { + var _url = makeApiUrl(url); var request = http.MultipartRequest(method, Uri.parse(_url)); @@ -536,6 +568,21 @@ class InvenTreeAPI { var response = await request.send(); + if (response.statusCode >= 500) { + // Server error + if (response.statusCode >= 500) { + sentryReportMessage( + "Server error on file upload", + context: { + "url": _url, + "statusCode": "${response.statusCode}", + "response": response.toString(), + "request": request.fields.toString(), + } + ); + } + } + return response; } @@ -543,7 +590,8 @@ class InvenTreeAPI { * Perform a HTTP POST request * Returns a json object (or null if unsuccessful) */ - Future post(String url, {Map body, int expectedStatusCode=201}) async { + Future post(String url, {Map body = const {}, int expectedStatusCode=201}) async { + var _url = makeApiUrl(url); print("POST: ${_url} -> ${body.toString()}"); @@ -560,7 +608,7 @@ class InvenTreeAPI { // Open a connection to the server HttpClientRequest request = await client.postUrl(uri) .timeout(Duration(seconds: 10)) - .catchError((error) { + .catchError((error, stackTrace) { print("POST request returned error"); print("URL: ${uri}"); print("Error: ${error.toString()}"); @@ -573,21 +621,19 @@ class InvenTreeAPI { error.toString() ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, error.toString() ); + + sentryReportError(error, stackTrace); } return null; }); - if (request == null) { - return null; - } - var data = json.encode(body); // Set headers @@ -602,7 +648,7 @@ class InvenTreeAPI { HttpClientResponse response = await request.close() .timeout(Duration(seconds: 30)) - .catchError((error) { + .catchError((error, stackTrace) { print("POST request returned error"); print("URL: ${_url}"); print("Error: ${error.toString()}"); @@ -615,12 +661,14 @@ class InvenTreeAPI { error.toString() ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, error.toString() ); + + sentryReportError(error, stackTrace); } return null; @@ -631,13 +679,31 @@ class InvenTreeAPI { return null; } + var responseData = await responseToJson(response); + if (response.statusCode != expectedStatusCode) { showStatusCodeError(response.statusCode); + + print("POST to ${_url} returned status code ${response.statusCode}"); + print("Data:"); + print(responseData); + + // Server error + if (response.statusCode >= 500) { + sentryReportMessage( + "Server error on POST request", + context: { + "url": _url, + "statusCode": "${response.statusCode}", + "response": responseData.toString(), + "request": body.toString(), + } + ); + } + return null; } - var responseData = await responseToJson(response); - return responseData; } @@ -674,13 +740,13 @@ class InvenTreeAPI { * and return the Response object * (or null if the request fails) */ - Future getResponse(String url, {Map params}) async { + Future getResponse(String url, {Map params = const {}}) async { var _url = makeApiUrl(url); print("GET: ${_url}"); // If query parameters are supplied, form a query string - if (params != null && params.isNotEmpty) { + if (params.isNotEmpty) { String query = '?'; params.forEach((K, V) => query += K + '=' + V + '&'); @@ -695,7 +761,12 @@ class InvenTreeAPI { var client = createClient(true); - final uri = Uri.parse(_url); + Uri? uri = Uri.tryParse(_url); + + if (uri == null) { + showServerError(L10().invalidHost, L10().invalidHostDetails); + return null; + } // Check for invalid host if (uri.host.isEmpty) { @@ -703,32 +774,60 @@ class InvenTreeAPI { return null; } - // Open a connection - HttpClientRequest request = await client.getUrl(uri) - .timeout(Duration(seconds: 10)) - .catchError((error) { - print("GET request returned error"); - print("URL: ${uri}"); - print("Error: ${error.toString()}"); + HttpClientRequest? request; - var ctx = OneContext().context; + try { + // Open a connection + request = await client.getUrl(uri) + .timeout(Duration(seconds: 10)) + .catchError((error, stackTrace) { + print("GET request returned error"); + print("URL: ${uri}"); + print("Error: ${error.toString()}"); - if (error is SocketException) { + var ctx = OneContext().context; + + if (error is SocketException) { + showServerError( + L10().connectionRefused, + error.toString() + ); + } else if (error is TimeoutException) { + showTimeoutError(); + } else { + showServerError( + L10().serverError, + error.toString() + ); + + sentryReportError(error, stackTrace); + } + + return null; + }); + } catch (error, stackTrace) { + if (error is FormatException) { + showServerError( + L10().invalidHost, + L10().invalidHostDetails) + ; + } else if (error is SocketException) { showServerError( L10().connectionRefused, error.toString() ); - } else if (error is TimeoutException) { - showTimeoutError(ctx); } else { showServerError( - L10().serverError, - error.toString() + L10().serverError, + error.toString() ); + + // Report to sentry + sentryReportError(error, stackTrace); } return null; - }); + } if (request == null) { return null; @@ -740,7 +839,7 @@ class InvenTreeAPI { HttpClientResponse response = await request.close() .timeout(Duration(seconds: 10)) - .catchError((error) { + .catchError((error, stackTrace) { print("GET request returned error"); print("URL: ${_url}"); print("Error: ${error.toString()}"); @@ -753,12 +852,14 @@ class InvenTreeAPI { error.toString() ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, error.toString() ); + + sentryReportError(error, stackTrace); } return null; @@ -769,10 +870,6 @@ class InvenTreeAPI { dynamic responseToJson(HttpClientResponse response) async { - if (response == null) { - return null; - } - String body = await response.transform(utf8.decoder).join(); try { @@ -797,7 +894,7 @@ class InvenTreeAPI { * Perform a HTTP GET request * Returns a json object (or null if did not complete) */ - Future get(String url, {Map params, int expectedStatusCode=200}) async { + Future get(String url, {Map params = const {}, int expectedStatusCode=200}) async { var response = await getResponse(url, params: params); @@ -807,15 +904,29 @@ class InvenTreeAPI { return null; } + var responseData = await responseToJson(response); + // Check the status code of the response if (response.statusCode != expectedStatusCode) { showStatusCodeError(response.statusCode); + + // Server error + if (response.statusCode >= 500) { + sentryReportMessage( + "Server error on GET request", + context: { + "url": url, + "statusCode": "${response.statusCode}", + "response": responseData.toString(), + "params": params.toString(), + } + ); + } + return null; } - var data = await responseToJson(response); - - return data; + return responseData; } Map defaultHeaders() { @@ -836,7 +947,7 @@ class InvenTreeAPI { if (_token.isNotEmpty) { return "Token $_token"; } else if (profile != null) { - return "Basic " + base64Encode(utf8.encode('${profile.username}:${profile.password}')); + return "Basic " + base64Encode(utf8.encode('${profile?.username}:${profile?.password}')); } else { return ""; } @@ -846,13 +957,11 @@ class InvenTreeAPI { static String get staticThumb => "/static/img/blank_image.thumbnail.png"; - - /** * Load image from the InvenTree server, * or from local cache (if it has been cached!) */ - CachedNetworkImage getImage(String imageUrl, {double height, double width}) { + CachedNetworkImage getImage(String imageUrl, {double? height, double? width}) { if (imageUrl.isEmpty) { imageUrl = staticImage; } diff --git a/lib/barcode.dart b/lib/barcode.dart index 2f7a79ea..e0dbbf4b 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -35,8 +35,8 @@ class BarcodeHandler { BarcodeHandler(); - QRViewController _controller; - BuildContext _context; + QRViewController? _controller; + BuildContext? _context; void successTone() async { @@ -58,12 +58,12 @@ class BarcodeHandler { } } - Future onBarcodeMatched(Map data) { + Future onBarcodeMatched(Map data) async { // Called when the server "matches" a barcode // Override this function } - Future onBarcodeUnknown(Map data) { + Future onBarcodeUnknown(Map data) async { // Called when the server does not know about a barcode // Override this function @@ -76,17 +76,17 @@ class BarcodeHandler { ); } - Future onBarcodeUnhandled(Map data) { + Future onBarcodeUnhandled(Map data) async { failureTone(); // Called when the server returns an unhandled response showServerError(L10().responseUnknown, data.toString()); - _controller.resumeCamera(); + _controller?.resumeCamera(); } - Future processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) async { + Future processBarcode(BuildContext? context, QRViewController? _controller, String barcode, {String url = "barcode/"}) async { this._context = context; this._controller = _controller; @@ -105,13 +105,13 @@ class BarcodeHandler { } if (data.containsKey('error')) { - _controller.resumeCamera(); + _controller?.resumeCamera(); onBarcodeUnknown(data); } else if (data.containsKey('success')) { - _controller.resumeCamera(); + _controller?.resumeCamera(); onBarcodeMatched(data); } else { - _controller.resumeCamera(); + _controller?.resumeCamera(); onBarcodeUnhandled(data); } } @@ -128,7 +128,7 @@ class BarcodeScanHandler extends BarcodeHandler { String getOverlayText(BuildContext context) => L10().barcodeScanGeneral; @override - Future onBarcodeUnknown(Map data) { + Future onBarcodeUnknown(Map data) async { failureTone(); @@ -140,8 +140,9 @@ class BarcodeScanHandler extends BarcodeHandler { } @override - Future onBarcodeMatched(Map data) { - int pk; + Future onBarcodeMatched(Map data) async { + + int pk = -1; print("Handle barcode:"); print(data); @@ -149,16 +150,21 @@ class BarcodeScanHandler extends BarcodeHandler { // A stocklocation has been passed? if (data.containsKey('stocklocation')) { - pk = data['stocklocation']['pk'] as int ?? null; + pk = (data['stocklocation']?['pk'] ?? -1) as int; - if (pk != null) { + if (pk > 0) { successTone(); - InvenTreeStockLocation().get(_context, pk).then((var loc) { + InvenTreeStockLocation().get(pk).then((var loc) { if (loc is InvenTreeStockLocation) { - Navigator.of(_context).pop(); - Navigator.push(_context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + + var _ctx = _context; + + if (_ctx != null) { + Navigator.of(_ctx).pop(); + Navigator.push(_ctx, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + } } }); } else { @@ -173,15 +179,24 @@ class BarcodeScanHandler extends BarcodeHandler { } else if (data.containsKey('stockitem')) { - pk = data['stockitem']['pk'] as int ?? null; + pk = (data['stockitem']?['pk'] ?? -1) as int; - if (pk != null) { + if (pk > 0) { successTone(); - InvenTreeStockItem().get(_context, pk).then((var item) { - Navigator.of(_context).pop(); - Navigator.push(_context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); + InvenTreeStockItem().get(pk).then((var item) { + + var _ctx = _context; + + if (_ctx != null) { + // Dispose of the barcode scanner + Navigator.of(_ctx).pop(); + + if (item is InvenTreeStockItem) { + Navigator.push(_ctx, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); + } + } }); } else { @@ -194,15 +209,24 @@ class BarcodeScanHandler extends BarcodeHandler { } } else if (data.containsKey('part')) { - pk = data['part']['pk'] as int ?? null; + pk = (data['part']?['pk'] ?? -1) as int; - if (pk != null) { + if (pk > 0) { successTone(); - InvenTreePart().get(_context, pk).then((var part) { - Navigator.of(_context).pop(); - Navigator.push(_context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + InvenTreePart().get(pk).then((var part) { + + var _ctx = _context; + + if (_ctx != null) { + // Dismiss the barcode scanner + Navigator.of(_ctx).pop(); + + if (part is InvenTreePart) { + Navigator.push(_ctx, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + } + } }); } else { @@ -221,8 +245,10 @@ class BarcodeScanHandler extends BarcodeHandler { L10().barcodeUnknown, success: false, onAction: () { - showDialog( - context: _context, + + var _ctx = OneContext().context; + + OneContext().showDialog( builder: (BuildContext context) => SimpleDialog( title: Text(L10().unknownResponse), children: [ @@ -257,7 +283,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { String getOverlayText(BuildContext context) => L10().barcodeScanAssign; @override - Future onBarcodeMatched(Map data) { + Future onBarcodeMatched(Map data) async { failureTone(); @@ -270,7 +296,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { } @override - Future onBarcodeUnknown(Map data) { + Future onBarcodeUnknown(Map data) async { // If the barcode is unknown, we *can* assign it to the stock item! if (!data.containsKey("hash")) { @@ -282,7 +308,6 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { // Send the 'hash' code as the UID for the stock item item.update( - _context, values: { "uid": data['hash'], } @@ -292,8 +317,13 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { failureTone(); // Close the barcode scanner - _controller.dispose(); - Navigator.of(_context).pop(); + _controller?.dispose(); + + var _ctx = (_context); + + if (_ctx != null) { + Navigator.of(_ctx).pop(); + } showSnackIcon( L10().barcodeAssigned, @@ -350,8 +380,13 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler { successTone(); // Close the scanner - _controller.dispose(); - Navigator.of(_context).pop(); + _controller?.dispose(); + + var _ctx = _context; + + if (_ctx != null) { + Navigator.of(_ctx).pop(); + } showSnackIcon( L10().barcodeScanIntoLocationSuccess, @@ -403,7 +438,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler { int item_id = data['stockitem']['pk'] as int; - final InvenTreeStockItem item = await InvenTreeStockItem().get(_context, item_id); + final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem; if (item == null) { @@ -459,7 +494,7 @@ class InvenTreeQRView extends StatefulWidget { final BarcodeHandler _handler; - InvenTreeQRView(this._handler, {Key key}) : super(key: key); + InvenTreeQRView(this._handler, {Key? key}) : super(key: key); @override State createState() => _QRViewState(_handler); @@ -468,11 +503,11 @@ class InvenTreeQRView extends StatefulWidget { class _QRViewState extends State { - QRViewController _controller; + QRViewController? _controller; final BarcodeHandler _handler; - BuildContext context; + BuildContext? _context; // 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. @@ -480,9 +515,9 @@ class _QRViewState extends State { void reassemble() { super.reassemble(); if (Platform.isAndroid) { - _controller.pauseCamera(); + _controller?.pauseCamera(); } else if (Platform.isIOS) { - _controller.resumeCamera(); + _controller?.resumeCamera(); } } @@ -494,7 +529,7 @@ class _QRViewState extends State { _controller = controller; controller.scannedDataStream.listen((barcode) { _controller?.pauseCamera(); - _handler.processBarcode(context, _controller, barcode.code); + _handler.processBarcode(_context, _controller, barcode.code); }); } @@ -508,7 +543,7 @@ class _QRViewState extends State { Widget build(BuildContext context) { // Save the context for later on! - this.context = context; + this._context = context; return Scaffold( body: Stack( diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 0d269c4d..678739a6 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -33,7 +33,7 @@ class InvenTreePageResponse { int get count => _count; - int get length => results?.length ?? 0; + int get length => results.length; List results = []; } @@ -91,16 +91,16 @@ class InvenTreeModel { } - int get pk => jsondata['pk'] ?? -1; + int get pk => (jsondata['pk'] ?? -1) as int; // Some common accessors String get name => jsondata['name'] ?? ''; String get description => jsondata['description'] ?? ''; - String get notes => jsondata['notes'] as String ?? ''; + String get notes => jsondata['notes'] ?? ''; - int get parentId => jsondata['parent'] as int ?? -1; + int get parentId => (jsondata['parent'] ?? -1) as int; // Legacy API provided external link as "URL", while newer API uses "link" String get link => jsondata['link'] ?? jsondata['URL'] ?? ''; @@ -127,7 +127,7 @@ class InvenTreeModel { } } - String get keywords => jsondata['keywords'] as String ?? ''; + String get keywords => jsondata['keywords'] ?? ''; // Create a new object from JSON data (not a constructor!) InvenTreeModel createFromJson(Map json) { @@ -142,15 +142,11 @@ class InvenTreeModel { // Search this Model type in the database - Future> search(BuildContext context, String searchTerm, {Map filters}) async { - - if (filters == null) { - filters = {}; - } + Future> search(BuildContext context, String searchTerm, {Map filters = const {}}) async { filters["search"] = searchTerm; - final results = list(context, filters: filters); + final results = list(filters: filters); return results; @@ -164,7 +160,7 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - Future reload(BuildContext context) async { + Future reload() async { var response = await api.get(url, params: defaultGetFilters()); @@ -178,7 +174,7 @@ class InvenTreeModel { } // POST data to update the model - Future update(BuildContext context, {Map values}) async { + Future update({Map values = const {}}) async { var addr = path.join(URL, pk.toString()); @@ -198,29 +194,27 @@ class InvenTreeModel { } // Return the detail view for the associated pk - Future get(BuildContext context, int pk, {Map filters}) async { + Future get(int pk, {Map filters = const {}}) async { // TODO - Add "timeout" // TODO - Add error catching - var addr = path.join(URL, pk.toString()); + var url = path.join(URL, pk.toString()); - if (!addr.endsWith("/")) { - addr += "/"; + if (!url.endsWith("/")) { + url += "/"; } var params = defaultGetFilters(); - if (filters != null) { - // Override any default values - for (String key in filters.keys) { - params[key] = filters[key]; - } + // Override any default values + for (String key in filters.keys) { + params[key] = filters[key] ?? ''; } - print("GET: $addr ${params.toString()}"); + print("GET: $url ${params.toString()}"); - var response = await api.get(addr, params: params); + var response = await api.get(url, params: params); if (response == null) { return null; @@ -229,7 +223,7 @@ class InvenTreeModel { return createFromJson(response); } - Future create(BuildContext context, Map data) async { + Future create(Map data) async { print("CREATE: ${URL} ${data.toString()}"); @@ -241,8 +235,6 @@ class InvenTreeModel { data.remove('id'); } - InvenTreeModel _model; - var response = await api.post(URL, body: data); if (response == null) { @@ -252,13 +244,11 @@ class InvenTreeModel { return createFromJson(response); } - Future listPaginated(int limit, int offset, {Map filters}) async { + Future listPaginated(int limit, int offset, {Map filters = const {}}) async { var params = defaultListFilters(); - if (filters != null) { - for (String key in filters.keys) { - params[key] = filters[key]; - } + for (String key in filters.keys) { + params[key] = filters[key] ?? ''; } params["limit"] = "${limit}"; @@ -285,25 +275,17 @@ class InvenTreeModel { return page; } else { - // Inavlid response - print("Invalid!"); return null; } } // Return list of objects from the database, with optional filters - Future> list(BuildContext context, {Map filters}) async { - - if (filters == null) { - filters = {}; - } + Future> list({Map filters = const {}}) async { var params = defaultListFilters(); - if (filters != null) { - for (String key in filters.keys) { - params[key] = filters[key]; - } + for (String key in filters.keys) { + params[key] = filters[key] ?? ''; } print("LIST: $URL ${params.toString()}"); @@ -311,7 +293,7 @@ class InvenTreeModel { var response = await api.get(URL, params: params); // A list of "InvenTreeModel" items - List results = new List(); + List results = []; if (response == null) { return results; diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 82c44a8b..4cc4b127 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -100,10 +100,11 @@ class InvenTreePartTestTemplate extends InvenTreeModel { } bool passFailStatus() { + var result = latestResult(); if (result == null) { - return null; + return false; } return result.result; @@ -113,7 +114,7 @@ class InvenTreePartTestTemplate extends InvenTreeModel { List results = []; // Return the most recent test result recorded against this template - InvenTreeStockItemTestResult latestResult() { + InvenTreeStockItemTestResult? latestResult() { if (results.isEmpty) { return null; } @@ -143,12 +144,12 @@ class InvenTreePart extends InvenTreeModel { @override Map defaultGetFilters() { return { - "category_detail": "1", // Include category detail information + "category_detail": "true", // Include category detail information }; } // Cached list of stock items - List stockItems = List(); + List stockItems = []; int get stockItemCount => stockItems.length; @@ -156,7 +157,6 @@ class InvenTreePart extends InvenTreeModel { Future getStockItems(BuildContext context, {bool showDialog=false}) async { await InvenTreeStockItem().list( - context, filters: { "part": "${pk}", "in_stock": "true", @@ -172,18 +172,17 @@ class InvenTreePart extends InvenTreeModel { }); } - int get supplier_count => jsondata['suppliers'] as int ?? 0; + int get supplier_count => (jsondata['suppliers'] ?? 0) as int; // Cached list of test templates - List testingTemplates = List(); + List testingTemplates = []; int get testTemplateCount => testingTemplates.length; // Request test templates from the serve - Future getTestTemplates(BuildContext context, {bool showDialog=false}) async { + Future getTestTemplates({bool showDialog=false}) async { InvenTreePartTestTemplate().list( - context, filters: { "part": "${pk}", }, @@ -200,10 +199,10 @@ class InvenTreePart extends InvenTreeModel { } // Get the number of stock on order for this Part - double get onOrder => double.tryParse(jsondata['ordering'].toString() ?? '0'); + double get onOrder => double.tryParse(jsondata['ordering'].toString()) ?? 0; // Get the stock count for this Part - double get inStock => double.tryParse(jsondata['in_stock'].toString() ?? '0'); + double get inStock => double.tryParse(jsondata['in_stock'].toString()) ?? 0; String get inStockString { @@ -215,51 +214,55 @@ class InvenTreePart extends InvenTreeModel { } // Get the number of units being build for this Part - double get building => double.tryParse(jsondata['building'].toString() ?? '0'); + double get building => double.tryParse(jsondata['building'].toString()) ?? 0; // Get the number of BOM items in this Part (if it is an assembly) - int get bomItemCount => jsondata['bom_items'] as int ?? 0; + int get bomItemCount => (jsondata['bom_items'] ?? 0) as int; // Get the number of BOMs this Part is used in (if it is a component) - int get usedInCount => jsondata['used_in'] as int ?? 0; + int get usedInCount => (jsondata['used_in'] ?? 0) as int; - bool get isAssembly => jsondata['assembly'] ?? false; + bool get isAssembly => (jsondata['assembly'] ?? false) as bool; - bool get isComponent => jsondata['component'] ?? false; + bool get isComponent => (jsondata['component'] ?? false) as bool; - bool get isPurchaseable => jsondata['purchaseable'] ?? false; + bool get isPurchaseable => (jsondata['purchaseable'] ?? false) as bool; - bool get isSalable => jsondata['salable'] ?? false; + bool get isSalable => (jsondata['salable'] ?? false) as bool; - bool get isActive => jsondata['active'] ?? false; + bool get isActive => (jsondata['active'] ?? false) as bool; - bool get isVirtual => jsondata['virtual'] ?? false; + bool get isVirtual => (jsondata['virtual'] ?? false) as bool; - bool get isTrackable => jsondata['trackable'] ?? false; + bool get isTrackable => (jsondata['trackable'] ?? false) as bool; // Get the IPN (internal part number) for the Part instance - String get IPN => jsondata['IPN'] as String ?? ''; + String get IPN => jsondata['IPN'] ?? ''; // Get the revision string for the Part instance - String get revision => jsondata['revision'] as String ?? ''; + String get revision => jsondata['revision'] ?? ''; // Get the category ID for the Part instance (or 'null' if does not exist) - int get categoryId => jsondata['category'] as int ?? null; + int get categoryId => (jsondata['category'] ?? -1) as int; // Get the category name for the Part instance String get categoryName { - if (categoryId == null) return ''; + // Inavlid category ID + if (categoryId <= 0) return ''; + if (!jsondata.containsKey('category_detail')) return ''; - return jsondata['category_detail']['name'] as String ?? ''; + return jsondata['category_detail']?['name'] ?? ''; } // Get the category description for the Part instance String get categoryDescription { - if (categoryId == null) return ''; + // Invalid category ID + if (categoryId <= 0) return ''; + if (!jsondata.containsKey('category_detail')) return ''; - return jsondata['category_detail']['description'] as String ?? ''; + return jsondata['category_detail']?['description'] ?? ''; } // Get the image URL for the Part instance String get _image => jsondata['image'] ?? ''; @@ -274,7 +277,7 @@ class InvenTreePart extends InvenTreeModel { if (fn.isNotEmpty) return fn; - List elements = List(); + List elements = []; if (IPN.isNotEmpty) elements.add(IPN); @@ -324,7 +327,7 @@ class InvenTreePart extends InvenTreeModel { } // Return the "starred" status of this part - bool get starred => jsondata['starred'] as bool ?? false; + bool get starred => (jsondata['starred'] ?? false) as bool; InvenTreePart() : super(); diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 65a997a0..da729e0a 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -84,6 +84,40 @@ bool isInDebugMode() { return inDebugMode; } +Future sentryReportMessage(String message, {Map? context}) async { + + final server_info = getServerInfo(); + final app_info = await getAppInfo(); + final device_info = await getDeviceInfo(); + + print("Sending user message to Sentry: ${message}"); + + if (isInDebugMode()) { + + print('----- In dev mode. Not sending message to Sentry.io -----'); + return true; + } + + Sentry.configureScope((scope) { + scope.setExtra("server", server_info); + scope.setExtra("app", app_info); + scope.setExtra("device", device_info); + + if (context != null) { + scope.setExtra("context", context); + } + }); + + final sentryId = await Sentry.captureMessage(message).catchError((error) { + print("Error uploading sentry messages..."); + print(error); + return null; + }); + + return sentryId != null; +} + + Future sentryReportError(dynamic error, dynamic stackTrace) async { print('----- Sentry Intercepted error: $error -----'); @@ -115,27 +149,3 @@ Future sentryReportError(dynamic error, dynamic stackTrace) async { print("Uploaded information to Sentry.io : ${response.toString()}"); }); } - - -Future sentryReportMessage(String message) async { - - final server_info = getServerInfo(); - final app_info = await getAppInfo(); - final device_info = await getDeviceInfo(); - - print("Sending user message to Sentry"); - - Sentry.configureScope((scope) { - scope.setExtra("server", server_info); - scope.setExtra("app", app_info); - scope.setExtra("device", device_info); - }); - - final sentryId = await Sentry.captureMessage(message).catchError((error) { - print("Error uploading sentry messages..."); - print(error); - return null; - }); - - return sentryId != null; -} \ No newline at end of file diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index ff8189cd..827553b6 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -138,14 +138,13 @@ class InvenTreeStockItem extends InvenTreeModel { // TODO } - List testTemplates = List(); + List testTemplates = []; int get testTemplateCount => testTemplates.length; // Get all the test templates associated with this StockItem - Future getTestTemplates(BuildContext context, {bool showDialog=false}) async { + Future getTestTemplates({bool showDialog=false}) async { await InvenTreePartTestTemplate().list( - context, filters: { "part": "${partId}", }, @@ -160,14 +159,13 @@ class InvenTreeStockItem extends InvenTreeModel { }); } - List testResults = List(); + List testResults = []; int get testResultCount => testResults.length; - Future getTestResults(BuildContext context) async { + Future getTestResults() async { await InvenTreeStockItemTestResult().list( - context, filters: { "stock_item": "${pk}", "user_detail": "true", @@ -183,7 +181,7 @@ class InvenTreeStockItem extends InvenTreeModel { }); } - Future uploadTestResult(BuildContext context, String testName, bool result, {String value, String notes, File attachment}) async { + Future uploadTestResult(BuildContext context, String testName, bool result, {String? value, String? notes, File? attachment}) async { Map data = { "stock_item": pk.toString(), @@ -204,7 +202,7 @@ class InvenTreeStockItem extends InvenTreeModel { * TODO: Is there a nice way to refactor this one? */ if (attachment == null) { - var _result = await InvenTreeStockItemTestResult().create(context, data); + var _result = await InvenTreeStockItemTestResult().create(data); return (_result != null) && (_result is InvenTreeStockItemTestResult); } else { @@ -224,12 +222,12 @@ class InvenTreeStockItem extends InvenTreeModel { int get partId => jsondata['part'] ?? -1; - int get trackingItemCount => jsondata['tracking_items'] as int ?? 0; + int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int; // Date of last update String get updated => jsondata["updated"] ?? ""; - DateTime get stocktakeDate { + DateTime? get stocktakeDate { if (jsondata.containsKey("stocktake_date")) { if (jsondata["stocktake_date"] == null) { return null; @@ -292,15 +290,18 @@ class InvenTreeStockItem extends InvenTreeModel { */ String get partThumbnail { - String thumb; + String thumb = ""; - if (jsondata.containsKey('part_detail')) { - thumb = jsondata['part_detail']['thumbnail'] as String ?? ''; + thumb = jsondata['part_detail']?['thumbnail'] ?? ''; + + // Use 'image' as a backup + if (thumb.isEmpty) { + thumb = jsondata['part_detail']?['image'] ?? ''; } // Try a different approach if (thumb.isEmpty) { - jsondata['part__thumbnail'] as String ?? ''; + thumb = jsondata['part__thumbnail'] ?? ''; } // Still no thumbnail? Use the 'no image' image @@ -309,7 +310,7 @@ class InvenTreeStockItem extends InvenTreeModel { return thumb; } - int get supplierPartId => jsondata['supplier_part'] as int ?? -1; + int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int; String get supplierImage { String thumb = ''; @@ -341,9 +342,9 @@ class InvenTreeStockItem extends InvenTreeModel { return sku; } - String get serialNumber => jsondata['serial'] as String ?? null; + String get serialNumber => jsondata['serial'] ?? ""; - double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0'); + double get quantity => double.tryParse(jsondata['quantity'].toString()) ?? 0; String get quantityString { @@ -354,9 +355,9 @@ class InvenTreeStockItem extends InvenTreeModel { } } - int get locationId => jsondata['location'] as int ?? -1; + int get locationId => (jsondata['location'] ?? -1) as int; - bool isSerialized() => serialNumber != null && quantity.toInt() == 1; + bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1; String serialOrQuantityDisplay() { if (isSerialized()) { @@ -397,10 +398,10 @@ class InvenTreeStockItem extends InvenTreeModel { String get displayQuantity { // Display either quantity or serial number! - if (serialNumber != null) { + if (serialNumber.isNotEmpty) { return "SN: $serialNumber"; } else { - return quantity.toString().trim(); + return quantityString; } } @@ -420,7 +421,7 @@ class InvenTreeStockItem extends InvenTreeModel { * - Remove * - Count */ - Future adjustStock(BuildContext context, String endpoint, double q, {String notes}) async { + Future adjustStock(BuildContext context, String endpoint, double q, {String? notes}) async { // Serialized stock cannot be adjusted if (isSerialized()) { @@ -456,30 +457,29 @@ class InvenTreeStockItem extends InvenTreeModel { return true; } - Future countStock(BuildContext context, double q, {String notes}) async { + Future countStock(BuildContext context, double q, {String? notes}) async { - final bool result = await adjustStock(context, "/stock/count", q, notes: notes); + final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); return result; } - Future addStock(BuildContext context, double q, {String notes}) async { + Future addStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); return result; } - Future removeStock(BuildContext context, double q, {String notes}) async { + Future removeStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); return result; } - Future transferStock(int location, {double quantity, String notes}) async { - if (quantity == null) {} else - if ((quantity < 0) || (quantity > this.quantity)) { + Future transferStock(int location, {double? quantity, String? notes}) async { + if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) { quantity = this.quantity; } diff --git a/lib/l10.dart b/lib/l10.dart index 5225a547..04b70f40 100644 --- a/lib/l10.dart +++ b/lib/l10.dart @@ -1,9 +1,22 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:one_context/one_context.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; +import 'package:one_context/one_context.dart'; +import 'package:flutter/material.dart'; // Shortcut function to reduce boilerplate! I18N L10() { - return I18N.of(OneContext().context); + BuildContext? _ctx = OneContext().context; + + if (_ctx != null) { + I18N? i18n = I18N.of(_ctx); + + if (i18n != null) { + return i18n; + } + } + + // Fallback for "null" context + return I18NEn(); } \ No newline at end of file diff --git a/lib/l10n b/lib/l10n index 05a5cbf6..9cc07cdb 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 05a5cbf63b4b5479162905def9fdadf21041212e +Subproject commit 9cc07cdb0ec0012abcec827fc776d7c5473bb75e diff --git a/lib/main.dart b/lib/main.dart index 3d8fbb56..a84abe83 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,7 +52,7 @@ class InvenTreeApp extends StatelessWidget { return MaterialApp( builder: OneContext().builder, navigatorKey: OneContext().key, - onGenerateTitle: (BuildContext context) => I18N.of(context).appTitle, + onGenerateTitle: (BuildContext context) => I18N.of(context)!.appTitle, theme: ThemeData( primarySwatch: Colors.lightBlue, secondaryHeaderColor: Colors.blueGrey, diff --git a/lib/preferences.dart b/lib/preferences.dart index 3be8e8c7..27a416f0 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -15,15 +15,19 @@ class InvenTreePreferencesDB { InvenTreePreferencesDB._(); - Completer _dbOpenCompleter; + Completer _dbOpenCompleter = Completer(); + + bool isOpen = false; Future get database async { - // If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened - if (_dbOpenCompleter == null) { - _dbOpenCompleter = Completer(); + + if (!isOpen) { // Calling _openDatabase will also complete the completer with database instance _openDatabase(); + + isOpen = true; } + // If the database is already opened, awaiting the future will happen instantly. // Otherwise, awaiting the returned future will take some time - until complete() is called // on the Completer in _openDatabase() below. diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 13105250..8dbaf324 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -26,7 +26,7 @@ class _InvenTreeLoginSettingsState extends State { final GlobalKey _addProfileKey = new GlobalKey(); - List profiles; + List profiles = []; _InvenTreeLoginSettingsState() { _reload(); @@ -40,14 +40,14 @@ class _InvenTreeLoginSettingsState extends State { }); } - void _editProfile(BuildContext context, {UserProfile userProfile, bool createNew = false}) { + void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) { var _name; var _server; var _username; var _password; - UserProfile profile; + UserProfile? profile; if (userProfile != null) { profile = userProfile; @@ -69,10 +69,10 @@ class _InvenTreeLoginSettingsState extends State { _addProfile(profile); } else { - profile.name = _name; - profile.server = _server; - profile.username = _username; - profile.password = _password; + profile?.name = _name; + profile?.server = _server; + profile?.username = _username; + profile?.password = _password; _updateProfile(profile); @@ -82,28 +82,28 @@ class _InvenTreeLoginSettingsState extends State { StringField( label: L10().name, hint: "Enter profile name", - initial: createNew ? '' : profile.name, + initial: createNew ? '' : profile?.name ?? '', onSaved: (value) => _name = value, validator: _validateProfileName, ), StringField( label: L10().server, hint: "http[s]://:", - initial: createNew ? '' : profile.server, + initial: createNew ? '' : profile?.server ?? '', validator: _validateServer, onSaved: (value) => _server = value, ), StringField( label: L10().username, hint: L10().enterPassword, - initial: createNew ? '' : profile.username, + initial: createNew ? '' : profile?.username ?? '', onSaved: (value) => _username = value, validator: _validateUsername, ), StringField( label: L10().password, hint: L10().enterUsername, - initial: createNew ? '' : profile.password, + initial: createNew ? '' : profile?.password ?? '', onSaved: (value) => _password = value, validator: _validatePassword, ) @@ -111,7 +111,7 @@ class _InvenTreeLoginSettingsState extends State { ); } - String _validateProfileName(String value) { + String? _validateProfileName(String value) { if (value.isEmpty) { return 'Profile name cannot be empty'; @@ -122,14 +122,14 @@ class _InvenTreeLoginSettingsState extends State { return null; } - String _validateServer(String value) { + String? _validateServer(String value) { if (value.isEmpty) { - return 'Server cannot be empty'; + return L10().serverEmpty; } if (!value.startsWith("http:") && !value.startsWith("https:")) { - return 'Server must start with http[s]'; + return L10().serverStart; } // TODO: URL validator @@ -137,17 +137,17 @@ class _InvenTreeLoginSettingsState extends State { return null; } - String _validateUsername(String value) { + String? _validateUsername(String value) { if (value.isEmpty) { - return 'Username cannot be empty'; + return L10().usernameEmpty; } return null; } - String _validatePassword(String value) { + String? _validatePassword(String value) { if (value.isEmpty) { - return 'Password cannot be empty'; + return L10().passwordEmpty; } return null; @@ -158,12 +158,18 @@ class _InvenTreeLoginSettingsState extends State { // Disconnect InvenTree InvenTreeAPI().disconnectFromServer(); - await UserProfileDBManager().selectProfile(profile.key); + var key = profile.key; + + if (key == null) { + return; + } + + await UserProfileDBManager().selectProfile(key); _reload(); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { + InvenTreeAPI().connectToServer().then((result) { _reload(); }); @@ -176,21 +182,25 @@ class _InvenTreeLoginSettingsState extends State { _reload(); - if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { + if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? '')) { InvenTreeAPI().disconnectFromServer(); } } - void _updateProfile(UserProfile profile) async { + void _updateProfile(UserProfile? profile) async { + + if (profile == null) { + return; + } await UserProfileDBManager().updateProfile(profile); _reload(); - if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { + if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) { // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { + InvenTreeAPI().connectToServer().then((result) { _reload(); }); } @@ -203,13 +213,13 @@ class _InvenTreeLoginSettingsState extends State { _reload(); } - Widget _getProfileIcon(UserProfile profile) { + Widget? _getProfileIcon(UserProfile profile) { // Not selected? No icon for you! - if (profile == null || !profile.selected) return null; + if (!profile.selected) return null; // Selected, but (for some reason) not the same as the API... - if (InvenTreeAPI().profile == null || InvenTreeAPI().profile.key != profile.key) { + if ((InvenTreeAPI().profile?.key ?? '') != profile.key) { return FaIcon( FontAwesomeIcons.questionCircle, color: Color.fromRGBO(250, 150, 50, 1) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index cef17f31..efbd81ca 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -9,32 +9,32 @@ class UserProfile { UserProfile({ this.key, - this.name, - this.server, - this.username, - this.password, - this.selected, + this.name = "", + this.server = "", + this.username = "", + this.password = "", + this.selected = false, }); // ID of the profile - int key; + int? key; // Name of the user profile - String name; + String name = ""; // Base address of the InvenTree server - String server; + String server = ""; // Username - String username; + String username = ""; // Password - String password; + String password = ""; bool selected = false; // User ID (will be provided by the server on log-in) - int user_id; + int user_id = -1; factory UserProfile.fromJson(int key, Map json, bool isSelected) => UserProfile( key: key, @@ -122,7 +122,7 @@ class UserProfileDBManager { print("Deleted user profile <${profile.key}> - '${profile.name}'"); } - Future getSelectedProfile() async { + Future getSelectedProfile() async { /* * Return the currently selected profile. * @@ -133,7 +133,7 @@ class UserProfileDBManager { final profiles = await store.find(await _db); - List profileList = new List(); + List profileList = []; for (int idx = 0; idx < profiles.length; idx++) { @@ -158,7 +158,7 @@ class UserProfileDBManager { final profiles = await store.find(await _db); - List profileList = new List(); + List profileList = []; for (int idx = 0; idx < profiles.length; idx++) { diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 4725cc30..3f4533d3 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -2,6 +2,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/app_settings.dart'; import 'package:InvenTree/inventree/part.dart'; +import 'package:InvenTree/inventree/sentry.dart'; import 'package:InvenTree/widget/progress.dart'; import 'package:InvenTree/l10.dart'; @@ -23,9 +24,9 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; class CategoryDisplayWidget extends StatefulWidget { - CategoryDisplayWidget(this.category, {Key key}) : super(key: key); + CategoryDisplayWidget(this.category, {Key? key}) : super(key: key); - final InvenTreePartCategory category; + final InvenTreePartCategory? category; @override _CategoryDisplayState createState() => _CategoryDisplayState(category); @@ -81,7 +82,7 @@ class _CategoryDisplayState extends RefreshableState { void _editCategory(Map values) async { - final bool result = await category.update(context, values: values); + final bool result = await category!.update(values: values); showSnackIcon( result ? "Category edited" : "Category editing failed", @@ -93,6 +94,11 @@ class _CategoryDisplayState extends RefreshableState { void _editCategoryDialog() { + // Cannot edit top-level category + if (category == null) { + return; + } + var _name; var _description; @@ -108,12 +114,12 @@ class _CategoryDisplayState extends RefreshableState { fields: [ StringField( label: L10().name, - initial: category.name, + initial: category?.name, onSaved: (value) => _name = value ), StringField( label: L10().description, - initial: category.description, + initial: category?.description, onSaved: (value) => _description = value ) ] @@ -123,9 +129,9 @@ class _CategoryDisplayState extends RefreshableState { _CategoryDisplayState(this.category) {} // The local InvenTreePartCategory object - final InvenTreePartCategory category; + final InvenTreePartCategory? category; - List _subcategories = List(); + List _subcategories = []; @override Future onBuild(BuildContext context) async { @@ -133,17 +139,17 @@ class _CategoryDisplayState extends RefreshableState { } @override - Future request(BuildContext context) async { + Future request() async { int pk = category?.pk ?? -1; // Update the category if (category != null) { - await category.reload(context); + await category!.reload(); } // Request a list of sub-categories under this one - await InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) { + await InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) { _subcategories.clear(); for (var cat in cats) { @@ -168,10 +174,10 @@ class _CategoryDisplayState extends RefreshableState { List children = [ ListTile( - title: Text("${category.name}", + title: Text("${category?.name}", style: TextStyle(fontWeight: FontWeight.bold) ), - subtitle: Text("${category.description}"), + subtitle: Text("${category?.description}"), ), ]; @@ -179,14 +185,14 @@ class _CategoryDisplayState extends RefreshableState { children.add( ListTile( title: Text(L10().parentCategory), - subtitle: Text("${category.parentpathstring}"), + subtitle: Text("${category?.parentpathstring}"), leading: FaIcon(FontAwesomeIcons.levelUpAlt), onTap: () { - if (category.parentId < 0) { + if (category == null || ((category?.parentId ?? 0) < 0)) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } else { // TODO - Refactor this code into the InvenTreePart class - InvenTreePartCategory().get(context, category.parentId).then((var cat) { + InvenTreePartCategory().get(category?.parentId ?? -1).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); } @@ -290,6 +296,8 @@ class _CategoryDisplayState extends RefreshableState { return ListView( children: actionTiles() ); + default: + return ListView(); } } } @@ -306,7 +314,7 @@ class SubcategoryList extends StatelessWidget { void _openCategory(BuildContext context, int pk) { // Attempt to load the sub-category. - InvenTreePartCategory().get(context, pk).then((var cat) { + InvenTreePartCategory().get(pk).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); @@ -348,7 +356,7 @@ class PaginatedPartList extends StatefulWidget { final Map filters; - Function onTotalChanged; + Function(int)? onTotalChanged; PaginatedPartList(this.filters, {this.onTotalChanged}); @@ -363,7 +371,7 @@ class _PaginatedPartListState extends State { String _searchTerm = ""; - Function onTotalChanged; + Function(int)? onTotalChanged; final Map filters; @@ -393,21 +401,21 @@ class _PaginatedPartListState extends State { Map params = filters; - params["search"] = _searchTerm ?? ""; + params["search"] = _searchTerm; final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", false); params["cascade"] = "${cascade}"; final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params); - int pageLength = page.length ?? 0; - int pageCount = page.count ?? 0; + int pageLength = page?.length ?? 0; + int pageCount = page?.count ?? 0; final isLastPage = pageLength < _pageSize; // Construct a list of part objects List parts = []; - if (page == null) { + if (page != null) { for (var result in page.results) { if (result is InvenTreePart) { parts.add(result); @@ -423,22 +431,24 @@ class _PaginatedPartListState extends State { } if (onTotalChanged != null) { - onTotalChanged(pageCount); + onTotalChanged!(pageCount); } setState(() { resultCount = pageCount; }); - } catch (error) { + } catch (error, stackTrace) { print("Error! - ${error.toString()}"); _pagingController.error = error; + + sentryReportError(error, stackTrace); } } void _openPart(BuildContext context, int pk) { // Attempt to load the part information - InvenTreePart().get(context, pk).then((var part) { + InvenTreePart().get(pk).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index 5aec037c..04134783 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -13,7 +13,7 @@ class CompanyDetailWidget extends StatefulWidget { final InvenTreeCompany company; - CompanyDetailWidget(this.company, {Key key}) : super(key: key); + CompanyDetailWidget(this.company, {Key? key}) : super(key: key); @override _CompanyDetailState createState() => _CompanyDetailState(company); @@ -31,8 +31,8 @@ class _CompanyDetailState extends RefreshableState { String getAppBarTitle(BuildContext context) => L10().company; @override - Future request(BuildContext context) async { - await company.reload(context); + Future request() async { + await company.reload(); } _CompanyDetailState(this.company) { @@ -42,7 +42,7 @@ class _CompanyDetailState extends RefreshableState { void _saveCompany(Map values) async { Navigator.of(context).pop(); - var response = await company.update(context, values: values); + var response = await company.update(values: values); refresh(); } @@ -66,8 +66,8 @@ class _CompanyDetailState extends RefreshableState { FlatButton( child: Text(L10().save), onPressed: () { - if (_editCompanyKey.currentState.validate()) { - _editCompanyKey.currentState.save(); + if (_editCompanyKey.currentState!.validate()) { + _editCompanyKey.currentState!.save(); _saveCompany({ "name": _name, @@ -107,7 +107,7 @@ class _CompanyDetailState extends RefreshableState { List _companyTiles() { - var tiles = List(); + List tiles = []; bool sep = false; diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index deada540..56fc76af 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -11,8 +11,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; abstract class CompanyListWidget extends StatefulWidget { - String title; - Map filters; + String title = ""; + Map filters = {}; @override _CompanyListState createState() => _CompanyListState(title, filters); @@ -39,9 +39,9 @@ class CustomerListWidget extends CompanyListWidget { class _CompanyListState extends RefreshableState { - var _companies = new List(); + List _companies = []; - var _filteredCompanies = new List(); + List _filteredCompanies = []; String _title = "Companies"; @@ -58,9 +58,9 @@ class _CompanyListState extends RefreshableState { } @override - Future request(BuildContext context) async { + Future request() async { - await InvenTreeCompany().list(context, filters: _filters).then((var companies) { + await InvenTreeCompany().list(filters: _filters).then((var companies) { _companies.clear(); @@ -96,8 +96,10 @@ class _CompanyListState extends RefreshableState { leading: InvenTreeAPI().getImage(company.image), onTap: () { if (company.pk > 0) { - InvenTreeCompany().get(context, company.pk).then((var c) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c))); + InvenTreeCompany().get(company.pk).then((var c) { + if (c != null && c is InvenTreeCompany) { + Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c))); + } }); } }, diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 6d636fc0..8cf13f6d 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -8,15 +8,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:InvenTree/l10.dart'; import 'package:one_context/one_context.dart'; -Future confirmationDialog(String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async { +Future confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async { - if (acceptText == null || acceptText.isEmpty) { - acceptText = L10().ok; - } - - if (rejectText == null || rejectText.isEmpty) { - rejectText = L10().cancel; - } + String _accept = acceptText ?? L10().ok; + String _reject = rejectText ?? L10().cancel; OneContext().showDialog( builder: (BuildContext context) { @@ -28,7 +23,7 @@ Future confirmationDialog(String title, String text, {String acceptText, S content: Text(text), actions: [ FlatButton( - child: Text(rejectText), + child: Text(_reject), onPressed: () { // Close this dialog Navigator.pop(context); @@ -39,7 +34,7 @@ Future confirmationDialog(String title, String text, {String acceptText, S } ), FlatButton( - child: Text(acceptText), + child: Text(_accept), onPressed: () { // Close this dialog Navigator.pop(context); @@ -56,17 +51,14 @@ Future confirmationDialog(String title, String text, {String acceptText, S } -Future showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info, Function onDismissed}) async { +Future showInfoDialog(String title, String description, {IconData icon = FontAwesomeIcons.info, String? info, Function()? onDismissed}) async { - if (info == null || info.isEmpty) { - info = L10().info; - } + String _info = info ?? L10().info; - showDialog( - context: context, + OneContext().showDialog( builder: (BuildContext context) => SimpleDialog( title: ListTile( - title: Text(info), + title: Text(_info), leading: FaIcon(icon), ), children: [ @@ -83,16 +75,14 @@ Future showInfoDialog(BuildContext context, String title, String descripti }); } -Future showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error, Function onDismissed}) async { +Future showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String? error, Function? onDismissed}) async { - if (error == null || error.isEmpty) { - error = L10().error; - } + String _error = error ?? L10().error; OneContext().showDialog( builder: (context) => SimpleDialog( title: ListTile( - title: Text(error), + title: Text(_error), leading: FaIcon(icon), ), children: [ @@ -111,7 +101,7 @@ Future showErrorDialog(String title, String description, {IconData icon = Future showServerError(String title, String description) async { - if (title == null || title.isEmpty) { + if (title.isEmpty) { title = L10().serverError; } @@ -140,8 +130,6 @@ Future showServerError(String title, String description) async { Future showStatusCodeError(int status, {int expected = 200}) async { - BuildContext ctx = OneContext().context; - String msg = L10().responseInvalid; String extra = "Server responded with status code ${status}"; @@ -174,7 +162,7 @@ Future showStatusCodeError(int status, {int expected = 200}) async { ); } -Future showTimeoutError(BuildContext context) async { +Future showTimeoutError() async { // Use OneContext as "sometimes" context is null here? var ctx = OneContext().context; @@ -182,42 +170,49 @@ Future showTimeoutError(BuildContext context) async { await showServerError(L10().timeout, L10().noResponse); } -void showFormDialog(String title, {String acceptText, String cancelText, GlobalKey key, List fields, List actions, Function callback}) { +void showFormDialog(String title, {String? acceptText, String? cancelText, GlobalKey? key, List? fields, List? actions, Function? callback}) { - BuildContext dialogContext; + BuildContext? dialogContext; var ctx = OneContext().context; - if (acceptText == null) { - acceptText = L10().save; - } - - if (cancelText == null) { - cancelText = L10().cancel; - } + String _accept = acceptText ?? L10().save; + String _cancel = cancelText ?? L10().cancel; // Undefined actions = OK + Cancel if (actions == null) { actions = [ FlatButton( - child: Text(cancelText), + child: Text(_cancel), onPressed: () { // Close the form - Navigator.pop(dialogContext); + var _ctx = dialogContext; + if (_ctx != null) { + Navigator.pop(_ctx); + } } ), FlatButton( - child: Text(acceptText), + child: Text(_accept), onPressed: () { - if (key.currentState.validate()) { - key.currentState.save(); - // Close the dialog - Navigator.pop(dialogContext); + var _key = key; - // Callback - if (callback != null) { - callback(); + if (_key != null && _key.currentState != null) { + if (_key.currentState!.validate()) { + _key.currentState!.save(); + + // Close the dialog + var _ctx = dialogContext; + + if (_ctx != null) { + Navigator.pop(_ctx); + } + + // Callback + if (callback != null) { + callback(); + } } } } @@ -225,6 +220,8 @@ void showFormDialog(String title, {String acceptText, String cancelText, GlobalK ]; } + List _fields = fields ?? []; + OneContext().showDialog( builder: (BuildContext context) { dialogContext = context; @@ -238,7 +235,7 @@ void showFormDialog(String title, {String acceptText, String cancelText, GlobalK mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - children: fields + children: _fields ) ) ) diff --git a/lib/widget/fields.dart b/lib/widget/fields.dart index 7039f128..0572f2b6 100644 --- a/lib/widget/fields.dart +++ b/lib/widget/fields.dart @@ -52,10 +52,10 @@ class ImagePickerField extends FormField { } - ImagePickerField(BuildContext context, {String label = "Attach Image", Function onSaved, bool required = false}) : + ImagePickerField(BuildContext context, {String? label, Function(File?)? onSaved, bool required = false}) : super( onSaved: onSaved, - validator: (File img) { + validator: (File? img) { if (required && (img == null)) { return L10().required; } @@ -63,10 +63,13 @@ class ImagePickerField extends FormField { return null; }, builder: (FormFieldState state) { + + String _label = label ?? L10().attachImage; + return InputDecorator( decoration: InputDecoration( errorText: state.errorText, - labelText: required ? label + "*" : label, + labelText: required ? _label + "*" : _label, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -92,7 +95,7 @@ class ImagePickerField extends FormField { class CheckBoxField extends FormField { - CheckBoxField({String label, String hint, bool initial = false, Function onSaved}) : + CheckBoxField({String? label, String? hint, bool initial = false, Function(bool?)? onSaved}) : super( onSaved: onSaved, initialValue: initial, @@ -111,7 +114,7 @@ class CheckBoxField extends FormField { class StringField extends TextFormField { - StringField({String label, String hint, String initial, Function onSaved, Function validator, bool allowEmpty = false, bool isEnabled = true}) : + StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) : super( decoration: InputDecoration( labelText: allowEmpty ? label : label + "*", @@ -121,7 +124,7 @@ class StringField extends TextFormField { onSaved: onSaved, enabled: isEnabled, validator: (value) { - if (!allowEmpty && value.isEmpty) { + if (!allowEmpty && value != null && value.isEmpty) { return L10().valueCannotBeEmpty; } @@ -140,7 +143,7 @@ class StringField extends TextFormField { */ class QuantityField extends TextFormField { - QuantityField({String label = "", String hint = "", String initial = "", double max = null, TextEditingController controller}) : + QuantityField({String label = "", String hint = "", String initial = "", double? max, TextEditingController? controller}) : super( decoration: InputDecoration( labelText: label, @@ -150,11 +153,10 @@ class QuantityField extends TextFormField { keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), validator: (value) { - if (value.isEmpty) return L10().quantityEmpty; + if (value != null && value.isEmpty) return L10().quantityEmpty; - double quantity = double.tryParse(value); + double quantity = double.tryParse(value.toString()) ?? 0; - if (quantity == null) return L10().quantityInvalid; if (quantity <= 0) return L10().quantityPositive; if ((max != null) && (quantity > max)) return "Quantity must not exceed ${max}"; diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 677522e3..f85c32bd 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -20,7 +20,7 @@ import 'package:InvenTree/widget/spinner.dart'; import 'package:InvenTree/widget/drawer.dart'; class InvenTreeHomePage extends StatefulWidget { - InvenTreeHomePage({Key key}) : super(key: key); + InvenTreeHomePage({Key? key}) : super(key: key); @override _InvenTreeHomePageState createState() => _InvenTreeHomePageState(); @@ -37,9 +37,7 @@ class _InvenTreeHomePageState extends State { } // Selected user profile - UserProfile _profile; - - BuildContext _context; + UserProfile? _profile; void _searchParts() { if (!InvenTreeAPI().checkConnection(context)) return; @@ -113,7 +111,7 @@ class _InvenTreeHomePageState extends State { if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { // Attempt server connection - InvenTreeAPI().connectToServer(_homeKey.currentContext).then((result) { + InvenTreeAPI().connectToServer().then((result) { setState(() {}); }); } @@ -171,7 +169,7 @@ class _InvenTreeHomePageState extends State { } else { return ListTile( title: Text(L10().serverCouldNotConnect), - subtitle: Text("${_profile.server}"), + subtitle: Text("${_profile!.server}"), leading: FaIcon(FontAwesomeIcons.server), trailing: FaIcon( FontAwesomeIcons.timesCircle, @@ -187,8 +185,6 @@ class _InvenTreeHomePageState extends State { @override Widget build(BuildContext context) { - _context = context; - // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index e6e75a12..8cc11536 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -1,6 +1,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/app_settings.dart'; import 'package:InvenTree/barcode.dart'; +import 'package:InvenTree/inventree/sentry.dart'; import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/widget/progress.dart'; @@ -20,11 +21,11 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; class LocationDisplayWidget extends StatefulWidget { - LocationDisplayWidget(this.location, {Key key}) : super(key: key); + LocationDisplayWidget(this.location, {Key? key}) : super(key: key); - final InvenTreeStockLocation location; + final InvenTreeStockLocation? location; - final String title = "Location"; + final String title = L10().stockLocation; @override _LocationDisplayState createState() => _LocationDisplayState(location); @@ -32,12 +33,12 @@ class LocationDisplayWidget extends StatefulWidget { class _LocationDisplayState extends RefreshableState { - final InvenTreeStockLocation location; + final InvenTreeStockLocation? location; final _editLocationKey = GlobalKey(); @override - String getAppBarTitle(BuildContext context) { return "Stock Location"; } + String getAppBarTitle(BuildContext context) { return L10().stockLocation; } @override List getAppBarActions(BuildContext context) { @@ -80,12 +81,16 @@ class _LocationDisplayState extends RefreshableState { void _editLocation(Map values) async { - final bool result = await location.update(context, values: values); + bool result = false; - showSnackIcon( - result ? "Location edited" : "Location editing failed", - success: result - ); + if (location != null) { + result = await location!.update(values: values); + + showSnackIcon( + result ? "Location edited" : "Location editing failed", + success: result + ); + } refresh(); } @@ -95,6 +100,10 @@ class _LocationDisplayState extends RefreshableState { var _name; var _description; + if (location == null) { + return; + } + showFormDialog(L10().editLocation, key: _editLocationKey, callback: () { @@ -106,12 +115,12 @@ class _LocationDisplayState extends RefreshableState { fields: [ StringField( label: L10().name, - initial: location.name, + initial: location?.name ?? '', onSaved: (value) => _name = value, ), StringField( label: L10().description, - initial: location.description, + initial: location?.description ?? '', onSaved: (value) => _description = value, ) ] @@ -120,7 +129,7 @@ class _LocationDisplayState extends RefreshableState { _LocationDisplayState(this.location) {} - List _sublocations = List(); + List _sublocations = []; String _locationFilter = ''; @@ -139,17 +148,17 @@ class _LocationDisplayState extends RefreshableState { } @override - Future request(BuildContext context) async { + Future request() async { int pk = location?.pk ?? -1; // Reload location information if (location != null) { - await location.reload(context); + await location?.reload(); } // Request a list of sub-locations under this one - await InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) { + await InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) { _sublocations.clear(); for (var loc in locs) { @@ -173,8 +182,9 @@ class _LocationDisplayState extends RefreshableState { List children = [ ListTile( - title: Text("${location.name}"), - subtitle: Text("${location.description}"), + title: Text("${location!.name}"), + subtitle: Text("${location!.description}"), + trailing: Text("${location!.itemcount}"), ), ]; @@ -182,13 +192,17 @@ class _LocationDisplayState extends RefreshableState { children.add( ListTile( title: Text(L10().parentCategory), - subtitle: Text("${location.parentpathstring}"), + subtitle: Text("${location!.parentpathstring}"), leading: FaIcon(FontAwesomeIcons.levelUpAlt), onTap: () { - if (location.parentId < 0) { + + int parent = location?.parentId ?? -1; + + if (parent < 0) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } else { - InvenTreeStockLocation().get(context, location.parentId).then((var loc) { + + InvenTreeStockLocation().get(parent).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } @@ -238,7 +252,7 @@ class _LocationDisplayState extends RefreshableState { Map filters = {}; if (location != null) { - filters["location"] = "${location.pk}"; + filters["location"] = "${location!.pk}"; } switch (index) { @@ -256,7 +270,7 @@ class _LocationDisplayState extends RefreshableState { ).toList() ); default: - return null; + return ListView(); } } @@ -297,7 +311,7 @@ List detailTiles() { List tiles = []; tiles.add(locationDescriptionCard(includeActions: false)); - + if (location != null) { // Stock adjustment actions if (InvenTreeAPI().checkPermission('stock', 'change')) { @@ -308,14 +322,19 @@ List detailTiles() { leading: FaIcon(FontAwesomeIcons.exchangeAlt), trailing: FaIcon(FontAwesomeIcons.qrcode), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => - InvenTreeQRView( - StockLocationScanInItemsHandler(location))) - ).then((context) { - refresh(); - }); + + var _loc = location; + + if (_loc != null) { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => + InvenTreeQRView( + StockLocationScanInItemsHandler(_loc))) + ).then((context) { + refresh(); + }); + } }, ) ); @@ -361,7 +380,7 @@ class SublocationList extends StatelessWidget { void _openLocation(BuildContext context, int pk) { - InvenTreeStockLocation().get(context, pk).then((var loc) { + InvenTreeStockLocation().get(pk).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); @@ -452,35 +471,43 @@ class _PaginatedStockListState extends State { params["cascade"] = "${cascade}"; final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params); - final isLastPage = page.length < _pageSize; + + int pageLength = page?.length ?? 0; + int pageCount = page?.count ?? 0; + + final isLastPage = pageLength < _pageSize; // Construct a list of stock item objects List items = []; - for (var result in page.results) { - if (result is InvenTreeStockItem) { - items.add(result); + if (page != null) { + for (var result in page.results) { + if (result is InvenTreeStockItem) { + items.add(result); + } } } if (isLastPage) { _pagingController.appendLastPage(items); } else { - final int nextPageKey = pageKey + page.length; + final int nextPageKey = pageKey + pageLength; _pagingController.appendPage(items, nextPageKey); } setState(() { - resultCount = page.count; + resultCount = pageCount; }); - } catch (error) { + } catch (error, stackTrace) { _pagingController.error = error; + + sentryReportError(error, stackTrace); } } void _openItem(BuildContext context, int pk) { - InvenTreeStockItem().get(context, pk).then((var item) { + InvenTreeStockItem().get(pk).then((var item) { if (item is InvenTreeStockItem) { Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index d28102f8..b13119e8 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -22,7 +22,7 @@ import 'location_display.dart'; class PartDetailWidget extends StatefulWidget { - PartDetailWidget(this.part, {Key key}) : super(key: key); + PartDetailWidget(this.part, {Key? key}) : super(key: key); final InvenTreePart part; @@ -87,22 +87,22 @@ class _PartDisplayState extends RefreshableState { } @override - Future request(BuildContext context) async { - await part.reload(context); - await part.getTestTemplates(context); + Future request() async { + await part.reload(); + await part.getTestTemplates(); } void _toggleStar() async { if (InvenTreeAPI().checkPermission('part', 'view')) { - await part.update(context, values: {"starred": "${!part.starred}"}); + await part.update(values: {"starred": "${!part.starred}"}); refresh(); } } void _savePart(Map values) async { - final bool result = await part.update(context, values: values); + final bool result = await part.update(values: values); if (result) { showSnackIcon(L10().partEdited, success: true); @@ -121,7 +121,11 @@ class _PartDisplayState extends RefreshableState { * Upload image for this Part. * Show a SnackBar with upload result. */ - void _uploadImage(File image) async { + void _uploadImage(File? image) async { + + if (image == null) { + return; + } final result = await part.uploadImage(image); @@ -143,7 +147,7 @@ class _PartDisplayState extends RefreshableState { void _selectImage() { - File _attachment; + File? _attachment; if (!InvenTreeAPI().checkPermission('part', 'change')) { return; @@ -261,7 +265,7 @@ class _PartDisplayState extends RefreshableState { } // Category information - if (part.categoryName != null && part.categoryName.isNotEmpty) { + if (part.categoryName.isNotEmpty) { tiles.add( ListTile( title: Text(L10().partCategory), @@ -269,9 +273,12 @@ class _PartDisplayState extends RefreshableState { leading: FaIcon(FontAwesomeIcons.sitemap), onTap: () { if (part.categoryId > 0) { - InvenTreePartCategory().get(context, part.categoryId).then((var cat) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => CategoryDisplayWidget(cat))); + InvenTreePartCategory().get(part.categoryId).then((var cat) { + + if (cat is InvenTreePartCategory) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => CategoryDisplayWidget(cat))); + } }); } }, @@ -499,7 +506,7 @@ class _PartDisplayState extends RefreshableState { ) ); default: - return null; + return Center(); } } diff --git a/lib/widget/part_notes.dart b/lib/widget/part_notes.dart index 8b84b640..929cbaa6 100644 --- a/lib/widget/part_notes.dart +++ b/lib/widget/part_notes.dart @@ -9,7 +9,7 @@ class PartNotesWidget extends StatefulWidget { final InvenTreePart part; - PartNotesWidget(this.part, {Key key}) : super(key: key); + PartNotesWidget(this.part, {Key? key}) : super(key: key); @override _PartNotesState createState() => _PartNotesState(part); diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 6a5dc052..d981e6a0 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -11,7 +11,7 @@ abstract class RefreshableState extends State { final refreshableKey = GlobalKey(); // Storage for context once "Build" is called - BuildContext context; + BuildContext? _context; // Current tab index (used for widgets which display bottom tabs) int tabIndex = 0; @@ -36,7 +36,7 @@ abstract class RefreshableState extends State { void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => onBuild(context)); + WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!)); } // Function called after the widget is first build @@ -45,7 +45,7 @@ abstract class RefreshableState extends State { } // Function to request data for this page - Future request(BuildContext context) async { + Future request() async { return; } @@ -55,7 +55,7 @@ abstract class RefreshableState extends State { loading = true; }); - await request(context); + await request(); setState(() { loading = false; @@ -77,14 +77,16 @@ abstract class RefreshableState extends State { // Function to construct a body (MUST BE PROVIDED) Widget getBody(BuildContext context) { + + // Default return is an empty ListView + return ListView(); + } + + Widget? getBottomNavBar(BuildContext context) { return null; } - Widget getBottomNavBar(BuildContext context) { - return null; - } - - Widget getFab(BuildContext context) { + Widget? getFab(BuildContext context) { return null; } @@ -92,7 +94,7 @@ abstract class RefreshableState extends State { Widget build(BuildContext context) { // Save the context for future use - this.context = context; + _context = context; return Scaffold( key: refreshableKey, diff --git a/lib/widget/search.dart b/lib/widget/search.dart index c6b9c0e9..97ccde01 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -15,23 +15,30 @@ import '../api.dart'; // TODO - Refactor duplicate code in this file! -class PartSearchDelegate extends SearchDelegate { +class PartSearchDelegate extends SearchDelegate { final partSearchKey = GlobalKey(); BuildContext context; // What did we search for last time? - String _cachedQuery; + String _cachedQuery = ""; bool _searching = false; // Custom filters for the part search - Map filters = {}; + Map _filters = {}; - PartSearchDelegate(this.context, {this.filters}) { - if (filters == null) { - filters = {}; + PartSearchDelegate(this.context, {Map filters = const {}}) { + + // Copy filter values + for (String key in filters.keys) { + + String? value = filters[key]; + + if (value != null) { + _filters[key] = value; + } } } @@ -62,16 +69,15 @@ class PartSearchDelegate extends SearchDelegate { showResults(context); - // Enable cascading part search by default - filters["cascade"] = "true"; + _filters["cascade"] = "true"; - final results = await InvenTreePart().search(context, query, filters: filters); + final results = await InvenTreePart().search(context, query, filters: _filters); partResults.clear(); for (int idx = 0; idx < results.length; idx++) { if (results[idx] is InvenTreePart) { - partResults.add(results[idx]); + partResults.add(results[idx] as InvenTreePart); } } @@ -132,7 +138,7 @@ class PartSearchDelegate extends SearchDelegate { ), trailing: Text(part.inStockString), onTap: () { - InvenTreePart().get(context, part.pk).then((var prt) { + InvenTreePart().get(part.pk).then((var prt) { if (prt is InvenTreePart) { Navigator.push( context, @@ -201,22 +207,29 @@ class PartSearchDelegate extends SearchDelegate { } -class StockSearchDelegate extends SearchDelegate { +class StockSearchDelegate extends SearchDelegate { final stockSearchKey = GlobalKey(); final BuildContext context; - String _cachedQuery; + String _cachedQuery = ""; bool _searching = false; // Custom filters for the stock item search - Map filters; + Map _filters = {}; - StockSearchDelegate(this.context, {this.filters}) { - if (filters == null) { - filters = {}; + StockSearchDelegate(this.context, {Map filters = const {}}) { + + // Copy filter values + for (String key in filters.keys) { + + String? value = filters[key]; + + if (value != null) { + _filters[key] = value; + } } } @@ -247,16 +260,16 @@ class StockSearchDelegate extends SearchDelegate { showResults(context); // Enable cascading part search by default - filters["cascade"] = "true"; + _filters["cascade"] = "true"; final results = await InvenTreeStockItem().search( - context, query, filters: filters); + context, query, filters: _filters); itemResults.clear(); for (int idx = 0; idx < results.length; idx++) { if (results[idx] is InvenTreeStockItem) { - itemResults.add(results[idx]); + itemResults.add(results[idx] as InvenTreeStockItem); } } @@ -315,7 +328,7 @@ class StockSearchDelegate extends SearchDelegate { ), trailing: Text(item.serialOrQuantityDisplay()), onTap: () { - InvenTreeStockItem().get(context, item.pk).then((var it) { + InvenTreeStockItem().get(item.pk).then((var it) { if (it is InvenTreeStockItem) { Navigator.push( context, diff --git a/lib/widget/snacks.dart b/lib/widget/snacks.dart index 6bba7277..2856cd99 100644 --- a/lib/widget/snacks.dart +++ b/lib/widget/snacks.dart @@ -15,14 +15,14 @@ import 'package:one_context/one_context.dart'; import 'package:InvenTree/l10.dart'; -void showSnackIcon(String text, {IconData icon, Function onAction, bool success, String actionText}) { +void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) { OneContext().hideCurrentSnackBar(); - Color backgroundColor; + Color backgroundColor = Colors.deepOrange; // Make some selections based on the "success" value - if (success == true) { + if (success != null && success == true) { backgroundColor = Colors.lightGreen; // Select an icon if we do not have an action @@ -30,26 +30,21 @@ void showSnackIcon(String text, {IconData icon, Function onAction, bool success, icon = FontAwesomeIcons.checkCircle; } - } else if (success == false) { + } else if (success != null && success == false) { backgroundColor = Colors.deepOrange; if (icon == null && onAction == null) { icon = FontAwesomeIcons.exclamationCircle; } - } - SnackBarAction action; + String _action = actionText ?? L10().details; + + SnackBarAction? action; if (onAction != null) { - - if (actionText == null) { - // Default action text - actionText = L10().details; - } - action = SnackBarAction( - label: actionText, + label: _action, onPressed: onAction, ); } diff --git a/lib/widget/spinner.dart b/lib/widget/spinner.dart index 5bab6c4f..eb049a11 100644 --- a/lib/widget/spinner.dart +++ b/lib/widget/spinner.dart @@ -4,13 +4,13 @@ import 'package:flutter/cupertino.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class Spinner extends StatefulWidget { - final IconData icon; + final IconData? icon; final Duration duration; final Color color; const Spinner({ this.color = const Color.fromRGBO(150, 150, 150, 1), - Key key, + Key? key, @required this.icon, this.duration = const Duration(milliseconds: 1800), }) : super(key: key); @@ -20,8 +20,8 @@ class Spinner extends StatefulWidget { } class _SpinnerState extends State with SingleTickerProviderStateMixin { - AnimationController _controller; - Widget _child; + AnimationController? _controller; + Widget? _child; @override void initState() { @@ -40,14 +40,14 @@ class _SpinnerState extends State with SingleTickerProviderStateMixin { @override void dispose() { - _controller.dispose(); + _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RotationTransition( - turns: _controller, + turns: _controller!, child: _child, ); } diff --git a/lib/widget/starred_parts.dart b/lib/widget/starred_parts.dart index 8d760400..a571b2fe 100644 --- a/lib/widget/starred_parts.dart +++ b/lib/widget/starred_parts.dart @@ -14,7 +14,7 @@ import '../api.dart'; class StarredPartWidget extends StatefulWidget { - StarredPartWidget({Key key}) : super(key: key); + StarredPartWidget({Key? key}) : super(key: key); @override _StarredPartState createState() => _StarredPartState(); @@ -29,15 +29,17 @@ class _StarredPartState extends RefreshableState { String getAppBarTitle(BuildContext context) => L10().partsStarred; @override - Future request(BuildContext context) async { + Future request() async { - final parts = await InvenTreePart().list(context, filters: {"starred": "true"}); + final parts = await InvenTreePart().list(filters: {"starred": "true"}); starredParts.clear(); - for (int idx = 0; idx < parts.length; idx++) { - if (parts[idx] is InvenTreePart) { - starredParts.add(parts[idx]); + if (parts != null) { + for (int idx = 0; idx < parts.length; idx++) { + if (parts[idx] is InvenTreePart) { + starredParts.add(parts[idx] as InvenTreePart); + } } } } @@ -54,7 +56,7 @@ class _StarredPartState extends RefreshableState { height: 40 ), onTap: () { - InvenTreePart().get(context, part.pk).then((var prt) { + InvenTreePart().get(part.pk).then((var prt) { if (prt is InvenTreePart) { Navigator.push( context, diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index e96381cb..1682968b 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -25,7 +25,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class StockDetailWidget extends StatefulWidget { - StockDetailWidget(this.item, {Key key}) : super(key: key); + StockDetailWidget(this.item, {Key? key}) : super(key: key); final InvenTreeStockItem item; @@ -77,7 +77,7 @@ class _StockItemDisplayState extends RefreshableState { final InvenTreeStockItem item; // Part object - InvenTreePart part; + InvenTreePart? part; @override Future onBuild(BuildContext context) async { @@ -89,14 +89,14 @@ class _StockItemDisplayState extends RefreshableState { } @override - Future request(BuildContext context) async { - await item.reload(context); + Future request() async { + await item.reload(); // Request part information - part = await InvenTreePart().get(context, item.partId); + part = await InvenTreePart().get(item.partId) as InvenTreePart; // Request test results... - await item.getTestResults(context); + await item.getTestResults(); } void _addStock() async { @@ -227,7 +227,7 @@ class _StockItemDisplayState extends RefreshableState { void _unassignBarcode(BuildContext context) async { - final bool result = await item.update(context, values: {'uid': ''}); + final bool result = await item.update(values: {'uid': ''}); if (result) { showSnackIcon( @@ -245,7 +245,7 @@ class _StockItemDisplayState extends RefreshableState { } - void _transferStock(BuildContext context, InvenTreeStockLocation location) async { + void _transferStock(InvenTreeStockLocation location) async { double quantity = double.tryParse(_quantityController.text) ?? item.quantity; String notes = _notesController.text; @@ -264,17 +264,21 @@ class _StockItemDisplayState extends RefreshableState { void _transferStockDialog() async { - var locations = await InvenTreeStockLocation().list(context); + var locations = await InvenTreeStockLocation().list(); final _selectedController = TextEditingController(); - InvenTreeStockLocation selectedLocation; + InvenTreeStockLocation? selectedLocation; _quantityController.text = "${item.quantityString}"; showFormDialog(L10().transferStock, key: _moveStockKey, callback: () { - _transferStock(context, selectedLocation); + var _loc = selectedLocation; + + if (_loc != null) { + _transferStock(_loc); + } }, fields: [ QuantityField( @@ -292,7 +296,7 @@ class _StockItemDisplayState extends RefreshableState { ) ), suggestionsCallback: (pattern) async { - var suggestions = List(); + List suggestions = []; for (var loc in locations) { if (loc.matchAgainstString(pattern)) { @@ -311,7 +315,7 @@ class _StockItemDisplayState extends RefreshableState { }, onSuggestionSelected: (suggestion) { selectedLocation = suggestion as InvenTreeStockLocation; - _selectedController.text = selectedLocation.pathstring; + _selectedController.text = selectedLocation!.pathstring; }, onSaved: (value) { }, @@ -342,7 +346,7 @@ class _StockItemDisplayState extends RefreshableState { ), onTap: () { if (item.partId > 0) { - InvenTreePart().get(context, item.partId).then((var part) { + InvenTreePart().get(item.partId).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); } @@ -397,9 +401,12 @@ class _StockItemDisplayState extends RefreshableState { leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), onTap: () { if (item.locationId > 0) { - InvenTreeStockLocation().get(context, item.locationId).then((var loc) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => LocationDisplayWidget(loc))); + InvenTreeStockLocation().get(item.locationId).then((var loc) { + + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => LocationDisplayWidget(loc))); + } }); } }, @@ -442,7 +449,7 @@ class _StockItemDisplayState extends RefreshableState { ); } - if ((item.testResultCount > 0) || (part != null && part.isTrackable)) { + if ((item.testResultCount > 0) || (part?.isTrackable ?? false)) { tiles.add( ListTile( title: Text(L10().testResults), @@ -641,7 +648,7 @@ class _StockItemDisplayState extends RefreshableState { ).toList() ); default: - return null; + return ListView(); } } diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart index c9c6cefe..4015742e 100644 --- a/lib/widget/stock_item_test_results.dart +++ b/lib/widget/stock_item_test_results.dart @@ -19,7 +19,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class StockItemTestResultsWidget extends StatefulWidget { - StockItemTestResultsWidget(this.item, {Key key}) : super(key: key); + StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key); final InvenTreeStockItem item; @@ -36,16 +36,16 @@ class _StockItemTestResultDisplayState extends RefreshableState L10().testResults; @override - Future request(BuildContext context) async { - await item.getTestTemplates(context); - await item.getTestResults(context); + Future request() async { + await item.getTestTemplates(); + await item.getTestResults(); } final InvenTreeStockItem item; _StockItemTestResultDisplayState(this.item); - void uploadTestResult(String name, bool result, String value, String notes, File attachment) async { + void uploadTestResult(String name, bool result, String value, String notes, File? attachment) async { final success = await item.uploadTestResult( context, name, result, @@ -64,11 +64,11 @@ class _StockItemTestResultDisplayState extends RefreshableState _name = value, + onSaved: (value) => _name = value ?? '', ), CheckBoxField( label: L10().result, hint: L10().testPassedOrFailed, initial: true, - onSaved: (value) => _result = value, + onSaved: (value) => _result = value ?? false, ), StringField( label: L10().value, initial: value, allowEmpty: true, - onSaved: (value) => _value = value, + onSaved: (value) => _value = value ?? '', validator: (String value) { - if (valueRequired && (value == null || value.isEmpty)) { + if (valueRequired && value.isEmpty) { return L10().valueRequired; } return null; @@ -109,7 +109,7 @@ class _StockItemTestResultDisplayState extends RefreshableState _notes = value, + onSaved: (value) => _notes = value ?? '', ), ] ); @@ -202,10 +202,11 @@ class _StockItemTestResultDisplayState extends RefreshableState _StockNotesState(item); diff --git a/pubspec.lock b/pubspec.lock index c1c9d3d1..9fd00be7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.19.0" + back_button_interceptor: + dependency: transitive + description: + name: back_button_interceptor + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" boolean_selector: dependency: transitive description: @@ -42,7 +49,7 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "3.0.0" camera: dependency: "direct main" description: @@ -105,7 +112,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.3" device_info: dependency: "direct main" description: @@ -152,21 +159,35 @@ packages: name: flutter_blurhash url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.6.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "3.1.2" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "5.0.2" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -199,7 +220,7 @@ packages: name: flutter_speed_dial url: "https://pub.dartlang.org" source: hosted - version: "1.2.5" + version: "3.0.5" flutter_test: dependency: "direct dev" description: flutter @@ -211,7 +232,7 @@ packages: name: flutter_typeahead url: "https://pub.dartlang.org" source: hosted - version: "1.8.8" + version: "3.1.3" flutter_web_plugins: dependency: transitive description: flutter @@ -223,7 +244,7 @@ packages: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "8.11.0" + version: "9.1.0" http: dependency: "direct main" description: @@ -272,7 +293,7 @@ packages: name: infinite_scroll_pagination url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "3.1.0" intl: dependency: "direct main" description: @@ -314,14 +335,14 @@ packages: name: octo_image url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "1.0.0+1" one_context: dependency: "direct main" description: name: one_context url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "1.1.0" package_info: dependency: "direct main" description: @@ -454,7 +475,7 @@ packages: name: qr_code_scanner url: "https://pub.dartlang.org" source: hosted - version: "0.3.5" + version: "0.5.1" quiver: dependency: transitive description: @@ -468,14 +489,14 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.25.0" + version: "0.27.1" sembast: dependency: "direct main" description: name: sembast url: "https://pub.dartlang.org" source: hosted - version: "2.4.9" + version: "3.1.0+2" sentry: dependency: transitive description: @@ -496,28 +517,42 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.7+3" + version: "2.0.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+11" + version: "2.0.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2+7" + version: "2.0.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -529,7 +564,7 @@ packages: name: sliver_tools url: "https://pub.dartlang.org" source: hosted - version: "0.1.10+1" + version: "0.2.5" source_span: dependency: transitive description: @@ -543,14 +578,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "1.3.2+3" + version: "2.0.0+3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "1.0.3+1" + version: "2.0.0+2" stack_trace: dependency: transitive description: @@ -585,7 +620,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "2.2.0+2" + version: "3.0.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6208add5..b3f634e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,10 @@ description: InvenTree stock management # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.2.5+13 +version: 0.2.6+14 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: @@ -21,25 +21,25 @@ dependencies: intl: ^0.17.0 - cupertino_icons: ^0.1.3 + cupertino_icons: ^1.0.3 http: ^0.13.0 - cached_network_image: ^2.5.0 - qr_code_scanner: ^0.3.5 # Barcode scanning + cached_network_image: ^3.0.0 # Download and cache remote images + qr_code_scanner: ^0.5.1 # Barcode scanning package_info: ^2.0.0 # App information introspection device_info: ^2.0.0 # Information about the device - font_awesome_flutter: ^8.8.1 # FontAwesome icon set - flutter_speed_dial: ^1.2.5 # FAB menu elements - sentry_flutter: 5.0.0 # Error reporting - flutter_typeahead: ^1.8.1 # Auto-complete input field + font_awesome_flutter: ^9.1.0 # FontAwesome icon set + flutter_speed_dial: ^3.0.5 # FAB menu elements + sentry_flutter: 5.0.0 # Error reporting + flutter_typeahead: ^3.1.0 # Auto-complete input field image_picker: ^0.8.0 # Select or take photos - url_launcher: 6.0.0 # Open link in system browser + url_launcher: 6.0.0 # Open link in system browser flutter_markdown: ^0.6.2 # Rendering markdown camera: # Camera - path_provider: 2.0.1 #^1.6.28 # Local file storage - 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: ^0.19.0 + path_provider: 2.0.1 # Local file storage + sembast: ^3.1.0+2 # NoSQL data storage + one_context: ^1.1.0 # Dialogs without requiring context + infinite_scroll_pagination: ^3.1.0 # Let the server do all the work! + audioplayers: ^0.19.0 # Play audio files path: dev_dependencies: