2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-05-14 13:03:11 +00:00

Display overlay screen for blocking operations (#186)

* Catch state error in homepage widget

* Add flutter_overlay_loader lib

- Displays an overlay screen to indicate blocking operation

* Wrap blocking widget transitions in a loading overlay

- Prevents user from doing other things while loading
- Shows the user that something is happening

* Linting fixes

* Show overlay when uploading attachment file

* Show overlay when downloading file also

* Show overlay when loading or submitting API forms

- Major improvements to usability "feel"

* UI improvements for stock item test results widget

* Fix API_FORM bug

- onSuccess function was not being called
This commit is contained in:
Oliver 2022-07-20 09:05:21 +10:00 committed by GitHub
parent 277193ecb0
commit 01dd046dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 150 additions and 183 deletions

View File

@ -651,12 +651,6 @@ class InvenTreeAPI {
*/ */
Future<void> downloadFile(String url, {bool openOnDownload = true}) async { Future<void> downloadFile(String url, {bool openOnDownload = true}) async {
showSnackIcon(
L10().downloading,
icon: FontAwesomeIcons.download,
success: true
);
// Find the local downlods directory // Find the local downlods directory
final Directory dir = await getTemporaryDirectory(); final Directory dir = await getTemporaryDirectory();

View File

@ -18,6 +18,7 @@ import "package:inventree/widget/fields.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
@ -859,7 +860,9 @@ Future<void> launchApiForm(
if (url.isNotEmpty) { if (url.isNotEmpty) {
showLoadingOverlay(context);
var options = await InvenTreeAPI().options(url); var options = await InvenTreeAPI().options(url);
hideLoadingOverlay();
// Invalid response from server // Invalid response from server
if (!options.isValid()) { if (!options.isValid()) {
@ -902,7 +905,7 @@ Future<void> launchApiForm(
field.definition = extractFieldDefinition(serverFields, field.lookupPath); field.definition = extractFieldDefinition(serverFields, field.lookupPath);
// Skip fields with empty definitions // Skip fields with empty definitions
if (field.definition.isEmpty) { if (url.isNotEmpty && field.definition.isEmpty) {
print("Warning: Empty field definition for field '${fieldName}'"); print("Warning: Empty field definition for field '${fieldName}'");
} }
@ -987,8 +990,6 @@ class _APIFormWidgetState extends State<APIFormWidget> {
List<String> nonFieldErrors = []; List<String> nonFieldErrors = [];
Function(Map<String, dynamic>)? onSuccess;
bool spacerRequired = false; bool spacerRequired = false;
List<Widget> _buildForm() { List<Widget> _buildForm() {
@ -1102,20 +1103,25 @@ class _APIFormWidgetState extends State<APIFormWidget> {
} }
if (widget.method == "POST") { if (widget.method == "POST") {
showLoadingOverlay(context);
final response = await InvenTreeAPI().post( final response = await InvenTreeAPI().post(
widget.url, widget.url,
body: data, body: data,
expectedStatusCode: null expectedStatusCode: null
); );
hideLoadingOverlay();
return response; return response;
} else { } else {
showLoadingOverlay(context);
final response = await InvenTreeAPI().patch( final response = await InvenTreeAPI().patch(
widget.url, widget.url,
body: data, body: data,
expectedStatusCode: null expectedStatusCode: null
); );
hideLoadingOverlay();
return response; return response;
} }
@ -1259,7 +1265,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
} }
// Run custom onSuccess function // Run custom onSuccess function
var successFunc = onSuccess; var successFunc = widget.onSuccess;
// An "empty" URL means we don't want to submit the form anywhere // An "empty" URL means we don't want to submit the form anywhere
// Perhaps we just want to process the data? // Perhaps we just want to process the data?
@ -1398,4 +1404,4 @@ class _APIFormWidgetState extends State<APIFormWidget> {
); );
} }
} }

View File

@ -708,9 +708,7 @@ class InvenTreeAttachment extends InvenTreeModel {
* Download this attachment file * Download this attachment file
*/ */
Future<void> downloadAttachment() async { Future<void> downloadAttachment() async {
await InvenTreeAPI().downloadFile(attachment); await InvenTreeAPI().downloadFile(attachment);
} }
} }

View File

@ -333,6 +333,12 @@
"filterTemplateDetail": "Show template parts", "filterTemplateDetail": "Show template parts",
"@filterTemplateDetail": {}, "@filterTemplateDetail": {},
"filterTrackable": "Trackable",
"@filterTrackable": {},
"filterTrackableDetail": "Show trackable parts",
"@filterTrackableDetail": {},
"filterVirtual": "Virtual", "filterVirtual": "Virtual",
"@filterVirtual": {}, "@filterVirtual": {},

View File

@ -6,6 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
import "package:inventree/widget/fields.dart"; import "package:inventree/widget/fields.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
@ -51,8 +52,8 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
icon: FaIcon(FontAwesomeIcons.plusCircle), icon: FaIcon(FontAwesomeIcons.plusCircle),
onPressed: () async { onPressed: () async {
FilePickerDialog.pickFile( FilePickerDialog.pickFile(
onPicked: (File file) { onPicked: (File file) async {
upload(file); await upload(context, file);
} }
); );
}, },
@ -63,9 +64,11 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
return actions; return actions;
} }
Future<void> upload(File file) async { Future<void> upload(BuildContext context, File file) async {
showLoadingOverlay(context);
final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId);
hideLoadingOverlay();
if (result) { if (result) {
showSnackIcon(L10().uploadSuccess, success: true); showSnackIcon(L10().uploadSuccess, success: true);
@ -121,7 +124,9 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
subtitle: Text(attachment.comment), subtitle: Text(attachment.comment),
leading: FaIcon(attachment.icon, color: COLOR_CLICK), leading: FaIcon(attachment.icon, color: COLOR_CLICK),
onTap: () async { onTap: () async {
showLoadingOverlay(context);
await attachment.downloadAttachment(); await attachment.downloadAttachment();
hideLoadingOverlay();
}, },
)); ));
} }

