From 06446998b25f12742185b4a67d45d3a7f843b29f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 15:00:05 +1000 Subject: [PATCH 01/14] Improve API image fetcher --- lib/api.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 169535ec..a02303f3 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -304,10 +304,17 @@ class InvenTreeAPI { imageUrl = staticImage; } - return new AdvancedNetworkImage(makeUrl(imageUrl), + String url = makeUrl(imageUrl); + + return new AdvancedNetworkImage(url, header: defaultHeaders(), useDiskCache: true, - cacheRule: CacheRule(maxAge: const Duration(days: 5)), + //retryDuration: const Duration(seconds: 2), + //retryLimit: 3, + cacheRule: CacheRule(maxAge: const Duration(days: 1)), + loadFailedCallback: () { + DiskCache().evict(url); + } ); } } \ No newline at end of file From f84ba65f6c3413c5e18e27faa485bd529e18c42d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 15:00:27 +1000 Subject: [PATCH 02/14] Some more value getters for Part model --- lib/inventree/model.dart | 2 ++ lib/inventree/part.dart | 55 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 8bf3bee0..5f8c651d 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -43,6 +43,8 @@ class InvenTreeModel { int get parentId => jsondata['parent'] ?? -1; + String get link => jsondata['URL'] ?? ''; + // Create a new object from JSON data (not a constructor!) InvenTreeModel createFromJson(Map json) { diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 5c9403c2..d881d9ff 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:InvenTree/api.dart'; import 'model.dart'; @@ -34,14 +36,63 @@ class InvenTreePart extends InvenTreeModel { @override String URL = "part/"; - int get categoryId => jsondata['category'] as int ?? -1; + // Get the number of stock on order for this Part + double get onOrder => double.tryParse(jsondata['on_order'].toString() ?? '0'); - String get categoryName => jsondata['category__name'] ?? ''; + // Get the stock count for this Part + double get inStock => double.tryParse(jsondata['total_stock'].toString() ?? '0'); + // Get the number of units being build for this Part + double get building => double.tryParse(jsondata['building'].toString() ?? '0'); + + bool get isAssembly => jsondata['assembly'] ?? false; + + bool get isComponent => jsondata['component'] ?? false; + + bool get isPurchaseable => jsondata['purchaseable'] ?? false; + + bool get isSaleable => jsondata['saleable'] ?? false; + + bool get isActive => jsondata['active'] ?? false; + + bool get isVirtual => jsondata['virtual'] ?? false; + + // Get the IPN (internal part number) for the Part instance + String get IPN => jsondata['IPN'] as String ?? ''; + + // Get the revision string for the Part instance + String get revision => jsondata['revision'] as String ?? ''; + + // Get the category ID for the Part instance (or 'null' if does not exist) + int get categoryId => jsondata['category'] as int ?? null; + + // Get the category name for the Part instance + String get categoryName => jsondata['category_name'] ?? ''; + + // Get the image URL for the Part instance String get _image => jsondata['image'] ?? ''; + // Get the thumbnail URL for the Part instance String get _thumbnail => jsondata['thumbnail'] ?? ''; + // Return the fully-qualified name for the Part instance + String get fullname { + + String fn = jsondata['full_name'] ?? ''; + + if (fn.isNotEmpty) return fn; + + List elements = List(); + + if (IPN.isNotEmpty) elements.add(IPN); + + elements.add(name); + + if (revision.isNotEmpty) elements.add(revision); + + return elements.join(" | "); + } + // Return a path to the image for this Part String get image { // Use thumbnail as a backup From d1fba27f3aea8185dfc10d1862f87220e5be153f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 15:00:58 +1000 Subject: [PATCH 03/14] Part display Much improve - so wow! --- lib/widget/part_display.dart | 129 ++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/lib/widget/part_display.dart b/lib/widget/part_display.dart index 725caf17..b3f790ec 100644 --- a/lib/widget/part_display.dart +++ b/lib/widget/part_display.dart @@ -1,10 +1,13 @@ import 'package:InvenTree/inventree/part.dart'; +import 'package:InvenTree/widget/category_display.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class PartDisplayWidget extends StatefulWidget { @@ -24,29 +27,133 @@ class _PartDisplayState extends State { // TODO } - final InvenTreePart part; + InvenTreePart part; - String get _title { - if (part == null) { - return "Part"; - } else { - return "Part '${part.name}'"; + /* + * Construct a list of detail elements about this part. + * Not all elements are set for each part, so only add the ones that are important. + */ + List partDetails() { + List widgets = [ + + // Image / name / description + ListTile( + title: Text("${part.fullname}"), + subtitle: Text("${part.description}"), + leading: Image( + image: InvenTreeAPI().getImage(part.image) + ), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ), + ) + ]; + + return widgets; + } + + /* + * Build a list of tiles to display under the part description + */ + List partTiles() { + + List tiles = [ + Card( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: partDetails(), + ) + ), + ]; + + // Category information + if (part.categoryName.isNotEmpty) { + tiles.add( + Card( + child: ListTile( + title: Text("${part.categoryName}"), + leading: FaIcon(FontAwesomeIcons.stream), + onTap: () { + InvenTreePartCategory().get(part.categoryId).then((var cat) { + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); + }); + }, + ) + ) + ); } + + // External link? + if (part.link.isNotEmpty) { + tiles.add( + Card( + child: ListTile( + title: Text("${part.link}"), + leading: FaIcon(FontAwesomeIcons.link), + ) + ) + ); + } + + // Stock information + tiles.add( + Card( + child: ListTile( + title: Text("In Stock"), + leading: FaIcon(FontAwesomeIcons.boxes), + trailing: FlatButton( + child: Text("${part.inStock}") + ), + ) + ) + ); + + // Parts on order + if (part.isPurchaseable) { + tiles.add( + Card( + child: ListTile( + title: Text("On Order"), + leading: FaIcon(FontAwesomeIcons.shoppingCart), + trailing: Text("${part.onOrder}"), + ) + ) + ); + } + + // Parts being built + if (part.isAssembly) { + tiles.add( + Card( + child: ListTile( + title: Text("Building"), + leading: FaIcon(FontAwesomeIcons.tools), + trailing: Text("${part.building}"), + ) + ) + ); + } + + tiles.add(Spacer()); + + return tiles; + } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(_title), + title: Text("Part Details"), ), drawer: new InvenTreeDrawer(context), body: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Description: ${part.description}"), - ] + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: partTiles(), ), ) ); From fbd52e1414e62056d7e7ceef04db8b921f5fc9ea Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 15:50:13 +1000 Subject: [PATCH 04/14] More display items for Part view: - BOM count - Used in count - Notes --- lib/inventree/model.dart | 2 ++ lib/inventree/part.dart | 6 ++++ lib/widget/category_display.dart | 29 ++++++++++++++-- lib/widget/part_display.dart | 57 ++++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 5f8c651d..509b92cc 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -41,6 +41,8 @@ class InvenTreeModel { String get description => jsondata['description'] ?? ''; + String get notes => jsondata['notes'] ?? ''; + int get parentId => jsondata['parent'] ?? -1; String get link => jsondata['URL'] ?? ''; diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index d881d9ff..e63246a5 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -45,6 +45,12 @@ class InvenTreePart extends InvenTreeModel { // Get the number of units being build for this Part double get building => double.tryParse(jsondata['building'].toString() ?? '0'); + // Get the number of BOM items in this Part (if it is an assembly) + int get bomItemCount => jsondata['bom_items'] as int ?? 0; + + // Get the number of BOMs this Part is used in (if it is a component) + int get usedInCount => jsondata['used_in'] as int ?? 0; + bool get isAssembly => jsondata['assembly'] ?? false; bool get isComponent => jsondata['component'] ?? false; diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 7a30fd91..0dfe3db4 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_advanced_networkimage/provider.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class CategoryDisplayWidget extends StatefulWidget { @@ -42,7 +43,7 @@ class _CategoryDisplayState extends State { if (category == null) { return "Part Categories"; } else { - return "Part Category '${category.name}'"; + return "Part Category - ${category.name}"; } } @@ -82,6 +83,28 @@ class _CategoryDisplayState extends State { }); } + Widget getCategoryDescriptionCard() { + if (category == null) { + return Card( + child: ListTile( + title: Text("Part Categories"), + subtitle: Text("Top level part category"), + ) + ); + } else { + return Card( + child: ListTile( + title: Text("${category.name}"), + subtitle: Text("${category.description}"), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ), + ) + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -91,8 +114,10 @@ class _CategoryDisplayState extends State { drawer: new InvenTreeDrawer(context), body: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, children: [ + getCategoryDescriptionCard(), Text( "Subcategories - ${_subcategories.length}", textAlign: TextAlign.left, diff --git a/lib/widget/part_display.dart b/lib/widget/part_display.dart index b3f790ec..0588a29e 100644 --- a/lib/widget/part_display.dart +++ b/lib/widget/part_display.dart @@ -44,7 +44,7 @@ class _PartDisplayState extends State { image: InvenTreeAPI().getImage(part.image) ), trailing: IconButton( - icon: FaIcon(FontAwesomeIcons.edit), + icon: FaIcon(FontAwesomeIcons.edit), onPressed: null, ), ) @@ -73,7 +73,8 @@ class _PartDisplayState extends State { tiles.add( Card( child: ListTile( - title: Text("${part.categoryName}"), + title: Text("Part Category"), + subtitle: Text("${part.categoryName}"), leading: FaIcon(FontAwesomeIcons.stream), onTap: () { InvenTreePartCategory().get(part.categoryId).then((var cat) { @@ -92,6 +93,8 @@ class _PartDisplayState extends State { child: ListTile( title: Text("${part.link}"), leading: FaIcon(FontAwesomeIcons.link), + trailing: Text(""), + onTap: null, ) ) ); @@ -103,10 +106,9 @@ class _PartDisplayState extends State { child: ListTile( title: Text("In Stock"), leading: FaIcon(FontAwesomeIcons.boxes), - trailing: FlatButton( - child: Text("${part.inStock}") - ), - ) + trailing: Text("${part.inStock}"), + onTap: null, + ), ) ); @@ -118,6 +120,7 @@ class _PartDisplayState extends State { title: Text("On Order"), leading: FaIcon(FontAwesomeIcons.shoppingCart), trailing: Text("${part.onOrder}"), + onTap: null, ) ) ); @@ -125,12 +128,52 @@ class _PartDisplayState extends State { // Parts being built if (part.isAssembly) { + + tiles.add( + Card( + child: ListTile( + title: Text("Bill of Materials"), + leading: FaIcon(FontAwesomeIcons.thList), + trailing: Text("${part.bomItemCount}"), + onTap: null, + ) + ) + ); + tiles.add( Card( child: ListTile( title: Text("Building"), leading: FaIcon(FontAwesomeIcons.tools), trailing: Text("${part.building}"), + onTap: null, + ) + ) + ); + } + + if (part.isComponent) { + tiles.add( + Card( + child: ListTile( + title: Text("Used In"), + leading: FaIcon(FontAwesomeIcons.sitemap), + trailing: Text("${part.usedInCount}"), + onTap: null, + ) + ) + ); + } + + // Notes field? + if (part.notes.isNotEmpty) { + tiles.add( + Card( + child: ListTile( + title: Text("Notes"), + leading: FaIcon(FontAwesomeIcons.stickyNote), + trailing: Text(""), + onTap: null, ) ) ); @@ -152,7 +195,7 @@ class _PartDisplayState extends State { body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: partTiles(), ), ) From 65d955bcc68b1921f1d1669bf0b2b719c8443dee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 19:19:44 +1000 Subject: [PATCH 05/14] Use the new API notation for external URL ('link') --- lib/inventree/model.dart | 3 ++- lib/widget/part_display.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 509b92cc..40ef716a 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -45,7 +45,8 @@ class InvenTreeModel { int get parentId => jsondata['parent'] ?? -1; - String get link => jsondata['URL'] ?? ''; + // Legacy API provided external link as "URL", while newer API uses "link" + String get link => jsondata['link'] ?? jsondata['URL'] ?? ''; // Create a new object from JSON data (not a constructor!) InvenTreeModel createFromJson(Map json) { diff --git a/lib/widget/part_display.dart b/lib/widget/part_display.dart index 0588a29e..712069e6 100644 --- a/lib/widget/part_display.dart +++ b/lib/widget/part_display.dart @@ -104,7 +104,7 @@ class _PartDisplayState extends State { tiles.add( Card( child: ListTile( - title: Text("In Stock"), + title: Text("Stock"), leading: FaIcon(FontAwesomeIcons.boxes), trailing: Text("${part.inStock}"), onTap: null, From 1f88f1d8ab04c6ac5de3cd775f335a5075418785 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 19:21:54 +1000 Subject: [PATCH 06/14] Spelling fix --- lib/inventree/part.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index e63246a5..1559c98a 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -57,7 +57,7 @@ class InvenTreePart extends InvenTreeModel { bool get isPurchaseable => jsondata['purchaseable'] ?? false; - bool get isSaleable => jsondata['saleable'] ?? false; + bool get isSalable => jsondata['salable'] ?? false; bool get isActive => jsondata['active'] ?? false; From 57ab6546ab1536043428e5728c7ad4fac3a8e4ee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 21:35:11 +1000 Subject: [PATCH 07/14] Expandable panel for subcategory and part list for category view --- lib/widget/category_display.dart | 79 ++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 0dfe3db4..d1a949af 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -83,6 +83,9 @@ class _CategoryDisplayState extends State { }); } + bool _subcategoriesExpanded = false; + bool _partListExpanded = true; + Widget getCategoryDescriptionCard() { if (category == null) { return Card( @@ -112,27 +115,63 @@ class _CategoryDisplayState extends State { title: Text(_titleString), ), drawer: new InvenTreeDrawer(context), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + body: ListView( + //mainAxisAlignment: MainAxisAlignment.start, + //mainAxisSize: MainAxisSize.max, children: [ getCategoryDescriptionCard(), - Text( - "Subcategories - ${_subcategories.length}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.bold), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + print("callback!"); + setState(() { + + switch (index) { + case 0: + _subcategoriesExpanded = !isExpanded; + break; + case 1: + _partListExpanded = !isExpanded; + break; + default: + break; + } + }); + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Subcategories"), + trailing: Text("${_subcategories.length}"), + onTap: () { + setState(() { + _subcategoriesExpanded = !_subcategoriesExpanded; + }); + }, + ); + }, + body: SubcategoryList(_subcategories), + isExpanded: _subcategoriesExpanded, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Parts"), + trailing: Text("${_parts.length}"), + onTap: () { + setState(() { + _partListExpanded = !_partListExpanded; + }); + }, + ); + }, + body: PartList(_parts), + isExpanded: _partListExpanded, + ) + ], ), - Expanded(child: SubcategoryList(_subcategories)), - Divider(), - Text("Parts - ${_parts.length}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.bold), - ), - Expanded(child: PartList(_parts)), ] ) - ) ); } } @@ -171,7 +210,9 @@ class SubcategoryList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _build, itemCount: _categories.length); + return ListView.builder( + shrinkWrap: true, + itemBuilder: _build, itemCount: _categories.length); } } @@ -217,6 +258,8 @@ class PartList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _build, itemCount: _parts.length); + return ListView.builder( + shrinkWrap: true, + itemBuilder: _build, itemCount: _parts.length); } } From f32c9f024e84a2b971afc9b613a023fbe559127e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 21:41:13 +1000 Subject: [PATCH 08/14] Display subcategory part count --- lib/inventree/part.dart | 2 ++ lib/widget/category_display.dart | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 1559c98a..2bdd4e8f 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -14,6 +14,8 @@ class InvenTreePartCategory extends InvenTreeModel { String get pathstring => jsondata['pathstring'] ?? ''; + int get partcount => jsondata['parts'] ?? 0; + InvenTreePartCategory() : super(); InvenTreePartCategory.fromJson(Map json) : super.fromJson(json) { diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index d1a949af..203bd337 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -202,6 +202,7 @@ class SubcategoryList extends StatelessWidget { return ListTile( title: Text("${cat.name}"), subtitle: Text("${cat.description}"), + trailing: Text("${cat.partcount}"), onTap: () { _openCategory(context, cat.pk); } From 0f2c88e630834e4ce3fbee3d0a312a57f34cbd63 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 21:49:00 +1000 Subject: [PATCH 09/14] Fix listview-within-listview scrolling https://stackoverflow.com/questions/45270900/child-listview-within-listview-parent#45274055 --- lib/widget/category_display.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 203bd337..3d34420e 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -213,6 +213,7 @@ class SubcategoryList extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, + physics: ClampingScrollPhysics(), itemBuilder: _build, itemCount: _categories.length); } } @@ -261,6 +262,7 @@ class PartList extends StatelessWidget { Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, + physics: ClampingScrollPhysics(), itemBuilder: _build, itemCount: _parts.length); } } From 683f1e8efdfcd8a3cd6e91b35d50908b55e6e91c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 22:03:01 +1000 Subject: [PATCH 10/14] Link to parent part category --- lib/inventree/part.dart | 16 ++++++++++++++++ lib/widget/category_display.dart | 30 +++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 2bdd4e8f..c9d1fb59 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -14,6 +14,22 @@ class InvenTreePartCategory extends InvenTreeModel { String get pathstring => jsondata['pathstring'] ?? ''; + String get parentpathstring { + List psplit = pathstring.split("/"); + + if (psplit.length > 0) { + psplit.removeLast(); + } + + String p = psplit.join("/"); + + if (p.isEmpty) { + p = "Top level parts category"; + } + + return p; + } + int get partcount => jsondata['parts'] ?? 0; InvenTreePartCategory() : super(); diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 3d34420e..e900a905 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -96,14 +96,30 @@ class _CategoryDisplayState extends State { ); } else { return Card( - child: ListTile( - title: Text("${category.name}"), - subtitle: Text("${category.description}"), - trailing: IconButton( - icon: FaIcon(FontAwesomeIcons.edit), - onPressed: null, + child: Column( + children: [ + ListTile( + title: Text("${category.name}"), + subtitle: Text("${category.description}"), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ), ), - ) + ListTile( + title: Text("Parent Category"), + subtitle: Text("${category.parentpathstring}"), + onTap: () { + // TODO - Refactor this code into the InvenTreePart class + InvenTreePartCategory().get(category.parentId).then((var cat) { + if (cat is InvenTreePartCategory) { + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); + } + }); + }, + ) + ] + ), ); } } From 15d5bf3fb5f3182e4c73aa5131dec07f1d495e42 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 22:06:56 +1000 Subject: [PATCH 11/14] Navigate "up" the category path to the parent category --- lib/widget/category_display.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index e900a905..f22fd616 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -110,12 +110,18 @@ class _CategoryDisplayState extends State { title: Text("Parent Category"), subtitle: Text("${category.parentpathstring}"), onTap: () { - // TODO - Refactor this code into the InvenTreePart class - InvenTreePartCategory().get(category.parentId).then((var cat) { - if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); - } - }); + if (category.parentId < 0) { + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); + } else { + // TODO - Refactor this code into the InvenTreePart class + InvenTreePartCategory().get(category.parentId).then(( + var cat) { + if (cat is InvenTreePartCategory) { + Navigator.push(context, MaterialPageRoute(builder: ( + context) => CategoryDisplayWidget(cat))); + } + }); + } }, ) ] @@ -158,6 +164,7 @@ class _CategoryDisplayState extends State { headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( title: Text("Subcategories"), + leading: FaIcon(FontAwesomeIcons.stream), trailing: Text("${_subcategories.length}"), onTap: () { setState(() { @@ -173,6 +180,7 @@ class _CategoryDisplayState extends State { headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( title: Text("Parts"), + leading: FaIcon(FontAwesomeIcons.shapes), trailing: Text("${_parts.length}"), onTap: () { setState(() { From 0cc210cf2b12f988eee8d1e8d8fc50b98265c014 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 22:19:40 +1000 Subject: [PATCH 12/14] Add more buttons to the main screen - Currently just display an "unsupported" dialog --- lib/main.dart | 55 +++++++++++++++++++++++++++++++++++++++++++---- lib/settings.dart | 7 ++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f738531c..41c32206 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -194,8 +194,19 @@ class _MyHomePageState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } - void _suppliers() { - // TODO + void _unsupported() { + showDialog( + context: context, + child: new SimpleDialog( + title: new Text("Unsupported"), + children: [ + ListTile( + title: Text("This feature is not yet supported"), + subtitle: Text("It will be supported in an upcoming release"), + ) + ], + ) + ); } @override @@ -259,7 +270,7 @@ class _MyHomePageState extends State { Column( children: [ IconButton( - icon: new Icon(Icons.category), + icon: new FaIcon(FontAwesomeIcons.shapes), tooltip: 'Parts', onPressed: _parts, ), @@ -281,7 +292,7 @@ class _MyHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.industry), tooltip: 'Suppliers', - onPressed: _suppliers, + onPressed: _unsupported, ), Text("Suppliers"), ] @@ -289,6 +300,42 @@ class _MyHomePageState extends State { ], ), Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + IconButton( + icon: new FaIcon(FontAwesomeIcons.tools), + tooltip: "Build", + onPressed: _unsupported, + ), + Text("Build"), + ], + ), + Column( + children: [ + IconButton( + icon: new FaIcon(FontAwesomeIcons.shoppingCart), + tooltip: "Order", + onPressed: _unsupported, + ), + Text("Order"), + ] + ), + Column( + children: [ + IconButton( + icon: new FaIcon(FontAwesomeIcons.shippingFast), + tooltip: "Sell", + onPressed: _unsupported, + ), + Text("Sell"), + ] + ) + ], + ), + Spacer(), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/settings.dart b/lib/settings.dart index 55b28ace..48d8d363 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:InvenTree/api.dart'; import 'login_settings.dart'; import 'package:package_info/package_info.dart'; @@ -56,6 +57,11 @@ class _InvenTreeSettingsState extends State { child: new SimpleDialog( title: new Text("About InvenTree"), children: [ + ListTile( + title: Text("Server Version"), + subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"), + ), + Divider(), ListTile( title: Text("App Name"), subtitle: Text("${info.appName}"), @@ -72,6 +78,7 @@ class _InvenTreeSettingsState extends State { title: Text("Build Number"), subtitle: Text("${info.buildNumber}") ), + Divider(), ListTile( title: Text("Submit Bug Report"), subtitle: Text("Submit a bug report or feature request at:\n https://github.com/inventree/inventree-app/issues/"), From 04ad279c5845d44cfcd25a62d40ace3327d47d43 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 22:31:43 +1000 Subject: [PATCH 13/14] Search icon displays unsupported dialog --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 41c32206..1b5b59eb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -246,7 +246,7 @@ class _MyHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.search), tooltip: 'Search', - onPressed: _search, + onPressed: _unsupported, ), Text("Search"), ], From 4c8bbd46e7d2fc0a40ef82bde71cb2491c0898c6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 5 Apr 2020 23:10:36 +1000 Subject: [PATCH 14/14] StockLocation display now improved - Expandable list of sublocations - Expandable list of stockitems - Traverse upward (to higher locations) - Fix rendering of unknown part thumbnails --- lib/inventree/part.dart | 3 +- lib/inventree/stock.dart | 31 ++++++- lib/main.dart | 6 +- lib/widget/category_display.dart | 9 +- lib/widget/location_display.dart | 151 ++++++++++++++++++++++++------- 5 files changed, 154 insertions(+), 46 deletions(-) diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index c9d1fb59..6b09842e 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -15,6 +15,7 @@ class InvenTreePartCategory extends InvenTreeModel { String get pathstring => jsondata['pathstring'] ?? ''; String get parentpathstring { + // TODO - Drive the refactor tractor through this List psplit = pathstring.split("/"); if (psplit.length > 0) { @@ -24,7 +25,7 @@ class InvenTreePartCategory extends InvenTreeModel { String p = psplit.join("/"); if (p.isEmpty) { - p = "Top level parts category"; + p = "Top level part category"; } return p; diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 842f32b8..7bfe2bd2 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -16,7 +16,13 @@ class InvenTreeStockItem extends InvenTreeModel { String get partDescription => jsondata['part__description'] as String ?? ''; - String get partThumbnail => jsondata['part__thumbnail'] as String ?? InvenTreeAPI.staticThumb; + String get partThumbnail { + String thumb = jsondata['part__thumbnail'] as String ?? ''; + + if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb; + + return thumb; + } int get serialNumber => jsondata['serial'] as int ?? null; @@ -49,10 +55,31 @@ class InvenTreeStockLocation extends InvenTreeModel { @override String URL = "stock/location/"; + String get pathstring => jsondata['pathstring'] ?? ''; + + String get parentpathstring { + // TODO - Drive the refactor tractor through this + List psplit = pathstring.split('/'); + + if (psplit.length > 0) { + psplit.removeLast(); + } + + String p = psplit.join('/'); + + if (p.isEmpty) { + p = "Top level stock location"; + } + + return p; + } + + int get itemcount => jsondata['items'] ?? 0; + InvenTreeStockLocation() : super(); InvenTreeStockLocation.fromJson(Map json) : super.fromJson(json) { - + // TODO } @override diff --git a/lib/main.dart b/lib/main.dart index 1b5b59eb..121fa93a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -326,11 +326,11 @@ class _MyHomePageState extends State { Column( children: [ IconButton( - icon: new FaIcon(FontAwesomeIcons.shippingFast), - tooltip: "Sell", + icon: new FaIcon(FontAwesomeIcons.truck), + tooltip: "Ship", onPressed: _unsupported, ), - Text("Sell"), + Text("Ship"), ] ) ], diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index f22fd616..0f68703f 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -114,11 +114,9 @@ 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(category.parentId).then((var cat) { if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute(builder: ( - context) => CategoryDisplayWidget(cat))); + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); } }); } @@ -138,13 +136,10 @@ class _CategoryDisplayState extends State { ), drawer: new InvenTreeDrawer(context), body: ListView( - //mainAxisAlignment: MainAxisAlignment.start, - //mainAxisSize: MainAxisSize.max, children: [ getCategoryDescriptionCard(), ExpansionPanelList( expansionCallback: (int index, bool isExpanded) { - print("callback!"); setState(() { switch (index) { diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 86d8a59c..49654ffb 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -5,6 +5,7 @@ import 'package:InvenTree/widget/stock_display.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class LocationDisplayWidget extends StatefulWidget { @@ -45,12 +46,19 @@ class _LocationDisplayState extends State { String get _title { if (location == null) { - return "Location:"; + return "Stock Locations"; } else { - return "Stock Location '${location.name}'"; + return "Stock Location - ${location.name}"; } } + /* + * 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() { int pk = location?.pk ?? -1; @@ -83,6 +91,50 @@ class _LocationDisplayState extends State { }); } + bool _locationListExpanded = false; + bool _stockListExpanded = true; + + Widget locationDescriptionCard() { + if (location == null) { + return Card( + child: ListTile( + title: Text("Stock Locations"), + subtitle: Text("Top level stock location") + ) + ); + } else { + return Card( + child: Column( + children: [ + ListTile( + title: Text("${location.name}"), + subtitle: Text("${location.description}"), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ), + ), + ListTile( + title: Text("Parent Category"), + subtitle: Text("${location.parentpathstring}"), + onTap: () { + if (location.parentId < 0) { + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); + } else { + InvenTreeStockLocation().get(location.parentId).then((var loc) { + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + } + }); + } + }, + ) + ] + ) + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -90,36 +142,62 @@ class _LocationDisplayState extends State { title: Text(_title), ), drawer: new InvenTreeDrawer(context), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Sublocations - ${_sublocations.length}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextField( - decoration: InputDecoration( - hintText: "Filter locations", + body: ListView( + children: [ + locationDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + switch (index) { + case 0: + _locationListExpanded = !isExpanded; + break; + case 1: + _stockListExpanded = !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(() { + _locationListExpanded = !_locationListExpanded; + }); + }, + ); + }, + body: SublocationList(_sublocations), + isExpanded: _locationListExpanded, ), - onChanged: (text) { - setState(() { - _locationFilter = text.trim().toLowerCase(); - }); - }, - ), - Expanded(child: SublocationList(sublocations)), - Divider(), - Text( - "Stock Items - ${_items.length}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.bold), - ), - Expanded(child: StockList(_items)), - ], - ) - ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Stock Items"), + leading: FaIcon(FontAwesomeIcons.boxes), + trailing: Text("${_items.length}"), + onTap: () { + setState(() { + _stockListExpanded = !_stockListExpanded; + }); + }, + ); + }, + body: StockList(_items), + isExpanded: _stockListExpanded, + ) + ] + ), + ] + ) ); } } @@ -146,6 +224,7 @@ class SublocationList extends StatelessWidget { return ListTile( title: Text('${loc.name}'), subtitle: Text("${loc.description}"), + trailing: Text("${loc.itemcount}"), onTap: () { _openLocation(context, loc.pk); }, @@ -154,7 +233,10 @@ class SublocationList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _build, itemCount: _locations.length); + return ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: _build, itemCount: _locations.length); } } @@ -192,6 +274,9 @@ class StockList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _build, itemCount: _items.length); + return ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: _build, itemCount: _items.length); } } \ No newline at end of file