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:
Oliver
2026-07-06 21:10:10 +10:00
committed by GitHub
parent f737172180
commit 349a8e0ef5
45 changed files with 1240 additions and 477 deletions
+1 -4
View File
@@ -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(),
),
+1 -4
View File
@@ -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: [
+4 -7
View File
@@ -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 -5
View File
@@ -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
View File
@@ -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),
+1 -5
View File
@@ -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 -5
View File
@@ -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: [
+2 -9
View File
@@ -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,
+1 -5
View File
@@ -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
View File
@@ -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(
+1 -4
View File
@@ -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 -5
View File
@@ -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: [