View File

@ -12,6 +12,7 @@ import "package:inventree/inventree/part.dart";
import "package:inventree/widget/paginator.dart"; import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
@ -125,11 +126,14 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
height: 40, height: 40,
), ),
onTap: subPart == null ? null : () async { onTap: subPart == null ? null : () async {
InvenTreePart().get(bomItem.subPartId).then((var part) {
if (part is InvenTreePart) { showLoadingOverlay(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); var part = await InvenTreePart().get(bomItem.subPartId);
} hideLoadingOverlay();
});
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
}, },
); );
} }

View File

@ -9,6 +9,7 @@ import "package:inventree/inventree/part.dart";
import "package:inventree/widget/category_list.dart"; import "package:inventree/widget/category_list.dart";
import "package:inventree/widget/part_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/snacks.dart";
import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/part_detail.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
@ -125,16 +126,21 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
FontAwesomeIcons.levelUpAlt, FontAwesomeIcons.levelUpAlt,
color: COLOR_CLICK, color: COLOR_CLICK,
), ),
onTap: () { onTap: () async {
if (category == null || ((category?.parentId ?? 0) < 0)) {
int parentId = category?.parentId ?? -1;
if (parentId < 0) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
} else { } else {
// TODO - Refactor this code into the InvenTreePart class
InvenTreePartCategory().get(category?.parentId ?? -1).then((var cat) { showLoadingOverlay(context);
if (cat is InvenTreePartCategory) { var cat = await InvenTreePartCategory().get(parentId);
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); hideLoadingOverlay();
}
}); if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
}
} }
}, },
) )

View File

@ -54,11 +54,13 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
}); });
InvenTreeAPI().registerCallback(() { InvenTreeAPI().registerCallback(() {
setState(() {
// Reload the widget
});
});
if (mounted) {
setState(() {
// Reload the widget
});
}
});
} }
// Index of bottom navigation bar // Index of bottom navigation bar
@ -192,6 +194,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
return; return;
} }
// Ignore if the widget is no longer active
if (!mounted) {
return;
}
final notifications = await InvenTreeNotification().list(); final notifications = await InvenTreeNotification().list();
setState(() { setState(() {

View File

@ -10,6 +10,7 @@ import "package:inventree/l10.dart";
import "package:inventree/inventree/stock.dart"; import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/location_list.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/refreshable_state.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_detail.dart"; import "package:inventree/widget/stock_detail.dart";
@ -218,19 +219,21 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
title: Text(L10().parentLocation), title: Text(L10().parentLocation),
subtitle: Text("${location!.parentPathString}"), subtitle: Text("${location!.parentPathString}"),
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK), 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))); Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
} else { } else {
InvenTreeStockLocation().get(parent).then((var loc) { showLoadingOverlay(context);
if (loc is InvenTreeStockLocation) { var loc = await InvenTreeStockLocation().get(parentId);
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); hideLoadingOverlay();
}
}); if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
}
} }
}, },
) )

View File

@ -384,8 +384,8 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
PagedSliverList.separated( PagedSliverList.separated(
pagingController: _pagingController, pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>( builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (context, item, index) { itemBuilder: (ctx, item, index) {
return buildItem(context, item); return buildItem(ctx, item);
}, },
noItemsFoundIndicatorBuilder: (context) { noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText); return NoResultsWidget(noResultsText);
@ -450,9 +450,11 @@ class NoResultsWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text(L10().noResults), title: Text(
subtitle: Text(description), description,
leading: FaIcon(FontAwesomeIcons.exclamationCircle), style: TextStyle(fontStyle: FontStyle.italic),
),
leading: FaIcon(FontAwesomeIcons.exclamationCircle, color: COLOR_WARNING),
); );
} }

View File

@ -253,15 +253,17 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
title: Text(L10().partCategory), title: Text(L10().partCategory),
subtitle: Text("${part.categoryName}"), subtitle: Text("${part.categoryName}"),
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
onTap: () { onTap: () async {
if (part.categoryId > 0) { if (part.categoryId > 0) {
InvenTreePartCategory().get(part.categoryId).then((var cat) {
if (cat is InvenTreePartCategory) { showLoadingOverlay(context);
Navigator.push(context, MaterialPageRoute( var cat = await InvenTreePartCategory().get(part.categoryId);
builder: (context) => CategoryDisplayWidget(cat))); hideLoadingOverlay();
}
}); if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(cat)));
}
} }
}, },
) )

View File

@ -104,6 +104,10 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
"label": L10().filterTemplate, "label": L10().filterTemplate,
"help_text": L10().filterTemplateDetail "help_text": L10().filterTemplateDetail
}, },
"trackable": {
"label": L10().filterTrackable,
"help_text": L10().filterTrackableDetail,
},
"virtual": { "virtual": {
"label": L10().filterVirtual, "label": L10().filterVirtual,
"help_text": L10().filterVirtualDetail, "help_text": L10().filterVirtualDetail,
@ -122,15 +126,6 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
return page; 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 @override
Widget buildItem(BuildContext context, InvenTreeModel model) { Widget buildItem(BuildContext context, InvenTreeModel model) {
@ -147,7 +142,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
height: 40, height: 40,
), ),
onTap: () { onTap: () {
_openPart(context, part.pk); Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}, },
); );
} }

View File

