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