From d3eec6a79ebe9cadacbb8f334dfd5aecf86fb044 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 23:56:38 +1000 Subject: [PATCH] Many many changes for null-safety support --- assets/release_notes.md | 6 ++ lib/api.dart | 70 +++++++------ lib/barcode.dart | 124 +++++++++++++++--------- lib/inventree/model.dart | 48 +++++---- lib/inventree/part.dart | 63 ++++++------ lib/inventree/stock.dart | 54 +++++------ lib/l10.dart | 2 +- lib/l10n | 2 +- lib/main.dart | 2 +- lib/preferences.dart | 12 ++- lib/settings/login.dart | 66 +++++++------ lib/user_profile.dart | 28 +++--- lib/widget/category_display.dart | 55 ++++++----- lib/widget/company_detail.dart | 14 +-- lib/widget/company_list.dart | 18 ++-- lib/widget/dialogs.dart | 89 ++++++++--------- lib/widget/fields.dart | 21 ++-- lib/widget/home.dart | 12 +-- lib/widget/location_display.dart | 103 ++++++++++++-------- lib/widget/part_detail.dart | 33 ++++--- lib/widget/part_notes.dart | 2 +- lib/widget/refreshable_state.dart | 22 +++-- lib/widget/search.dart | 28 +++--- lib/widget/snacks.dart | 21 ++-- lib/widget/spinner.dart | 12 +-- lib/widget/starred_parts.dart | 16 +-- lib/widget/stock_detail.dart | 45 +++++---- lib/widget/stock_item_test_results.dart | 39 ++++---- lib/widget/stock_notes.dart | 2 +- pubspec.yaml | 10 +- 30 files changed, 563 insertions(+), 456 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index b5545c17..2fd43b44 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,12 @@ ## InvenTree App Release Notes --- +### 0.2.6 - July 2021 +--- + +- Major code update with "null safety" features +- Updated translations + ### 0.2.5 - June 2021 --- diff --git a/lib/api.dart b/lib/api.dart index 1d062680..50db640e 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -22,24 +22,29 @@ 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 { final Uri resolved = Uri.base.resolve(url); - final HttpClientRequest req = await _client.getUrl(resolved); - headers?.forEach((key, value) { + final HttpClientRequest req = await _client!.getUrl(resolved); + + 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 +106,7 @@ class InvenTreeAPI { String makeUrl(String endpoint) => _makeUrl(endpoint); - UserProfile profile; + UserProfile? profile = null; Map roles = {}; @@ -171,15 +176,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 +211,8 @@ class InvenTreeAPI { print("Connecting to ${apiUrl} -> username=${username}"); - HttpClientResponse response; + HttpClientResponse? response; + dynamic data; response = await getResponse(""); @@ -237,7 +247,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 +325,7 @@ class InvenTreeAPI { } - bool disconnectFromServer() { + void disconnectFromServer() { print("InvenTreeAPI().disconnectFromServer()"); _connected = false; @@ -324,7 +334,7 @@ class InvenTreeAPI { profile = null; } - Future connectToServer(BuildContext context) async { + Future connectToServer() async { // Ensure server is first disconnected disconnectFromServer(); @@ -345,7 +355,7 @@ class InvenTreeAPI { _connecting = true; - _connected = await _connect(context); + _connected = await _connect(); print("_connect() returned result: ${_connected}"); @@ -411,12 +421,14 @@ class InvenTreeAPI { // Perform a PATCH request - Future patch(String url, {Map body, int expectedStatusCode=200}) async { + Future patch(String url, {Map? body, int expectedStatusCode=200}) async { var _url = makeApiUrl(url); var _body = Map(); // Copy across provided data - body.forEach((K, V) => _body[K] = V); + if (body != null) { + body.forEach((K, V) => _body[K] = V); + } print("PATCH: " + _url); @@ -517,7 +529,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)); @@ -543,7 +556,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, int expectedStatusCode=201}) async { + var _url = makeApiUrl(url); print("POST: ${_url} -> ${body.toString()}"); @@ -615,7 +629,7 @@ class InvenTreeAPI { error.toString() ); } else if (error is TimeoutException) { - showTimeoutError(ctx); + showTimeoutError(); } else { showServerError( L10().serverError, @@ -674,7 +688,7 @@ 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}) async { var _url = makeApiUrl(url); print("GET: ${_url}"); @@ -797,7 +811,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, int expectedStatusCode=200}) async { var response = await getResponse(url, params: params); @@ -836,7 +850,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 +860,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 = 0, double width = 0}) { if (imageUrl.isEmpty) { imageUrl = staticImage; } diff --git a/lib/barcode.dart b/lib/barcode.dart index 2f7a79ea..d5386566 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,11 @@ class BarcodeScanHandler extends BarcodeHandler { L10().barcodeUnknown, success: false, onAction: () { + + var _ctx = OneContext().context; + showDialog( - context: _context, + context: _ctx, builder: (BuildContext context) => SimpleDialog( title: Text(L10().unknownResponse), children: [ @@ -257,7 +284,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler { String getOverlayText(BuildContext context) => L10().barcodeScanAssign; @override - Future onBarcodeMatched(Map data) { + Future onBarcodeMatched(Map data) async { failureTone(); @@ -270,7 +297,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 +309,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 +318,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 +381,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 +439,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 +495,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 +504,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 +516,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 +530,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 +544,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..f0dccd81 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,7 +142,7 @@ class InvenTreeModel { // Search this Model type in the database - Future> search(BuildContext context, String searchTerm, {Map filters}) async { + Future> search(BuildContext context, String searchTerm, {Map? filters}) async { if (filters == null) { filters = {}; @@ -150,7 +150,7 @@ class InvenTreeModel { filters["search"] = searchTerm; - final results = list(context, filters: filters); + final results = list(filters: filters); return results; @@ -164,7 +164,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 +178,7 @@ class InvenTreeModel { } // POST data to update the model - Future update(BuildContext context, {Map values}) async { + Future update({Map? values}) async { var addr = path.join(URL, pk.toString()); @@ -198,15 +198,15 @@ 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}) 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(); @@ -214,13 +214,13 @@ class InvenTreeModel { if (filters != null) { // Override any default values for (String key in filters.keys) { - params[key] = filters[key]; + 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 +229,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 +241,6 @@ class InvenTreeModel { data.remove('id'); } - InvenTreeModel _model; - var response = await api.post(URL, body: data); if (response == null) { @@ -252,12 +250,12 @@ class InvenTreeModel { return createFromJson(response); } - Future listPaginated(int limit, int offset, {Map filters}) async { + Future listPaginated(int limit, int offset, {Map? filters = null}) async { var params = defaultListFilters(); if (filters != null) { for (String key in filters.keys) { - params[key] = filters[key]; + params[key] = filters[key] ?? ''; } } @@ -285,14 +283,12 @@ 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 { + Future> list({Map? filters}) async { if (filters == null) { filters = {}; @@ -302,7 +298,7 @@ class InvenTreeModel { if (filters != null) { for (String key in filters.keys) { - params[key] = filters[key]; + params[key] = filters[key] ?? ''; } } @@ -311,7 +307,7 @@ class InvenTreeModel { var response = await api.get(URL, params: params); // A list of "InvenTreeModel" items - List results = new List(); + List results = new List.empty(); if (response == null) { return results; diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 82c44a8b..0ec4647a 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 = List.empty(); 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 = List.empty(); 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']) ?? 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']) ?? 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']) ?? 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 = List.empty(); 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/stock.dart b/lib/inventree/stock.dart index ff8189cd..84bb531d 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 = List.empty(); 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 = List.empty(); 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']) ?? 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,7 +398,7 @@ 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(); @@ -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); 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..571bd8f0 100644 --- a/lib/l10.dart +++ b/lib/l10.dart @@ -5,5 +5,5 @@ import 'package:one_context/one_context.dart'; // Shortcut function to reduce boilerplate! I18N L10() { - return I18N.of(OneContext().context); + return I18N.of(OneContext().context)!; } \ 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..20055dee 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 = new List.empty(); _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..067db1f3 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 = new List.empty(); 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 = new List.empty(); for (int idx = 0; idx < profiles.length; idx++) { diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 4725cc30..ba459957 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -23,9 +23,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 +81,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 +93,11 @@ class _CategoryDisplayState extends RefreshableState { void _editCategoryDialog() { + // Cannot edit top-level category + if (category == null) { + return; + } + var _name; var _description; @@ -108,12 +113,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 +128,9 @@ class _CategoryDisplayState extends RefreshableState { _CategoryDisplayState(this.category) {} // The local InvenTreePartCategory object - final InvenTreePartCategory category; + final InvenTreePartCategory? category; - List _subcategories = List(); + List _subcategories = List.empty(); @override Future onBuild(BuildContext context) async { @@ -133,17 +138,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 +173,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 +184,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 +295,8 @@ class _CategoryDisplayState extends RefreshableState { return ListView( children: actionTiles() ); + default: + return ListView(); } } } @@ -306,7 +313,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 +355,7 @@ class PaginatedPartList extends StatefulWidget { final Map filters; - Function onTotalChanged; + Function(int)? onTotalChanged; PaginatedPartList(this.filters, {this.onTotalChanged}); @@ -363,7 +370,7 @@ class _PaginatedPartListState extends State { String _searchTerm = ""; - Function onTotalChanged; + Function(int)? onTotalChanged; final Map filters; @@ -393,21 +400,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,7 +430,7 @@ class _PaginatedPartListState extends State { } if (onTotalChanged != null) { - onTotalChanged(pageCount); + onTotalChanged!(pageCount); } setState(() { @@ -438,7 +445,7 @@ class _PaginatedPartListState extends State { 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..0de3e249 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(); + var tiles = List.empty(); bool sep = false; diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index deada540..40be988d 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(); + var _companies = new List.empty(); - var _filteredCompanies = new List(); + var _filteredCompanies = new List.empty(); 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..24345196 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 ]; } + var _fields = fields ?? List.empty(); + 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..a1487ffc 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,9 +153,9 @@ 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 ?? '0') ?? 0; if (quantity == null) return L10().quantityInvalid; if (quantity <= 0) return L10().quantityPositive; 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..6c2b8e36 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -20,11 +20,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 +32,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 +80,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 +99,10 @@ class _LocationDisplayState extends RefreshableState { var _name; var _description; + if (location == null) { + return; + } + showFormDialog(L10().editLocation, key: _editLocationKey, callback: () { @@ -106,12 +114,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 +128,7 @@ class _LocationDisplayState extends RefreshableState { _LocationDisplayState(this.location) {} - List _sublocations = List(); + List _sublocations = List.empty(); String _locationFilter = ''; @@ -139,17 +147,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 +181,8 @@ class _LocationDisplayState extends RefreshableState { List children = [ ListTile( - title: Text("${location.name}"), - subtitle: Text("${location.description}"), + title: Text("${location!.name}"), + subtitle: Text("${location!.description}"), ), ]; @@ -182,13 +190,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 +250,7 @@ class _LocationDisplayState extends RefreshableState { Map filters = {}; if (location != null) { - filters["location"] = "${location.pk}"; + filters["location"] = "${location!.pk}"; } switch (index) { @@ -256,7 +268,7 @@ class _LocationDisplayState extends RefreshableState { ).toList() ); default: - return null; + return ListView(); } } @@ -297,7 +309,7 @@ List detailTiles() { List tiles = []; tiles.add(locationDescriptionCard(includeActions: false)); - + if (location != null) { // Stock adjustment actions if (InvenTreeAPI().checkPermission('stock', 'change')) { @@ -308,14 +320,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 +378,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,26 +469,32 @@ 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) { @@ -480,7 +503,7 @@ class _PaginatedStockListState extends State { } 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..f97ea59b 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -15,25 +15,21 @@ 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 = {}; - PartSearchDelegate(this.context, {this.filters}) { - if (filters == null) { - filters = {}; - } - } + PartSearchDelegate(this.context, {this.filters = const {}}); @override String get searchFieldLabel => L10().searchParts; @@ -71,7 +67,7 @@ class PartSearchDelegate extends SearchDelegate { 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 +128,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,18 +197,18 @@ 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) { @@ -247,7 +243,9 @@ class StockSearchDelegate extends SearchDelegate { showResults(context); // Enable cascading part search by default - filters["cascade"] = "true"; + if (filters != null) { + filters?["cascade"] = "true"; + } final results = await InvenTreeStockItem().search( context, query, filters: filters); @@ -256,7 +254,7 @@ class StockSearchDelegate extends SearchDelegate { 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 +313,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..17451f39 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(); + var suggestions = List.empty(); 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.yaml b/pubspec.yaml index 560b1e1c..3f8a169d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,23 +23,23 @@ dependencies: cupertino_icons: ^0.1.3 http: ^0.13.0 - cached_network_image: ^2.5.0 + cached_network_image: ^2.5.0 # Download and cache remote images qr_code_scanner: ^0.3.5 # 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 + sentry_flutter: 5.0.0 # Error reporting flutter_typeahead: ^1.8.1 # 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 + path_provider: 2.0.1 # 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 + audioplayers: ^0.19.0 # Play audio files path: dev_dependencies: