mirror of
https://github.com/inventree/inventree-app.git
synced 2026-09-09 03:10:14 +00:00
UI updates (#849)
* Updated dashboard - Display "outstanding" badges - Display "overdue" badges - Pull-to-refresh * Remove dead code * Refactor app colors * Add "pending shipments" icon * Remove custom spinner * Refactor error dialog * Updated redirect after login * Refactor login/account pages - Better UX and confirmation messages * Refactor API error messages * Improve token management - Secure storage - Handle session expiry - Per-profile HTTPS certificate checks * Improved error messages
This commit is contained in:
@@ -289,10 +289,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().appAbout),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().appAbout)),
|
||||
body: ListView(
|
||||
children: ListTile.divideTiles(context: context, tiles: tiles).toList(),
|
||||
),
|
||||
|
||||
@@ -163,10 +163,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
|
||||
|
||||
return Scaffold(
|
||||
key: _settingsKey,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().appSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().appSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
|
||||
@@ -68,7 +68,7 @@ class _InvenTreeBarcodeSettingsState
|
||||
),
|
||||
actions: <Widget>[
|
||||
MaterialButton(
|
||||
color: Colors.red,
|
||||
color: COLOR_DANGER,
|
||||
textColor: Colors.white,
|
||||
child: Text(L10().cancel),
|
||||
onPressed: () {
|
||||
@@ -78,7 +78,7 @@ class _InvenTreeBarcodeSettingsState
|
||||
},
|
||||
),
|
||||
MaterialButton(
|
||||
color: Colors.green,
|
||||
color: COLOR_SUCCESS,
|
||||
textColor: Colors.white,
|
||||
child: Text(L10().ok),
|
||||
onPressed: () async {
|
||||
@@ -120,10 +120,7 @@ class _InvenTreeBarcodeSettingsState
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().barcodeSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().barcodeSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
@@ -72,10 +71,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _settingsKey,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().homeScreen),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().homeScreen)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
+14
-6
@@ -44,19 +44,28 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
|
||||
|
||||
showLoadingOverlay();
|
||||
|
||||
// Attempt login
|
||||
// Attempt login - suppress the generic error dialog, since this screen
|
||||
// shows its own contextual inline error message below
|
||||
final response = await InvenTreeAPI().fetchToken(
|
||||
widget.profile,
|
||||
username,
|
||||
password,
|
||||
showDialog: false,
|
||||
);
|
||||
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (response.successful()) {
|
||||
// Return to the server selector screen
|
||||
Navigator.of(context).pop();
|
||||
// A token was issued - immediately connect using it, then return
|
||||
// directly to the home screen (rather than leaving the user on the
|
||||
// server-selector screen to navigate back manually)
|
||||
await InvenTreeAPI().connectToServer(widget.profile);
|
||||
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} else {
|
||||
hideLoadingOverlay();
|
||||
var data = response.asMap();
|
||||
|
||||
String err;
|
||||
@@ -104,7 +113,6 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().login),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(TablerIcons.transition_right, color: COLOR_SUCCESS),
|
||||
|
||||
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
class InvenTreePartSettingsWidget extends StatefulWidget {
|
||||
@@ -46,10 +45,7 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().partSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().partSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
@@ -45,10 +44,7 @@ class _InvenTreePurchaseOrderSettingsState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().purchaseOrderSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().purchaseOrderSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_markdown/flutter_markdown.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
@@ -14,10 +13,7 @@ class ReleaseNotesWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().releaseNotes),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().releaseNotes)),
|
||||
body: Markdown(
|
||||
selectable: false,
|
||||
data: releaseNotes,
|
||||
@@ -49,10 +45,7 @@ class CreditsWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().credits),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().credits)),
|
||||
body: Markdown(
|
||||
selectable: false,
|
||||
data: credits,
|
||||
|
||||
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
class InvenTreeSalesOrderSettingsWidget extends StatefulWidget {
|
||||
@@ -40,10 +39,7 @@ class _InvenTreeSalesOrderSettingsState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().salesOrderSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().salesOrderSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
+130
-87
@@ -6,7 +6,6 @@ import "package:inventree/settings/login.dart";
|
||||
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/spinner.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
@@ -70,7 +69,34 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Select the given profile as "active".
|
||||
* If a *different* server is currently connected, confirm with the
|
||||
* user first, since this immediately tears down the active session.
|
||||
*/
|
||||
Future<void> _selectProfile(BuildContext context, UserProfile profile) async {
|
||||
final bool switchingAwayFromActiveSession =
|
||||
InvenTreeAPI().isConnected() &&
|
||||
InvenTreeAPI().profile?.key != profile.key;
|
||||
|
||||
if (switchingAwayFromActiveSession) {
|
||||
confirmationDialog(
|
||||
L10().profileConnect,
|
||||
L10().profileSwitchConfirm,
|
||||
icon: TablerIcons.server,
|
||||
onAccept: () {
|
||||
_doSelectProfile(context, profile);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_doSelectProfile(context, profile);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doSelectProfile(
|
||||
BuildContext context,
|
||||
UserProfile profile,
|
||||
) async {
|
||||
// Disconnect InvenTree
|
||||
InvenTreeAPI().disconnectFromServer();
|
||||
|
||||
@@ -90,22 +116,13 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
|
||||
// First check if the profile has an associate token
|
||||
if (!prf.hasToken) {
|
||||
// Redirect user to login screen
|
||||
Navigator.push(
|
||||
// Redirect user to the login screen - it connects on success itself
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)),
|
||||
).then((value) async {
|
||||
_reload();
|
||||
// Reload profile
|
||||
prf = await UserProfileDBManager().getProfileByKey(key);
|
||||
if (prf?.hasToken ?? false) {
|
||||
InvenTreeAPI().connectToServer(prf!).then((result) {
|
||||
_reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Exit now, login handled by next widget
|
||||
_reload();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,12 +130,8 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
_reload();
|
||||
|
||||
// Attempt server login (this will load the newly selected profile
|
||||
InvenTreeAPI().connectToServer(prf).then((result) {
|
||||
_reload();
|
||||
});
|
||||
// Attempt server connection using the existing token
|
||||
await InvenTreeAPI().connectToServer(prf);
|
||||
|
||||
_reload();
|
||||
}
|
||||
@@ -151,12 +164,86 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
if (InvenTreeAPI().isConnected()) {
|
||||
return Icon(TablerIcons.circle_check, color: COLOR_SUCCESS);
|
||||
} else if (InvenTreeAPI().isConnecting()) {
|
||||
return Spinner(icon: TablerIcons.loader_2, color: COLOR_PROGRESS);
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(color: COLOR_PROGRESS, strokeWidth: 2),
|
||||
);
|
||||
} else {
|
||||
return Icon(TablerIcons.circle_x, color: COLOR_DANGER);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Show the profile action menu (Connect / Edit / Logout / Delete).
|
||||
* Reachable via the always-visible trailing "more" button, or long-press
|
||||
* as a bonus shortcut.
|
||||
*/
|
||||
void _showProfileActions(BuildContext context, UserProfile profile) {
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text(profile.name),
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_selectProfile(context, profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileConnect),
|
||||
leading: Icon(TablerIcons.server),
|
||||
),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_editProfile(context, userProfile: profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileEdit),
|
||||
leading: Icon(TablerIcons.edit),
|
||||
),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_logoutProfile(context, userProfile: profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileLogout),
|
||||
leading: Icon(TablerIcons.logout),
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
confirmationDialog(
|
||||
L10().delete,
|
||||
L10().profileDelete + "?",
|
||||
color: COLOR_DANGER,
|
||||
icon: TablerIcons.trash,
|
||||
onAccept: () {
|
||||
_deleteProfile(profile);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
L10().profileDelete,
|
||||
style: TextStyle(color: COLOR_DANGER),
|
||||
),
|
||||
leading: Icon(TablerIcons.trash, color: COLOR_DANGER),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
@@ -175,74 +262,24 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
leading: profile.hasToken
|
||||
? Icon(TablerIcons.user_check, color: COLOR_SUCCESS)
|
||||
: Icon(TablerIcons.user_cancel, color: COLOR_WARNING),
|
||||
trailing: _getProfileIcon(profile),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_getProfileIcon(profile) != null) _getProfileIcon(profile)!,
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert),
|
||||
tooltip: L10().actions,
|
||||
onPressed: () {
|
||||
_showProfileActions(context, profile);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile(context, profile);
|
||||
},
|
||||
onLongPress: () {
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text(profile.name),
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_selectProfile(context, profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileConnect),
|
||||
leading: Icon(TablerIcons.server),
|
||||
),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_editProfile(context, userProfile: profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileEdit),
|
||||
leading: Icon(TablerIcons.edit),
|
||||
),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_logoutProfile(context, userProfile: profile);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(L10().profileLogout),
|
||||
leading: Icon(TablerIcons.logout),
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
// Navigator.of(context, rootNavigator: true).pop();
|
||||
confirmationDialog(
|
||||
L10().delete,
|
||||
L10().profileDelete + "?",
|
||||
color: Colors.red,
|
||||
icon: TablerIcons.trash,
|
||||
onAccept: () {
|
||||
_deleteProfile(profile);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
L10().profileDelete,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
leading: Icon(TablerIcons.trash, color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
_showProfileActions(context, profile);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -256,7 +293,6 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
|
||||
key: _loginKey,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().profileSelect),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(TablerIcons.circle_plus),
|
||||
@@ -305,7 +341,6 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
title: Text(
|
||||
widget.profile == null ? L10().profileAdd : L10().profileEdit,
|
||||
),
|
||||
@@ -415,6 +450,14 @@ class _ProfileEditState extends State<ProfileEditWidget> {
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(
|
||||
L10().connectionCheckDetail,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
|
||||
@@ -42,10 +42,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().settings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().settings)),
|
||||
body: Center(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
@@ -44,10 +43,7 @@ class _InvenTreeStockSettingsState extends State<InvenTreeStockSettingsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().stockSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
appBar: AppBar(title: Text(L10().stockSettings)),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user