2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-06-12 18:25:26 +00:00

UserProfile

This commit is contained in:
Oliver Walters
2021-02-08 20:32:49 +11:00
parent b18daf3acf
commit b34a91b865
10 changed files with 460 additions and 44 deletions

View File

@ -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;
}
});

View File

@ -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<bool> 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;

View File

@ -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<Database> _dbOpenCompleter;
Future<Database> 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';

View File

@ -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<UserProfile> _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<InvenTreeLoginSettingsWidget> {
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
final _addProfileKey = new GlobalKey<FormState>();
final SharedPreferences _preferences;
List<UserProfile> 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: <Widget> [
FlatButton(
child: Text(I18N.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
}
),
FlatButton(
child: Text(I18N.of(context).save),
onPressed: () {
// TODO
}
)
],
fields: <Widget> [
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<InvenTreeLoginSettingsWidget> {
}
}
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<Widget> 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: <Widget> [
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"),

View File

@ -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<InvenTreeSettingsWidget> {
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<InvenTreeSettingsWidget> {
var prefs = await SharedPreferences.getInstance();
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(prefs)));
List<UserProfile> profiles = await UserProfileDBManager().getAllProfiles();
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget(profiles, prefs)));
}
void _about() async {

109
lib/user_profile.dart Normal file
View File

@ -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<String, dynamic> json) => UserProfile(
name: json['name'],
server: json['server'],
username: json['username'],
password: json['password'],
);
Map<String, dynamic> 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<Database> 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<UserProfile> 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<List<UserProfile>> getAllProfiles() async {
final profiles = await _folder.find(await _db);
List<UserProfile> profileList = new List<UserProfile>();
for (int idx = 0; idx < profiles.length; idx++) {
profileList.add(UserProfile.fromJson(profiles[idx].value));
}
return profileList;
}
}

View File

@ -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<void> 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<void> 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(