mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 05:26:47 +00:00
Many many changes for null-safety support
This commit is contained in:
parent
2988716bf3
commit
d3eec6a79e
@ -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
|
||||
---
|
||||
|
||||
|
70
lib/api.dart
70
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<FileServiceResponse> get(String url,
|
||||
{Map<String, String> 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<String, dynamic> roles = {};
|
||||
|
||||
@ -171,15 +176,19 @@ class InvenTreeAPI {
|
||||
* - Request user token from the server
|
||||
* - Request user roles from the server
|
||||
*/
|
||||
Future<bool> _connect(BuildContext context) async {
|
||||
Future<bool> _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<bool> connectToServer(BuildContext context) async {
|
||||
Future<bool> 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<dynamic> patch(String url, {Map<String, String> body, int expectedStatusCode=200}) async {
|
||||
Future<dynamic> patch(String url, {Map<String, String>? body, int expectedStatusCode=200}) async {
|
||||
var _url = makeApiUrl(url);
|
||||
var _body = Map<String, String>();
|
||||
|
||||
// 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<http.StreamedResponse> uploadFile(String url, File f,
|
||||
{String name = "attachment", String method="POST", Map<String, String> fields}) async {
|
||||
{String name = "attachment", String method="POST", Map<String, String>? 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<dynamic> post(String url, {Map<String, dynamic> body, int expectedStatusCode=201}) async {
|
||||
Future<dynamic> post(String url, {Map<String, dynamic>? 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<HttpClientResponse> getResponse(String url, {Map<String, String> params}) async {
|
||||
Future<HttpClientResponse?> getResponse(String url, {Map<String, String>? 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<dynamic> get(String url, {Map<String, String> params, int expectedStatusCode=200}) async {
|
||||
Future<dynamic> get(String url, {Map<String, String>? 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;
|
||||
}
|
||||
|
124
lib/barcode.dart
124
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<void> onBarcodeMatched(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// Called when the server "matches" a barcode
|
||||
// Override this function
|
||||
}
|
||||
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
// Called when the server does not know about a barcode
|
||||
// Override this function
|
||||
|
||||
@ -76,17 +76,17 @@ class BarcodeHandler {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
// Called when the server returns an unhandled response
|
||||
showServerError(L10().responseUnknown, data.toString());
|
||||
|
||||
_controller.resumeCamera();
|
||||
_controller?.resumeCamera();
|
||||
}
|
||||
|
||||
Future<void> processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) async {
|
||||
Future<void> 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<void> onBarcodeUnknown(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
@ -140,8 +140,9 @@ class BarcodeScanHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) {
|
||||
int pk;
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> 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: <Widget>[
|
||||
@ -257,7 +284,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
@ -270,7 +297,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) {
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> 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<StatefulWidget> createState() => _QRViewState(_handler);
|
||||
@ -468,11 +504,11 @@ class InvenTreeQRView extends StatefulWidget {
|
||||
|
||||
class _QRViewState extends State<InvenTreeQRView> {
|
||||
|
||||
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<InvenTreeQRView> {
|
||||
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<InvenTreeQRView> {
|
||||
_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<InvenTreeQRView> {
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
// Save the context for later on!
|
||||
this.context = context;
|
||||
this._context = context;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
|
@ -33,7 +33,7 @@ class InvenTreePageResponse {
|
||||
|
||||
int get count => _count;
|
||||
|
||||
int get length => results?.length ?? 0;
|
||||
int get length => results.length;
|
||||
|
||||
List<InvenTreeModel> 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<String, dynamic> json) {
|
||||
@ -142,7 +142,7 @@ class InvenTreeModel {
|
||||
|
||||
|
||||
// Search this Model type in the database
|
||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters}) async {
|
||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String>? 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<bool> reload(BuildContext context) async {
|
||||
Future<bool> reload() async {
|
||||
|
||||
var response = await api.get(url, params: defaultGetFilters());
|
||||
|
||||
@ -178,7 +178,7 @@ class InvenTreeModel {
|
||||
}
|
||||
|
||||
// POST data to update the model
|
||||
Future<bool> update(BuildContext context, {Map<String, String> values}) async {
|
||||
Future<bool> update({Map<String, String>? values}) async {
|
||||
|
||||
var addr = path.join(URL, pk.toString());
|
||||
|
||||
@ -198,15 +198,15 @@ class InvenTreeModel {
|
||||
}
|
||||
|
||||
// Return the detail view for the associated pk
|
||||
Future<InvenTreeModel> get(BuildContext context, int pk, {Map<String, String> filters}) async {
|
||||
Future<InvenTreeModel?> get(int pk, {Map<String, String>? 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<InvenTreeModel> create(BuildContext context, Map<String, dynamic> data) async {
|
||||
Future<InvenTreeModel?> create(Map<String, dynamic> 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<InvenTreePageResponse> listPaginated(int limit, int offset, {Map<String, String> filters}) async {
|
||||
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String>? 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<InvenTreeModel>> list(BuildContext context, {Map<String, String> filters}) async {
|
||||
Future<List<InvenTreeModel>> list({Map<String, String>? 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<InvenTreeModel> results = new List<InvenTreeModel>();
|
||||
List<InvenTreeModel> results = new List<InvenTreeModel>.empty();
|
||||
|
||||
if (response == null) {
|
||||
return results;
|
||||
|
@ -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<InvenTreeStockItemTestResult> 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<String, String> defaultGetFilters() {
|
||||
return {
|
||||
"category_detail": "1", // Include category detail information
|
||||
"category_detail": "true", // Include category detail information
|
||||
};
|
||||
}
|
||||
|
||||
// Cached list of stock items
|
||||
List<InvenTreeStockItem> stockItems = List<InvenTreeStockItem>();
|
||||
List<InvenTreeStockItem> stockItems = List<InvenTreeStockItem>.empty();
|
||||
|
||||
int get stockItemCount => stockItems.length;
|
||||
|
||||
@ -156,7 +157,6 @@ class InvenTreePart extends InvenTreeModel {
|
||||
Future<void> 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<InvenTreePartTestTemplate> testingTemplates = List<InvenTreePartTestTemplate>();
|
||||
List<InvenTreePartTestTemplate> testingTemplates = List<InvenTreePartTestTemplate>.empty();
|
||||
|
||||
int get testTemplateCount => testingTemplates.length;
|
||||
|
||||
// Request test templates from the serve
|
||||
Future<void> getTestTemplates(BuildContext context, {bool showDialog=false}) async {
|
||||
Future<void> 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<String> elements = List<String>();
|
||||
List<String> elements = List<String>.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();
|
||||
|
||||
|
@ -138,14 +138,13 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
// TODO
|
||||
}
|
||||
|
||||
List<InvenTreePartTestTemplate> testTemplates = List<InvenTreePartTestTemplate>();
|
||||
List<InvenTreePartTestTemplate> testTemplates = List<InvenTreePartTestTemplate>.empty();
|
||||
|
||||
int get testTemplateCount => testTemplates.length;
|
||||
|
||||
// Get all the test templates associated with this StockItem
|
||||
Future<void> getTestTemplates(BuildContext context, {bool showDialog=false}) async {
|
||||
Future<void> getTestTemplates({bool showDialog=false}) async {
|
||||
await InvenTreePartTestTemplate().list(
|
||||
context,
|
||||
filters: {
|
||||
"part": "${partId}",
|
||||
},
|
||||
@ -160,14 +159,13 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
List<InvenTreeStockItemTestResult> testResults = List<InvenTreeStockItemTestResult>();
|
||||
List<InvenTreeStockItemTestResult> testResults = List<InvenTreeStockItemTestResult>.empty();
|
||||
|
||||
int get testResultCount => testResults.length;
|
||||
|
||||
Future<void> getTestResults(BuildContext context) async {
|
||||
Future<void> getTestResults() async {
|
||||
|
||||
await InvenTreeStockItemTestResult().list(
|
||||
context,
|
||||
filters: {
|
||||
"stock_item": "${pk}",
|
||||
"user_detail": "true",
|
||||
@ -183,7 +181,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> uploadTestResult(BuildContext context, String testName, bool result, {String value, String notes, File attachment}) async {
|
||||
Future<bool> uploadTestResult(BuildContext context, String testName, bool result, {String? value, String? notes, File? attachment}) async {
|
||||
|
||||
Map<String, String> 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<bool> adjustStock(BuildContext context, String endpoint, double q, {String notes}) async {
|
||||
Future<bool> 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<bool> countStock(BuildContext context, double q, {String notes}) async {
|
||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/count", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> addStock(BuildContext context, double q, {String notes}) async {
|
||||
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> removeStock(BuildContext context, double q, {String notes}) async {
|
||||
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> transferStock(int location, {double quantity, String notes}) async {
|
||||
if (quantity == null) {} else
|
||||
if ((quantity < 0) || (quantity > this.quantity)) {
|
||||
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
|
||||
if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
|
||||
quantity = this.quantity;
|
||||
}
|
||||
|
||||
|
@ -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)!;
|
||||
}
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
||||
Subproject commit 05a5cbf63b4b5479162905def9fdadf21041212e
|
||||
Subproject commit 9cc07cdb0ec0012abcec827fc776d7c5473bb75e
|
@ -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,
|
||||
|
@ -15,15 +15,19 @@ class InvenTreePreferencesDB {
|
||||
|
||||
InvenTreePreferencesDB._();
|
||||
|
||||
Completer<Database> _dbOpenCompleter;
|
||||
Completer<Database> _dbOpenCompleter = Completer();
|
||||
|
||||
bool isOpen = false;
|
||||
|
||||
Future<Database> 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.
|
||||
|
@ -26,7 +26,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
|
||||
|
||||
List<UserProfile> profiles;
|
||||
List<UserProfile> profiles = new List<UserProfile>.empty();
|
||||
|
||||
_InvenTreeLoginSettingsState() {
|
||||
_reload();
|
||||
@ -40,14 +40,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
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<InvenTreeLoginSettingsWidget> {
|
||||
_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<InvenTreeLoginSettingsWidget> {
|
||||
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]://<server>:<port>",
|
||||
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<InvenTreeLoginSettingsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
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<InvenTreeLoginSettingsWidget> {
|
||||
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<InvenTreeLoginSettingsWidget> {
|
||||
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<InvenTreeLoginSettingsWidget> {
|
||||
// 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<InvenTreeLoginSettingsWidget> {
|
||||
|
||||
_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<InvenTreeLoginSettingsWidget> {
|
||||
_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)
|
||||
|
@ -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<String, dynamic> json, bool isSelected) => UserProfile(
|
||||
key: key,
|
||||
@ -122,7 +122,7 @@ class UserProfileDBManager {
|
||||
print("Deleted user profile <${profile.key}> - '${profile.name}'");
|
||||
}
|
||||
|
||||
Future<UserProfile> getSelectedProfile() async {
|
||||
Future<UserProfile?> getSelectedProfile() async {
|
||||
/*
|
||||
* Return the currently selected profile.
|
||||
*
|
||||
@ -133,7 +133,7 @@ class UserProfileDBManager {
|
||||
|
||||
final profiles = await store.find(await _db);
|
||||
|
||||
List<UserProfile> profileList = new List<UserProfile>();
|
||||
List<UserProfile> profileList = new List<UserProfile>.empty();
|
||||
|
||||
for (int idx = 0; idx < profiles.length; idx++) {
|
||||
|
||||
@ -158,7 +158,7 @@ class UserProfileDBManager {
|
||||
|
||||
final profiles = await store.find(await _db);
|
||||
|
||||
List<UserProfile> profileList = new List<UserProfile>();
|
||||
List<UserProfile> profileList = new List<UserProfile>.empty();
|
||||
|
||||
for (int idx = 0; idx < profiles.length; idx++) {
|
||||
|
||||
|
@ -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<CategoryDisplayWidget> {
|
||||
|
||||
void _editCategory(Map<String, String> 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<CategoryDisplayWidget> {
|
||||
|
||||
void _editCategoryDialog() {
|
||||
|
||||
// Cannot edit top-level category
|
||||
if (category == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var _name;
|
||||
var _description;
|
||||
|
||||
@ -108,12 +113,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
fields: <Widget>[
|
||||
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<CategoryDisplayWidget> {
|
||||
_CategoryDisplayState(this.category) {}
|
||||
|
||||
// The local InvenTreePartCategory object
|
||||
final InvenTreePartCategory category;
|
||||
final InvenTreePartCategory? category;
|
||||
|
||||
List<InvenTreePartCategory> _subcategories = List<InvenTreePartCategory>();
|
||||
List<InvenTreePartCategory> _subcategories = List<InvenTreePartCategory>.empty();
|
||||
|
||||
@override
|
||||
Future<void> onBuild(BuildContext context) async {
|
||||
@ -133,17 +138,17 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
Future<void> 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<CategoryDisplayWidget> {
|
||||
|
||||
List<Widget> 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<CategoryDisplayWidget> {
|
||||
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<CategoryDisplayWidget> {
|
||||
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<String, String> filters;
|
||||
|
||||
Function onTotalChanged;
|
||||
Function(int)? onTotalChanged;
|
||||
|
||||
PaginatedPartList(this.filters, {this.onTotalChanged});
|
||||
|
||||
@ -363,7 +370,7 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
|
||||
|
||||
String _searchTerm = "";
|
||||
|
||||
Function onTotalChanged;
|
||||
Function(int)? onTotalChanged;
|
||||
|
||||
final Map<String, String> filters;
|
||||
|
||||
@ -393,21 +400,21 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
|
||||
|
||||
Map<String, String> 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<InvenTreePart> 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<PaginatedPartList> {
|
||||
}
|
||||
|
||||
if (onTotalChanged != null) {
|
||||
onTotalChanged(pageCount);
|
||||
onTotalChanged!(pageCount);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@ -438,7 +445,7 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
|
||||
|
||||
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)));
|
||||
|
@ -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<CompanyDetailWidget> {
|
||||
String getAppBarTitle(BuildContext context) => L10().company;
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
await company.reload(context);
|
||||
Future<void> request() async {
|
||||
await company.reload();
|
||||
}
|
||||
|
||||
_CompanyDetailState(this.company) {
|
||||
@ -42,7 +42,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
void _saveCompany(Map<String, String> 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<CompanyDetailWidget> {
|
||||
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<CompanyDetailWidget> {
|
||||
|
||||
List<Widget> _companyTiles() {
|
||||
|
||||
var tiles = List<Widget>();
|
||||
var tiles = List<Widget>.empty();
|
||||
|
||||
bool sep = false;
|
||||
|
||||
|
@ -11,8 +11,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
abstract class CompanyListWidget extends StatefulWidget {
|
||||
|
||||
String title;
|
||||
Map<String, String> filters;
|
||||
String title = "";
|
||||
Map<String, String> filters = {};
|
||||
|
||||
@override
|
||||
_CompanyListState createState() => _CompanyListState(title, filters);
|
||||
@ -39,9 +39,9 @@ class CustomerListWidget extends CompanyListWidget {
|
||||
|
||||
class _CompanyListState extends RefreshableState<CompanyListWidget> {
|
||||
|
||||
var _companies = new List<InvenTreeCompany>();
|
||||
var _companies = new List<InvenTreeCompany>.empty();
|
||||
|
||||
var _filteredCompanies = new List<InvenTreeCompany>();
|
||||
var _filteredCompanies = new List<InvenTreeCompany>.empty();
|
||||
|
||||
String _title = "Companies";
|
||||
|
||||
@ -58,9 +58,9 @@ class _CompanyListState extends RefreshableState<CompanyListWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
Future<void> 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<CompanyListWidget> {
|
||||
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)));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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<void> confirmationDialog(String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async {
|
||||
Future<void> 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<void> 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<void> 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<void> confirmationDialog(String title, String text, {String acceptText, S
|
||||
}
|
||||
|
||||
|
||||
Future<void> showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info, Function onDismissed}) async {
|
||||
Future<void> 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: <Widget>[
|
||||
@ -83,16 +75,14 @@ Future<void> showInfoDialog(BuildContext context, String title, String descripti
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error, Function onDismissed}) async {
|
||||
Future<void> 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<void> showErrorDialog(String title, String description, {IconData icon =
|
||||
|
||||
Future<void> showServerError(String title, String description) async {
|
||||
|
||||
if (title == null || title.isEmpty) {
|
||||
if (title.isEmpty) {
|
||||
title = L10().serverError;
|
||||
}
|
||||
|
||||
@ -140,8 +130,6 @@ Future<void> showServerError(String title, String description) async {
|
||||
|
||||
Future<void> 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<void> showStatusCodeError(int status, {int expected = 200}) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTimeoutError(BuildContext context) async {
|
||||
Future<void> showTimeoutError() async {
|
||||
|
||||
// Use OneContext as "sometimes" context is null here?
|
||||
var ctx = OneContext().context;
|
||||
@ -182,42 +170,49 @@ Future<void> showTimeoutError(BuildContext context) async {
|
||||
await showServerError(L10().timeout, L10().noResponse);
|
||||
}
|
||||
|
||||
void showFormDialog(String title, {String acceptText, String cancelText, GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions, Function callback}) {
|
||||
void showFormDialog(String title, {String? acceptText, String? cancelText, GlobalKey<FormState>? key, List<Widget>? fields, List<Widget>? 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 = <Widget>[
|
||||
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<Widget>.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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -52,10 +52,10 @@ class ImagePickerField extends FormField<File> {
|
||||
|
||||
}
|
||||
|
||||
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<File> {
|
||||
return null;
|
||||
},
|
||||
builder: (FormFieldState<File> 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<File> {
|
||||
|
||||
|
||||
class CheckBoxField extends FormField<bool> {
|
||||
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<bool> {
|
||||
|
||||
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;
|
||||
|
@ -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<InvenTreeHomePage> {
|
||||
}
|
||||
|
||||
// Selected user profile
|
||||
UserProfile _profile;
|
||||
|
||||
BuildContext _context;
|
||||
UserProfile? _profile;
|
||||
|
||||
void _searchParts() {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
@ -113,7 +111,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
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<InvenTreeHomePage> {
|
||||
} 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<InvenTreeHomePage> {
|
||||
@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.
|
||||
//
|
||||
|
@ -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<LocationDisplayWidget> {
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
final InvenTreeStockLocation? location;
|
||||
|
||||
final _editLocationKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) { return "Stock Location"; }
|
||||
String getAppBarTitle(BuildContext context) { return L10().stockLocation; }
|
||||
|
||||
@override
|
||||
List<Widget> getAppBarActions(BuildContext context) {
|
||||
@ -80,12 +80,16 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
|
||||
void _editLocation(Map<String, String> 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<LocationDisplayWidget> {
|
||||
var _name;
|
||||
var _description;
|
||||
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFormDialog(L10().editLocation,
|
||||
key: _editLocationKey,
|
||||
callback: () {
|
||||
@ -106,12 +114,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
fields: <Widget> [
|
||||
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<LocationDisplayWidget> {
|
||||
|
||||
_LocationDisplayState(this.location) {}
|
||||
|
||||
List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>();
|
||||
List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>.empty();
|
||||
|
||||
String _locationFilter = '';
|
||||
|
||||
@ -139,17 +147,17 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
Future<void> 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<LocationDisplayWidget> {
|
||||
|
||||
List<Widget> 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<LocationDisplayWidget> {
|
||||
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<LocationDisplayWidget> {
|
||||
Map<String, String> filters = {};
|
||||
|
||||
if (location != null) {
|
||||
filters["location"] = "${location.pk}";
|
||||
filters["location"] = "${location!.pk}";
|
||||
}
|
||||
|
||||
switch (index) {
|
||||
@ -256,7 +268,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
).toList()
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
return ListView();
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,14 +320,19 @@ List<Widget> 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<PaginatedStockList> {
|
||||
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<InvenTreeStockItem> 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<PaginatedStockList> {
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
@ -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<PartDetailWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
await part.reload(context);
|
||||
await part.getTestTemplates(context);
|
||||
Future<void> 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<String, String> 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<PartDetailWidget> {
|
||||
* 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<PartDetailWidget> {
|
||||
|
||||
void _selectImage() {
|
||||
|
||||
File _attachment;
|
||||
File? _attachment;
|
||||
|
||||
if (!InvenTreeAPI().checkPermission('part', 'change')) {
|
||||
return;
|
||||
@ -261,7 +265,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
}
|
||||
|
||||
// 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<PartDetailWidget> {
|
||||
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<PartDetailWidget> {
|
||||
)
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
return Center();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -11,7 +11,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
final refreshableKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
// 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<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
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<T extends StatefulWidget> extends State<T> {
|
||||
}
|
||||
|
||||
// Function to request data for this page
|
||||
Future<void> request(BuildContext context) async {
|
||||
Future<void> request() async {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
loading = true;
|
||||
});
|
||||
|
||||
await request(context);
|
||||
await request();
|
||||
|
||||
setState(() {
|
||||
loading = false;
|
||||
@ -77,14 +77,16 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
// 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<T extends StatefulWidget> extends State<T> {
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
// Save the context for future use
|
||||
this.context = context;
|
||||
_context = context;
|
||||
|
||||
return Scaffold(
|
||||
key: refreshableKey,
|
||||
|
@ -15,25 +15,21 @@ import '../api.dart';
|
||||
|
||||
// TODO - Refactor duplicate code in this file!
|
||||
|
||||
class PartSearchDelegate extends SearchDelegate<InvenTreePart> {
|
||||
class PartSearchDelegate extends SearchDelegate<InvenTreePart?> {
|
||||
|
||||
final partSearchKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
BuildContext context;
|
||||
|
||||
// What did we search for last time?
|
||||
String _cachedQuery;
|
||||
String _cachedQuery = "";
|
||||
|
||||
bool _searching = false;
|
||||
|
||||
// Custom filters for the part search
|
||||
Map<String, String> 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<InvenTreePart> {
|
||||
|
||||
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<InvenTreePart> {
|
||||
),
|
||||
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<InvenTreePart> {
|
||||
}
|
||||
|
||||
|
||||
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> {
|
||||
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem?> {
|
||||
|
||||
final stockSearchKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
String _cachedQuery;
|
||||
String _cachedQuery = "";
|
||||
|
||||
bool _searching = false;
|
||||
|
||||
// Custom filters for the stock item search
|
||||
Map<String, String> filters;
|
||||
Map<String, String>? filters;
|
||||
|
||||
StockSearchDelegate(this.context, {this.filters}) {
|
||||
if (filters == null) {
|
||||
@ -247,7 +243,9 @@ class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> {
|
||||
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<InvenTreeStockItem> {
|
||||
|
||||
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<InvenTreeStockItem> {
|
||||
),
|
||||
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,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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<Spinner> with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Widget _child;
|
||||
AnimationController? _controller;
|
||||
Widget? _child;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -40,14 +40,14 @@ class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_controller!.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RotationTransition(
|
||||
turns: _controller,
|
||||
turns: _controller!,
|
||||
child: _child,
|
||||
);
|
||||
}
|
||||
|
@ -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<StarredPartWidget> {
|
||||
String getAppBarTitle(BuildContext context) => L10().partsStarred;
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
Future<void> 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<StarredPartWidget> {
|
||||
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,
|
||||
|
@ -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<StockDetailWidget> {
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
// Part object
|
||||
InvenTreePart part;
|
||||
InvenTreePart? part;
|
||||
|
||||
@override
|
||||
Future<void> onBuild(BuildContext context) async {
|
||||
@ -89,14 +89,14 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
await item.reload(context);
|
||||
Future<void> 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<StockDetailWidget> {
|
||||
|
||||
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<StockDetailWidget> {
|
||||
}
|
||||
|
||||
|
||||
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<StockDetailWidget> {
|
||||
|
||||
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: <Widget>[
|
||||
QuantityField(
|
||||
@ -292,7 +296,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
)
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
var suggestions = List<InvenTreeStockLocation>();
|
||||
var suggestions = List<InvenTreeStockLocation>.empty();
|
||||
|
||||
for (var loc in locations) {
|
||||
if (loc.matchAgainstString(pattern)) {
|
||||
@ -311,7 +315,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
selectedLocation = suggestion as InvenTreeStockLocation;
|
||||
_selectedController.text = selectedLocation.pathstring;
|
||||
_selectedController.text = selectedLocation!.pathstring;
|
||||
},
|
||||
onSaved: (value) {
|
||||
},
|
||||
@ -342,7 +346,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
),
|
||||
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<StockDetailWidget> {
|
||||
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<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
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<StockDetailWidget> {
|
||||
).toList()
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
return ListView();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<StockItemTestRes
|
||||
String getAppBarTitle(BuildContext context) => L10().testResults;
|
||||
|
||||
@override
|
||||
Future<void> request(BuildContext context) async {
|
||||
await item.getTestTemplates(context);
|
||||
await item.getTestResults(context);
|
||||
Future<void> 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<StockItemTestRes
|
||||
|
||||
void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
|
||||
|
||||
String _name;
|
||||
bool _result;
|
||||
String _value;
|
||||
String _notes;
|
||||
File _attachment;
|
||||
String _name = "";
|
||||
bool _result = false;
|
||||
String _value = "";
|
||||
String _notes = "";
|
||||
File? _attachment;
|
||||
|
||||
showFormDialog(L10().testResultAdd,
|
||||
key: _addResultKey,
|
||||
@ -80,21 +80,21 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
label: L10().testName,
|
||||
initial: name,
|
||||
isEnabled: nameIsEditable,
|
||||
onSaved: (value) => _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<StockItemTestRes
|
||||
StringField(
|
||||
allowEmpty: true,
|
||||
label: L10().notes,
|
||||
onSaved: (value) => _notes = value,
|
||||
onSaved: (value) => _notes = value ?? '',
|
||||
),
|
||||
]
|
||||
);
|
||||
@ -202,10 +202,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
for (var item in results) {
|
||||
|
||||
bool _required = false;
|
||||
String _test;
|
||||
bool _result = null;
|
||||
String _value;
|
||||
String _notes;
|
||||
String _test = "";
|
||||
bool _result = false;
|
||||
String _value = "";
|
||||
String _notes = "";
|
||||
|
||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1));
|
||||
bool _valueRequired = false;
|
||||
bool _attachmentRequired = false;
|
||||
|
@ -10,7 +10,7 @@ class StockNotesWidget extends StatefulWidget {
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
StockNotesWidget(this.item, {Key key}) : super(key: key);
|
||||
StockNotesWidget(this.item, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_StockNotesState createState() => _StockNotesState(item);
|
||||
|
10
pubspec.yaml
10
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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user