@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_overlay_loader/flutter_overlay_loader.dart";
/* /*
* Construct a circular progress indicator * Construct a circular progress indicator
@ -10,4 +11,17 @@ Widget progressIndicator() {
return Center( return Center(
child: CircularProgressIndicator() child: CircularProgressIndicator()
); );
} }
void showLoadingOverlay(BuildContext context) {
Loader.show(
context,
themeData: Theme.of(context).copyWith(colorScheme: ColorScheme.fromSwatch())
);
}
void hideLoadingOverlay() {
Loader.hide();
}

View File

@ -64,9 +64,12 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
// Update current tab selection // Update current tab selection
void onTabSelectionChanged(int index) { void onTabSelectionChanged(int index) {
setState(() {
tabIndex = index; if (mounted) {
}); setState(() {
tabIndex = index;
});
}
} }
@override @override
@ -87,6 +90,10 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
Future<void> refresh(BuildContext context) async { Future<void> refresh(BuildContext context) async {
if (!mounted) {
return;
}
setState(() { setState(() {
loading = true; loading = true;
}); });

View File

@ -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<StarredPartWidget> {
List<InvenTreePart> starredParts = [];
@override
String getAppBarTitle(BuildContext context) => L10().partsStarred;
@override
Future<void> 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(),
);
}
}

View File

@ -475,13 +475,16 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
color: item.statusColor color: item.statusColor
) )
), ),
onTap: () { onTap: () async {
if (item.partId > 0) { if (item.partId > 0) {
InvenTreePart().get(item.partId).then((var part) {
if (part is InvenTreePart) { showLoadingOverlay(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); var part = await InvenTreePart().get(item.partId);
} hideLoadingOverlay();
});
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
} }
}, },
//trailing: Text(item.serialOrQuantityDisplay()), //trailing: Text(item.serialOrQuantityDisplay()),
@ -533,15 +536,17 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
FontAwesomeIcons.mapMarkerAlt, FontAwesomeIcons.mapMarkerAlt,
color: COLOR_CLICK, color: COLOR_CLICK,
), ),
onTap: () { onTap: () async {
if (item.locationId > 0) { if (item.locationId > 0) {
InvenTreeStockLocation().get(item.locationId).then((var loc) {
if (loc is InvenTreeStockLocation) { showLoadingOverlay(context);
Navigator.push(context, MaterialPageRoute( var loc = await InvenTreeStockLocation().get(item.locationId);
builder: (context) => LocationDisplayWidget(loc))); hideLoadingOverlay();
}
}); if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
}
} }
}, },
), ),

View File

@ -156,6 +156,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
String _test = ""; String _test = "";
bool _result = false; bool _result = false;
String _value = ""; String _value = "";
String _notes = "";
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE); FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
bool _valueRequired = false; bool _valueRequired = false;
@ -168,11 +169,13 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_value = item.latestResult()?.value ?? ""; _value = item.latestResult()?.value ?? "";
_valueRequired = item.requiresValue; _valueRequired = item.requiresValue;
_attachmentRequired = item.requiresAttachment; _attachmentRequired = item.requiresAttachment;
_notes = item.latestResult()?.notes ?? "";
} else if (item is InvenTreeStockItemTestResult) { } else if (item is InvenTreeStockItemTestResult) {
_result = item.result; _result = item.result;
_test = item.testName; _test = item.testName;
_required = false; _required = false;
_value = item.value; _value = item.value;
_notes = item.notes;
} }
if (_result == true) { if (_result == true) {
@ -187,8 +190,9 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
tiles.add(ListTile( tiles.add(ListTile(
title: Text(_test, style: TextStyle(fontWeight: _required ? FontWeight.bold : FontWeight.normal)), title: Text(_test, style: TextStyle(fontWeight: _required ? FontWeight.bold : FontWeight.normal)),
subtitle: Text(_value), subtitle: Text(_notes),
trailing: _icon, trailing: Text(_value),
leading: _icon,
onLongPress: () { onLongPress: () {
addTestResult( addTestResult(
context, context,

View File

@ -104,14 +104,6 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
return page; return page;
} }
void _openItem(BuildContext context, int pk) {
InvenTreeStockItem().get(pk).then((var item) {
if (item is InvenTreeStockItem) {
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}
});
}
@override @override
Widget buildItem(BuildContext context, InvenTreeModel model) { Widget buildItem(BuildContext context, InvenTreeModel model) {
@ -132,7 +124,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
), ),
), ),
onTap: () { onTap: () {
_openItem(context, item.pk); Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}, },
); );
} }

View File

@ -291,6 +291,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.9+1" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:

View File

@ -22,12 +22,13 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_markdown: ^0.6.9 # Rendering markdown 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 font_awesome_flutter: ^9.1.0 # FontAwesome icon set
http: ^0.13.4 http: ^0.13.4
image_picker: ^0.8.3 # Select or take photos image_picker: ^0.8.3 # Select or take photos
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work! infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
intl: ^0.17.0 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 open_file: ^3.2.1 # Open local files
package_info_plus: ^1.0.4 # App information introspection package_info_plus: ^1.0.4 # App information introspection
path: ^1.8.0 path: ^1.8.0