diff --git a/lib/api.dart b/lib/api.dart index 6d272520..b2d8eaeb 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -182,15 +182,22 @@ class InvenTreeAPI { var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { if (error is SocketException) { - errorMessage = "Could not connect to server."; - print(errorMessage); - throw errorMessage; + print("Could not connect to server"); + return null; + } else if (error is TimeoutException) { + print("Server timeout"); + return null; } else { // Unknown error type, re-throw error + print("Unknown error: ${error.toString()}"); throw error; } }); + if (response == null) { + return false; + } + if (response.statusCode != 200) { print("Invalid status code: " + response.statusCode.toString()); return false; diff --git a/lib/barcode.dart b/lib/barcode.dart index 975fd088..44c173b9 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -1,3 +1,4 @@ +import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:qr_utils/qr_utils.dart'; @@ -5,6 +6,8 @@ import 'package:qr_utils/qr_utils.dart'; import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; +import 'package:InvenTree/api.dart'; + import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/category_display.dart'; @@ -12,59 +15,139 @@ import 'package:InvenTree/widget/stock_detail.dart'; import 'dart:convert'; -void scanQrCode(BuildContext context) async { +Future scanQrCode(BuildContext context) async { - QrUtils.scanQR.then((String result) { + QrUtils.scanQR.then((String barcode) { - print("Scanned: $result"); + print("Scanned: $barcode"); - // Look for JSON data in the result... - final data = json.decode(result); + showProgressDialog(context, "Querying Server", "Sending barcode data to server"); - // Look for an 'InvenTree' style barcode - if ((data['tool'] ?? '').toString().toLowerCase() == 'inventree') { - _handleInvenTreeBarcode(context, data); - } + /* + * POST the scanned barcode data to the server. + * It is the responsibility of the server to validate and sanitize the barcode data, + * and return a "common" response that we know how to deal with. + */ + InvenTreeAPI().post("barcode/", body: {"barcode": barcode}).then((var response) { - // Unknown barcode style! - else { - showDialog( - context: context, - child: new SimpleDialog( - title: new Text("Unknown barcode"), - children: [ - Text("Data: $result"), - ] - ) - ); - } + hideProgressDialog(context); + if (response.statusCode != 200) { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Server Error"), + children: [ + ListTile( + title: Text("Error ${response.statusCode}"), + subtitle: Text("${response.body.toString().split("\n").first}"), + ) + ], + ), + ); + + return; + } + + final Map body = json.decode(response.body); + + // TODO - Handle potential error decoding response + + print("Barcode response:"); + print(body.toString()); + + if (body.containsKey('error')) { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Barcode Error"), + children: [ + ListTile( + title: Text("${body['error']}"), + subtitle: Text("Plugin: ${body['plugin'] ?? ''}"), + ) + ], + ) + ); + } else if (body.containsKey('success')) { + // Decode the barcode! + // Ideally, the server has returned unto us something sensible... + _handleBarcode(context, body); + } else { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Unknown response"), + children: [ + ListTile( + title: Text("Response data"), + subtitle: Text("${body.toString()}"), + ) + ], + ) + ); + } + + print("body: ${body.toString()}"); + + }); }); } -void _handleInvenTreeBarcode(BuildContext context, Map data) { +void _handleBarcode(BuildContext context, Map data) { - final String codeType = (data['type'] ?? '').toString().toLowerCase(); + int pk; - final int pk = (data['id'] ?? -1) as int; + // A stocklocation has been passed? + if (data.containsKey('stocklocation')) { - if (codeType == 'stocklocation') { + pk = data['stocklocation']['pk'] as int ?? null; - // Try to open a stock location... - InvenTreeStockLocation().get(pk).then((var loc) { - if (loc is InvenTreeStockLocation) { - Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); - } - }); + if (pk != null) { + InvenTreeStockLocation().get(context, pk).then((var loc) { + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + } + }); + } else { + // TODO - Show an error here! + } - } else if (codeType == 'stockitem') { - InvenTreeStockItem().get(pk).then((var item) { - Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); - }); - } else if (codeType == 'part') { - InvenTreePart().get(pk).then((var part) { - Navigator.push(context, - MaterialPageRoute(builder: (context) => PartDetailWidget(part))); - }); + } else if (data.containsKey('stockitem')) { + + pk = data['stockitem']['pk'] as int ?? null; + + if (pk != null) { + InvenTreeStockItem().get(context, pk).then((var item) { + Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); + }); + } else { + // TODO - Show an error here! + } + } else if (data.containsKey('part')) { + + pk = data['part']['pk'] as int ?? null; + + if (pk != null) { + InvenTreePart().get(context, pk).then((var part) { + Navigator.push(context, + MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + }); + } else { + // TODO - Show an error here! + } + } else { + showDialog( + context: context, + child: SimpleDialog( + title: Text("Unknown response"), + children: [ + ListTile( + title: Text("Response data"), + subtitle: Text(data.toString()), + ) + ], + ) + ); } } \ No newline at end of file diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index 74ff87c8..7958d7f3 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -5,6 +5,10 @@ import 'model.dart'; * The InvenTreeCompany class repreents the Company model in the InvenTree database. */ class InvenTreeCompany extends InvenTreeModel { + + @override + String NAME = "Company"; + @override String URL = "company/"; diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index a501ce7a..96847ee6 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + import 'package:InvenTree/api.dart'; +import 'package:InvenTree/widget/dialogs.dart'; +import 'package:flutter/cupertino.dart'; import 'dart:convert'; @@ -15,6 +19,8 @@ class InvenTreeModel { // Override the endpoint URL for each subclass String URL = ""; + String NAME = "Model"; + // JSON data which defines this object Map jsondata = {}; @@ -83,9 +89,30 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - Future reload() async { + Future reload(BuildContext context) async { - var response = await api.get(url, params: defaultGetFilters()); + showProgressDialog(context, "Refreshing data", "Refreshing data for ${NAME}"); + + var response = await api.get(url, params: defaultGetFilters()) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + + return null; + }); + + if (response == null) { + return false; + } + + hideProgressDialog(context); if (response.statusCode != 200) { print("Error retrieving data"); @@ -100,7 +127,7 @@ class InvenTreeModel { } // Return the detail view for the associated pk - Future get(int pk, {Map filters}) async { + Future get(BuildContext context, int pk, {Map filters}) async { // TODO - Add "timeout" // TODO - Add error catching @@ -122,7 +149,27 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); - var response = await api.get(addr, params: params); + showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + + var response = await api.get(addr, params: params) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + return null; + }); + + if (response == null) { + return null; + } + + hideProgressDialog(context); if (response.statusCode != 200) { print("Error retrieving data"); @@ -135,7 +182,7 @@ class InvenTreeModel { } // Return list of objects from the database, with optional filters - Future> list({Map filters}) async { + Future> list(BuildContext context, {Map filters}) async { if (filters == null) { filters = {}; @@ -154,7 +201,28 @@ class InvenTreeModel { // TODO - Add "timeout" // TODO - Add error catching - var response = await api.get(URL, params:params); + showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + + var response = await api.get(URL, params:params) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + + return null; + }); + + if (response == null) { + return null; + } + + hideProgressDialog(context); // A list of "InvenTreeModel" items List results = new List(); diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 3283a19c..d3b3e269 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -9,6 +9,10 @@ import 'package:path/path.dart' as path; import 'package:http/http.dart' as http; class InvenTreePartCategory extends InvenTreeModel { + + @override + String NAME = "PartCategory"; + @override String URL = "part/category/"; @@ -61,6 +65,9 @@ class InvenTreePartCategory extends InvenTreeModel { class InvenTreePart extends InvenTreeModel { + @override + String NAME = "Part"; + @override String URL = "part/"; diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index e8cfcf22..ebcef711 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -6,6 +6,10 @@ import 'model.dart'; import 'package:InvenTree/api.dart'; class InvenTreeStockItem extends InvenTreeModel { + + @override + String NAME = "StockItem"; + @override String URL = "stock/"; @@ -208,6 +212,10 @@ class InvenTreeStockItem extends InvenTreeModel { class InvenTreeStockLocation extends InvenTreeModel { + + @override + String NAME = "StockLocation"; + @override String URL = "stock/location/"; diff --git a/lib/main.dart b/lib/main.dart index 4dd424d9..991d2452 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,8 @@ import 'preferences.dart'; import 'package:InvenTree/inventree/part.dart'; + + void main() async { // await PrefService.init(prefix: "inventree_"); @@ -31,10 +33,10 @@ void main() async { // Load login details InvenTreePreferences().loadLoginDetails(); - runApp(MyApp()); + runApp(InvenTreeApp()); } -class MyApp extends StatelessWidget { +class InvenTreeApp extends StatelessWidget { // This widget is the root of your application. @override @@ -42,16 +44,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'InvenTree', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.lightGreen, + primarySwatch: Colors.lightBlue, + secondaryHeaderColor: Colors.blueGrey, ), home: MyHomePage(title: 'InvenTree'), ); @@ -59,34 +53,6 @@ class MyApp extends StatelessWidget { } -class ProductList extends StatelessWidget { - final List _parts; - - ProductList(this._parts); - - Widget _buildPart(BuildContext context, int index) { - InvenTreePart part; - - if (index < _parts.length) { - part = _parts[index]; - } - - return Card( - child: Column( - children: [ - Text('${part.name} - ${part.description}'), - ] - ) - ); - } - - @override - Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _buildPart, itemCount: _parts.length); - } -} - - class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); @@ -168,22 +134,6 @@ class _MyHomePageState extends State { onConnectFailure("Could not connect to server"); } - }).catchError((e) { - - String fault = "Connection error"; - - _serverConnection = false; - _serverStatusColor = Color.fromARGB(255, 250, 50, 50); - - _serverStatus = "Error connecting to $_serverAddress"; - - if (e is TimeoutException) { - fault = "Timeout: No response from server"; - } else { - fault = e.toString(); - } - - onConnectFailure(fault); }); // Update widget state diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 7f7f47d2..6832b212 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -5,6 +5,7 @@ import 'package:InvenTree/preferences.dart'; import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -26,11 +27,12 @@ class CategoryDisplayWidget extends StatefulWidget { } -class _CategoryDisplayState extends State { +class _CategoryDisplayState extends RefreshableState { - _CategoryDisplayState(this.category) { - _requestData(); - } + @override + String getAppBarTitle(BuildContext context) { return "Part Category"; } + + _CategoryDisplayState(this.category) {} // The local InvenTreePartCategory object final InvenTreePartCategory category; @@ -39,24 +41,18 @@ class _CategoryDisplayState extends State { List _parts = List(); - String get _titleString { - - if (category == null) { - return "Part Categories"; - } else { - return "Part Category - ${category.name}"; - } + @override + Future onBuild(BuildContext context) async { + refresh(); } - /* - * Request data from the server - */ - void _requestData() { + @override + Future request(BuildContext context) async { int pk = category?.pk ?? -1; // Request a list of sub-categories under this one - InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) { + InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) { _subcategories.clear(); for (var cat in cats) { @@ -70,7 +66,7 @@ class _CategoryDisplayState extends State { }); // Request a list of parts under this category - InvenTreePart().list(filters: {"category": "$pk"}).then((var parts) { + InvenTreePart().list(context, filters: {"category": "$pk"}).then((var parts) { _parts.clear(); for (var part in parts) { @@ -112,7 +108,7 @@ class _CategoryDisplayState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } else { // TODO - Refactor this code into the InvenTreePart class - InvenTreePartCategory().get(category.parentId).then((var cat) { + InvenTreePartCategory().get(context, category.parentId).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); } @@ -127,74 +123,68 @@ class _CategoryDisplayState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(_titleString), - ), - drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - getCategoryDescriptionCard(), - ExpansionPanelList( - expansionCallback: (int index, bool isExpanded) { - setState(() { + Widget getBody(BuildContext context) { + return ListView( + children: [ + getCategoryDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { - switch (index) { - case 0: - InvenTreePreferences().expandCategoryList = !isExpanded; - break; - case 1: - InvenTreePreferences().expandPartList = !isExpanded; - break; - default: - break; - } - }); + switch (index) { + case 0: + InvenTreePreferences().expandCategoryList = !isExpanded; + break; + case 1: + InvenTreePreferences().expandPartList = !isExpanded; + break; + default: + break; + } + }); + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Subcategories"), + leading: FaIcon(FontAwesomeIcons.stream), + trailing: Text("${_subcategories.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandCategoryList = !InvenTreePreferences().expandCategoryList; + }); + }, + onLongPress: () { + // TODO - Context menu for e.g. creating a new PartCategory + }, + ); }, - children: [ - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Subcategories"), - leading: FaIcon(FontAwesomeIcons.stream), - trailing: Text("${_subcategories.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandCategoryList = !InvenTreePreferences().expandCategoryList; - }); - }, - onLongPress: () { - // TODO - Context menu for e.g. creating a new PartCategory - }, - ); - }, - body: SubcategoryList(_subcategories), - isExpanded: InvenTreePreferences().expandCategoryList && _subcategories.length > 0, - ), - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Parts"), - leading: FaIcon(FontAwesomeIcons.shapes), - trailing: Text("${_parts.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandPartList = !InvenTreePreferences().expandPartList; - }); - }, - onLongPress: () { - // TODO - Context menu for e.g. creating a new Part - }, - ); - }, - body: PartList(_parts), - isExpanded: InvenTreePreferences().expandPartList && _parts.length > 0, - ) - ], + body: SubcategoryList(_subcategories), + isExpanded: InvenTreePreferences().expandCategoryList && _subcategories.length > 0, ), - ] - ) + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Parts"), + leading: FaIcon(FontAwesomeIcons.shapes), + trailing: Text("${_parts.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandPartList = !InvenTreePreferences().expandPartList; + }); + }, + onLongPress: () { + // TODO - Context menu for e.g. creating a new Part + }, + ); + }, + body: PartList(_parts), + isExpanded: InvenTreePreferences().expandPartList && _parts.length > 0, + ) + ], + ), + ] ); } } @@ -211,7 +201,7 @@ class SubcategoryList extends StatelessWidget { void _openCategory(BuildContext context, int pk) { // Attempt to load the sub-category. - InvenTreePartCategory().get(pk).then((var cat) { + InvenTreePartCategory().get(context, pk).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); @@ -252,7 +242,7 @@ class PartList extends StatelessWidget { void _openPart(BuildContext context, int pk) { // Attempt to load the part information - InvenTreePart().get(pk).then((var part) { + InvenTreePart().get(context, pk).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index 56d16d7e..16201d5a 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -2,6 +2,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/inventree/company.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -18,10 +19,18 @@ class CompanyDetailWidget extends StatefulWidget { } -class _CompanyDetailState extends State { +class _CompanyDetailState extends RefreshableState { final InvenTreeCompany company; + @override + String getAppBarTitle(BuildContext context) { return "Company"; } + + @override + Future request(BuildContext context) async { + await company.reload(context); + } + _CompanyDetailState(this.company) { // TODO } @@ -122,17 +131,11 @@ class _CompanyDetailState extends State { } @override - Widget build(BuildContext context) { + Widget getBody(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("${company.name}"), - ), - drawer: new InvenTreeDrawer(context), - body: Center( - child: ListView( - children: _companyTiles(), - ) + return Center( + child: ListView( + children: _companyTiles(), ) ); } diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index 512c198f..a627ee74 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/inventree/company.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; abstract class CompanyListWidget extends StatefulWidget { @@ -30,23 +31,30 @@ class CustomerListWidget extends CompanyListWidget { } -class _CompanyListState extends State { +class _CompanyListState extends RefreshableState { var _companies = new List(); var _filteredCompanies = new List(); - var _title = "Companies"; + String _title = "Companies"; + + @override + String getAppBarTitle(BuildContext context) { return _title; } Map _filters = Map(); - _CompanyListState(this._title, this._filters) { - _requestData(); + _CompanyListState(this._title, this._filters) {} + + @override + Future onBuild(BuildContext context) async { + refresh(); } - void _requestData() { + @override + Future request(BuildContext context) async { - InvenTreeCompany().list(filters: _filters).then((var companies) { + InvenTreeCompany().list(context, filters: _filters).then((var companies) { _companies.clear(); @@ -85,7 +93,7 @@ class _CompanyListState extends State { ), onTap: () { if (company.pk > 0) { - InvenTreeCompany().get(company.pk).then((var c) { + InvenTreeCompany().get(context, company.pk).then((var c) { Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c))); }); } @@ -94,38 +102,24 @@ class _CompanyListState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("$_title"), - actions: [ - IconButton( - icon: FaIcon(FontAwesomeIcons.plus), - tooltip: 'New', - onPressed: null, - ) - ], + Widget getBody(BuildContext context) { + return ListView( + children: [ + TextField( + decoration: InputDecoration( + hintText: 'Filter results', + ), + onChanged: (String text) { + setState(() { + _filterResults(text); + }); + }, ), - drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - TextField( - decoration: InputDecoration( - hintText: 'Filter results', - ), - onChanged: (String text) { - setState(() { - _filterResults(text); - }); - }, - ), - ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: _showCompany, itemCount: _filteredCompanies.length) - ], - ) + ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: _showCompany, itemCount: _filteredCompanies.length) + ], ); } - } \ No newline at end of file diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart new file mode 100644 index 00000000..ceb94158 --- /dev/null +++ b/lib/widget/dialogs.dart @@ -0,0 +1,41 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +void showErrorDialog(BuildContext context, String title, String description) { + showDialog( + context: context, + child: SimpleDialog( + title: ListTile( + title: Text("Error"), + leading: FaIcon(FontAwesomeIcons.exclamationCircle), + ), + children: [ + ListTile( + title: Text(title), + subtitle: Text(description) + ) + ] + ) + ); +} + +void showProgressDialog(BuildContext context, String title, String description) { + + showDialog( + context: context, + barrierDismissible: false, + child: SimpleDialog( + title: Text(title), + children: [ + CircularProgressIndicator(), + Text(description), + ], + ) + ); +} + +void hideProgressDialog(BuildContext context) { + Navigator.pop(context); +} \ No newline at end of file diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index fa1456ac..f8ab0c8d 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -8,6 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; + class LocationDisplayWidget extends StatefulWidget { LocationDisplayWidget(this.location, {Key key}) : super(key: key); @@ -20,15 +22,15 @@ class LocationDisplayWidget extends StatefulWidget { _LocationDisplayState createState() => _LocationDisplayState(location); } - -class _LocationDisplayState extends State { - - _LocationDisplayState(this.location) { - _requestData(); - } +class _LocationDisplayState extends RefreshableState { final InvenTreeStockLocation location; + @override + String getAppBarTitle(BuildContext context) { return "Stock Location"; } + + _LocationDisplayState(this.location) {} + List _sublocations = List(); String _locationFilter = ''; @@ -44,28 +46,18 @@ class _LocationDisplayState extends State { List _items = List(); - String get _title { - - if (location == null) { - return "Stock Locations"; - } else { - return "Stock Location - ${location.name}"; - } + @override + Future onBuild(BuildContext context) async { + refresh(); } - /* - * Request data from the server. - * It will be displayed once loaded - * - * - List of sublocations under this one - * - List of stock items at this location - */ - void _requestData() { + @override + Future request(BuildContext context) async { int pk = location?.pk ?? -1; // Request a list of sub-locations under this one - InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) { + InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) { _sublocations.clear(); for (var loc in locs) { @@ -76,18 +68,18 @@ class _LocationDisplayState extends State { setState(() {}); - // Request a list of stock-items under this one - InvenTreeStockItem().list(filters: {"location": "$pk"}).then((var items) { - _items.clear(); + // Request a list of stock-items under this one + InvenTreeStockItem().list(context, filters: {"location": "$pk"}).then((var items) { + _items.clear(); - for (var item in items) { - if (item is InvenTreeStockItem) { - _items.add(item); + for (var item in items) { + if (item is InvenTreeStockItem) { + _items.add(item); + } } - } - setState(() {}); - }); + setState(() {}); + }); }); } @@ -119,7 +111,7 @@ class _LocationDisplayState extends State { if (location.parentId < 0) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } else { - InvenTreeStockLocation().get(location.parentId).then((var loc) { + InvenTreeStockLocation().get(context, location.parentId).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } @@ -134,68 +126,63 @@ class _LocationDisplayState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(_title), - ), - drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - locationDescriptionCard(), - ExpansionPanelList( - expansionCallback: (int index, bool isExpanded) { - setState(() { - switch (index) { - case 0: - InvenTreePreferences().expandLocationList = !isExpanded; - break; - case 1: - InvenTreePreferences().expandStockList = !isExpanded; - break; - default: - break; - } - }); + Widget getBody(BuildContext context) { - }, - children: [ - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Sublocations"), - leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), - trailing: Text("${_sublocations.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; - }); - }, - ); - }, - body: SublocationList(_sublocations), - isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, - ), - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Stock Items"), - leading: FaIcon(FontAwesomeIcons.boxes), - trailing: Text("${_items.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; - }); - }, - ); - }, - body: StockList(_items), - isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, - ) - ] - ), - ] - ) + return ListView( + children: [ + locationDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + switch (index) { + case 0: + InvenTreePreferences().expandLocationList = !isExpanded; + break; + case 1: + InvenTreePreferences().expandStockList = !isExpanded; + break; + default: + break; + } + }); + + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Sublocations"), + leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), + trailing: Text("${_sublocations.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; + }); + }, + ); + }, + body: SublocationList(_sublocations), + isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Stock Items"), + leading: FaIcon(FontAwesomeIcons.boxes), + trailing: Text("${_items.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; + }); + }, + ); + }, + body: StockList(_items), + isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, + ) + ] + ), + ] ); } } @@ -208,7 +195,7 @@ class SublocationList extends StatelessWidget { void _openLocation(BuildContext context, int pk) { - InvenTreeStockLocation().get(pk).then((var loc) { + InvenTreeStockLocation().get(context, pk).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); @@ -244,7 +231,7 @@ class StockList extends StatelessWidget { StockList(this._items); void _openItem(BuildContext context, int pk) { - InvenTreeStockItem().get(pk).then((var item) { + InvenTreeStockItem().get(context, pk).then((var item) { if (item is InvenTreeStockItem) { Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index e207db40..4efc7a69 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:InvenTree/widget/drawer.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -21,7 +22,10 @@ class PartDetailWidget extends StatefulWidget { } -class _PartDisplayState extends State { +class _PartDisplayState extends RefreshableState { + + @override + String getAppBarTitle(BuildContext context) { return "Part"; } _PartDisplayState(this.part) { // TODO @@ -29,6 +33,11 @@ class _PartDisplayState extends State { InvenTreePart part; + @override + Future request(BuildContext context) async { + await part.reload(context); + } + /* * Build a list of tiles to display under the part description */ @@ -62,7 +71,7 @@ class _PartDisplayState extends State { leading: FaIcon(FontAwesomeIcons.stream), onTap: () { if (part.categoryId > 0) { - InvenTreePartCategory().get(part.categoryId).then((var cat) { + InvenTreePartCategory().get(context, part.categoryId).then((var cat) { Navigator.push(context, MaterialPageRoute( builder: (context) => CategoryDisplayWidget(cat))); }); @@ -154,22 +163,11 @@ class _PartDisplayState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Part Details"), + Widget getBody(BuildContext context) { + return Center( + child: ListView( + children: partTiles(), ), - drawer: new InvenTreeDrawer(context), - floatingActionButton: FloatingActionButton( - child: FaIcon(FontAwesomeIcons.ellipsisH), - // TODO - Add pop-up icons - // Ref: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722 - ), - body: Center( - child: ListView( - children: partTiles(), - ), - ) ); } } \ No newline at end of file diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart new file mode 100644 index 00000000..b0b9ad3c --- /dev/null +++ b/lib/widget/refreshable_state.dart @@ -0,0 +1,66 @@ +import 'package:InvenTree/widget/drawer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:InvenTree/widget/drawer.dart'; + + +abstract class RefreshableState extends State { + + // Storage for context once "Build" is called + BuildContext context; + + String getAppBarTitle(BuildContext context) { return "App Bar Title"; } + + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => onBuild(context)); + } + + // Function called after the widget is first build + Future onBuild(BuildContext context) async { + return; + } + + // Function to request data for this page + Future request(BuildContext context) async { + return; + } + + Future refresh() async { + await request(context); + setState(() {}); + } + + // Function to construct an appbar (override if needed) + AppBar getAppBar(BuildContext context) { + return AppBar( + title: Text(getAppBarTitle(context)) + ); + } + + // Function to construct a drawer (override if needed) + Widget getDrawer(BuildContext context) { + return InvenTreeDrawer(context); + } + + // Function to construct a body (MUST BE PROVIDED) + Widget getBody(BuildContext context); + + @override + Widget build(BuildContext context) { + + // Save the context for future use + this.context = context; + + return Scaffold( + appBar: getAppBar(context), + drawer: getDrawer(context), + body: RefreshIndicator( + onRefresh: refresh, + child: getBody(context) + ) + ); + } +} \ No newline at end of file diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index a76873e5..e77e6cc8 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -4,12 +4,15 @@ import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/part_detail.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; + import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; @@ -25,7 +28,10 @@ class StockDetailWidget extends StatefulWidget { } -class _StockItemDisplayState extends State { +class _StockItemDisplayState extends RefreshableState { + + @override + String getAppBarTitle(BuildContext context) { return "Stock Item"; } final TextEditingController _quantityController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); @@ -42,13 +48,9 @@ class _StockItemDisplayState extends State { final InvenTreeStockItem item; - /** - * Function to reload the page data - */ - Future _refresh() async { - - await item.reload(); - setState(() {}); + @override + Future request(BuildContext context) async { + await item.reload(context); } void _editStockItem() { @@ -92,7 +94,7 @@ class _StockItemDisplayState extends State { _notesController.clear(); // TODO - Handle error cases - _refresh(); + refresh(); } void _addStockDialog() async { @@ -158,7 +160,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - _refresh(); + refresh(); } void _removeStockDialog() { @@ -227,7 +229,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - _refresh(); + refresh(); } void _countStockDialog() async { @@ -322,7 +324,7 @@ class _StockItemDisplayState extends State { leading: FaIcon(FontAwesomeIcons.shapes), onTap: () { if (item.partId > 0) { - InvenTreePart().get(item.partId).then((var part) { + InvenTreePart().get(context, item.partId).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); } @@ -362,7 +364,7 @@ class _StockItemDisplayState extends State { leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), onTap: () { if (item.locationId > 0) { - InvenTreeStockLocation().get(item.locationId).then((var loc) { + InvenTreeStockLocation().get(context, item.locationId).then((var loc) { Navigator.push(context, MaterialPageRoute( builder: (context) => LocationDisplayWidget(loc))); }); @@ -464,26 +466,30 @@ class _StockItemDisplayState extends State { return buttons; } + @override + Widget getBody(BuildContext context) { + return ListView( + children: stockTiles() + ); + } + @override Widget build(BuildContext context) { + + this.context = context; + return Scaffold( - appBar: AppBar( - title: Text("Stock Item"), - ), - drawer: new InvenTreeDrawer(context), + appBar: getAppBar(context), + drawer: getDrawer(context), floatingActionButton: SpeedDial( visible: true, animatedIcon: AnimatedIcons.menu_close, heroTag: 'stock-item-fab', children: actionButtons(), ), - body: Center( - child: new RefreshIndicator( - onRefresh: _refresh, - child: ListView( - children: stockTiles(), - ) - ) + body: RefreshIndicator( + onRefresh: refresh, + child: getBody(context) ) ); }