From b34a91b865df1b8a8e694777c5cde3eaf70505be Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 8 Feb 2021 20:32:49 +1100 Subject: [PATCH 01/15] UserProfile --- lib/api.dart | 6 +- lib/inventree/model.dart | 49 +++++++++---- lib/l10n | 2 +- lib/preferences.dart | 49 +++++++++++++ lib/settings/login.dart | 142 ++++++++++++++++++++++++++++++++++++- lib/settings/settings.dart | 25 ++++++- lib/user_profile.dart | 109 ++++++++++++++++++++++++++++ lib/widget/dialogs.dart | 59 +++++++++++++++ pubspec.lock | 57 ++++++++++----- pubspec.yaml | 6 +- 10 files changed, 460 insertions(+), 44 deletions(-) create mode 100644 lib/user_profile.dart diff --git a/lib/api.dart b/lib/api.dart index 29e2e749..ed721c28 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -227,10 +227,8 @@ class InvenTreeAPI { errorMessage = "Server timeout"; return null; } else { - // Unknown error type - errorMessage = error.toString(); - // Unknown error type, re-throw error - return null; + // Unknown error type - re-throw the error and Sentry will catch it + throw error; } }); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 6d7d1a01..e7aec6d3 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -5,6 +5,9 @@ import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + import 'dart:convert'; import 'package:path/path.dart' as path; @@ -108,7 +111,7 @@ class InvenTreeModel { Future reload(BuildContext context, {bool dialog = false}) async { if (dialog) { - showProgressDialog(context, "Refreshing data", "Refreshing data for ${NAME}"); + showProgressDialog(context, I18N.of(context).refreshing, "Refreshing data for ${NAME}"); } var response = await api.get(url, params: defaultGetFilters()) @@ -120,9 +123,10 @@ class InvenTreeModel { } if (e is TimeoutException) { - showErrorDialog(context, "Timeout", "No response from server"); + showTimeoutDialog(context); } else { - showErrorDialog(context, "Error", e.toString()); + // Re-throw the error (Sentry will catch) + throw e; } return null; @@ -137,6 +141,12 @@ class InvenTreeModel { } if (response.statusCode != 200) { + showErrorDialog( + context, + I18N.of(context).serverError, + "${I18N.of(context).statusCode}: ${response.statusCode}" + ); + print("Error retrieving data"); return false; } @@ -170,9 +180,10 @@ class InvenTreeModel { } if (e is TimeoutException) { - showErrorDialog(context, "Timeout", "No response from server"); + showTimeoutDialog(context); } else { - showErrorDialog(context, "Error", e.toString()); + // Re-throw the error, let Sentry report it + throw e; } return null; @@ -185,7 +196,7 @@ class InvenTreeModel { } if (response.statusCode != 200) { - print("Error updating ${NAME}: Status code ${response.statusCode}"); + showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); return false; } @@ -217,7 +228,7 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); if (dialog) { - showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); } var response = await api.get(addr, params: params) @@ -229,9 +240,10 @@ class InvenTreeModel { } if (e is TimeoutException) { - showErrorDialog(context, "Timeout", "No response from server"); + showTimeoutDialog(context); } else { - showErrorDialog(context, "Error", e.toString()); + // Re-throw the error (handled by Sentry) + throw e; } return null; }); @@ -243,7 +255,7 @@ class InvenTreeModel { hideProgressDialog(context); if (response.statusCode != 200) { - print("Error retrieving data"); + showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); return null; } @@ -269,8 +281,12 @@ class InvenTreeModel { await api.post(URL, body: data) .timeout(Duration(seconds: 5)) .catchError((e) { - print("Error creating new ${NAME}:"); print(e.toString()); + showErrorDialog( + context, + I18N.of(context).serverError, + e.toString() + ); return null; }) .then((http.Response response) { @@ -279,8 +295,11 @@ class InvenTreeModel { var decoded = json.decode(response.body); _model = createFromJson(decoded); } else { - print("Error creating object: Status Code ${response.statusCode}"); - print(response.body); + showErrorDialog( + context, + I18N.of(context).serverError, + "${I18N.of(context).statusCode}: ${response.statusCode}" + ); } }); @@ -308,7 +327,7 @@ class InvenTreeModel { // TODO - Add error catching if (dialog) { - showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); } var response = await api.get(URL, params:params) @@ -320,7 +339,7 @@ class InvenTreeModel { } if (e is TimeoutException) { - showErrorDialog(context, "Timeout", "No response from server"); + showTimeoutDialog(context); } else { // Re-throw the error throw e; diff --git a/lib/l10n b/lib/l10n index 58e2c502..86fbf66a 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 58e2c5027b481a3a620c27b90ae20b888a02bd96 +Subproject commit 86fbf66aea18c43cc8920eddce22df5966a7a875 diff --git a/lib/preferences.dart b/lib/preferences.dart index 91ced0b2..201f4c27 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -1,8 +1,57 @@ import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'api.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:path/path.dart'; +import 'dart:async'; +/* + * Class for storing InvenTree preferences in a NoSql DB + */ +class InvenTreePreferencesDB { + + static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._(); + + static InvenTreePreferencesDB get instance => _singleton; + + InvenTreePreferencesDB._(); + + Completer _dbOpenCompleter; + + Future get database async { + // If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened + if (_dbOpenCompleter == null) { + _dbOpenCompleter = Completer(); + // Calling _openDatabase will also complete the completer with database instance + _openDatabase(); + } + // If the database is already opened, awaiting the future will happen instantly. + // Otherwise, awaiting the returned future will take some time - until complete() is called + // on the Completer in _openDatabase() below. + return _dbOpenCompleter.future; + } + + Future _openDatabase() async { + // Get a platform-specific directory where persistent app data can be stored + final appDocumentDir = await getApplicationDocumentsDirectory(); + + print("Documents Dir: ${appDocumentDir.toString()}"); + + print("Path: ${appDocumentDir.path}"); + + // Path with the form: /platform-specific-directory/demo.db + final dbPath = join(appDocumentDir.path, 'InvenTreeSettings.db'); + + final database = await databaseFactoryIo.openDatabase(dbPath); + + // Any code awaiting the Completer's future will now start executing + _dbOpenCompleter.complete(database); + } +} + class InvenTreePreferences { static const String _SERVER = 'server'; diff --git a/lib/settings/login.dart b/lib/settings/login.dart index dbe95461..e298604d 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,18 +1,27 @@ +import 'dart:ffi'; + +import 'package:InvenTree/widget/dialogs.dart'; +import 'package:InvenTree/widget/fields.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../api.dart'; import '../preferences.dart'; +import '../user_profile.dart'; class InvenTreeLoginSettingsWidget extends StatefulWidget { final SharedPreferences _preferences; - InvenTreeLoginSettingsWidget(this._preferences) : super(); + final List _profiles; + + InvenTreeLoginSettingsWidget(this._profiles, this._preferences) : super(); @override - _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_preferences); + _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_profiles, _preferences); } @@ -20,18 +29,61 @@ class _InvenTreeLoginSettingsState extends State { final GlobalKey _formKey = new GlobalKey(); + final _addProfileKey = new GlobalKey(); + final SharedPreferences _preferences; + List profiles; + String _server = ''; String _username = ''; String _password = ''; - _InvenTreeLoginSettingsState(this._preferences) : super() { + _InvenTreeLoginSettingsState(this.profiles, this._preferences) : super() { _server = _preferences.getString('server') ?? ''; _username = _preferences.getString('username') ?? ''; _password = _preferences.getString('password') ?? ''; } + void _createProfile(BuildContext context) { + + showFormDialog( + context, + I18N.of(context).profileAdd, + key: _addProfileKey, + actions: [ + FlatButton( + child: Text(I18N.of(context).cancel), + onPressed: () { + Navigator.of(context).pop(); + } + ), + FlatButton( + child: Text(I18N.of(context).save), + onPressed: () { + // TODO + } + ) + ], + fields: [ + StringField( + label: I18N.of(context).name, + initial: "profile", + ), + StringField( + label: "Server", + initial: "http://127.0.0.1:8000", + ), + StringField( + label: "Username", + ), + StringField( + label: "Password" + ) + ] + ); + } + String _validateServer(String value) { @@ -71,11 +123,95 @@ class _InvenTreeLoginSettingsState extends State { } } + void _deleteProfile(UserProfile profile) async { + + await UserProfileDBManager().deleteProfile(profile); + + // Reload profiles + profiles = await UserProfileDBManager().getAllProfiles(); + + setState(() { + }); + } + @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; + List children = []; + + for (int idx = 0; idx < profiles.length; idx++) { + + UserProfile profile = profiles[idx]; + + children.add(ListTile( + title: Text(profile.name), + subtitle: Text(profile.server), + trailing: FaIcon(FontAwesomeIcons.checkCircle), + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: Text(profile.name), + children: [ + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + // TODO - Mark profile as selected + }, + child: Text(I18N.of(context).profileSelect), + ), + SimpleDialogOption( + onPressed: () { + //Navigator.of(context).pop(); + // TODO - Edit profile! + }, + child: Text(I18N.of(context).profileEdit), + ), + SimpleDialogOption( + onPressed: () { + // Navigator.of(context, rootNavigator: true).pop(); + confirmationDialog( + context, + "Delete", + "Delete this profile?", + onAccept: () { + _deleteProfile(profile); + } + ); + }, + child: Text(I18N.of(context).profileDelete), + ) + ], + ); + } + ); + }, + onTap: () { + + }, + )); + } + + return Scaffold( + appBar: AppBar( + title: Text(I18N.of(context).profile), + ), + body: Container( + child: ListView( + children: children, + ) + ), + floatingActionButton: FloatingActionButton( + child: Icon(FontAwesomeIcons.plus), + onPressed: () { + _createProfile(context); + }, + ) + ); + return Scaffold( appBar: AppBar( title: Text("Login Settings"), diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index a2cf1663..387ff4f4 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -1,6 +1,8 @@ import 'package:InvenTree/settings/about.dart'; import 'package:InvenTree/settings/login.dart'; import 'package:InvenTree/settings/release.dart'; +import 'package:InvenTree/user_profile.dart'; +import 'package:InvenTree/preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -58,6 +60,25 @@ class _InvenTreeSettingsState extends State { leading: FaIcon(FontAwesomeIcons.bug), onTap: null, ), + ListTile( + title: Text("Throw Error"), + onTap: () { + throw("My custom error"); + }, + ), + ListTile( + title: Text("add profile"), + onTap: () { + UserProfileDBManager().addProfile( + UserProfile( + name: "My Profile", + server: "https://127.0.0.1:8000", + username: "Oliver", + password: "hunter2", + ) + ); + }, + ) ], ) ) @@ -68,7 +89,9 @@ class _InvenTreeSettingsState extends State { var prefs = await SharedPreferences.getInstance(); - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(prefs))); + List profiles = await UserProfileDBManager().getAllProfiles(); + + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(profiles, prefs))); } void _about() async { diff --git a/lib/user_profile.dart b/lib/user_profile.dart new file mode 100644 index 00000000..d896274f --- /dev/null +++ b/lib/user_profile.dart @@ -0,0 +1,109 @@ + +/* + * Class for InvenTree user / login details + */ +import 'package:sembast/sembast.dart'; +import 'preferences.dart'; + +class UserProfile { + + UserProfile({ + this.name, + this.server, + this.username, + this.password + }); + + // Name of the user profile + String name; + + // Base address of the InvenTree server + String server; + + // Username + String username; + + // Password + String password; + + // User ID (will be provided by the server on log-in) + int user_id; + + factory UserProfile.fromJson(Map json) => UserProfile( + name: json['name'], + server: json['server'], + username: json['username'], + password: json['password'], + ); + + Map toJson() => { + "name": name, + "server": server, + "username": username, + "password": password, + }; + + @override + String toString() { + return "${server} - ${username}:${password}"; + } +} + +class UserProfileDBManager { + + static const String folder_name = "profiles"; + + final _folder = intMapStoreFactory.store(folder_name); + + Future get _db async => await InvenTreePreferencesDB.instance.database; + + Future addProfile(UserProfile profile) async { + + UserProfile existingProfile = await getProfile(profile.name); + + if (existingProfile != null) { + print("UserProfile '${profile.name}' already exists"); + return; + } + + await _folder.add(await _db, profile.toJson()); + + print("Added user profile '${profile.name}'"); + } + + Future deleteProfile(UserProfile profile) async { + final finder = Finder(filter: Filter.equals("name", profile.name)); + await _folder.delete(await _db, finder: finder); + + print("Deleted user profile ${profile.name}"); + } + + Future getProfile(String name) async { + // Lookup profile by name (or return null if does not exist) + final finder = Finder(filter: Filter.equals("name", name)); + + final profiles = await _folder.find(await _db, finder: finder); + + if (profiles.length == 0) { + return null; + } + + // Return the first matching profile object + return UserProfile.fromJson(profiles[0].value); + } + + /* + * Return all user profile objects + */ + Future> getAllProfiles() async { + final profiles = await _folder.find(await _db); + + List profileList = new List(); + + for (int idx = 0; idx < profiles.length; idx++) { + profileList.add(UserProfile.fromJson(profiles[idx].value)); + } + + return profileList; + } +} diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 1816df20..828a4476 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -4,6 +4,53 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Future confirmationDialog(BuildContext context, String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async { + + if (acceptText == null || acceptText.isEmpty) { + acceptText = I18N.of(context).ok; + } + + if (rejectText == null || rejectText.isEmpty) { + rejectText = I18N.of(context).cancel; + } + + AlertDialog dialog = AlertDialog( + title: ListTile( + title: Text(title), + leading: FaIcon(FontAwesomeIcons.questionCircle), + ), + content: Text(text), + actions: [ + FlatButton( + child: Text(rejectText), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + if (onReject != null) { + onReject(); + } + } + ), + FlatButton( + child: Text(acceptText), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + if (onAccept != null) { + onAccept(); + } + } + ) + ], + ); + + showDialog( + context: context, + builder: (BuildContext context) { + return dialog; + } + ); +} + void showMessage(BuildContext context, String message) { Scaffold.of(context).showSnackBar(SnackBar( content: Text(message), @@ -64,6 +111,18 @@ Future showErrorDialog(BuildContext context, String title, String descript }); } +void showTimeoutDialog(BuildContext context) { + /* + Show a server timeout dialog + */ + + showErrorDialog( + context, + I18N.of(context).timeout, + I18N.of(context).noResponse + ); +} + void showProgressDialog(BuildContext context, String title, String description) { showDialog( diff --git a/pubspec.lock b/pubspec.lock index 04026bf0..cd28fab8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: camera url: "https://pub.dartlang.org" source: hosted - version: "0.6.4+5" + version: "0.7.0+2" camera_platform_interface: dependency: transitive description: @@ -106,6 +106,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + device_info: + dependency: "direct main" + description: + name: device_info + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + device_info_platform_interface: + dependency: transitive + description: + name: device_info_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" fake_async: dependency: transitive description: @@ -152,7 +166,7 @@ packages: name: flutter_keyboard_visibility url: "https://pub.dartlang.org" source: hosted - version: "4.0.2" + version: "4.0.3" flutter_keyboard_visibility_platform_interface: dependency: transitive description: @@ -251,14 +265,14 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.6.7+21" + version: "0.6.7+22" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.6" intl: dependency: "direct main" description: @@ -300,7 +314,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.3+2" + version: "0.4.3+4" path: dependency: "direct main" description: @@ -392,6 +406,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.14" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" rxdart: dependency: transitive description: @@ -399,13 +420,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.25.0" + sembast: + dependency: "direct main" + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.9" sentry: dependency: "direct main" description: name: sentry url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "4.0.4" shared_preferences: dependency: "direct main" description: @@ -447,7 +475,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "0.0.2+3" sky_engine: dependency: transitive description: flutter @@ -466,14 +494,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "1.3.2+2" + version: "1.3.2+3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.3+1" stack_trace: dependency: transitive description: @@ -564,7 +592,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "0.1.5+3" url_launcher_windows: dependency: transitive description: @@ -572,13 +600,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+3" - usage: - dependency: transitive - description: - name: usage - url: "https://pub.dartlang.org" - source: hosted - version: "3.4.2" uuid: dependency: transitive description: @@ -599,7 +620,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4" + version: "1.7.4+1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ad7660c2..c212ecc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,15 +33,17 @@ dependencies: preferences: ^5.2.0 # Persistent settings storage qr_code_scanner: ^0.0.13 package_info: ^0.4.0 # App information introspection + device_info: ^1.0.0 # Information about the device font_awesome_flutter: ^8.8.1 # FontAwesome icon set flutter_speed_dial: ^1.2.5 # FAB menu elements - sentry: ^3.0.1 # Error reporting + sentry: ^4.0.4 # Error reporting flutter_typeahead: ^1.8.1 # Auto-complete input field image_picker: ^0.6.6 # Select or take photos url_launcher: ^5.7.10 # Open link in system browser flutter_markdown: ^0.5.2 # Rendering markdown camera: - path_provider: + path_provider: ^1.6.27 # Local file storage + sembast: ^2.4.9 # NoSQL data storage path: dev_dependencies: From c2abfbe00fa293c8600dcd4e8f9ca2830cc6b6e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 8 Feb 2021 20:39:19 +1100 Subject: [PATCH 02/15] Add function to update user profile --- lib/user_profile.dart | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/user_profile.dart b/lib/user_profile.dart index d896274f..72553094 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -8,12 +8,16 @@ import 'preferences.dart'; class UserProfile { UserProfile({ + this.key, this.name, this.server, this.username, this.password }); + // ID of the profile + int key; + // Name of the user profile String name; @@ -29,7 +33,8 @@ class UserProfile { // User ID (will be provided by the server on log-in) int user_id; - factory UserProfile.fromJson(Map json) => UserProfile( + factory UserProfile.fromJson(int key, Map json) => UserProfile( + key: key, name: json['name'], server: json['server'], username: json['username'], @@ -66,16 +71,32 @@ class UserProfileDBManager { return; } - await _folder.add(await _db, profile.toJson()); + int key = await _folder.add(await _db, profile.toJson()); - print("Added user profile '${profile.name}'"); + print("Added user profile <${key}> - '${profile.name}'"); + + // Record the key + profile.key = key; + } + + Future updateProfile(UserProfile profile) async { + + if (profile.key == null) { + addProfile(profile); + return; + } + + final finder = Finder(filter: Filter.byKey(profile.key)); + await _folder.update(await _db, profile.toJson(), finder: finder); + + print("Updated user profile <%{profile.key}> - '${profile.name}"); } Future deleteProfile(UserProfile profile) async { final finder = Finder(filter: Filter.equals("name", profile.name)); await _folder.delete(await _db, finder: finder); - print("Deleted user profile ${profile.name}"); + print("Deleted user profile <${profile.key}> - '${profile.name}'"); } Future getProfile(String name) async { @@ -89,7 +110,7 @@ class UserProfileDBManager { } // Return the first matching profile object - return UserProfile.fromJson(profiles[0].value); + return UserProfile.fromJson(profiles[0].key, profiles[0].value); } /* @@ -101,7 +122,7 @@ class UserProfileDBManager { List profileList = new List(); for (int idx = 0; idx < profiles.length; idx++) { - profileList.add(UserProfile.fromJson(profiles[idx].value)); + profileList.add(UserProfile.fromJson(profiles[idx].key, profiles[idx].value)); } return profileList; From e8a88997b40dc89e8775eb915f480b499e197a01 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 8 Feb 2021 21:24:11 +1100 Subject: [PATCH 03/15] Add / delete / select user profiles --- lib/settings/login.dart | 94 ++++++++++++++++++++++++++++++++++++----- lib/user_profile.dart | 61 +++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 12 deletions(-) diff --git a/lib/settings/login.dart b/lib/settings/login.dart index e298604d..7447d93e 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -29,7 +29,7 @@ class _InvenTreeLoginSettingsState extends State { final GlobalKey _formKey = new GlobalKey(); - final _addProfileKey = new GlobalKey(); + final GlobalKey _addProfileKey = new GlobalKey(); final SharedPreferences _preferences; @@ -45,8 +45,21 @@ class _InvenTreeLoginSettingsState extends State { _password = _preferences.getString('password') ?? ''; } + void _reload() async { + + profiles = await UserProfileDBManager().getAllProfiles(); + + setState(() { + }); + } + void _createProfile(BuildContext context) { + var _name; + var _server; + var _username; + var _password; + showFormDialog( context, I18N.of(context).profileAdd, @@ -61,7 +74,19 @@ class _InvenTreeLoginSettingsState extends State { FlatButton( child: Text(I18N.of(context).save), onPressed: () { - // TODO + if (_addProfileKey.currentState.validate()) { + _addProfileKey.currentState.save(); + + // TODO - create the new profile... + UserProfile profile = UserProfile( + name: _name, + server: _server, + username: _username, + password: _password + ); + + _addProfile(profile); + } } ) ], @@ -69,21 +94,40 @@ class _InvenTreeLoginSettingsState extends State { StringField( label: I18N.of(context).name, initial: "profile", + onSaved: (value) => _name = value, + validator: _validateProfileName, ), StringField( label: "Server", initial: "http://127.0.0.1:8000", + hint: "http[s]://:", + validator: _validateServer, + onSaved: (value) => _server = value, ), StringField( label: "Username", + onSaved: (value) => _username = value, + validator: _validateUsername, ), StringField( - label: "Password" + label: "Password", + onSaved: (value) => _password = value, + validator: _validatePassword, ) ] ); } + String _validateProfileName(String value) { + + if (value.isEmpty) { + return 'Profile name cannot be empty'; + } + + // TODO: Check if a profile already exists with ths name + + return null; + } String _validateServer(String value) { @@ -95,6 +139,8 @@ class _InvenTreeLoginSettingsState extends State { return 'Server must start with http[s]'; } + // TODO: URL validator + return null; } @@ -118,20 +164,46 @@ class _InvenTreeLoginSettingsState extends State { if (_formKey.currentState.validate()) { _formKey.currentState.save(); + // TODO await InvenTreePreferences().saveLoginDetails(context, _server, _username, _password); } } + void _selectProfile(UserProfile profile) async { + + // Mark currently selected profile as unselected + final selected = await UserProfileDBManager().getSelectedProfile(); + + selected.selected = false; + + await UserProfileDBManager().updateProfile(selected); + + profile.selected = true; + + await UserProfileDBManager().updateProfile(profile); + + _reload(); + } + void _deleteProfile(UserProfile profile) async { await UserProfileDBManager().deleteProfile(profile); - // Reload profiles - profiles = await UserProfileDBManager().getAllProfiles(); + // Close the dialog + Navigator.of(context).pop(); - setState(() { - }); + _reload(); + } + + void _addProfile(UserProfile profile) async { + + await UserProfileDBManager().addProfile(profile); + + // Dismiss the create dialog + Navigator.of(context).pop(); + + _reload(); } @override @@ -146,9 +218,11 @@ class _InvenTreeLoginSettingsState extends State { UserProfile profile = profiles[idx]; children.add(ListTile( - title: Text(profile.name), + title: Text( + profile.name, + ), subtitle: Text(profile.server), - trailing: FaIcon(FontAwesomeIcons.checkCircle), + trailing: profile.selected ? FaIcon(FontAwesomeIcons.checkCircle) : null, onLongPress: () { showDialog( context: context, @@ -159,7 +233,7 @@ class _InvenTreeLoginSettingsState extends State { SimpleDialogOption( onPressed: () { Navigator.of(context).pop(); - // TODO - Mark profile as selected + _selectProfile(profile); }, child: Text(I18N.of(context).profileSelect), ), diff --git a/lib/user_profile.dart b/lib/user_profile.dart index 72553094..ba70f075 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -12,7 +12,8 @@ class UserProfile { this.name, this.server, this.username, - this.password + this.password, + this.selected, }); // ID of the profile @@ -30,6 +31,8 @@ class UserProfile { // Password String password; + bool selected = false; + // User ID (will be provided by the server on log-in) int user_id; @@ -39,6 +42,7 @@ class UserProfile { server: json['server'], username: json['username'], password: json['password'], + selected: json['selected'] ?? false, ); Map toJson() => { @@ -46,6 +50,7 @@ class UserProfile { "server": server, "username": username, "password": password, + "selected": selected, }; @override @@ -89,7 +94,7 @@ class UserProfileDBManager { final finder = Finder(filter: Filter.byKey(profile.key)); await _folder.update(await _db, profile.toJson(), finder: finder); - print("Updated user profile <%{profile.key}> - '${profile.name}"); + print("Updated user profile <${profile.key}> - '${profile.name}"); } Future deleteProfile(UserProfile profile) async { @@ -99,13 +104,65 @@ class UserProfileDBManager { print("Deleted user profile <${profile.key}> - '${profile.name}'"); } + Future getSelectedProfile() async { + /* + * Return the currently selected profile. + * + * If multiple profiles are selected, + * mark all but the first as unselected + * + * If no profile is currently selected, + * then force the first profile to be selected. + */ + + final selected_finder = Finder(filter: Filter.equals("selected", true)); + + final selected_profiles = await _folder.find(await _db, finder: selected_finder); + + if (selected_profiles.length == 1) { + // A single profile is selected + return UserProfile.fromJson(selected_profiles[0].key, selected_profiles[0].value); + } else if (selected_profiles.length > 1) { + // Multiple selected profiles - de-select others + for (int idx = 1; idx < selected_profiles.length; idx++) { + UserProfile profile = UserProfile.fromJson(selected_profiles[idx].key, selected_profiles[idx].value); + + profile.selected = false; + updateProfile(profile); + } + + // And return the first profile + return UserProfile.fromJson(selected_profiles[0].key, selected_profiles[0].value); + } else { + // No profiles selected! + + final all_profiles = await getAllProfiles(); + + if (all_profiles.length == 0) { + // No profiles available + return null; + } else { + UserProfile prf = all_profiles[0]; + prf.selected = true; + updateProfile(prf); + + // Return the selected profile + return prf; + } + } + } + Future getProfile(String name) async { + + print("Looking for user profile '${name}'"); + // Lookup profile by name (or return null if does not exist) final finder = Finder(filter: Filter.equals("name", name)); final profiles = await _folder.find(await _db, finder: finder); if (profiles.length == 0) { + print("No matching profiles found"); return null; } From 7fe1b32bf6cbf7633b0afedf9f06f28a5e92e253 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 8 Feb 2021 21:28:16 +1100 Subject: [PATCH 04/15] I18N updates --- lib/settings/login.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 7447d93e..0733710a 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -93,24 +93,26 @@ class _InvenTreeLoginSettingsState extends State { fields: [ StringField( label: I18N.of(context).name, - initial: "profile", + hint: "Enter profile name", onSaved: (value) => _name = value, validator: _validateProfileName, ), StringField( - label: "Server", + label: I18N.of(context).server, initial: "http://127.0.0.1:8000", hint: "http[s]://:", validator: _validateServer, onSaved: (value) => _server = value, ), StringField( - label: "Username", + label: I18N.of(context).username, + hint: "Enter username", onSaved: (value) => _username = value, validator: _validateUsername, ), StringField( - label: "Password", + label: I18N.of(context).password, + hint: "Enter password", onSaved: (value) => _password = value, validator: _validatePassword, ) From d5649af2f9f8d4228e5445bd31bf9bd0a727f1b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 8 Feb 2021 22:03:14 +1100 Subject: [PATCH 05/15] Server login now references the UserProfile class --- lib/api.dart | 42 ++++++++++------- lib/preferences.dart | 10 +---- lib/settings/login.dart | 92 ++------------------------------------ lib/settings/settings.dart | 21 +-------- lib/widget/home.dart | 24 +++++----- pubspec.lock | 7 --- pubspec.yaml | 3 +- 7 files changed, 45 insertions(+), 154 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index ed721c28..d66a7c56 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:InvenTree/user_profile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -108,8 +109,7 @@ class InvenTreeAPI { String makeUrl(String endpoint) => _makeUrl(endpoint); - String _username = ""; - String _password = ""; + UserProfile _profile; // Authentication token (initially empty, must be requested) String _token = ""; @@ -168,25 +168,34 @@ class InvenTreeAPI { InvenTreeAPI._internal(); Future connect(BuildContext context) async { - var prefs = await SharedPreferences.getInstance(); - String server = prefs.getString("server"); - String username = prefs.getString("username"); - String password = prefs.getString("password"); + _profile = await UserProfileDBManager().getSelectedProfile(); - return connectToServer(context, server, username, password); + if (_profile == null) { + await showErrorDialog( + context, + "Select Profile", + "User profile not selected" + ); + return false; + } + + return connectToServer(context); } - Future connectToServer(BuildContext context, String address, String username, String password) async { + Future connectToServer(BuildContext context) async { /* Address is the base address for the InvenTree server, * e.g. http://127.0.0.1:8000 */ + if (_profile == null) return false; + String errorMessage = ""; - address = address.trim(); - username = username.trim(); + String address = _profile.server.trim(); + String username = _profile.username.trim(); + String password = _profile.password.trim(); if (address.isEmpty || username.isEmpty || password.isEmpty) { await showErrorDialog( @@ -211,14 +220,13 @@ class InvenTreeAPI { */ _BASE_URL = address; - _username = username; - _password = password; - _connected = false; print("Connecting to " + apiUrl + " -> " + username + ":" + password); - var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { + var response = await get("").timeout(Duration(seconds: 5)).catchError((error) { + + print("Error connecting to server: ${error.toString()}"); if (error is SocketException) { errorMessage = "Could not connect to server"; @@ -397,7 +405,7 @@ class InvenTreeAPI { Map defaultHeaders() { var headers = Map(); - headers[HttpHeaders.authorizationHeader] = _authorizationHeader(); + headers[HttpHeaders.authorizationHeader] = _authorizationHeader(_profile.username, _profile.password); return headers; } @@ -408,11 +416,11 @@ class InvenTreeAPI { return headers; } - String _authorizationHeader() { + String _authorizationHeader(String username, String password) { if (_token.isNotEmpty) { return "Token $_token"; } else { - return "Basic " + base64Encode(utf8.encode('$_username:$_password')); + return "Basic " + base64Encode(utf8.encode('${username}:${password}')); } } diff --git a/lib/preferences.dart b/lib/preferences.dart index 201f4c27..b4512d30 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -89,13 +89,7 @@ class InvenTreePreferences { print("Loading login details"); - SharedPreferences prefs = await SharedPreferences.getInstance(); - - var server = prefs.getString(_SERVER) ?? ''; - var username = prefs.getString(_USERNAME) ?? ''; - var password = prefs.getString(_PASSWORD) ?? ''; - - await InvenTreeAPI().connectToServer(context, server, username, password); + await InvenTreeAPI().connectToServer(context); } void saveLoginDetails(BuildContext context, String server, String username, String password) async { @@ -107,6 +101,6 @@ class InvenTreePreferences { await prefs.setString(_PASSWORD, password); // Reconnect the API - await InvenTreeAPI().connectToServer(context, server, username, password); + await InvenTreeAPI().connectToServer(context); } } \ No newline at end of file diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 0733710a..06b99ed9 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,10 +1,6 @@ -import 'dart:ffi'; - import 'package:InvenTree/widget/dialogs.dart'; import 'package:InvenTree/widget/fields.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_speed_dial/flutter_speed_dial.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -14,14 +10,12 @@ import '../user_profile.dart'; class InvenTreeLoginSettingsWidget extends StatefulWidget { - final SharedPreferences _preferences; - final List _profiles; - InvenTreeLoginSettingsWidget(this._profiles, this._preferences) : super(); + InvenTreeLoginSettingsWidget(this._profiles) : super(); @override - _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_profiles, _preferences); + _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_profiles); } @@ -31,18 +25,10 @@ class _InvenTreeLoginSettingsState extends State { final GlobalKey _addProfileKey = new GlobalKey(); - final SharedPreferences _preferences; - List profiles; - String _server = ''; - String _username = ''; - String _password = ''; + _InvenTreeLoginSettingsState(this.profiles) { - _InvenTreeLoginSettingsState(this.profiles, this._preferences) : super() { - _server = _preferences.getString('server') ?? ''; - _username = _preferences.getString('username') ?? ''; - _password = _preferences.getString('password') ?? ''; } void _reload() async { @@ -99,7 +85,6 @@ class _InvenTreeLoginSettingsState extends State { ), StringField( label: I18N.of(context).server, - initial: "http://127.0.0.1:8000", hint: "http[s]://:", validator: _validateServer, onSaved: (value) => _server = value, @@ -162,16 +147,6 @@ class _InvenTreeLoginSettingsState extends State { return null; } - void _save(BuildContext context) async { - if (_formKey.currentState.validate()) { - _formKey.currentState.save(); - - // TODO - await InvenTreePreferences().saveLoginDetails(context, _server, _username, _password); - - } - } - void _selectProfile(UserProfile profile) async { // Mark currently selected profile as unselected @@ -287,66 +262,5 @@ class _InvenTreeLoginSettingsState extends State { }, ) ); - - return Scaffold( - appBar: AppBar( - title: Text("Login Settings"), - ), - body: new Container( - padding: new EdgeInsets.all(20.0), - child: new Form( - key: _formKey, - child: new ListView( - children: [ - Text(I18N.of(context).serverAddress), - new TextFormField( - initialValue: _server, - decoration: InputDecoration( - hintText: "127.0.0.1:8000", - ), - validator: _validateServer, - onSaved: (String value) { - _server = value; - }, - ), - Divider(), - Text(I18N.of(context).accountDetails), - TextFormField( - initialValue: _username, - decoration: InputDecoration( - hintText: I18N.of(context).username, - labelText: I18N.of(context).username, - ), - validator: _validateUsername, - onSaved: (String value) { - _username = value; - } - ), - TextFormField( - initialValue: _password, - obscureText: true, - decoration: InputDecoration( - hintText: I18N.of(context).password, - labelText: I18N.of(context).password, - ), - validator: _validatePassword, - onSaved: (String value) { - _password = value; - }, - ), - Container( - width: screenSize.width, - child: RaisedButton( - child: Text(I18N.of(context).save), - onPressed: () { - _save(context); - } - ) - ) - ], - ) - ) - ) - ); } } \ No newline at end of file diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 387ff4f4..ffd2731a 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -60,25 +60,6 @@ class _InvenTreeSettingsState extends State { leading: FaIcon(FontAwesomeIcons.bug), onTap: null, ), - ListTile( - title: Text("Throw Error"), - onTap: () { - throw("My custom error"); - }, - ), - ListTile( - title: Text("add profile"), - onTap: () { - UserProfileDBManager().addProfile( - UserProfile( - name: "My Profile", - server: "https://127.0.0.1:8000", - username: "Oliver", - password: "hunter2", - ) - ); - }, - ) ], ) ) @@ -91,7 +72,7 @@ class _InvenTreeSettingsState extends State { List profiles = await UserProfileDBManager().getAllProfiles(); - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(profiles, prefs))); + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(profiles))); } void _about() async { diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 0a2d0129..c1bc56c6 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -1,10 +1,10 @@ +import 'package:InvenTree/user_profile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/api.dart'; @@ -27,8 +27,6 @@ class _InvenTreeHomePageState extends State { _InvenTreeHomePageState() : super() { } - String _serverAddress = ""; - String _serverStatus = "Connecting to server"; String _serverMessage = ""; @@ -39,20 +37,28 @@ class _InvenTreeHomePageState extends State { Color _serverStatusColor = Color.fromARGB(255, 50, 50, 250); - void onConnectSuccess(String msg) { + void onConnectSuccess(String msg) async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + + String address = profile?.server ?? 'unknown server address'; + _serverConnection = true; _serverMessage = msg; - _serverStatus = "Connected to $_serverAddress"; + _serverStatus = "Connected to ${address}"; _serverStatusColor = Color.fromARGB(255, 50, 250, 50); _serverIcon = new FaIcon(FontAwesomeIcons.checkCircle, color: _serverStatusColor); setState(() {}); } - void onConnectFailure(String msg) { + void onConnectFailure(String msg) async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + _serverConnection = false; _serverMessage = msg; - _serverStatus = "Could not connect to $_serverAddress"; + _serverStatus = "Could not connect to ${profile?.server}"; _serverStatusColor = Color.fromARGB(255, 250, 50, 50); _serverIcon = new FaIcon(FontAwesomeIcons.timesCircle, color: _serverStatusColor); @@ -64,10 +70,6 @@ class _InvenTreeHomePageState extends State { */ void _checkServerConnection(BuildContext context) async { - var prefs = await SharedPreferences.getInstance(); - - _serverAddress = prefs.getString("server"); - // Reset the connection status variables _serverStatus = "Connecting to server"; _serverMessage = ""; diff --git a/pubspec.lock b/pubspec.lock index cd28fab8..8d39f12c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -385,13 +385,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - preferences: - dependency: "direct main" - description: - name: preferences - url: "https://pub.dartlang.org" - source: hosted - version: "5.2.1" process: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c212ecc8..0902d4da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: InvenTree stock management # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.1.0+1 +version: 0.1.0+2 environment: sdk: ">=2.1.0 <3.0.0" @@ -30,7 +30,6 @@ dependencies: shared_preferences: ^0.5.7 cached_network_image: ^2.5.0 - preferences: ^5.2.0 # Persistent settings storage qr_code_scanner: ^0.0.13 package_info: ^0.4.0 # App information introspection device_info: ^1.0.0 # Information about the device From 74e6315a57731f075e94abc1bb5e3899361c6195 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 08:57:56 +1100 Subject: [PATCH 06/15] Edit existing profile --- lib/l10n | 2 +- lib/settings/login.dart | 63 ++++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/lib/l10n b/lib/l10n index 86fbf66a..ef139565 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 86fbf66aea18c43cc8920eddce22df5966a7a875 +Subproject commit ef139565bba4fcd5889aa242a0062827eab5eceb diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 06b99ed9..868760dd 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -27,9 +27,7 @@ class _InvenTreeLoginSettingsState extends State { List profiles; - _InvenTreeLoginSettingsState(this.profiles) { - - } + _InvenTreeLoginSettingsState(this.profiles); void _reload() async { @@ -39,13 +37,19 @@ class _InvenTreeLoginSettingsState extends State { }); } - void _createProfile(BuildContext context) { + void _editProfile(BuildContext context, {UserProfile userProfile, bool createNew = false}) { var _name; var _server; var _username; var _password; + UserProfile profile; + + if (userProfile != null) { + profile = userProfile; + } + showFormDialog( context, I18N.of(context).profileAdd, @@ -63,15 +67,26 @@ class _InvenTreeLoginSettingsState extends State { if (_addProfileKey.currentState.validate()) { _addProfileKey.currentState.save(); - // TODO - create the new profile... - UserProfile profile = UserProfile( - name: _name, - server: _server, - username: _username, - password: _password - ); + if (createNew) { + // TODO - create the new profile... + UserProfile profile = UserProfile( + name: _name, + server: _server, + username: _username, + password: _password + ); - _addProfile(profile); + _addProfile(profile); + } else { + + profile.name = _name; + profile.server = _server; + profile.username = _username; + profile.password = _password; + + _updateProfile(profile); + + } } } ) @@ -80,24 +95,28 @@ class _InvenTreeLoginSettingsState extends State { StringField( label: I18N.of(context).name, hint: "Enter profile name", + initial: createNew ? '' : profile.name, onSaved: (value) => _name = value, validator: _validateProfileName, ), StringField( label: I18N.of(context).server, hint: "http[s]://:", + initial: createNew ? '' : profile.server, validator: _validateServer, onSaved: (value) => _server = value, ), StringField( label: I18N.of(context).username, hint: "Enter username", + initial: createNew ? '' : profile.username, onSaved: (value) => _username = value, validator: _validateUsername, ), StringField( label: I18N.of(context).password, hint: "Enter password", + initial: createNew ? '' : profile.password, onSaved: (value) => _password = value, validator: _validatePassword, ) @@ -173,6 +192,16 @@ class _InvenTreeLoginSettingsState extends State { _reload(); } + void _updateProfile(UserProfile profile) async { + + await UserProfileDBManager().updateProfile(profile); + + // Dismiss the dialog + Navigator.of(context).pop(); + + _reload(); + } + void _addProfile(UserProfile profile) async { await UserProfileDBManager().addProfile(profile); @@ -216,8 +245,8 @@ class _InvenTreeLoginSettingsState extends State { ), SimpleDialogOption( onPressed: () { - //Navigator.of(context).pop(); - // TODO - Edit profile! + Navigator.of(context).pop(); + _editProfile(context, userProfile: profile); }, child: Text(I18N.of(context).profileEdit), ), @@ -226,7 +255,7 @@ class _InvenTreeLoginSettingsState extends State { // Navigator.of(context, rootNavigator: true).pop(); confirmationDialog( context, - "Delete", + I18N.of(context).delete, "Delete this profile?", onAccept: () { _deleteProfile(profile); @@ -248,7 +277,7 @@ class _InvenTreeLoginSettingsState extends State { return Scaffold( appBar: AppBar( - title: Text(I18N.of(context).profile), + title: Text(I18N.of(context).profileSelect), ), body: Container( child: ListView( @@ -258,7 +287,7 @@ class _InvenTreeLoginSettingsState extends State { floatingActionButton: FloatingActionButton( child: Icon(FontAwesomeIcons.plus), onPressed: () { - _createProfile(context); + _editProfile(context, createNew: true); }, ) ); From d9180794404f28ebc8fe3233114c7d2b4f5dae36 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 09:12:47 +1100 Subject: [PATCH 07/15] Cleanup --- lib/l10n | 2 +- lib/preferences.dart | 20 ------ lib/settings/login.dart | 125 ++++++++++++++++++++++--------------- lib/settings/settings.dart | 8 +-- lib/widget/drawer.dart | 3 +- 5 files changed, 80 insertions(+), 78 deletions(-) diff --git a/lib/l10n b/lib/l10n index ef139565..c342d99f 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit ef139565bba4fcd5889aa242a0062827eab5eceb +Subproject commit c342d99f068f8ac662dfb729fea0b5867aac1cc6 diff --git a/lib/preferences.dart b/lib/preferences.dart index b4512d30..ea43720f 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -83,24 +83,4 @@ class InvenTreePreferences { } InvenTreePreferences._internal(); - - // Load saved login details, and attempt connection - void loadLoginDetails(BuildContext context) async { - - print("Loading login details"); - - await InvenTreeAPI().connectToServer(context); - } - - void saveLoginDetails(BuildContext context, String server, String username, String password) async { - - SharedPreferences prefs = await SharedPreferences.getInstance(); - - await prefs.setString(_SERVER, server); - await prefs.setString(_USERNAME, username); - await prefs.setString(_PASSWORD, password); - - // Reconnect the API - await InvenTreeAPI().connectToServer(context); - } } \ No newline at end of file diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 868760dd..c4380b98 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -166,7 +166,7 @@ class _InvenTreeLoginSettingsState extends State { return null; } - void _selectProfile(UserProfile profile) async { + void _selectProfile(BuildContext context, UserProfile profile) async { // Mark currently selected profile as unselected final selected = await UserProfileDBManager().getSelectedProfile(); @@ -180,6 +180,9 @@ class _InvenTreeLoginSettingsState extends State { await UserProfileDBManager().updateProfile(profile); _reload(); + + // Attempt server login (this will load the newly selected profile + InvenTreeAPI().connect(context); } void _deleteProfile(UserProfile profile) async { @@ -219,60 +222,78 @@ class _InvenTreeLoginSettingsState extends State { List children = []; - for (int idx = 0; idx < profiles.length; idx++) { + if (profiles.length > 0) { + for (int idx = 0; idx < profiles.length; idx++) { + UserProfile profile = profiles[idx]; - UserProfile profile = profiles[idx]; - - children.add(ListTile( - title: Text( + children.add(ListTile( + title: Text( profile.name, - ), - subtitle: Text(profile.server), - trailing: profile.selected ? FaIcon(FontAwesomeIcons.checkCircle) : null, - onLongPress: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SimpleDialog( - title: Text(profile.name), - children: [ - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(); - _selectProfile(profile); - }, - child: Text(I18N.of(context).profileSelect), - ), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(); - _editProfile(context, userProfile: profile); - }, - child: Text(I18N.of(context).profileEdit), - ), - SimpleDialogOption( - onPressed: () { - // Navigator.of(context, rootNavigator: true).pop(); - confirmationDialog( - context, - I18N.of(context).delete, - "Delete this profile?", - onAccept: () { - _deleteProfile(profile); - } - ); - }, - child: Text(I18N.of(context).profileDelete), - ) - ], - ); - } - ); - }, - onTap: () { + ), + subtitle: Text(profile.server), + trailing: profile.selected + ? FaIcon(FontAwesomeIcons.checkCircle) + : null, + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: Text(profile.name), + children: [ + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + _selectProfile(context, profile); + }, + child: Text(I18N + .of(context) + .profileSelect), + ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(); + _editProfile(context, userProfile: profile); + }, + child: Text(I18N + .of(context) + .profileEdit), + ), + SimpleDialogOption( + onPressed: () { + // Navigator.of(context, rootNavigator: true).pop(); + confirmationDialog( + context, + I18N + .of(context) + .delete, + "Delete this profile?", + onAccept: () { + _deleteProfile(profile); + } + ); + }, + child: Text(I18N + .of(context) + .profileDelete), + ) + ], + ); + } + ); + }, + onTap: () { - }, - )); + }, + )); + } + } else { + // No profile available! + children.add( + ListTile( + title: Text("No profiles available"), + ) + ); } return Scaffold( diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index ffd2731a..0661ddff 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -30,15 +30,15 @@ class _InvenTreeSettingsState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("InvenTree Settings"), + title: Text(I18N.of(context).settings), ), body: Center( child: ListView( children: [ ListTile( - title: Text(I18N.of(context).serverSettings), - subtitle: Text("Configure server and login settings"), - leading: FaIcon(FontAwesomeIcons.server), + title: Text(I18N.of(context).profile), + subtitle: Text("Configure user profile settings"), + leading: FaIcon(FontAwesomeIcons.user), onTap: _editServerSettings, ), Divider(), diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index cc9ec7b2..b412da5e 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -3,6 +3,7 @@ import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/widget/company_list.dart'; import 'package:InvenTree/widget/search.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:InvenTree/api.dart'; @@ -155,7 +156,7 @@ class InvenTreeDrawer extends StatelessWidget { */ new Divider(), new ListTile( - title: new Text("Settings"), + title: new Text(I18N.of(context).settings), leading: new Icon(Icons.settings), onTap: _settings, ), From 18b4783c11cfb97cc1af1245523eae1ef0e7a9b9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 09:34:04 +1100 Subject: [PATCH 08/15] Slightly improved login info on main screen --- lib/main.dart | 30 +++++++-------- lib/settings/login.dart | 10 ++--- lib/settings/settings.dart | 2 +- lib/widget/home.dart | 76 +++++++++++++++++++++++++++++++------- 4 files changed, 82 insertions(+), 36 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2c0e410a..f0a14c94 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,12 +8,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'dsn.dart'; -import 'preferences.dart'; import 'package:sentry/sentry.dart'; // Use the secret app key -final SentryClient _sentry = SentryClient(dsn: SENTRY_DSN_KEY); +final SentryClient _sentry = SentryClient( + SentryOptions( + dsn: SENTRY_DSN_KEY, + )); bool isInDebugMode() { bool inDebugMode = false; @@ -31,13 +33,15 @@ Future _reportError(dynamic error, dynamic stackTrace) async { print(stackTrace); return; } else { - // Send the Exception and Stacktrace to Sentry in Production mode. - _sentry.captureException( - exception: error, - stackTrace: stackTrace, - ); - - print("Sending error to sentry.io"); + try { + await _sentry.captureException( + error, + stackTrace: stackTrace + ); + } catch (e) { + print("Sending error report to sentry.io failed: ${e}"); + print("Original error: ${error}"); + } } } @@ -47,12 +51,8 @@ void main() async { runZoned>(() async { runApp(InvenTreeApp()); - }, onError: (error, stackTrace) { - // Whenever an error occurs, call the `_reportError` function. This sends - // Dart errors to the dev console or Sentry depending on the environment. - _reportError(error, stackTrace); - }); - + }, onError: _reportError + ); } class InvenTreeApp extends StatelessWidget { diff --git a/lib/settings/login.dart b/lib/settings/login.dart index c4380b98..9ae57d34 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -10,12 +10,8 @@ import '../user_profile.dart'; class InvenTreeLoginSettingsWidget extends StatefulWidget { - final List _profiles; - - InvenTreeLoginSettingsWidget(this._profiles) : super(); - @override - _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_profiles); + _InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(); } @@ -27,7 +23,9 @@ class _InvenTreeLoginSettingsState extends State { List profiles; - _InvenTreeLoginSettingsState(this.profiles); + _InvenTreeLoginSettingsState() { + _reload(); + } void _reload() async { diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 0661ddff..16604134 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -72,7 +72,7 @@ class _InvenTreeSettingsState extends State { List profiles = await UserProfileDBManager().getAllProfiles(); - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(profiles))); + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())); } void _about() async { diff --git a/lib/widget/home.dart b/lib/widget/home.dart index c1bc56c6..ae035516 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -9,6 +9,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/api.dart'; +import 'package:InvenTree/settings/login.dart'; + import 'package:InvenTree/widget/category_display.dart'; import 'package:InvenTree/widget/company_list.dart'; import 'package:InvenTree/widget/location_display.dart'; @@ -25,6 +27,8 @@ class InvenTreeHomePage extends StatefulWidget { class _InvenTreeHomePageState extends State { _InvenTreeHomePageState() : super() { + + _loadProfile(); } String _serverStatus = "Connecting to server"; @@ -37,6 +41,59 @@ class _InvenTreeHomePageState extends State { Color _serverStatusColor = Color.fromARGB(255, 50, 50, 250); + // Selected user profile + UserProfile _profile; + + void _loadProfile() async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + + print("Loaded selected profile"); + + // If a different profile is selected, re-connect + if (_profile == null || (_profile.key != profile.key)) { + // TODO + } + + _profile = profile; + + setState(() { + + }); + } + + ListTile _serverTile() { + + // No profile selected + // Tap to select / create a profile + if (_profile == null) { + return ListTile( + title: Text("No Profile Selected"), + subtitle: Text("Tap to create or select a profile"), + leading: FaIcon(FontAwesomeIcons.user), + onTap: () { + _selectProfile(); + }, + ); + } + + // Profile is selected ... + if (InvenTreeAPI().isConnected()) { + return ListTile( + title: Text("Connected to ${_profile.server}"), + ); + } else { + return ListTile( + title: Text("Could not connect to server"), + subtitle: Text("Error connecting to ${_profile.server}"), + leading: FaIcon(FontAwesomeIcons.times), + onTap: () { + _selectProfile(); + }, + ); + } + } + void onConnectSuccess(String msg) async { final profile = await UserProfileDBManager().getSelectedProfile(); @@ -134,6 +191,10 @@ class _InvenTreeHomePageState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => CustomerListWidget())); } + void _selectProfile() { + Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())); + } + void _unsupported() { showDialog( context: context, @@ -313,20 +374,7 @@ class _InvenTreeHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: ListTile( - title: Text("$_serverStatus", - style: TextStyle(color: _serverStatusColor), - ), - subtitle: Text("$_serverMessage", - style: TextStyle(color: _serverStatusColor), - ), - leading: _serverIcon, - onTap: () { - if (!_serverConnection) { - _checkServerConnection(context); - } - }, - ), + child: _serverTile(), ), ], ), From 1c0b469020795ffe079c0ddaf4da080ac16da33c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 12:28:14 +1100 Subject: [PATCH 09/15] Add separator divider elements in lists --- lib/settings/about.dart | 122 +++++++++++++++++------- lib/settings/login.dart | 5 +- lib/settings/settings.dart | 56 +++++------ lib/widget/category_display.dart | 6 +- lib/widget/drawer.dart | 116 +++++++++++----------- lib/widget/location_display.dart | 11 ++- lib/widget/part_detail.dart | 11 ++- lib/widget/part_stock_detail.dart | 3 +- lib/widget/stock_detail.dart | 10 +- lib/widget/stock_item_test_results.dart | 5 +- 10 files changed, 211 insertions(+), 134 deletions(-) diff --git a/lib/settings/about.dart b/lib/settings/about.dart index e0b07107..666c86de 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -15,48 +15,96 @@ class InvenTreeAboutWidget extends StatelessWidget { @override Widget build(BuildContext context) { + List tiles = []; + + tiles.add( + ListTile( + title: Text( + I18N.of(context).serverDetails, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ) + ); + + if (InvenTreeAPI().isConnected()) { + tiles.add( + ListTile( + title: Text(I18N.of(context).address), + subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"), + ) + ); + + tiles.add( + ListTile( + title: Text(I18N.of(context).version), + subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"), + ) + ); + + tiles.add( + ListTile( + title: Text("Server Instance"), + subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"), + ) + ); + } else { + tiles.add( + ListTile( + title: Text("Not Connected"), + subtitle: Text( + "InvenTree server not connected", + style: TextStyle(fontStyle: FontStyle.italic), + ) + ) + ); + } + + tiles.add( + ListTile( + title: Text( + I18N.of(context).appDetails, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ) + ); + + tiles.add( + ListTile( + title: Text(I18N.of(context).name), + subtitle: Text("${info.appName}"), + ) + ); + + tiles.add( + ListTile( + title: Text("Package Name"), + subtitle: Text("${info.packageName}"), + ) + ); + + tiles.add( + ListTile( + title: Text(I18N.of(context).version), + subtitle: Text("${info.version}"), + ) + ); + + tiles.add( + ListTile( + title: Text(I18N.of(context).build), + subtitle: Text("${info.buildNumber}"), + ) + ); + return Scaffold( appBar: AppBar( title: Text("About InvenTree"), ), body: ListView( - children: [ - ListTile( - title: Text(I18N.of(context).serverDetails), - ), - ListTile( - title: Text(I18N.of(context).address), - subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"), - ), - ListTile( - title: Text(I18N.of(context).version), - subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"), - ), - ListTile( - title: Text("Server Instance"), - subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"), - ), - Divider(), - ListTile( - title: Text(I18N.of(context).appDetails), - ), - ListTile( - title: Text(I18N.of(context).name), - subtitle: Text("${info.appName}"), - ), - ListTile( - title: Text("Package Name"), - subtitle: Text("${info.packageName}"), - ), - ListTile( - title: Text(I18N.of(context).version), - subtitle: Text("${info.version}"), - ), - ListTile( - title: Text(I18N.of(context).build), - subtitle: Text("${info.buildNumber}"), - ) - ], + children: ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(), ) ); } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 9ae57d34..53beff37 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -300,7 +300,10 @@ class _InvenTreeLoginSettingsState extends State { ), body: Container( child: ListView( - children: children, + children: ListTile.divideTiles( + context: context, + tiles: children + ).toList(), ) ), floatingActionButton: FloatingActionButton( diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 16604134..a93c8821 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -34,33 +34,35 @@ class _InvenTreeSettingsState extends State { ), body: Center( child: ListView( - children: [ - ListTile( - title: Text(I18N.of(context).profile), - subtitle: Text("Configure user profile settings"), - leading: FaIcon(FontAwesomeIcons.user), - onTap: _editServerSettings, - ), - Divider(), - ListTile( - title: Text(I18N.of(context).about), - subtitle: Text(I18N.of(context).appDetails), - leading: FaIcon(FontAwesomeIcons.infoCircle), - onTap: _about, - ), - ListTile( - title: Text(I18N.of(context).releaseNotes), - subtitle: Text("Display app release notes"), - leading: FaIcon(FontAwesomeIcons.fileAlt), - onTap: _releaseNotes, - ), - ListTile( - title: Text(I18N.of(context).reportBug), - subtitle: Text("Report bug or suggest new feature"), - leading: FaIcon(FontAwesomeIcons.bug), - onTap: null, - ), - ], + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: Text(I18N.of(context).profile), + subtitle: Text("Configure user profile settings"), + leading: FaIcon(FontAwesomeIcons.user), + onTap: _editServerSettings, + ), + ListTile( + title: Text(I18N.of(context).about), + subtitle: Text(I18N.of(context).appDetails), + leading: FaIcon(FontAwesomeIcons.infoCircle), + onTap: _about, + ), + ListTile( + title: Text(I18N.of(context).releaseNotes), + subtitle: Text("Display app release notes"), + leading: FaIcon(FontAwesomeIcons.fileAlt), + onTap: _releaseNotes, + ), + ListTile( + title: Text(I18N.of(context).reportBug), + subtitle: Text("Report bug or suggest new feature"), + leading: FaIcon(FontAwesomeIcons.bug), + onTap: null, + ), + ] + ).toList() ) ) ); diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 5e26daf9..02a91527 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -231,9 +231,10 @@ class SubcategoryList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( + return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(), itemBuilder: _build, itemCount: _categories.length); } } @@ -282,9 +283,10 @@ class PartList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( + return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: _build, itemCount: _parts.length); } } diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index b412da5e..70083365 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -102,65 +102,65 @@ class InvenTreeDrawer extends StatelessWidget { Widget build(BuildContext context) { return Drawer( child: new ListView( - children: [ - new ListTile( - leading: new Image.asset( - "assets/image/icon.png", - fit: BoxFit.scaleDown, - width: 40, + children: ListTile.divideTiles( + context: context, + tiles: [ + new ListTile( + leading: new Image.asset( + "assets/image/icon.png", + fit: BoxFit.scaleDown, + width: 40, + ), + title: new Text("InvenTree"), + onTap: _home, ), - title: new Text("InvenTree"), - onTap: _home, - ), - new Divider(), - /* - // TODO - Add search functionality! - new ListTile( - title: new Text("Search"), - leading: new FaIcon(FontAwesomeIcons.search), - onTap: _search, - ), - */ - new ListTile( - title: new Text("Scan Barcode"), - onTap: _scan, - leading: new FaIcon(FontAwesomeIcons.barcode), - ), - new Divider(), - new ListTile( - title: new Text("Parts"), - leading: new Icon(Icons.category), - onTap: _showParts, - ), - new ListTile( - title: new Text("Stock"), - leading: new FaIcon(FontAwesomeIcons.boxes), - onTap: _showStock, - ), - /* - new ListTile( - title: new Text("Suppliers"), - leading: new FaIcon(FontAwesomeIcons.building), - onTap: _showSuppliers, - ), - new ListTile( - title: Text("Manufacturers"), - leading: new FaIcon(FontAwesomeIcons.industry), - onTap: _showManufacturers, - ), - new ListTile( - title: new Text("Customers"), - leading: new FaIcon(FontAwesomeIcons.users), - onTap: _showCustomers, - ), - */ - new Divider(), - new ListTile( - title: new Text(I18N.of(context).settings), - leading: new Icon(Icons.settings), - onTap: _settings, - ), - ] + /* + // TODO - Add search functionality! + new ListTile( + title: new Text("Search"), + leading: new FaIcon(FontAwesomeIcons.search), + onTap: _search, + ), + */ + new ListTile( + title: new Text(I18N.of(context).scanBarcode), + onTap: _scan, + leading: new FaIcon(FontAwesomeIcons.barcode), + ), + new ListTile( + title: new Text(I18N.of(context).parts), + leading: new Icon(Icons.category), + onTap: _showParts, + ), + new ListTile( + title: new Text(I18N.of(context).stock), + leading: new FaIcon(FontAwesomeIcons.boxes), + onTap: _showStock, + ), + /* + new ListTile( + title: new Text("Suppliers"), + leading: new FaIcon(FontAwesomeIcons.building), + onTap: _showSuppliers, + ), + new ListTile( + title: Text("Manufacturers"), + leading: new FaIcon(FontAwesomeIcons.industry), + onTap: _showManufacturers, + ), + new ListTile( + title: new Text("Customers"), + leading: new FaIcon(FontAwesomeIcons.users), + onTap: _showCustomers, + ), + */ + new ListTile( + title: new Text(I18N.of(context).settings), + leading: new Icon(Icons.settings), + onTap: _settings, + ), + ] + ).toList(), ) ); } diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 6e8563de..b8fe8e06 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -156,7 +156,10 @@ class _LocationDisplayState extends RefreshableState { ); case 1: return ListView( - children: actionTiles(), + children: ListTile.divideTiles( + context: context, + tiles: actionTiles() + ).toList() ); default: return null; @@ -298,10 +301,11 @@ class SublocationList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( + return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: _build, + separatorBuilder: (_, __) => const Divider(), itemCount: _locations.length ); } @@ -342,9 +346,10 @@ class StockList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( + return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(), itemBuilder: _build, itemCount: _items.length); } } \ No newline at end of file diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 45f2fcfe..fbd91132 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -82,6 +82,7 @@ class _PartDisplayState extends RefreshableState { void _showStock(BuildContext context) async { await part.getStockItems(context); + Navigator.push( context, MaterialPageRoute(builder: (context) => PartStockDetailWidget(part)) @@ -349,13 +350,19 @@ class _PartDisplayState extends RefreshableState { case 0: return Center( child: ListView( - children: partTiles(), + children: ListTile.divideTiles( + context: context, + tiles: partTiles() + ).toList() ), ); case 1: return Center( child: ListView( - children: actionTiles(), + children: ListTile.divideTiles( + context: context, + tiles: actionTiles() + ).toList() ) ); default: diff --git a/lib/widget/part_stock_detail.dart b/lib/widget/part_stock_detail.dart index 36528351..e5b4f27f 100644 --- a/lib/widget/part_stock_detail.dart +++ b/lib/widget/part_stock_detail.dart @@ -100,10 +100,11 @@ class PartStockList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( + return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: _build, + separatorBuilder: (_, __) => const Divider(height: 1), itemCount: _items.length ); } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 10bf44b2..f9ba55df 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -579,11 +579,17 @@ class _StockItemDisplayState extends RefreshableState { switch (index) { case 0: return ListView( - children: detailTiles(), + children: ListTile.divideTiles( + context: context, + tiles: detailTiles() + ).toList(), ); case 1: return ListView( - children: actionTiles(), + children: ListTile.divideTiles( + context: context, + tiles: actionTiles() + ).toList() ); default: return null; diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart index 0563b355..1ff75209 100644 --- a/lib/widget/stock_item_test_results.dart +++ b/lib/widget/stock_item_test_results.dart @@ -250,7 +250,10 @@ class _StockItemTestResultDisplayState extends RefreshableState Date: Tue, 9 Feb 2021 14:16:31 +1100 Subject: [PATCH 10/15] Refactor API connection code --- lib/api.dart | 87 ++++++++------- lib/settings/login.dart | 4 +- lib/widget/home.dart | 227 ++++++++++++++++++++++------------------ 3 files changed, 177 insertions(+), 141 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index d66a7c56..8567965d 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -109,13 +109,11 @@ class InvenTreeAPI { String makeUrl(String endpoint) => _makeUrl(endpoint); - UserProfile _profile; + UserProfile profile; // Authentication token (initially empty, must be requested) String _token = ""; - bool isConnected() => _token.isNotEmpty; - /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. @@ -154,8 +152,14 @@ class InvenTreeAPI { // Connection status flag - set once connection has been validated bool _connected = false; - bool get connected { - return _connected && baseUrl.isNotEmpty && _token.isNotEmpty; + bool _connecting = true; + + bool isConnected() { + return profile != null && _connected && baseUrl.isNotEmpty && _token.isNotEmpty; + } + + bool isConnecting() { + return !isConnected() && _connecting; } // Ensure we only ever create a single instance of the API class @@ -167,35 +171,15 @@ class InvenTreeAPI { InvenTreeAPI._internal(); - Future connect(BuildContext context) async { - - _profile = await UserProfileDBManager().getSelectedProfile(); - - if (_profile == null) { - await showErrorDialog( - context, - "Select Profile", - "User profile not selected" - ); - return false; - } - - return connectToServer(context); - } - - Future connectToServer(BuildContext context) async { + Future _connect(BuildContext context) async { /* Address is the base address for the InvenTree server, * e.g. http://127.0.0.1:8000 */ - if (_profile == null) return false; - - String errorMessage = ""; - - String address = _profile.server.trim(); - String username = _profile.username.trim(); - String password = _profile.password.trim(); + String address = profile.server.trim(); + String username = profile.username.trim(); + String password = profile.password.trim(); if (address.isEmpty || username.isEmpty || password.isEmpty) { await showErrorDialog( @@ -220,19 +204,18 @@ class InvenTreeAPI { */ _BASE_URL = address; - _connected = false; - print("Connecting to " + apiUrl + " -> " + username + ":" + password); + print("Connecting to ${apiUrl} -> ${username}:${password}"); var response = await get("").timeout(Duration(seconds: 5)).catchError((error) { print("Error connecting to server: ${error.toString()}"); if (error is SocketException) { - errorMessage = "Could not connect to server"; + showServerError(context, "Connection Refused"); return null; } else if (error is TimeoutException) { - errorMessage = "Server timeout"; + showTimeoutDialog(context); return null; } else { // Unknown error type - re-throw the error and Sentry will catch it @@ -242,8 +225,6 @@ class InvenTreeAPI { if (response == null) { // Null (or error) response: Show dialog and exit - - await showServerError(context, errorMessage); return false; } @@ -322,6 +303,38 @@ class InvenTreeAPI { }; } + Future connectToServer(BuildContext context) async { + print("InvenTreeAPI().connectToServer()"); + + // Clear connection flag + _connected = false; + + // Clear token + _token = ''; + + // Load selected profile + profile = await UserProfileDBManager().getSelectedProfile(); + + if (profile == null) { + await showErrorDialog( + context, + "Select Profile", + "User profile not selected" + ); + return false; + } + + _connecting = true; + + _connect(context).then((result) { + + print("_connect() returned result: ${result}"); + _connecting = false; + + return result; + }); + } + // Perform a PATCH request Future patch(String url, {Map body}) async { var _url = makeApiUrl(url); @@ -405,7 +418,9 @@ class InvenTreeAPI { Map defaultHeaders() { var headers = Map(); - headers[HttpHeaders.authorizationHeader] = _authorizationHeader(_profile.username, _profile.password); + if (profile != null) { + headers[HttpHeaders.authorizationHeader] = _authorizationHeader(profile.username, profile.password); + } return headers; } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 53beff37..c20ad6de 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -180,7 +180,7 @@ class _InvenTreeLoginSettingsState extends State { _reload(); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connect(context); + InvenTreeAPI().connectToServer(context); } void _deleteProfile(UserProfile profile) async { @@ -220,7 +220,7 @@ class _InvenTreeLoginSettingsState extends State { List children = []; - if (profiles.length > 0) { + if (profiles != null && profiles.length > 0) { for (int idx = 0; idx < profiles.length; idx++) { UserProfile profile = profiles[idx]; diff --git a/lib/widget/home.dart b/lib/widget/home.dart index ae035516..31439b7e 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -44,109 +44,7 @@ class _InvenTreeHomePageState extends State { // Selected user profile UserProfile _profile; - void _loadProfile() async { - - final profile = await UserProfileDBManager().getSelectedProfile(); - - print("Loaded selected profile"); - - // If a different profile is selected, re-connect - if (_profile == null || (_profile.key != profile.key)) { - // TODO - } - - _profile = profile; - - setState(() { - - }); - } - - ListTile _serverTile() { - - // No profile selected - // Tap to select / create a profile - if (_profile == null) { - return ListTile( - title: Text("No Profile Selected"), - subtitle: Text("Tap to create or select a profile"), - leading: FaIcon(FontAwesomeIcons.user), - onTap: () { - _selectProfile(); - }, - ); - } - - // Profile is selected ... - if (InvenTreeAPI().isConnected()) { - return ListTile( - title: Text("Connected to ${_profile.server}"), - ); - } else { - return ListTile( - title: Text("Could not connect to server"), - subtitle: Text("Error connecting to ${_profile.server}"), - leading: FaIcon(FontAwesomeIcons.times), - onTap: () { - _selectProfile(); - }, - ); - } - } - - void onConnectSuccess(String msg) async { - - final profile = await UserProfileDBManager().getSelectedProfile(); - - String address = profile?.server ?? 'unknown server address'; - - _serverConnection = true; - _serverMessage = msg; - _serverStatus = "Connected to ${address}"; - _serverStatusColor = Color.fromARGB(255, 50, 250, 50); - _serverIcon = new FaIcon(FontAwesomeIcons.checkCircle, color: _serverStatusColor); - - setState(() {}); - } - - void onConnectFailure(String msg) async { - - final profile = await UserProfileDBManager().getSelectedProfile(); - - _serverConnection = false; - _serverMessage = msg; - _serverStatus = "Could not connect to ${profile?.server}"; - _serverStatusColor = Color.fromARGB(255, 250, 50, 50); - _serverIcon = new FaIcon(FontAwesomeIcons.timesCircle, color: _serverStatusColor); - - setState(() {}); - } - - /* - * Test the server connection - */ - void _checkServerConnection(BuildContext context) async { - - // Reset the connection status variables - _serverStatus = "Connecting to server"; - _serverMessage = ""; - _serverConnection = false; - _serverIcon = new FaIcon(FontAwesomeIcons.spinner); - _serverStatusColor = Color.fromARGB(255, 50, 50, 250); - - InvenTreeAPI().connect(context).then((bool result) { - - if (result) { - onConnectSuccess(""); - } else { - onConnectFailure("Could not connect to server"); - } - - }); - - // Update widget state - setState(() {}); - } + BuildContext _context; void _search() { if (!InvenTreeAPI().checkConnection(context)) return; @@ -210,8 +108,130 @@ class _InvenTreeHomePageState extends State { ); } + + void _loadProfile() async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + + // If a different profile is selected, re-connect + if (_profile == null || (_profile.key != profile.key)) { + + if (_context != null) { + print("Connecting Profile: ${profile.name} - ${profile.server}"); + + InvenTreeAPI().connectToServer(_context).then((result) { + setState(() { + + }); + }); + + setState(() { + }); + } + } + + _profile = profile; + + setState(() { + + }); + } + + ListTile _serverTile() { + + // No profile selected + // Tap to select / create a profile + if (_profile == null) { + return ListTile( + title: Text("No Profile Selected"), + subtitle: Text("Tap to create or select a profile"), + leading: FaIcon(FontAwesomeIcons.server), + trailing: FaIcon( + FontAwesomeIcons.user, + color: Color.fromRGBO(250, 50, 50, 1), + ), + onTap: () { + _selectProfile(); + }, + ); + } + + // Profile is selected ... + if (InvenTreeAPI().isConnecting()) { + return ListTile( + title: Text("Connecting to server..."), + subtitle: Text("${InvenTreeAPI().baseUrl}"), + leading: FaIcon(FontAwesomeIcons.server), + trailing: FaIcon( + FontAwesomeIcons.spinner, + color: Color.fromRGBO(50, 50, 250, 1), + ) + ); + } else if (InvenTreeAPI().isConnected()) { + return ListTile( + title: Text("Connected to server"), + subtitle: Text("${InvenTreeAPI().baseUrl}"), + leading: FaIcon(FontAwesomeIcons.server), + trailing: FaIcon( + FontAwesomeIcons.checkCircle, + color: Color.fromRGBO(50, 250, 50, 1) + ), + onTap: () { + _selectProfile(); + }, + ); + } else { + return ListTile( + title: Text("Could not connect to server"), + subtitle: Text("${_profile.server}"), + leading: FaIcon(FontAwesomeIcons.server), + trailing: FaIcon( + FontAwesomeIcons.timesCircle, + color: Color.fromRGBO(250, 50, 50, 1), + ), + onTap: () { + _selectProfile(); + }, + ); + } + } + + void onConnectSuccess(String msg) async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + + String address = profile?.server ?? 'unknown server address'; + + _serverConnection = true; + _serverMessage = msg; + _serverStatus = "Connected to ${address}"; + _serverStatusColor = Color.fromARGB(255, 50, 250, 50); + _serverIcon = new FaIcon(FontAwesomeIcons.checkCircle, color: _serverStatusColor); + + setState(() {}); + } + + void onConnectFailure(String msg) async { + + final profile = await UserProfileDBManager().getSelectedProfile(); + + _serverConnection = false; + _serverMessage = msg; + _serverStatus = "Could not connect to ${profile?.server}"; + _serverStatusColor = Color.fromARGB(255, 250, 50, 50); + _serverIcon = new FaIcon(FontAwesomeIcons.timesCircle, color: _serverStatusColor); + + setState(() {}); + } + + @override Widget build(BuildContext context) { + + _context = context; + + _loadProfile(); + // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // @@ -369,6 +389,7 @@ class _InvenTreeHomePageState extends State { ), Spacer(), */ + Spacer(), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, From 4080b4177b53ab33f3560df5a4dee34582d3c572 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 19:40:31 +1100 Subject: [PATCH 11/15] Refactoring / fixing --- lib/api.dart | 36 ++++++++---- lib/settings/login.dart | 98 ++++++++++++++++++++++--------- lib/user_profile.dart | 126 ++++++++++++++++++---------------------- lib/widget/home.dart | 76 +++++------------------- lib/widget/spinner.dart | 54 +++++++++++++++++ 5 files changed, 220 insertions(+), 170 deletions(-) create mode 100644 lib/widget/spinner.dart diff --git a/lib/api.dart b/lib/api.dart index 8567965d..d5a86631 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -177,6 +177,8 @@ class InvenTreeAPI { * e.g. http://127.0.0.1:8000 */ + if (profile == null) return false; + String address = profile.server.trim(); String username = profile.username.trim(); String password = profile.password.trim(); @@ -212,10 +214,13 @@ class InvenTreeAPI { print("Error connecting to server: ${error.toString()}"); if (error is SocketException) { - showServerError(context, "Connection Refused"); + print("Error: socket exception: ${error.toString()}"); + // TODO - Display error dialog!! + //showServerError(context, "Connection Refused"); return null; } else if (error is TimeoutException) { - showTimeoutDialog(context); + // TODO - Display timeout dialog here + //showTimeoutDialog(context); return null; } else { // Unknown error type - re-throw the error and Sentry will catch it @@ -303,18 +308,25 @@ class InvenTreeAPI { }; } - Future connectToServer(BuildContext context) async { - print("InvenTreeAPI().connectToServer()"); + bool disconnectFromServer() { + print("InvenTreeAPI().disconnectFromServer()"); - // Clear connection flag _connected = false; - - // Clear token + _connecting = false; _token = ''; + profile = null; + } + + Future connectToServer(BuildContext context) async { + + // Ensure server is first disconnected + disconnectFromServer(); // Load selected profile profile = await UserProfileDBManager().getSelectedProfile(); + print("API Profile: ${profile.toString()}"); + if (profile == null) { await showErrorDialog( context, @@ -326,13 +338,13 @@ class InvenTreeAPI { _connecting = true; - _connect(context).then((result) { + bool result = await _connect(context); - print("_connect() returned result: ${result}"); - _connecting = false; + print("_connect() returned result: ${result}"); - return result; - }); + _connecting = false; + + return result; } // Perform a PATCH request diff --git a/lib/settings/login.dart b/lib/settings/login.dart index c20ad6de..f5b6fe38 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,5 +1,7 @@ import 'package:InvenTree/widget/dialogs.dart'; import 'package:InvenTree/widget/fields.dart'; +import 'package:InvenTree/widget/spinner.dart'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -166,21 +168,20 @@ class _InvenTreeLoginSettingsState extends State { void _selectProfile(BuildContext context, UserProfile profile) async { - // Mark currently selected profile as unselected - final selected = await UserProfileDBManager().getSelectedProfile(); + // Disconnect InvenTree + InvenTreeAPI().disconnectFromServer(); - selected.selected = false; - - await UserProfileDBManager().updateProfile(selected); - - profile.selected = true; - - await UserProfileDBManager().updateProfile(profile); + await UserProfileDBManager().selectProfile(profile.key); _reload(); + print("CONNECT FROM A"); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(context); + InvenTreeAPI().connectToServer(context).then((result) { + _reload(); + }); + + _reload(); } void _deleteProfile(UserProfile profile) async { @@ -191,6 +192,10 @@ class _InvenTreeLoginSettingsState extends State { Navigator.of(context).pop(); _reload(); + + if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { + InvenTreeAPI().disconnectFromServer(); + } } void _updateProfile(UserProfile profile) async { @@ -201,6 +206,15 @@ class _InvenTreeLoginSettingsState extends State { Navigator.of(context).pop(); _reload(); + + if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { + // Attempt server login (this will load the newly selected profile + + print("Connect froM A"); + InvenTreeAPI().connectToServer(context).then((result) { + _reload(); + }); + } } void _addProfile(UserProfile profile) async { @@ -213,11 +227,45 @@ class _InvenTreeLoginSettingsState extends State { _reload(); } + Widget _getProfileIcon(UserProfile profile) { + + // Not selected? No icon for you! + if (profile == null || !profile.selected) return null; + + // Selected, but (for some reason) not the same as the API... + if (InvenTreeAPI().profile == null || InvenTreeAPI().profile.key != profile.key) { + return FaIcon( + FontAwesomeIcons.questionCircle, + color: Color.fromRGBO(250, 150, 50, 1) + ); + } + + // Reflect the connection status of the server + if (InvenTreeAPI().isConnected()) { + return FaIcon( + FontAwesomeIcons.checkCircle, + color: Color.fromRGBO(50, 250, 50, 1) + ); + } else if (InvenTreeAPI().isConnecting()) { + return Spinner( + icon: FontAwesomeIcons.spinner, + color: Color.fromRGBO(50, 50, 250, 1), + ); + } else { + return FaIcon( + FontAwesomeIcons.timesCircle, + color: Color.fromRGBO(250, 50, 50, 1), + ); + } + } + @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; + print("Building!"); + List children = []; if (profiles != null && profiles.length > 0) { @@ -228,10 +276,12 @@ class _InvenTreeLoginSettingsState extends State { title: Text( profile.name, ), - subtitle: Text(profile.server), - trailing: profile.selected - ? FaIcon(FontAwesomeIcons.checkCircle) - : null, + tileColor: profile.selected ? Color.fromRGBO(0, 0, 0, 0.05) : null, + subtitle: Text("${profile.server}"), + trailing: _getProfileIcon(profile), + onTap: () { + _selectProfile(context, profile); + }, onLongPress: () { showDialog( context: context, @@ -239,50 +289,40 @@ class _InvenTreeLoginSettingsState extends State { return SimpleDialog( title: Text(profile.name), children: [ + Divider(), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(); _selectProfile(context, profile); }, - child: Text(I18N - .of(context) - .profileSelect), + child: Text(I18N.of(context).profileSelect), ), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(); _editProfile(context, userProfile: profile); }, - child: Text(I18N - .of(context) - .profileEdit), + child: Text(I18N.of(context).profileEdit), ), SimpleDialogOption( onPressed: () { // Navigator.of(context, rootNavigator: true).pop(); confirmationDialog( context, - I18N - .of(context) - .delete, + I18N.of(context).delete, "Delete this profile?", onAccept: () { _deleteProfile(profile); } ); }, - child: Text(I18N - .of(context) - .profileDelete), + child: Text(I18N.of(context).profileDelete), ) ], ); } ); }, - onTap: () { - - }, )); } } else { diff --git a/lib/user_profile.dart b/lib/user_profile.dart index ba70f075..cef17f31 100644 --- a/lib/user_profile.dart +++ b/lib/user_profile.dart @@ -36,13 +36,13 @@ class UserProfile { // User ID (will be provided by the server on log-in) int user_id; - factory UserProfile.fromJson(int key, Map json) => UserProfile( + factory UserProfile.fromJson(int key, Map json, bool isSelected) => UserProfile( key: key, name: json['name'], server: json['server'], username: json['username'], password: json['password'], - selected: json['selected'] ?? false, + selected: isSelected, ); Map toJson() => { @@ -50,57 +50,75 @@ class UserProfile { "server": server, "username": username, "password": password, - "selected": selected, }; @override String toString() { - return "${server} - ${username}:${password}"; + return "<${key}> ${name} : ${server} - ${username}:${password}"; } } class UserProfileDBManager { - static const String folder_name = "profiles"; - - final _folder = intMapStoreFactory.store(folder_name); + final store = StoreRef("profiles"); Future get _db async => await InvenTreePreferencesDB.instance.database; + Future profileNameExists(String name) async { + + final finder = Finder(filter: Filter.equals("name", name)); + + final profiles = await store.find(await _db, finder: finder); + + return profiles.length > 0; + } + Future addProfile(UserProfile profile) async { - UserProfile existingProfile = await getProfile(profile.name); + // Check if a profile already exists with the name + final bool exists = await profileNameExists(profile.name); - if (existingProfile != null) { + if (exists) { print("UserProfile '${profile.name}' already exists"); return; } - int key = await _folder.add(await _db, profile.toJson()); + int key = await store.add(await _db, profile.toJson()); print("Added user profile <${key}> - '${profile.name}'"); // Record the key profile.key = key; } + + Future selectProfile(int key) async { + /* + * Mark the particular profile as selected + */ + + final result = await store.record("selected").put(await _db, key); + + return result; + } Future updateProfile(UserProfile profile) async { if (profile.key == null) { - addProfile(profile); + await addProfile(profile); return; } - final finder = Finder(filter: Filter.byKey(profile.key)); - await _folder.update(await _db, profile.toJson(), finder: finder); + final result = await store.record(profile.key).update(await _db, profile.toJson()); - print("Updated user profile <${profile.key}> - '${profile.name}"); + print("Updated user profile <${profile.key}> - '${profile.name}'"); + + return result; } Future deleteProfile(UserProfile profile) async { final finder = Finder(filter: Filter.equals("name", profile.name)); - await _folder.delete(await _db, finder: finder); + await store.record(profile.key).delete(await _db); print("Deleted user profile <${profile.key}> - '${profile.name}'"); } @@ -108,78 +126,50 @@ class UserProfileDBManager { /* * Return the currently selected profile. * - * If multiple profiles are selected, - * mark all but the first as unselected - * - * If no profile is currently selected, - * then force the first profile to be selected. + * key should match the "selected" property */ - final selected_finder = Finder(filter: Filter.equals("selected", true)); + final selected = await store.record("selected").get(await _db); - final selected_profiles = await _folder.find(await _db, finder: selected_finder); + final profiles = await store.find(await _db); - if (selected_profiles.length == 1) { - // A single profile is selected - return UserProfile.fromJson(selected_profiles[0].key, selected_profiles[0].value); - } else if (selected_profiles.length > 1) { - // Multiple selected profiles - de-select others - for (int idx = 1; idx < selected_profiles.length; idx++) { - UserProfile profile = UserProfile.fromJson(selected_profiles[idx].key, selected_profiles[idx].value); + List profileList = new List(); - profile.selected = false; - updateProfile(profile); - } + for (int idx = 0; idx < profiles.length; idx++) { - // And return the first profile - return UserProfile.fromJson(selected_profiles[0].key, selected_profiles[0].value); - } else { - // No profiles selected! - - final all_profiles = await getAllProfiles(); - - if (all_profiles.length == 0) { - // No profiles available - return null; - } else { - UserProfile prf = all_profiles[0]; - prf.selected = true; - updateProfile(prf); - - // Return the selected profile - return prf; + if (profiles[idx].key is int && profiles[idx].key == selected) { + return UserProfile.fromJson( + profiles[idx].key, + profiles[idx].value, + profiles[idx].key == selected, + ); } } - } - Future getProfile(String name) async { - - print("Looking for user profile '${name}'"); - - // Lookup profile by name (or return null if does not exist) - final finder = Finder(filter: Filter.equals("name", name)); - - final profiles = await _folder.find(await _db, finder: finder); - - if (profiles.length == 0) { - print("No matching profiles found"); - return null; - } - - // Return the first matching profile object - return UserProfile.fromJson(profiles[0].key, profiles[0].value); + return null; } /* * Return all user profile objects */ Future> getAllProfiles() async { - final profiles = await _folder.find(await _db); + + final selected = await store.record("selected").get(await _db); + + final profiles = await store.find(await _db); List profileList = new List(); for (int idx = 0; idx < profiles.length; idx++) { - profileList.add(UserProfile.fromJson(profiles[idx].key, profiles[idx].value)); + + if (profiles[idx].key is int) { + profileList.add( + UserProfile.fromJson( + profiles[idx].key, + profiles[idx].value, + profiles[idx].key == selected, + )); + } } return profileList; diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 31439b7e..636dc46b 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -15,6 +15,7 @@ import 'package:InvenTree/widget/category_display.dart'; import 'package:InvenTree/widget/company_list.dart'; import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/search.dart'; +import 'package:InvenTree/widget/spinner.dart'; import 'package:InvenTree/widget/drawer.dart'; class InvenTreeHomePage extends StatefulWidget { @@ -28,19 +29,10 @@ class _InvenTreeHomePageState extends State { _InvenTreeHomePageState() : super() { + // Initially load the profile and attempt server connection _loadProfile(); } - String _serverStatus = "Connecting to server"; - - String _serverMessage = ""; - - bool _serverConnection = false; - - FaIcon _serverIcon = new FaIcon(FontAwesomeIcons.spinner); - - Color _serverStatusColor = Color.fromARGB(255, 50, 50, 250); - // Selected user profile UserProfile _profile; @@ -111,30 +103,20 @@ class _InvenTreeHomePageState extends State { void _loadProfile() async { - final profile = await UserProfileDBManager().getSelectedProfile(); + _profile = await UserProfileDBManager().getSelectedProfile(); - // If a different profile is selected, re-connect - if (_profile == null || (_profile.key != profile.key)) { - - if (_context != null) { - print("Connecting Profile: ${profile.name} - ${profile.server}"); + // A valid profile was loaded! + if (_profile != null) { + if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { + print("Connect from C"); InvenTreeAPI().connectToServer(_context).then((result) { - setState(() { - - }); - }); - - setState(() { + setState(() {}); }); } } - _profile = profile; - - setState(() { - - }); + setState(() {}); } ListTile _serverTile() { @@ -162,10 +144,13 @@ class _InvenTreeHomePageState extends State { title: Text("Connecting to server..."), subtitle: Text("${InvenTreeAPI().baseUrl}"), leading: FaIcon(FontAwesomeIcons.server), - trailing: FaIcon( - FontAwesomeIcons.spinner, + trailing: Spinner( + icon: FontAwesomeIcons.spinner, color: Color.fromRGBO(50, 50, 250, 1), - ) + ), + onTap: () { + _selectProfile(); + } ); } else if (InvenTreeAPI().isConnected()) { return ListTile( @@ -196,42 +181,11 @@ class _InvenTreeHomePageState extends State { } } - void onConnectSuccess(String msg) async { - - final profile = await UserProfileDBManager().getSelectedProfile(); - - String address = profile?.server ?? 'unknown server address'; - - _serverConnection = true; - _serverMessage = msg; - _serverStatus = "Connected to ${address}"; - _serverStatusColor = Color.fromARGB(255, 50, 250, 50); - _serverIcon = new FaIcon(FontAwesomeIcons.checkCircle, color: _serverStatusColor); - - setState(() {}); - } - - void onConnectFailure(String msg) async { - - final profile = await UserProfileDBManager().getSelectedProfile(); - - _serverConnection = false; - _serverMessage = msg; - _serverStatus = "Could not connect to ${profile?.server}"; - _serverStatusColor = Color.fromARGB(255, 250, 50, 50); - _serverIcon = new FaIcon(FontAwesomeIcons.timesCircle, color: _serverStatusColor); - - setState(() {}); - } - - @override Widget build(BuildContext context) { _context = context; - _loadProfile(); - // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // diff --git a/lib/widget/spinner.dart b/lib/widget/spinner.dart new file mode 100644 index 00000000..5bab6c4f --- /dev/null +++ b/lib/widget/spinner.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class Spinner extends StatefulWidget { + final IconData icon; + final Duration duration; + final Color color; + + const Spinner({ + this.color = const Color.fromRGBO(150, 150, 150, 1), + Key key, + @required this.icon, + this.duration = const Duration(milliseconds: 1800), + }) : super(key: key); + + @override + _SpinnerState createState() => _SpinnerState(); +} + +class _SpinnerState extends State with SingleTickerProviderStateMixin { + AnimationController _controller; + Widget _child; + + @override + void initState() { + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 2000), + ) + ..repeat(); + _child = FaIcon( + widget.icon, + color: widget.color + ); + + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RotationTransition( + turns: _controller, + child: _child, + ); + } +} \ No newline at end of file From 0c4803b88945a0a61231586d83de277ddbe9adb3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 20:09:24 +1100 Subject: [PATCH 12/15] Bug fix for routing issues --- lib/inventree/model.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index e7aec6d3..96abf38c 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -252,7 +252,9 @@ class InvenTreeModel { return null; } - hideProgressDialog(context); + if (dialog) { + hideProgressDialog(context); + } if (response.statusCode != 200) { showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); From c2aa51ed95b3fd03fd22dc02dfd4807a755d296f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 20:10:36 +1100 Subject: [PATCH 13/15] Cleanup --- lib/inventree/part.dart | 2 +- lib/inventree/stock.dart | 5 +++++ lib/settings/login.dart | 2 -- lib/widget/category_display.dart | 4 ++-- lib/widget/home.dart | 21 +++++++++++++-------- lib/widget/location_display.dart | 4 ++-- lib/widget/part_detail.dart | 6 ++++-- lib/widget/part_stock_detail.dart | 3 +-- 8 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index ac98857b..ac5c469c 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -155,7 +155,7 @@ class InvenTreePart extends InvenTreeModel { // Request stock items for this part Future getStockItems(BuildContext context, {bool showDialog=false}) async { - InvenTreeStockItem().list( + await InvenTreeStockItem().list( context, filters: { "part": "${pk}", diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index c174e048..60f2ee50 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -287,6 +287,11 @@ class InvenTreeStockItem extends InvenTreeModel { return 'SN ${serialNumber}'; } + // Is an integer? + if (quantity.toInt() == quantity) { + return '${quantity.toInt()}'; + } + return '${quantity}'; } diff --git a/lib/settings/login.dart b/lib/settings/login.dart index f5b6fe38..00032824 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -175,7 +175,6 @@ class _InvenTreeLoginSettingsState extends State { _reload(); - print("CONNECT FROM A"); // Attempt server login (this will load the newly selected profile InvenTreeAPI().connectToServer(context).then((result) { _reload(); @@ -210,7 +209,6 @@ class _InvenTreeLoginSettingsState extends State { if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { // Attempt server login (this will load the newly selected profile - print("Connect froM A"); InvenTreeAPI().connectToServer(context).then((result) { _reload(); }); diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 02a91527..ca0e4776 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -234,7 +234,7 @@ class SubcategoryList extends StatelessWidget { return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), - separatorBuilder: (_, __) => const Divider(), + separatorBuilder: (_, __) => const Divider(height: 3), itemBuilder: _build, itemCount: _categories.length); } } @@ -286,7 +286,7 @@ class PartList extends StatelessWidget { return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, __) => const Divider(height: 3), itemBuilder: _build, itemCount: _parts.length); } } diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 636dc46b..44ae938b 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -45,19 +45,19 @@ class _InvenTreeHomePageState extends State { } - void _scan() { + void _scan(BuildContext context) { if (!InvenTreeAPI().checkConnection(context)) return; scanQrCode(context); } - void _parts() { + void _parts(BuildContext context) { if (!InvenTreeAPI().checkConnection(context)) return; Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } - void _stock() { + void _stock(BuildContext context) { if (!InvenTreeAPI().checkConnection(context)) return; Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); @@ -82,7 +82,12 @@ class _InvenTreeHomePageState extends State { } void _selectProfile() { - Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())); + Navigator.push( + context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget()) + ).then((context) { + // Once we return + _loadProfile(); + }); } void _unsupported() { @@ -109,7 +114,7 @@ class _InvenTreeHomePageState extends State { if (_profile != null) { if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { - print("Connect from C"); + // Attempt server connection InvenTreeAPI().connectToServer(_context).then((result) { setState(() {}); }); @@ -234,7 +239,7 @@ class _InvenTreeHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.barcode), tooltip: I18N.of(context).scanBarcode, - onPressed: _scan, + onPressed: () { _scan(context); }, ), Text(I18N.of(context).scanBarcode), ], @@ -250,7 +255,7 @@ class _InvenTreeHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.shapes), tooltip: I18N.of(context).parts, - onPressed: _parts, + onPressed: () { _parts(context); }, ), Text(I18N.of(context).parts), ], @@ -260,7 +265,7 @@ class _InvenTreeHomePageState extends State { IconButton( icon: new FaIcon(FontAwesomeIcons.boxes), tooltip: I18N.of(context).stock, - onPressed: _stock, + onPressed: () { _stock(context); }, ), Text(I18N.of(context).stock), ], diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index b8fe8e06..c6a7ac3c 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -305,7 +305,7 @@ class SublocationList extends StatelessWidget { shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: _build, - separatorBuilder: (_, __) => const Divider(), + separatorBuilder: (_, __) => const Divider(height: 3), itemCount: _locations.length ); } @@ -349,7 +349,7 @@ class StockList extends StatelessWidget { return ListView.separated( shrinkWrap: true, physics: ClampingScrollPhysics(), - separatorBuilder: (_, __) => const Divider(), + separatorBuilder: (_, __) => const Divider(height: 3), itemBuilder: _build, itemCount: _items.length); } } \ No newline at end of file diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index fbd91132..935c2f70 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -214,7 +214,7 @@ class _PartDisplayState extends RefreshableState { // Stock information tiles.add( ListTile( - title: Text("Stock"), + title: Text(I18N.of(context).stock), leading: FaIcon(FontAwesomeIcons.boxes), trailing: Text("${part.inStock}"), onTap: () { @@ -230,7 +230,9 @@ class _PartDisplayState extends RefreshableState { title: Text("On Order"), leading: FaIcon(FontAwesomeIcons.shoppingCart), trailing: Text("${part.onOrder}"), - onTap: null, + onTap: () { + // TODO: Click through to show items on order + }, ) ); } diff --git a/lib/widget/part_stock_detail.dart b/lib/widget/part_stock_detail.dart index e5b4f27f..521d7025 100644 --- a/lib/widget/part_stock_detail.dart +++ b/lib/widget/part_stock_detail.dart @@ -38,7 +38,6 @@ class _PartStockDisplayState extends RefreshableState { @override Future onBuild(BuildContext context) async { refresh(); - print("onBuild"); } @override @@ -104,7 +103,7 @@ class PartStockList extends StatelessWidget { shrinkWrap: true, physics: ClampingScrollPhysics(), itemBuilder: _build, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, __) => const Divider(height: 3), itemCount: _items.length ); } From 90072904a0815061846fcb1ccf23ca4e1e5edc1b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 20:31:55 +1100 Subject: [PATCH 14/15] String translations --- lib/api.dart | 4 ++-- lib/barcode.dart | 3 ++- lib/l10n | 2 +- lib/settings/about.dart | 10 +++++----- lib/widget/drawer.dart | 2 +- lib/widget/location_display.dart | 12 ++++++------ lib/widget/part_detail.dart | 9 +++++---- lib/widget/stock_detail.dart | 25 ++++++++++++++++++++----- 8 files changed, 42 insertions(+), 25 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index d5a86631..f50f65e9 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -124,10 +124,10 @@ class InvenTreeAPI { showDialog( context: context, child: new SimpleDialog( - title: new Text("Not Connected"), + title: new Text(I18N.of(context).notConnected), children: [ ListTile( - title: Text("Server not connected"), + title: Text(I18N.of(context).serverNotConnected), ) ] ) diff --git a/lib/barcode.dart b/lib/barcode.dart index e28c2688..67dfdf09 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -7,6 +7,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:InvenTree/api.dart'; @@ -207,7 +208,7 @@ class BarcodeScanHandler extends BarcodeHandler { showDialog( context: _context, child: SimpleDialog( - title: Text("Unknown response"), + title: Text(I18N.of(_context).unknownResponse), children: [ ListTile( title: Text("Response data"), diff --git a/lib/l10n b/lib/l10n index c342d99f..79b2c87e 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit c342d99f068f8ac662dfb729fea0b5867aac1cc6 +Subproject commit 79b2c87e9611abbae7a7251ac68cbfed475f7699 diff --git a/lib/settings/about.dart b/lib/settings/about.dart index 666c86de..8f93a118 100644 --- a/lib/settings/about.dart +++ b/lib/settings/about.dart @@ -43,16 +43,16 @@ class InvenTreeAboutWidget extends StatelessWidget { tiles.add( ListTile( - title: Text("Server Instance"), + title: Text(I18N.of(context).serverInstance), subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"), ) ); } else { tiles.add( ListTile( - title: Text("Not Connected"), + title: Text(I18N.of(context).notConnected), subtitle: Text( - "InvenTree server not connected", + I18N.of(context).serverNotConnected, style: TextStyle(fontStyle: FontStyle.italic), ) ) @@ -77,7 +77,7 @@ class InvenTreeAboutWidget extends StatelessWidget { tiles.add( ListTile( - title: Text("Package Name"), + title: Text(I18N.of(context).packageName), subtitle: Text("${info.packageName}"), ) ); @@ -98,7 +98,7 @@ class InvenTreeAboutWidget extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text("About InvenTree"), + title: Text(I18N.of(context).appAbout), ), body: ListView( children: ListTile.divideTiles( diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index 70083365..40e70631 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -111,7 +111,7 @@ class InvenTreeDrawer extends StatelessWidget { fit: BoxFit.scaleDown, width: 40, ), - title: new Text("InvenTree"), + title: new Text(I18N.of(context).appTitle), onTap: _home, ), /* diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index c6a7ac3c..ef75f408 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -6,7 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:InvenTree/widget/refreshable_state.dart'; class LocationDisplayWidget extends StatefulWidget { @@ -97,8 +97,8 @@ class _LocationDisplayState extends RefreshableState { if (location == null) { return Card( child: ListTile( - title: Text("Stock Locations"), - subtitle: Text("Top level stock location") + title: Text(I18N.of(context).stockLocations), + subtitle: Text(I18N.of(context).stockTopLevel), ) ); } else { @@ -135,14 +135,14 @@ class _LocationDisplayState extends RefreshableState { return BottomNavigationBar( currentIndex: tabIndex, onTap: onTabSelectionChanged, - items: const [ + items: [ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.boxes), - title: Text("Stock"), + title: Text(I18N.of(context).stock), ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), - title: Text("Actions"), + title: Text(I18N.of(context).actions), ) ] ); diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 935c2f70..a0930752 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:InvenTree/inventree/part.dart'; import 'package:InvenTree/widget/full_screen_image.dart'; @@ -328,7 +329,7 @@ class _PartDisplayState extends RefreshableState { tiles.add( ListTile( - title: Text("Create Stock Item"), + title: Text(I18N.of(context).stockItemCreate), leading: FaIcon(FontAwesomeIcons.box), onTap: null, ) @@ -377,14 +378,14 @@ class _PartDisplayState extends RefreshableState { return BottomNavigationBar( currentIndex: tabIndex, onTap: onTabSelectionChanged, - items: const [ + items: [ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.infoCircle), - title: Text("Details"), + title: Text(I18N.of(context).details), ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), - title: Text("Actions"), + title: Text(I18N.of(context).actions), ), ] ); diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index f9ba55df..95d9fb9d 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -303,6 +303,7 @@ class _StockItemDisplayState extends RefreshableState { title: Text("${item.partName}"), subtitle: Text("${item.partDescription}"), leading: InvenTreeAPI().getImage(item.partImage), + trailing: Text(item.serialOrQuantityDisplay()), ) ); } @@ -365,7 +366,7 @@ class _StockItemDisplayState extends RefreshableState { if (item.isSerialized()) { tiles.add( ListTile( - title: Text("Serial Number"), + title: Text(I18N.of(context).serialNumber), leading: FaIcon(FontAwesomeIcons.hashtag), trailing: Text("${item.serialNumber}"), ) @@ -381,7 +382,8 @@ class _StockItemDisplayState extends RefreshableState { } // Supplier part? - if (item.supplierPartId > 0) { + // TODO: Display supplier part info page? + if (false && item.supplierPartId > 0) { tiles.add( ListTile( title: Text("${item.supplierName}"), @@ -410,7 +412,12 @@ class _StockItemDisplayState extends RefreshableState { leading: FaIcon(FontAwesomeIcons.tasks), trailing: Text("${item.testResultCount}"), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => StockItemTestResultsWidget(item))); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => StockItemTestResultsWidget(item)) + ).then((context) { + refresh(); + }); } ) ); @@ -421,7 +428,12 @@ class _StockItemDisplayState extends RefreshableState { title: Text(I18N.of(context).history), leading: FaIcon(FontAwesomeIcons.history), trailing: Text("${item.trackingItemCount}"), - onTap: null, + onTap: () { + // TODO: Load tracking history + + // TODO: Push tracking history page to the route + + }, ) ); } @@ -432,7 +444,10 @@ class _StockItemDisplayState extends RefreshableState { title: Text(I18N.of(context).notes), leading: FaIcon(FontAwesomeIcons.stickyNote), trailing: Text(""), - onTap: null, + onTap: () { + // TODO: Load notes in markdown viewer widget + // TODO: Make this widget editable? + } ) ); } From 33bb6148de9f6fe9411c81313406dfb9174e05da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Feb 2021 21:38:50 +1100 Subject: [PATCH 15/15] Cleanup for API error handling Ref: https://stackoverflow.com/questions/54617432/looking-up-a-deactivated-widgets-ancestor-is-unsafe --- lib/api.dart | 75 +++++++++-------- lib/barcode.dart | 13 +-- lib/inventree/model.dart | 139 +++++++++++++------------------ lib/inventree/part.dart | 11 ++- lib/inventree/stock.dart | 4 +- lib/l10n | 2 +- lib/settings/login.dart | 9 +- lib/widget/category_display.dart | 2 +- lib/widget/dialogs.dart | 67 +++++++++------ lib/widget/home.dart | 5 +- lib/widget/part_detail.dart | 2 +- 11 files changed, 162 insertions(+), 167 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index f50f65e9..55e2b6b5 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -68,15 +68,6 @@ class InvenTreeAPI { static const _URL_GET_TOKEN = "user/token/"; static const _URL_GET_VERSION = ""; - Future showServerError(BuildContext context, String description) async { - showErrorDialog( - context, - I18N.of(context).serverError, - description, - icon: FontAwesomeIcons.server - ); - } - // Base URL for InvenTree API e.g. http://192.168.120.10:8000 String _BASE_URL = ""; @@ -209,18 +200,18 @@ class InvenTreeAPI { print("Connecting to ${apiUrl} -> ${username}:${password}"); - var response = await get("").timeout(Duration(seconds: 5)).catchError((error) { + var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { print("Error connecting to server: ${error.toString()}"); if (error is SocketException) { - print("Error: socket exception: ${error.toString()}"); - // TODO - Display error dialog!! - //showServerError(context, "Connection Refused"); + showServerError( + context, + I18N.of(context).connectionRefused, + error.toString()); return null; } else if (error is TimeoutException) { - // TODO - Display timeout dialog here - //showTimeoutDialog(context); + showTimeoutError(context); return null; } else { // Unknown error type - re-throw the error and Sentry will catch it @@ -236,8 +227,9 @@ class InvenTreeAPI { if (response.statusCode != 200) { // Any status code other than 200! + showStatusCodeError(context, response.statusCode); + // TODO: Interpret the error codes and show custom message? - await showServerError(context, "Invalid response code: ${response.statusCode.toString()}"); return false; } @@ -246,29 +238,30 @@ class InvenTreeAPI { print("Response from server: $data"); // We expect certain response from the server - if (!data.containsKey("server") || !data.containsKey("version")) { + if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) { + + showServerError( + context, + "Missing Data", + "Server response missing required fields" + ); - await showServerError(context, "Server response missing required fields"); return false; } - print("Server: " + data["server"]); - print("Version: " + data["version"]); - + // Record server information _version = data["version"]; - - if (!_checkServerVersion(_version)) { - await showServerError(context, "Server version is too old.\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}"); - return false; - } - - // Record the instance name of the server instance = data['instance'] ?? ''; - // Request token from the server if we do not already have one - if (false && _token.isNotEmpty) { - print("Already have token - $_token"); - return true; + // Check that the remote server version is *new* enough + if (!_checkServerVersion(_version)) { + showServerError( + context, + "Old Server Version", + "\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}" + ); + + return false; } // Clear the existing token value @@ -277,24 +270,33 @@ class InvenTreeAPI { print("Requesting token from server"); response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { + print("Error requesting token:"); print(error); - return null; + }); if (response == null) { - await showServerError(context, "Error requesting access token"); + showServerError( + context, "Token Error", "Error requesting access token from server" + ); + return false; } if (response.statusCode != 200) { - await showServerError(context, "Invalid status code: ${response.statusCode.toString()}"); + showStatusCodeError(context, response.statusCode); return false; } else { var data = json.decode(response.body); if (!data.containsKey("token")) { - await showServerError(context, "No token provided in response"); + showServerError( + context, + "Missing Token", + "Access token missing from response" + ); + return false; } @@ -304,6 +306,7 @@ class InvenTreeAPI { _connected = true; + // Ok, probably pretty good... return true; }; } diff --git a/lib/barcode.dart b/lib/barcode.dart index 67dfdf09..a58c37b3 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -66,14 +66,11 @@ class BarcodeHandler { ); } - Future processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/", bool show_dialog = false}) { + Future processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) { this._context = context; this._controller = _controller; print("Scanned barcode data: ${barcode}"); - if (show_dialog) { - showProgressDialog(context, "Scanning", "Sending barcode data to server"); - } // Send barcode request to server InvenTreeAPI().post( @@ -83,10 +80,6 @@ class BarcodeHandler { } ).then((var response) { - if (show_dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { showErrorDialog( context, @@ -119,10 +112,6 @@ class BarcodeHandler { Duration(seconds: 5) ).catchError((error) { - if (show_dialog) { - hideProgressDialog(context); - } - showErrorDialog( context, "Error", diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 96abf38c..9c0358df 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -1,11 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:convert'; @@ -108,22 +108,21 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - Future reload(BuildContext context, {bool dialog = false}) async { - - if (dialog) { - showProgressDialog(context, I18N.of(context).refreshing, "Refreshing data for ${NAME}"); - } + Future reload(BuildContext context) async { var response = await api.get(url, params: defaultGetFilters()) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error (Sentry will catch) throw e; @@ -136,17 +135,8 @@ class InvenTreeModel { return false; } - if (dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog( - context, - I18N.of(context).serverError, - "${I18N.of(context).statusCode}: ${response.statusCode}" - ); - + showStatusCodeError(context, response.statusCode); print("Error retrieving data"); return false; } @@ -159,7 +149,7 @@ class InvenTreeModel { } // POST data to update the model - Future update(BuildContext context, {Map values, bool show_dialog = false}) async { + Future update(BuildContext context, {Map values}) async { var addr = path.join(URL, pk.toString()); @@ -167,20 +157,18 @@ class InvenTreeModel { addr += "/"; } - if (show_dialog) { - showProgressDialog(context, "Updating ${NAME}", "Sending data to server"); - } - var response = await api.patch(addr, body: values) .timeout(Duration(seconds: 10)) .catchError((e) { - if (show_dialog) { - hideProgressDialog(context); - } - - if (e is TimeoutException) { - showTimeoutDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); + } else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error, let Sentry report it throw e; @@ -191,12 +179,8 @@ class InvenTreeModel { if (response == null) return false; - if (show_dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); + showStatusCodeError(context, response.statusCode); return false; } @@ -205,7 +189,7 @@ class InvenTreeModel { } // Return the detail view for the associated pk - Future get(BuildContext context, int pk, {Map filters, bool dialog = false}) async { + Future get(BuildContext context, int pk, {Map filters}) async { // TODO - Add "timeout" // TODO - Add error catching @@ -227,20 +211,15 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); - if (dialog) { - showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); - } - var response = await api.get(addr, params: params) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError(context, I18N.of(context).connectionRefused, e.toString()); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error (handled by Sentry) throw e; @@ -252,12 +231,8 @@ class InvenTreeModel { return null; } - if (dialog) { - hideProgressDialog(context); - } - if (response.statusCode != 200) { - showErrorDialog(context, I18N.of(context).serverError, "${I18N.of(context).statusCode}: ${response.statusCode}"); + showStatusCodeError(context, response.statusCode); return null; } @@ -280,15 +255,24 @@ class InvenTreeModel { InvenTreeModel _model; - await api.post(URL, body: data) - .timeout(Duration(seconds: 5)) - .catchError((e) { + await api.post(URL, body: data).timeout(Duration(seconds: 10)).catchError((e) { + print("Error during CREATE"); print(e.toString()); - showErrorDialog( - context, - I18N.of(context).serverError, - e.toString() - ); + + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); + } + else if (e is TimeoutException) { + showTimeoutError(context); + } else { + // Re-throw the error (Sentry will catch) + throw e; + } + return null; }) .then((http.Response response) { @@ -297,11 +281,7 @@ class InvenTreeModel { var decoded = json.decode(response.body); _model = createFromJson(decoded); } else { - showErrorDialog( - context, - I18N.of(context).serverError, - "${I18N.of(context).statusCode}: ${response.statusCode}" - ); + showStatusCodeError(context, response.statusCode); } }); @@ -309,7 +289,7 @@ class InvenTreeModel { } // Return list of objects from the database, with optional filters - Future> list(BuildContext context, {Map filters, bool dialog=false}) async { + Future> list(BuildContext context, {Map filters}) async { if (filters == null) { filters = {}; @@ -328,20 +308,19 @@ class InvenTreeModel { // TODO - Add "timeout" // TODO - Add error catching - if (dialog) { - showProgressDialog(context, I18N.of(context).requestingData, "Requesting ${NAME} data from server"); - } - var response = await api.get(URL, params:params) .timeout(Duration(seconds: 10)) .catchError((e) { - if (dialog) { - hideProgressDialog(context); + if (e is SocketException) { + showServerError( + context, + I18N.of(context).connectionRefused, + e.toString() + ); } - - if (e is TimeoutException) { - showTimeoutDialog(context); + else if (e is TimeoutException) { + showTimeoutError(context); } else { // Re-throw the error throw e; @@ -354,15 +333,13 @@ class InvenTreeModel { return null; } - if (dialog) { - hideProgressDialog(context); - } - // A list of "InvenTreeModel" items List results = new List(); if (response.statusCode != 200) { - print("Error retreiving data"); + showStatusCodeError(context, response.statusCode); + + // Return empty list return results; } @@ -420,8 +397,6 @@ class InvenTreeModel { return true; } - - } diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index ac5c469c..c136a41b 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -161,7 +161,6 @@ class InvenTreePart extends InvenTreeModel { "part": "${pk}", "in_stock": "true", }, - dialog: showDialog, ).then((var items) { stockItems.clear(); @@ -186,7 +185,6 @@ class InvenTreePart extends InvenTreeModel { filters: { "part": "${pk}", }, - dialog: showDialog, ).then((var templates) { testingTemplates.clear(); @@ -205,6 +203,15 @@ class InvenTreePart extends InvenTreeModel { // Get the stock count for this Part double get inStock => double.tryParse(jsondata['in_stock'].toString() ?? '0'); + String get inStockString { + + if (inStock == inStock.toInt()) { + return inStock.toInt().toString(); + } else { + return inStock.toString(); + } + } + // Get the number of units being build for this Part double get building => double.tryParse(jsondata['building'].toString() ?? '0'); diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 60f2ee50..494b3aea 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -97,7 +97,6 @@ class InvenTreeStockItem extends InvenTreeModel { filters: { "part": "${partId}", }, - dialog: showDialog, ).then((var templates) { testTemplates.clear(); @@ -113,7 +112,7 @@ class InvenTreeStockItem extends InvenTreeModel { int get testResultCount => testResults.length; - Future getTestResults(BuildContext context, {bool showDialog=false}) async { + Future getTestResults(BuildContext context) async { await InvenTreeStockItemTestResult().list( context, @@ -121,7 +120,6 @@ class InvenTreeStockItem extends InvenTreeModel { "stock_item": "${pk}", "user_detail": "true", }, - dialog: showDialog, ).then((var results) { testResults.clear(); diff --git a/lib/l10n b/lib/l10n index 79b2c87e..90f3bbf1 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 79b2c87e9611abbae7a7251ac68cbfed475f7699 +Subproject commit 90f3bbf1fae86efd0bb0686bef12452a09507669 diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 00032824..d0695020 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -19,6 +19,8 @@ class InvenTreeLoginSettingsWidget extends StatefulWidget { class _InvenTreeLoginSettingsState extends State { + final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>(); + final GlobalKey _formKey = new GlobalKey(); final GlobalKey _addProfileKey = new GlobalKey(); @@ -176,7 +178,7 @@ class _InvenTreeLoginSettingsState extends State { _reload(); // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(context).then((result) { + InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { _reload(); }); @@ -209,7 +211,7 @@ class _InvenTreeLoginSettingsState extends State { if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { // Attempt server login (this will load the newly selected profile - InvenTreeAPI().connectToServer(context).then((result) { + InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { _reload(); }); } @@ -262,8 +264,6 @@ class _InvenTreeLoginSettingsState extends State { final Size screenSize = MediaQuery.of(context).size; - print("Building!"); - List children = []; if (profiles != null && profiles.length > 0) { @@ -333,6 +333,7 @@ class _InvenTreeLoginSettingsState extends State { } return Scaffold( + key: _loginKey, appBar: AppBar( title: Text(I18N.of(context).profileSelect), ), diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index ca0e4776..2a6c6722 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -268,7 +268,7 @@ class PartList extends StatelessWidget { return ListTile( title: Text("${part.name}"), subtitle: Text("${part.description}"), - trailing: Text("${part.inStock}"), + trailing: Text("${part.inStockString}"), leading: InvenTreeAPI().getImage( part.thumbnail, width: 40, diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 828a4476..803476ab 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; Future confirmationDialog(BuildContext context, String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async { @@ -92,37 +93,55 @@ Future showErrorDialog(BuildContext context, String title, String descript showDialog( context: context, - child: SimpleDialog( - title: ListTile( - title: Text(error), - leading: FaIcon(icon), - ), - children: [ - ListTile( - title: Text(title), - subtitle: Text(description) - ) - ] - ) - ).then((value) { - if (onDismissed != null) { - onDismissed(); - } - }); + builder: (dialogContext) { + return SimpleDialog( + title: ListTile( + title: Text(error), + leading: FaIcon(icon), + ), + children: [ + ListTile( + title: Text(title), + subtitle: Text(description) + ) + ] + ); + }).then((value) { + if (onDismissed != null) { + onDismissed(); + } + }); } -void showTimeoutDialog(BuildContext context) { - /* - Show a server timeout dialog - */ +Future showServerError(BuildContext context, String title, String description) async { - showErrorDialog( + if (title == null || title.isEmpty) { + title = I18N.of(context).serverError; + } + + await showErrorDialog( context, - I18N.of(context).timeout, - I18N.of(context).noResponse + title, + description, + error: I18N.of(context).serverError, + icon: FontAwesomeIcons.server ); } +Future showStatusCodeError(BuildContext context, int status, {int expected = 200}) async { + + await showServerError( + context, + "Invalid Response Code", + "Server responded with status code ${status}" + ); +} + +Future showTimeoutError(BuildContext context) async { + + await showServerError(context, I18N.of(context).timeout, I18N.of(context).noResponse); +} + void showProgressDialog(BuildContext context, String title, String description) { showDialog( diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 44ae938b..3955668e 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -27,6 +27,8 @@ class InvenTreeHomePage extends StatefulWidget { class _InvenTreeHomePageState extends State { + final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>(); + _InvenTreeHomePageState() : super() { // Initially load the profile and attempt server connection @@ -115,7 +117,7 @@ class _InvenTreeHomePageState extends State { if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { // Attempt server connection - InvenTreeAPI().connectToServer(_context).then((result) { + InvenTreeAPI().connectToServer(_homeKey.currentContext).then((result) { setState(() {}); }); } @@ -198,6 +200,7 @@ class _InvenTreeHomePageState extends State { // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( + key: _homeKey, appBar: AppBar( title: Text(I18N.of(context).appTitle), actions: [ diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index a0930752..64cedbe9 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -217,7 +217,7 @@ class _PartDisplayState extends RefreshableState { ListTile( title: Text(I18N.of(context).stock), leading: FaIcon(FontAwesomeIcons.boxes), - trailing: Text("${part.inStock}"), + trailing: Text("${part.inStockString}"), onTap: () { _showStock(context); },