diff --git a/lib/api.dart b/lib/api.dart index 8ab68d01..0d8e9682 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -651,12 +651,6 @@ class InvenTreeAPI { */ Future downloadFile(String url, {bool openOnDownload = true}) async { - showSnackIcon( - L10().downloading, - icon: FontAwesomeIcons.download, - success: true - ); - // Find the local downlods directory final Directory dir = await getTemporaryDirectory(); diff --git a/lib/api_form.dart b/lib/api_form.dart index a4a1ed5c..5c07f023 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -18,6 +18,7 @@ import "package:inventree/widget/fields.dart"; import "package:inventree/l10.dart"; import "package:flutter/material.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/snacks.dart"; @@ -859,7 +860,9 @@ Future launchApiForm( if (url.isNotEmpty) { + showLoadingOverlay(context); var options = await InvenTreeAPI().options(url); + hideLoadingOverlay(); // Invalid response from server if (!options.isValid()) { @@ -902,7 +905,7 @@ Future launchApiForm( field.definition = extractFieldDefinition(serverFields, field.lookupPath); // Skip fields with empty definitions - if (field.definition.isEmpty) { + if (url.isNotEmpty && field.definition.isEmpty) { print("Warning: Empty field definition for field '${fieldName}'"); } @@ -987,8 +990,6 @@ class _APIFormWidgetState extends State { List nonFieldErrors = []; - Function(Map)? onSuccess; - bool spacerRequired = false; List _buildForm() { @@ -1102,20 +1103,25 @@ class _APIFormWidgetState extends State { } if (widget.method == "POST") { + + showLoadingOverlay(context); final response = await InvenTreeAPI().post( widget.url, body: data, expectedStatusCode: null ); + hideLoadingOverlay(); return response; } else { + showLoadingOverlay(context); final response = await InvenTreeAPI().patch( widget.url, body: data, expectedStatusCode: null ); + hideLoadingOverlay(); return response; } @@ -1259,7 +1265,7 @@ class _APIFormWidgetState extends State { } // Run custom onSuccess function - var successFunc = onSuccess; + var successFunc = widget.onSuccess; // An "empty" URL means we don't want to submit the form anywhere // Perhaps we just want to process the data? @@ -1398,4 +1404,4 @@ class _APIFormWidgetState extends State { ); } -} \ No newline at end of file +} diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index e6d81334..f11a7130 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -708,9 +708,7 @@ class InvenTreeAttachment extends InvenTreeModel { * Download this attachment file */ Future downloadAttachment() async { - await InvenTreeAPI().downloadFile(attachment); - } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4fbf5fd7..763f66a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -333,6 +333,12 @@ "filterTemplateDetail": "Show template parts", "@filterTemplateDetail": {}, + "filterTrackable": "Trackable", + "@filterTrackable": {}, + + "filterTrackableDetail": "Show trackable parts", + "@filterTrackableDetail": {}, + "filterVirtual": "Virtual", "@filterVirtual": {}, diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart index 518316fd..9dc3550b 100644 --- a/lib/widget/attachment_widget.dart +++ b/lib/widget/attachment_widget.dart @@ -6,6 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/widget/fields.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; @@ -51,8 +52,8 @@ class _AttachmentWidgetState extends RefreshableState { icon: FaIcon(FontAwesomeIcons.plusCircle), onPressed: () async { FilePickerDialog.pickFile( - onPicked: (File file) { - upload(file); + onPicked: (File file) async { + await upload(context, file); } ); }, @@ -63,9 +64,11 @@ class _AttachmentWidgetState extends RefreshableState { return actions; } - Future upload(File file) async { + Future upload(BuildContext context, File file) async { + showLoadingOverlay(context); final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); + hideLoadingOverlay(); if (result) { showSnackIcon(L10().uploadSuccess, success: true); @@ -121,7 +124,9 @@ class _AttachmentWidgetState extends RefreshableState { subtitle: Text(attachment.comment), leading: FaIcon(attachment.icon, color: COLOR_CLICK), onTap: () async { + showLoadingOverlay(context); await attachment.downloadAttachment(); + hideLoadingOverlay(); }, )); } diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index 402bffcd..461d1bc4 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -12,6 +12,7 @@ import "package:inventree/inventree/part.dart"; import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/part_detail.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/refreshable_state.dart"; @@ -125,11 +126,14 @@ class _PaginatedBomListState extends PaginatedSearchState { height: 40, ), onTap: subPart == null ? null : () async { - InvenTreePart().get(bomItem.subPartId).then((var part) { - if (part is InvenTreePart) { - Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); - } - }); + + showLoadingOverlay(context); + var part = await InvenTreePart().get(bomItem.subPartId); + hideLoadingOverlay(); + + if (part is InvenTreePart) { + Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + } }, ); } diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 74575169..f414203b 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -9,6 +9,7 @@ import "package:inventree/inventree/part.dart"; import "package:inventree/widget/category_list.dart"; import "package:inventree/widget/part_list.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; @@ -125,16 +126,21 @@ class _CategoryDisplayState extends RefreshableState { FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK, ), - onTap: () { - if (category == null || ((category?.parentId ?? 0) < 0)) { + onTap: () async { + + int parentId = category?.parentId ?? -1; + + if (parentId < 0) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } else { - // TODO - Refactor this code into the InvenTreePart class - InvenTreePartCategory().get(category?.parentId ?? -1).then((var cat) { - if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); - } - }); + + showLoadingOverlay(context); + var cat = await InvenTreePartCategory().get(parentId); + hideLoadingOverlay(); + + if (cat is InvenTreePartCategory) { + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); + } } }, ) diff --git a/lib/widget/home.dart b/lib/widget/home.dart index dc4333b7..3b31f235 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -54,11 +54,13 @@ class _InvenTreeHomePageState extends State { }); InvenTreeAPI().registerCallback(() { - setState(() { - // Reload the widget - }); - }); + if (mounted) { + setState(() { + // Reload the widget + }); + } + }); } // Index of bottom navigation bar @@ -192,6 +194,11 @@ class _InvenTreeHomePageState extends State { return; } + // Ignore if the widget is no longer active + if (!mounted) { + return; + } + final notifications = await InvenTreeNotification().list(); setState(() { diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 887f17bc..7e32c5bf 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -10,6 +10,7 @@ import "package:inventree/l10.dart"; import "package:inventree/inventree/stock.dart"; import "package:inventree/widget/location_list.dart"; +import "package:inventree/widget/progress.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/stock_detail.dart"; @@ -218,19 +219,21 @@ class _LocationDisplayState extends RefreshableState { title: Text(L10().parentLocation), subtitle: Text("${location!.parentPathString}"), leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK), - onTap: () { + onTap: () async { - int parent = location?.parentId ?? -1; + int parentId = location?.parentId ?? -1; - if (parent < 0) { + if (parentId < 0) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } else { - InvenTreeStockLocation().get(parent).then((var loc) { - if (loc is InvenTreeStockLocation) { - Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); - } - }); + showLoadingOverlay(context); + var loc = await InvenTreeStockLocation().get(parentId); + hideLoadingOverlay(); + + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + } } }, ) diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 12e1233f..71f64856 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -384,8 +384,8 @@ abstract class PaginatedSearchState extends Sta PagedSliverList.separated( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return buildItem(context, item); + itemBuilder: (ctx, item, index) { + return buildItem(ctx, item); }, noItemsFoundIndicatorBuilder: (context) { return NoResultsWidget(noResultsText); @@ -450,9 +450,11 @@ class NoResultsWidget extends StatelessWidget { Widget build(BuildContext context) { return ListTile( - title: Text(L10().noResults), - subtitle: Text(description), - leading: FaIcon(FontAwesomeIcons.exclamationCircle), + title: Text( + description, + style: TextStyle(fontStyle: FontStyle.italic), + ), + leading: FaIcon(FontAwesomeIcons.exclamationCircle, color: COLOR_WARNING), ); } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 8f021f1c..ae15e9c0 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -253,15 +253,17 @@ class _PartDisplayState extends RefreshableState { title: Text(L10().partCategory), subtitle: Text("${part.categoryName}"), leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), - onTap: () { + onTap: () async { if (part.categoryId > 0) { - InvenTreePartCategory().get(part.categoryId).then((var cat) { - if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => CategoryDisplayWidget(cat))); - } - }); + showLoadingOverlay(context); + var cat = await InvenTreePartCategory().get(part.categoryId); + hideLoadingOverlay(); + + if (cat is InvenTreePartCategory) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => CategoryDisplayWidget(cat))); + } } }, ) diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart index 37c4dc22..3a3cc2f9 100644 --- a/lib/widget/part_list.dart +++ b/lib/widget/part_list.dart @@ -104,6 +104,10 @@ class _PaginatedPartListState extends PaginatedSearchState { "label": L10().filterTemplate, "help_text": L10().filterTemplateDetail }, + "trackable": { + "label": L10().filterTrackable, + "help_text": L10().filterTrackableDetail, + }, "virtual": { "label": L10().filterVirtual, "help_text": L10().filterVirtualDetail, @@ -122,15 +126,6 @@ class _PaginatedPartListState extends PaginatedSearchState { return page; } - void _openPart(BuildContext context, int pk) { - // Attempt to load the part information - InvenTreePart().get(pk).then((var part) { - if (part is InvenTreePart) { - - Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); - } - }); - } @override Widget buildItem(BuildContext context, InvenTreeModel model) { @@ -147,7 +142,7 @@ class _PaginatedPartListState extends PaginatedSearchState { height: 40, ), onTap: () { - _openPart(context, part.pk); + Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); }, ); } diff --git a/lib/widget/progress.dart b/lib/widget/progress.dart index 92ef69c6..8877a699 100644 --- a/lib/widget/progress.dart +++ b/lib/widget/progress.dart @@ -1,6 +1,7 @@ import "package:flutter/material.dart"; +import "package:flutter_overlay_loader/flutter_overlay_loader.dart"; /* * Construct a circular progress indicator @@ -10,4 +11,17 @@ Widget progressIndicator() { return Center( child: CircularProgressIndicator() ); -} \ No newline at end of file +} + + +void showLoadingOverlay(BuildContext context) { + Loader.show( + context, + themeData: Theme.of(context).copyWith(colorScheme: ColorScheme.fromSwatch()) + ); +} + + +void hideLoadingOverlay() { + Loader.hide(); +} diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 0d280de5..3ea9e5d9 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -64,9 +64,12 @@ abstract class RefreshableState extends State with // Update current tab selection void onTabSelectionChanged(int index) { - setState(() { - tabIndex = index; - }); + + if (mounted) { + setState(() { + tabIndex = index; + }); + } } @override @@ -87,6 +90,10 @@ abstract class RefreshableState extends State with Future refresh(BuildContext context) async { + if (!mounted) { + return; + } + setState(() { loading = true; }); diff --git a/lib/widget/starred_parts.dart b/lib/widget/starred_parts.dart deleted file mode 100644 index e667a94c..00000000 --- a/lib/widget/starred_parts.dart +++ /dev/null @@ -1,91 +0,0 @@ -import "package:inventree/inventree/part.dart"; -import "package:inventree/widget/part_detail.dart"; -import "package:inventree/widget/progress.dart"; -import "package:inventree/widget/refreshable_state.dart"; -import "package:flutter/material.dart"; - -import "package:inventree/l10.dart"; - -import "package:inventree/api.dart"; - - -class StarredPartWidget extends StatefulWidget { - - const StarredPartWidget({Key? key}) : super(key: key); - - @override - _StarredPartState createState() => _StarredPartState(); -} - - -class _StarredPartState extends RefreshableState { - - List starredParts = []; - - @override - String getAppBarTitle(BuildContext context) => L10().partsStarred; - - @override - Future request(BuildContext context) async { - - 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] as InvenTreePart); - } - } - } - - Widget _partResult(BuildContext context, int index) { - final part = starredParts[index]; - - return ListTile( - title: Text(part.fullname), - subtitle: Text(part.description), - leading: InvenTreeAPI().getImage( - part.thumbnail, - width: 40, - height: 40 - ), - onTap: () { - InvenTreePart().get(part.pk).then((var prt) { - if (prt is InvenTreePart) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => PartDetailWidget(prt)) - ); - } - }); - } - ); - } - - @override - Widget getBody(BuildContext context) { - - if (loading) { - return progressIndicator(); - } - - if (starredParts.isEmpty) { - return ListView( - children: [ - ListTile( - title: Text(L10().partsNone), - subtitle: Text(L10().partsStarredNone) - ) - ], - ); - } - - return ListView.separated( - itemCount: starredParts.length, - itemBuilder: _partResult, - separatorBuilder: (_, __) => const Divider(height: 3), - physics: ClampingScrollPhysics(), - ); - } -} \ No newline at end of file diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index d982b0bc..d214746b 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -475,13 +475,16 @@ class _StockItemDisplayState extends RefreshableState { color: item.statusColor ) ), - onTap: () { + onTap: () async { if (item.partId > 0) { - InvenTreePart().get(item.partId).then((var part) { - if (part is InvenTreePart) { - Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); - } - }); + + showLoadingOverlay(context); + var part = await InvenTreePart().get(item.partId); + hideLoadingOverlay(); + + if (part is InvenTreePart) { + Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + } } }, //trailing: Text(item.serialOrQuantityDisplay()), @@ -533,15 +536,17 @@ class _StockItemDisplayState extends RefreshableState { FontAwesomeIcons.mapMarkerAlt, color: COLOR_CLICK, ), - onTap: () { + onTap: () async { if (item.locationId > 0) { - InvenTreeStockLocation().get(item.locationId).then((var loc) { - if (loc is InvenTreeStockLocation) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => LocationDisplayWidget(loc))); - } - }); + showLoadingOverlay(context); + var loc = await InvenTreeStockLocation().get(item.locationId); + hideLoadingOverlay(); + + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => LocationDisplayWidget(loc))); + } } }, ), diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart index df34bb67..0410a9b8 100644 --- a/lib/widget/stock_item_test_results.dart +++ b/lib/widget/stock_item_test_results.dart @@ -156,6 +156,7 @@ class _StockItemTestResultDisplayState extends RefreshableState StockDetailWidget(item))); - } - }); - } - @override Widget buildItem(BuildContext context, InvenTreeModel model) { @@ -132,7 +124,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState StockDetailWidget(item))); }, ); } diff --git a/pubspec.lock b/pubspec.lock index 0f7c12f7..ea5db0a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -291,6 +291,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.9+1" + flutter_overlay_loader: + dependency: "direct main" + description: + name: flutter_overlay_loader + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e4fd7662..a3f4f444 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,12 +22,13 @@ dependencies: flutter_localizations: sdk: flutter flutter_markdown: ^0.6.9 # Rendering markdown + flutter_overlay_loader: ^2.0.0 # Overlay screen support font_awesome_flutter: ^9.1.0 # FontAwesome icon set http: ^0.13.4 image_picker: ^0.8.3 # Select or take photos infinite_scroll_pagination: ^3.1.0 # Let the server do all the work! intl: ^0.17.0 - one_context: ^1.1.0 # Dialogs without requiring context + one_context: ^1.1.1 # Dialogs without requiring context open_file: ^3.2.1 # Open local files package_info_plus: ^1.0.4 # App information introspection path: ^1.8.0