mirror of
https://github.com/inventree/inventree-app.git
synced 2025-05-02 07:26:50 +00:00
Merge branch 'profile-support'
This commit is contained in:
commit
3c560c395c
172
lib/api.dart
172
lib/api.dart
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:InvenTree/user_profile.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
@ -67,15 +68,6 @@ class InvenTreeAPI {
|
|||||||
static const _URL_GET_TOKEN = "user/token/";
|
static const _URL_GET_TOKEN = "user/token/";
|
||||||
static const _URL_GET_VERSION = "";
|
static const _URL_GET_VERSION = "";
|
||||||
|
|
||||||
Future<void> 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
|
// Base URL for InvenTree API e.g. http://192.168.120.10:8000
|
||||||
String _BASE_URL = "";
|
String _BASE_URL = "";
|
||||||
|
|
||||||
@ -108,14 +100,11 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
String makeUrl(String endpoint) => _makeUrl(endpoint);
|
String makeUrl(String endpoint) => _makeUrl(endpoint);
|
||||||
|
|
||||||
String _username = "";
|
UserProfile profile;
|
||||||
String _password = "";
|
|
||||||
|
|
||||||
// Authentication token (initially empty, must be requested)
|
// Authentication token (initially empty, must be requested)
|
||||||
String _token = "";
|
String _token = "";
|
||||||
|
|
||||||
bool isConnected() => _token.isNotEmpty;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Check server connection and display messages if not connected.
|
* Check server connection and display messages if not connected.
|
||||||
* Useful as a precursor check before performing operations.
|
* Useful as a precursor check before performing operations.
|
||||||
@ -126,10 +115,10 @@ class InvenTreeAPI {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
child: new SimpleDialog(
|
child: new SimpleDialog(
|
||||||
title: new Text("Not Connected"),
|
title: new Text(I18N.of(context).notConnected),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Server not connected"),
|
title: Text(I18N.of(context).serverNotConnected),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -154,8 +143,14 @@ class InvenTreeAPI {
|
|||||||
// Connection status flag - set once connection has been validated
|
// Connection status flag - set once connection has been validated
|
||||||
bool _connected = false;
|
bool _connected = false;
|
||||||
|
|
||||||
bool get connected {
|
bool _connecting = true;
|
||||||
return _connected && baseUrl.isNotEmpty && _token.isNotEmpty;
|
|
||||||
|
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
|
// Ensure we only ever create a single instance of the API class
|
||||||
@ -167,26 +162,17 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
InvenTreeAPI._internal();
|
InvenTreeAPI._internal();
|
||||||
|
|
||||||
Future<bool> connect(BuildContext context) async {
|
Future<bool> _connect(BuildContext context) async {
|
||||||
var prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
String server = prefs.getString("server");
|
|
||||||
String username = prefs.getString("username");
|
|
||||||
String password = prefs.getString("password");
|
|
||||||
|
|
||||||
return connectToServer(context, server, username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> connectToServer(BuildContext context, String address, String username, String password) async {
|
|
||||||
|
|
||||||
/* Address is the base address for the InvenTree server,
|
/* Address is the base address for the InvenTree server,
|
||||||
* e.g. http://127.0.0.1:8000
|
* e.g. http://127.0.0.1:8000
|
||||||
*/
|
*/
|
||||||
|
|
||||||
String errorMessage = "";
|
if (profile == null) return false;
|
||||||
|
|
||||||
address = address.trim();
|
String address = profile.server.trim();
|
||||||
username = username.trim();
|
String username = profile.username.trim();
|
||||||
|
String password = profile.password.trim();
|
||||||
|
|
||||||
if (address.isEmpty || username.isEmpty || password.isEmpty) {
|
if (address.isEmpty || username.isEmpty || password.isEmpty) {
|
||||||
await showErrorDialog(
|
await showErrorDialog(
|
||||||
@ -211,41 +197,39 @@ class InvenTreeAPI {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
_BASE_URL = address;
|
_BASE_URL = address;
|
||||||
_username = username;
|
|
||||||
_password = password;
|
|
||||||
|
|
||||||
_connected = false;
|
print("Connecting to ${apiUrl} -> ${username}:${password}");
|
||||||
|
|
||||||
print("Connecting to " + apiUrl + " -> " + username + ":" + password);
|
|
||||||
|
|
||||||
var response = await get("").timeout(Duration(seconds: 10)).catchError((error) {
|
var response = await get("").timeout(Duration(seconds: 10)).catchError((error) {
|
||||||
|
|
||||||
|
print("Error connecting to server: ${error.toString()}");
|
||||||
|
|
||||||
if (error is SocketException) {
|
if (error is SocketException) {
|
||||||
errorMessage = "Could not connect to server";
|
showServerError(
|
||||||
|
context,
|
||||||
|
I18N.of(context).connectionRefused,
|
||||||
|
error.toString());
|
||||||
return null;
|
return null;
|
||||||
} else if (error is TimeoutException) {
|
} else if (error is TimeoutException) {
|
||||||
errorMessage = "Server timeout";
|
showTimeoutError(context);
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
// Unknown error type
|
// Unknown error type - re-throw the error and Sentry will catch it
|
||||||
errorMessage = error.toString();
|
throw error;
|
||||||
// Unknown error type, re-throw error
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
// Null (or error) response: Show dialog and exit
|
// Null (or error) response: Show dialog and exit
|
||||||
|
|
||||||
await showServerError(context, errorMessage);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
// Any status code other than 200!
|
// Any status code other than 200!
|
||||||
|
|
||||||
|
showStatusCodeError(context, response.statusCode);
|
||||||
|
|
||||||
// TODO: Interpret the error codes and show custom message?
|
// TODO: Interpret the error codes and show custom message?
|
||||||
await showServerError(context, "Invalid response code: ${response.statusCode.toString()}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,29 +238,30 @@ class InvenTreeAPI {
|
|||||||
print("Response from server: $data");
|
print("Response from server: $data");
|
||||||
|
|
||||||
// We expect certain response from the server
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Server: " + data["server"]);
|
// Record server information
|
||||||
print("Version: " + data["version"]);
|
|
||||||
|
|
||||||
_version = data["version"];
|
_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'] ?? '';
|
instance = data['instance'] ?? '';
|
||||||
|
|
||||||
// Request token from the server if we do not already have one
|
// Check that the remote server version is *new* enough
|
||||||
if (false && _token.isNotEmpty) {
|
if (!_checkServerVersion(_version)) {
|
||||||
print("Already have token - $_token");
|
showServerError(
|
||||||
return true;
|
context,
|
||||||
|
"Old Server Version",
|
||||||
|
"\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}"
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the existing token value
|
// Clear the existing token value
|
||||||
@ -285,24 +270,33 @@ class InvenTreeAPI {
|
|||||||
print("Requesting token from server");
|
print("Requesting token from server");
|
||||||
|
|
||||||
response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) {
|
response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) {
|
||||||
|
|
||||||
print("Error requesting token:");
|
print("Error requesting token:");
|
||||||
print(error);
|
print(error);
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
await showServerError(context, "Error requesting access token");
|
showServerError(
|
||||||
|
context, "Token Error", "Error requesting access token from server"
|
||||||
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
await showServerError(context, "Invalid status code: ${response.statusCode.toString()}");
|
showStatusCodeError(context, response.statusCode);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
var data = json.decode(response.body);
|
var data = json.decode(response.body);
|
||||||
|
|
||||||
if (!data.containsKey("token")) {
|
if (!data.containsKey("token")) {
|
||||||
await showServerError(context, "No token provided in response");
|
showServerError(
|
||||||
|
context,
|
||||||
|
"Missing Token",
|
||||||
|
"Access token missing from response"
|
||||||
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,10 +306,50 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
_connected = true;
|
_connected = true;
|
||||||
|
|
||||||
|
// Ok, probably pretty good...
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool disconnectFromServer() {
|
||||||
|
print("InvenTreeAPI().disconnectFromServer()");
|
||||||
|
|
||||||
|
_connected = false;
|
||||||
|
_connecting = false;
|
||||||
|
_token = '';
|
||||||
|
profile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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,
|
||||||
|
"Select Profile",
|
||||||
|
"User profile not selected"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connecting = true;
|
||||||
|
|
||||||
|
bool result = await _connect(context);
|
||||||
|
|
||||||
|
print("_connect() returned result: ${result}");
|
||||||
|
|
||||||
|
_connecting = false;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Perform a PATCH request
|
// Perform a PATCH request
|
||||||
Future<http.Response> patch(String url, {Map<String, String> body}) async {
|
Future<http.Response> patch(String url, {Map<String, String> body}) async {
|
||||||
var _url = makeApiUrl(url);
|
var _url = makeApiUrl(url);
|
||||||
@ -399,7 +433,9 @@ class InvenTreeAPI {
|
|||||||
Map<String, String> defaultHeaders() {
|
Map<String, String> defaultHeaders() {
|
||||||
var headers = Map<String, String>();
|
var headers = Map<String, String>();
|
||||||
|
|
||||||
headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
|
if (profile != null) {
|
||||||
|
headers[HttpHeaders.authorizationHeader] = _authorizationHeader(profile.username, profile.password);
|
||||||
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
@ -410,11 +446,11 @@ class InvenTreeAPI {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _authorizationHeader() {
|
String _authorizationHeader(String username, String password) {
|
||||||
if (_token.isNotEmpty) {
|
if (_token.isNotEmpty) {
|
||||||
return "Token $_token";
|
return "Token $_token";
|
||||||
} else {
|
} else {
|
||||||
return "Basic " + base64Encode(utf8.encode('$_username:$_password'));
|
return "Basic " + base64Encode(utf8.encode('${username}:${password}'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
|
|||||||
|
|
||||||
import 'package:InvenTree/inventree/stock.dart';
|
import 'package:InvenTree/inventree/stock.dart';
|
||||||
import 'package:InvenTree/inventree/part.dart';
|
import 'package:InvenTree/inventree/part.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
|
|
||||||
@ -65,14 +66,11 @@ class BarcodeHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/", bool show_dialog = false}) {
|
Future<void> processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._controller = _controller;
|
this._controller = _controller;
|
||||||
|
|
||||||
print("Scanned barcode data: ${barcode}");
|
print("Scanned barcode data: ${barcode}");
|
||||||
if (show_dialog) {
|
|
||||||
showProgressDialog(context, "Scanning", "Sending barcode data to server");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send barcode request to server
|
// Send barcode request to server
|
||||||
InvenTreeAPI().post(
|
InvenTreeAPI().post(
|
||||||
@ -82,10 +80,6 @@ class BarcodeHandler {
|
|||||||
}
|
}
|
||||||
).then((var response) {
|
).then((var response) {
|
||||||
|
|
||||||
if (show_dialog) {
|
|
||||||
hideProgressDialog(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
showErrorDialog(
|
showErrorDialog(
|
||||||
context,
|
context,
|
||||||
@ -118,10 +112,6 @@ class BarcodeHandler {
|
|||||||
Duration(seconds: 5)
|
Duration(seconds: 5)
|
||||||
).catchError((error) {
|
).catchError((error) {
|
||||||
|
|
||||||
if (show_dialog) {
|
|
||||||
hideProgressDialog(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
showErrorDialog(
|
showErrorDialog(
|
||||||
context,
|
context,
|
||||||
"Error",
|
"Error",
|
||||||
@ -207,7 +197,7 @@ class BarcodeScanHandler extends BarcodeHandler {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: _context,
|
context: _context,
|
||||||
child: SimpleDialog(
|
child: SimpleDialog(
|
||||||
title: Text("Unknown response"),
|
title: Text(I18N.of(_context).unknownResponse),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Response data"),
|
title: Text("Response data"),
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
import 'package:InvenTree/widget/dialogs.dart';
|
import 'package:InvenTree/widget/dialogs.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@ -105,24 +108,24 @@ class InvenTreeModel {
|
|||||||
/*
|
/*
|
||||||
* Reload this object, by requesting data from the server
|
* Reload this object, by requesting data from the server
|
||||||
*/
|
*/
|
||||||
Future<bool> reload(BuildContext context, {bool dialog = false}) async {
|
Future<bool> reload(BuildContext context) async {
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
showProgressDialog(context, "Refreshing data", "Refreshing data for ${NAME}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await api.get(url, params: defaultGetFilters())
|
var response = await api.get(url, params: defaultGetFilters())
|
||||||
.timeout(Duration(seconds: 10))
|
.timeout(Duration(seconds: 10))
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
|
|
||||||
if (dialog) {
|
if (e is SocketException) {
|
||||||
hideProgressDialog(context);
|
showServerError(
|
||||||
|
context,
|
||||||
|
I18N.of(context).connectionRefused,
|
||||||
|
e.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
else if (e is TimeoutException) {
|
||||||
if (e is TimeoutException) {
|
showTimeoutError(context);
|
||||||
showErrorDialog(context, "Timeout", "No response from server");
|
|
||||||
} else {
|
} else {
|
||||||
showErrorDialog(context, "Error", e.toString());
|
// Re-throw the error (Sentry will catch)
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -132,11 +135,8 @@ class InvenTreeModel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
hideProgressDialog(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
|
showStatusCodeError(context, response.statusCode);
|
||||||
print("Error retrieving data");
|
print("Error retrieving data");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST data to update the model
|
// POST data to update the model
|
||||||
Future<bool> update(BuildContext context, {Map<String, String> values, bool show_dialog = false}) async {
|
Future<bool> update(BuildContext context, {Map<String, String> values}) async {
|
||||||
|
|
||||||
var addr = path.join(URL, pk.toString());
|
var addr = path.join(URL, pk.toString());
|
||||||
|
|
||||||
@ -157,22 +157,21 @@ class InvenTreeModel {
|
|||||||
addr += "/";
|
addr += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show_dialog) {
|
|
||||||
showProgressDialog(context, "Updating ${NAME}", "Sending data to server");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await api.patch(addr, body: values)
|
var response = await api.patch(addr, body: values)
|
||||||
.timeout(Duration(seconds: 10))
|
.timeout(Duration(seconds: 10))
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
|
|
||||||
if (show_dialog) {
|
if (e is SocketException) {
|
||||||
hideProgressDialog(context);
|
showServerError(
|
||||||
}
|
context,
|
||||||
|
I18N.of(context).connectionRefused,
|
||||||
if (e is TimeoutException) {
|
e.toString()
|
||||||
showErrorDialog(context, "Timeout", "No response from server");
|
);
|
||||||
|
} else if (e is TimeoutException) {
|
||||||
|
showTimeoutError(context);
|
||||||
} else {
|
} else {
|
||||||
showErrorDialog(context, "Error", e.toString());
|
// Re-throw the error, let Sentry report it
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -180,12 +179,8 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
if (response == null) return false;
|
if (response == null) return false;
|
||||||
|
|
||||||
if (show_dialog) {
|
|
||||||
hideProgressDialog(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
print("Error updating ${NAME}: Status code ${response.statusCode}");
|
showStatusCodeError(context, response.statusCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +189,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the detail view for the associated pk
|
// Return the detail view for the associated pk
|
||||||
Future<InvenTreeModel> get(BuildContext context, int pk, {Map<String, String> filters, bool dialog = false}) async {
|
Future<InvenTreeModel> get(BuildContext context, int pk, {Map<String, String> filters}) async {
|
||||||
|
|
||||||
// TODO - Add "timeout"
|
// TODO - Add "timeout"
|
||||||
// TODO - Add error catching
|
// TODO - Add error catching
|
||||||
@ -216,22 +211,18 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
print("GET: $addr ${params.toString()}");
|
print("GET: $addr ${params.toString()}");
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await api.get(addr, params: params)
|
var response = await api.get(addr, params: params)
|
||||||
.timeout(Duration(seconds: 10))
|
.timeout(Duration(seconds: 10))
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
|
|
||||||
if (dialog) {
|
if (e is SocketException) {
|
||||||
hideProgressDialog(context);
|
showServerError(context, I18N.of(context).connectionRefused, e.toString());
|
||||||
}
|
}
|
||||||
|
else if (e is TimeoutException) {
|
||||||
if (e is TimeoutException) {
|
showTimeoutError(context);
|
||||||
showErrorDialog(context, "Timeout", "No response from server");
|
|
||||||
} else {
|
} else {
|
||||||
showErrorDialog(context, "Error", e.toString());
|
// Re-throw the error (handled by Sentry)
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
@ -240,10 +231,8 @@ class InvenTreeModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideProgressDialog(context);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
print("Error retrieving data");
|
showStatusCodeError(context, response.statusCode);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,11 +255,24 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
InvenTreeModel _model;
|
InvenTreeModel _model;
|
||||||
|
|
||||||
await api.post(URL, body: data)
|
await api.post(URL, body: data).timeout(Duration(seconds: 10)).catchError((e) {
|
||||||
.timeout(Duration(seconds: 5))
|
print("Error during CREATE");
|
||||||
.catchError((e) {
|
|
||||||
print("Error creating new ${NAME}:");
|
|
||||||
print(e.toString());
|
print(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;
|
return null;
|
||||||
})
|
})
|
||||||
.then((http.Response response) {
|
.then((http.Response response) {
|
||||||
@ -279,8 +281,7 @@ class InvenTreeModel {
|
|||||||
var decoded = json.decode(response.body);
|
var decoded = json.decode(response.body);
|
||||||
_model = createFromJson(decoded);
|
_model = createFromJson(decoded);
|
||||||
} else {
|
} else {
|
||||||
print("Error creating object: Status Code ${response.statusCode}");
|
showStatusCodeError(context, response.statusCode);
|
||||||
print(response.body);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -288,7 +289,7 @@ class InvenTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return list of objects from the database, with optional filters
|
// Return list of objects from the database, with optional filters
|
||||||
Future<List<InvenTreeModel>> list(BuildContext context, {Map<String, String> filters, bool dialog=false}) async {
|
Future<List<InvenTreeModel>> list(BuildContext context, {Map<String, String> filters}) async {
|
||||||
|
|
||||||
if (filters == null) {
|
if (filters == null) {
|
||||||
filters = {};
|
filters = {};
|
||||||
@ -307,20 +308,19 @@ class InvenTreeModel {
|
|||||||
// TODO - Add "timeout"
|
// TODO - Add "timeout"
|
||||||
// TODO - Add error catching
|
// TODO - Add error catching
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await api.get(URL, params:params)
|
var response = await api.get(URL, params:params)
|
||||||
.timeout(Duration(seconds: 10))
|
.timeout(Duration(seconds: 10))
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
|
|
||||||
if (dialog) {
|
if (e is SocketException) {
|
||||||
hideProgressDialog(context);
|
showServerError(
|
||||||
|
context,
|
||||||
|
I18N.of(context).connectionRefused,
|
||||||
|
e.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
else if (e is TimeoutException) {
|
||||||
if (e is TimeoutException) {
|
showTimeoutError(context);
|
||||||
showErrorDialog(context, "Timeout", "No response from server");
|
|
||||||
} else {
|
} else {
|
||||||
// Re-throw the error
|
// Re-throw the error
|
||||||
throw e;
|
throw e;
|
||||||
@ -333,15 +333,13 @@ class InvenTreeModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
hideProgressDialog(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A list of "InvenTreeModel" items
|
// A list of "InvenTreeModel" items
|
||||||
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
print("Error retreiving data");
|
showStatusCodeError(context, response.statusCode);
|
||||||
|
|
||||||
|
// Return empty list
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,8 +397,6 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,13 +155,12 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
// Request stock items for this part
|
// Request stock items for this part
|
||||||
Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
|
Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
|
||||||
|
|
||||||
InvenTreeStockItem().list(
|
await InvenTreeStockItem().list(
|
||||||
context,
|
context,
|
||||||
filters: {
|
filters: {
|
||||||
"part": "${pk}",
|
"part": "${pk}",
|
||||||
"in_stock": "true",
|
"in_stock": "true",
|
||||||
},
|
},
|
||||||
dialog: showDialog,
|
|
||||||
).then((var items) {
|
).then((var items) {
|
||||||
stockItems.clear();
|
stockItems.clear();
|
||||||
|
|
||||||
@ -186,7 +185,6 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
filters: {
|
filters: {
|
||||||
"part": "${pk}",
|
"part": "${pk}",
|
||||||
},
|
},
|
||||||
dialog: showDialog,
|
|
||||||
).then((var templates) {
|
).then((var templates) {
|
||||||
|
|
||||||
testingTemplates.clear();
|
testingTemplates.clear();
|
||||||
@ -205,6 +203,15 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
// Get the stock count for this Part
|
// Get the stock count for this Part
|
||||||
double get inStock => double.tryParse(jsondata['in_stock'].toString() ?? '0');
|
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
|
// Get the number of units being build for this Part
|
||||||
double get building => double.tryParse(jsondata['building'].toString() ?? '0');
|
double get building => double.tryParse(jsondata['building'].toString() ?? '0');
|
||||||
|
|
||||||
|
@ -97,7 +97,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
filters: {
|
filters: {
|
||||||
"part": "${partId}",
|
"part": "${partId}",
|
||||||
},
|
},
|
||||||
dialog: showDialog,
|
|
||||||
).then((var templates) {
|
).then((var templates) {
|
||||||
testTemplates.clear();
|
testTemplates.clear();
|
||||||
|
|
||||||
@ -113,7 +112,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
|
|
||||||
int get testResultCount => testResults.length;
|
int get testResultCount => testResults.length;
|
||||||
|
|
||||||
Future<void> getTestResults(BuildContext context, {bool showDialog=false}) async {
|
Future<void> getTestResults(BuildContext context) async {
|
||||||
|
|
||||||
await InvenTreeStockItemTestResult().list(
|
await InvenTreeStockItemTestResult().list(
|
||||||
context,
|
context,
|
||||||
@ -121,7 +120,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
"stock_item": "${pk}",
|
"stock_item": "${pk}",
|
||||||
"user_detail": "true",
|
"user_detail": "true",
|
||||||
},
|
},
|
||||||
dialog: showDialog,
|
|
||||||
).then((var results) {
|
).then((var results) {
|
||||||
testResults.clear();
|
testResults.clear();
|
||||||
|
|
||||||
@ -287,6 +285,11 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
return 'SN ${serialNumber}';
|
return 'SN ${serialNumber}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is an integer?
|
||||||
|
if (quantity.toInt() == quantity) {
|
||||||
|
return '${quantity.toInt()}';
|
||||||
|
}
|
||||||
|
|
||||||
return '${quantity}';
|
return '${quantity}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
|||||||
Subproject commit 58e2c5027b481a3a620c27b90ae20b888a02bd96
|
Subproject commit 90f3bbf1fae86efd0bb0686bef12452a09507669
|
@ -8,12 +8,14 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'dsn.dart';
|
import 'dsn.dart';
|
||||||
import 'preferences.dart';
|
|
||||||
|
|
||||||
import 'package:sentry/sentry.dart';
|
import 'package:sentry/sentry.dart';
|
||||||
|
|
||||||
// Use the secret app key
|
// 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 isInDebugMode() {
|
||||||
bool inDebugMode = false;
|
bool inDebugMode = false;
|
||||||
@ -31,13 +33,15 @@ Future<void> _reportError(dynamic error, dynamic stackTrace) async {
|
|||||||
print(stackTrace);
|
print(stackTrace);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Send the Exception and Stacktrace to Sentry in Production mode.
|
try {
|
||||||
_sentry.captureException(
|
await _sentry.captureException(
|
||||||
exception: error,
|
error,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
print("Sending error to sentry.io");
|
print("Sending error report to sentry.io failed: ${e}");
|
||||||
|
print("Original error: ${error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,12 +51,8 @@ void main() async {
|
|||||||
|
|
||||||
runZoned<Future<void>>(() async {
|
runZoned<Future<void>>(() async {
|
||||||
runApp(InvenTreeApp());
|
runApp(InvenTreeApp());
|
||||||
}, onError: (error, stackTrace) {
|
}, onError: _reportError
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvenTreeApp extends StatelessWidget {
|
class InvenTreeApp extends StatelessWidget {
|
||||||
|
@ -1,8 +1,57 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'api.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 {
|
class InvenTreePreferences {
|
||||||
|
|
||||||
static const String _SERVER = 'server';
|
static const String _SERVER = 'server';
|
||||||
@ -34,30 +83,4 @@ class InvenTreePreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InvenTreePreferences._internal();
|
InvenTreePreferences._internal();
|
||||||
|
|
||||||
// Load saved login details, and attempt connection
|
|
||||||
void loadLoginDetails(BuildContext context) async {
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, server, username, password);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -15,48 +15,96 @@ class InvenTreeAboutWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
return Scaffold(
|
List<Widget> tiles = [];
|
||||||
appBar: AppBar(
|
|
||||||
title: Text("About InvenTree"),
|
tiles.add(
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).serverDetails),
|
title: Text(
|
||||||
|
I18N.of(context).serverDetails,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (InvenTreeAPI().isConnected()) {
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).address),
|
title: Text(I18N.of(context).address),
|
||||||
subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"),
|
subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"),
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).version),
|
title: Text(I18N.of(context).version),
|
||||||
subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"),
|
subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"),
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Server Instance"),
|
title: Text(I18N.of(context).serverInstance),
|
||||||
subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"),
|
subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"),
|
||||||
),
|
)
|
||||||
Divider(),
|
);
|
||||||
|
} else {
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).appDetails),
|
title: Text(I18N.of(context).notConnected),
|
||||||
|
subtitle: Text(
|
||||||
|
I18N.of(context).serverNotConnected,
|
||||||
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
I18N.of(context).appDetails,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).name),
|
title: Text(I18N.of(context).name),
|
||||||
subtitle: Text("${info.appName}"),
|
subtitle: Text("${info.appName}"),
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Package Name"),
|
title: Text(I18N.of(context).packageName),
|
||||||
subtitle: Text("${info.packageName}"),
|
subtitle: Text("${info.packageName}"),
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).version),
|
title: Text(I18N.of(context).version),
|
||||||
subtitle: Text("${info.version}"),
|
subtitle: Text("${info.version}"),
|
||||||
),
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).build),
|
title: Text(I18N.of(context).build),
|
||||||
subtitle: Text("${info.buildNumber}"),
|
subtitle: Text("${info.buildNumber}"),
|
||||||
)
|
)
|
||||||
],
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(I18N.of(context).appAbout),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: tiles,
|
||||||
|
).toList(),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,141 @@
|
|||||||
|
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/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
import '../api.dart';
|
import '../api.dart';
|
||||||
import '../preferences.dart';
|
import '../preferences.dart';
|
||||||
|
import '../user_profile.dart';
|
||||||
|
|
||||||
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
class InvenTreeLoginSettingsWidget extends StatefulWidget {
|
||||||
|
|
||||||
final SharedPreferences _preferences;
|
|
||||||
|
|
||||||
InvenTreeLoginSettingsWidget(this._preferences) : super();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState(_preferences);
|
_InvenTreeLoginSettingsState createState() => _InvenTreeLoginSettingsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||||
|
|
||||||
|
final GlobalKey<_InvenTreeLoginSettingsState> _loginKey = GlobalKey<_InvenTreeLoginSettingsState>();
|
||||||
|
|
||||||
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
||||||
|
|
||||||
final SharedPreferences _preferences;
|
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
|
||||||
|
|
||||||
String _server = '';
|
List<UserProfile> profiles;
|
||||||
String _username = '';
|
|
||||||
String _password = '';
|
|
||||||
|
|
||||||
_InvenTreeLoginSettingsState(this._preferences) : super() {
|
_InvenTreeLoginSettingsState() {
|
||||||
_server = _preferences.getString('server') ?? '';
|
_reload();
|
||||||
_username = _preferences.getString('username') ?? '';
|
|
||||||
_password = _preferences.getString('password') ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _reload() async {
|
||||||
|
|
||||||
|
profiles = await UserProfileDBManager().getAllProfiles();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
key: _addProfileKey,
|
||||||
|
actions: <Widget> [
|
||||||
|
FlatButton(
|
||||||
|
child: Text(I18N.of(context).cancel),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text(I18N.of(context).save),
|
||||||
|
onPressed: () {
|
||||||
|
if (_addProfileKey.currentState.validate()) {
|
||||||
|
_addProfileKey.currentState.save();
|
||||||
|
|
||||||
|
if (createNew) {
|
||||||
|
// TODO - create the new profile...
|
||||||
|
UserProfile profile = UserProfile(
|
||||||
|
name: _name,
|
||||||
|
server: _server,
|
||||||
|
username: _username,
|
||||||
|
password: _password
|
||||||
|
);
|
||||||
|
|
||||||
|
_addProfile(profile);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
profile.name = _name;
|
||||||
|
profile.server = _server;
|
||||||
|
profile.username = _username;
|
||||||
|
profile.password = _password;
|
||||||
|
|
||||||
|
_updateProfile(profile);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
fields: <Widget> [
|
||||||
|
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]://<server>:<port>",
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
String _validateServer(String value) {
|
||||||
|
|
||||||
@ -43,6 +147,8 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
return 'Server must start with http[s]';
|
return 'Server must start with http[s]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: URL validator
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +168,94 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _save(BuildContext context) async {
|
void _selectProfile(BuildContext context, UserProfile profile) async {
|
||||||
if (_formKey.currentState.validate()) {
|
|
||||||
_formKey.currentState.save();
|
|
||||||
|
|
||||||
await InvenTreePreferences().saveLoginDetails(context, _server, _username, _password);
|
// Disconnect InvenTree
|
||||||
|
InvenTreeAPI().disconnectFromServer();
|
||||||
|
|
||||||
|
await UserProfileDBManager().selectProfile(profile.key);
|
||||||
|
|
||||||
|
_reload();
|
||||||
|
|
||||||
|
// Attempt server login (this will load the newly selected profile
|
||||||
|
InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) {
|
||||||
|
_reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
_reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteProfile(UserProfile profile) async {
|
||||||
|
|
||||||
|
await UserProfileDBManager().deleteProfile(profile);
|
||||||
|
|
||||||
|
// Close the dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
_reload();
|
||||||
|
|
||||||
|
if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) {
|
||||||
|
InvenTreeAPI().disconnectFromServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateProfile(UserProfile profile) async {
|
||||||
|
|
||||||
|
await UserProfileDBManager().updateProfile(profile);
|
||||||
|
|
||||||
|
// Dismiss the dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
_reload();
|
||||||
|
|
||||||
|
if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) {
|
||||||
|
// Attempt server login (this will load the newly selected profile
|
||||||
|
|
||||||
|
InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) {
|
||||||
|
_reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addProfile(UserProfile profile) async {
|
||||||
|
|
||||||
|
await UserProfileDBManager().addProfile(profile);
|
||||||
|
|
||||||
|
// Dismiss the create dialog
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
_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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,64 +264,92 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
|
|
||||||
final Size screenSize = MediaQuery.of(context).size;
|
final Size screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return Scaffold(
|
List<Widget> children = [];
|
||||||
appBar: AppBar(
|
|
||||||
title: Text("Login Settings"),
|
if (profiles != null && profiles.length > 0) {
|
||||||
|
for (int idx = 0; idx < profiles.length; idx++) {
|
||||||
|
UserProfile profile = profiles[idx];
|
||||||
|
|
||||||
|
children.add(ListTile(
|
||||||
|
title: Text(
|
||||||
|
profile.name,
|
||||||
),
|
),
|
||||||
body: new Container(
|
tileColor: profile.selected ? Color.fromRGBO(0, 0, 0, 0.05) : null,
|
||||||
padding: new EdgeInsets.all(20.0),
|
subtitle: Text("${profile.server}"),
|
||||||
child: new Form(
|
trailing: _getProfileIcon(profile),
|
||||||
key: _formKey,
|
onTap: () {
|
||||||
child: new ListView(
|
_selectProfile(context, profile);
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(profile.name),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
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(),
|
Divider(),
|
||||||
Text(I18N.of(context).accountDetails),
|
SimpleDialogOption(
|
||||||
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: () {
|
onPressed: () {
|
||||||
_save(context);
|
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),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No profile available!
|
||||||
|
children.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text("No profiles available"),
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
key: _loginKey,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(I18N.of(context).profileSelect),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
child: ListView(
|
||||||
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: children
|
||||||
|
).toList(),
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
child: Icon(FontAwesomeIcons.plus),
|
||||||
|
onPressed: () {
|
||||||
|
_editProfile(context, createNew: true);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:InvenTree/settings/about.dart';
|
import 'package:InvenTree/settings/about.dart';
|
||||||
import 'package:InvenTree/settings/login.dart';
|
import 'package:InvenTree/settings/login.dart';
|
||||||
import 'package:InvenTree/settings/release.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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -28,18 +30,19 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("InvenTree Settings"),
|
title: Text(I18N.of(context).settings),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).serverSettings),
|
title: Text(I18N.of(context).profile),
|
||||||
subtitle: Text("Configure server and login settings"),
|
subtitle: Text("Configure user profile settings"),
|
||||||
leading: FaIcon(FontAwesomeIcons.server),
|
leading: FaIcon(FontAwesomeIcons.user),
|
||||||
onTap: _editServerSettings,
|
onTap: _editServerSettings,
|
||||||
),
|
),
|
||||||
Divider(),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(I18N.of(context).about),
|
title: Text(I18N.of(context).about),
|
||||||
subtitle: Text(I18N.of(context).appDetails),
|
subtitle: Text(I18N.of(context).appDetails),
|
||||||
@ -58,7 +61,8 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.bug),
|
leading: FaIcon(FontAwesomeIcons.bug),
|
||||||
onTap: null,
|
onTap: null,
|
||||||
),
|
),
|
||||||
],
|
]
|
||||||
|
).toList()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -68,7 +72,9 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||||||
|
|
||||||
var prefs = await SharedPreferences.getInstance();
|
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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _about() async {
|
void _about() async {
|
||||||
|
177
lib/user_profile.dart
Normal file
177
lib/user_profile.dart
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* Class for InvenTree user / login details
|
||||||
|
*/
|
||||||
|
import 'package:sembast/sembast.dart';
|
||||||
|
import 'preferences.dart';
|
||||||
|
|
||||||
|
class UserProfile {
|
||||||
|
|
||||||
|
UserProfile({
|
||||||
|
this.key,
|
||||||
|
this.name,
|
||||||
|
this.server,
|
||||||
|
this.username,
|
||||||
|
this.password,
|
||||||
|
this.selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ID of the profile
|
||||||
|
int key;
|
||||||
|
|
||||||
|
// Name of the user profile
|
||||||
|
String name;
|
||||||
|
|
||||||
|
// Base address of the InvenTree server
|
||||||
|
String server;
|
||||||
|
|
||||||
|
// Username
|
||||||
|
String username;
|
||||||
|
|
||||||
|
// Password
|
||||||
|
String password;
|
||||||
|
|
||||||
|
bool selected = false;
|
||||||
|
|
||||||
|
// User ID (will be provided by the server on log-in)
|
||||||
|
int user_id;
|
||||||
|
|
||||||
|
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
|
||||||
|
key: key,
|
||||||
|
name: json['name'],
|
||||||
|
server: json['server'],
|
||||||
|
username: json['username'],
|
||||||
|
password: json['password'],
|
||||||
|
selected: isSelected,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"name": name,
|
||||||
|
"server": server,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "<${key}> ${name} : ${server} - ${username}:${password}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserProfileDBManager {
|
||||||
|
|
||||||
|
final store = StoreRef("profiles");
|
||||||
|
|
||||||
|
Future<Database> get _db async => await InvenTreePreferencesDB.instance.database;
|
||||||
|
|
||||||
|
Future<bool> 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 {
|
||||||
|
|
||||||
|
// Check if a profile already exists with the name
|
||||||
|
final bool exists = await profileNameExists(profile.name);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
print("UserProfile '${profile.name}' already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
await addProfile(profile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await store.record(profile.key).update(await _db, profile.toJson());
|
||||||
|
|
||||||
|
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 store.record(profile.key).delete(await _db);
|
||||||
|
print("Deleted user profile <${profile.key}> - '${profile.name}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserProfile> getSelectedProfile() async {
|
||||||
|
/*
|
||||||
|
* Return the currently selected profile.
|
||||||
|
*
|
||||||
|
* key should match the "selected" property
|
||||||
|
*/
|
||||||
|
|
||||||
|
final selected = await store.record("selected").get(await _db);
|
||||||
|
|
||||||
|
final profiles = await store.find(await _db);
|
||||||
|
|
||||||
|
List<UserProfile> profileList = new List<UserProfile>();
|
||||||
|
|
||||||
|
for (int idx = 0; idx < profiles.length; idx++) {
|
||||||
|
|
||||||
|
if (profiles[idx].key is int && profiles[idx].key == selected) {
|
||||||
|
return UserProfile.fromJson(
|
||||||
|
profiles[idx].key,
|
||||||
|
profiles[idx].value,
|
||||||
|
profiles[idx].key == selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return all user profile objects
|
||||||
|
*/
|
||||||
|
Future<List<UserProfile>> getAllProfiles() async {
|
||||||
|
|
||||||
|
final selected = await store.record("selected").get(await _db);
|
||||||
|
|
||||||
|
final profiles = await store.find(await _db);
|
||||||
|
|
||||||
|
List<UserProfile> profileList = new List<UserProfile>();
|
||||||
|
|
||||||
|
for (int idx = 0; idx < profiles.length; idx++) {
|
||||||
|
|
||||||
|
if (profiles[idx].key is int) {
|
||||||
|
profileList.add(
|
||||||
|
UserProfile.fromJson(
|
||||||
|
profiles[idx].key,
|
||||||
|
profiles[idx].value,
|
||||||
|
profiles[idx].key == selected,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
}
|
@ -231,9 +231,10 @@ class SubcategoryList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: ClampingScrollPhysics(),
|
physics: ClampingScrollPhysics(),
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||||
itemBuilder: _build, itemCount: _categories.length);
|
itemBuilder: _build, itemCount: _categories.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,7 +268,7 @@ class PartList extends StatelessWidget {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text("${part.name}"),
|
title: Text("${part.name}"),
|
||||||
subtitle: Text("${part.description}"),
|
subtitle: Text("${part.description}"),
|
||||||
trailing: Text("${part.inStock}"),
|
trailing: Text("${part.inStockString}"),
|
||||||
leading: InvenTreeAPI().getImage(
|
leading: InvenTreeAPI().getImage(
|
||||||
part.thumbnail,
|
part.thumbnail,
|
||||||
width: 40,
|
width: 40,
|
||||||
@ -282,9 +283,10 @@ class PartList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: ClampingScrollPhysics(),
|
physics: ClampingScrollPhysics(),
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||||
itemBuilder: _build, itemCount: _parts.length);
|
itemBuilder: _build, itemCount: _parts.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,54 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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';
|
||||||
|
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) {
|
void showMessage(BuildContext context, String message) {
|
||||||
Scaffold.of(context).showSnackBar(SnackBar(
|
Scaffold.of(context).showSnackBar(SnackBar(
|
||||||
@ -45,7 +93,8 @@ Future<void> showErrorDialog(BuildContext context, String title, String descript
|
|||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
child: SimpleDialog(
|
builder: (dialogContext) {
|
||||||
|
return SimpleDialog(
|
||||||
title: ListTile(
|
title: ListTile(
|
||||||
title: Text(error),
|
title: Text(error),
|
||||||
leading: FaIcon(icon),
|
leading: FaIcon(icon),
|
||||||
@ -56,14 +105,43 @@ Future<void> showErrorDialog(BuildContext context, String title, String descript
|
|||||||
subtitle: Text(description)
|
subtitle: Text(description)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
);
|
||||||
).then((value) {
|
}).then((value) {
|
||||||
if (onDismissed != null) {
|
if (onDismissed != null) {
|
||||||
onDismissed();
|
onDismissed();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> showServerError(BuildContext context, String title, String description) async {
|
||||||
|
|
||||||
|
if (title == null || title.isEmpty) {
|
||||||
|
title = I18N.of(context).serverError;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showErrorDialog(
|
||||||
|
context,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
error: I18N.of(context).serverError,
|
||||||
|
icon: FontAwesomeIcons.server
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showStatusCodeError(BuildContext context, int status, {int expected = 200}) async {
|
||||||
|
|
||||||
|
await showServerError(
|
||||||
|
context,
|
||||||
|
"Invalid Response Code",
|
||||||
|
"Server responded with status code ${status}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showTimeoutError(BuildContext context) async {
|
||||||
|
|
||||||
|
await showServerError(context, I18N.of(context).timeout, I18N.of(context).noResponse);
|
||||||
|
}
|
||||||
|
|
||||||
void showProgressDialog(BuildContext context, String title, String description) {
|
void showProgressDialog(BuildContext context, String title, String description) {
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:InvenTree/barcode.dart';
|
|||||||
import 'package:InvenTree/widget/company_list.dart';
|
import 'package:InvenTree/widget/company_list.dart';
|
||||||
import 'package:InvenTree/widget/search.dart';
|
import 'package:InvenTree/widget/search.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
|
|
||||||
@ -101,17 +102,18 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: new ListView(
|
child: new ListView(
|
||||||
children: <Widget>[
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: <Widget>[
|
||||||
new ListTile(
|
new ListTile(
|
||||||
leading: new Image.asset(
|
leading: new Image.asset(
|
||||||
"assets/image/icon.png",
|
"assets/image/icon.png",
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
title: new Text("InvenTree"),
|
title: new Text(I18N.of(context).appTitle),
|
||||||
onTap: _home,
|
onTap: _home,
|
||||||
),
|
),
|
||||||
new Divider(),
|
|
||||||
/*
|
/*
|
||||||
// TODO - Add search functionality!
|
// TODO - Add search functionality!
|
||||||
new ListTile(
|
new ListTile(
|
||||||
@ -121,18 +123,17 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
*/
|
*/
|
||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Scan Barcode"),
|
title: new Text(I18N.of(context).scanBarcode),
|
||||||
onTap: _scan,
|
onTap: _scan,
|
||||||
leading: new FaIcon(FontAwesomeIcons.barcode),
|
leading: new FaIcon(FontAwesomeIcons.barcode),
|
||||||
),
|
),
|
||||||
new Divider(),
|
|
||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Parts"),
|
title: new Text(I18N.of(context).parts),
|
||||||
leading: new Icon(Icons.category),
|
leading: new Icon(Icons.category),
|
||||||
onTap: _showParts,
|
onTap: _showParts,
|
||||||
),
|
),
|
||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Stock"),
|
title: new Text(I18N.of(context).stock),
|
||||||
leading: new FaIcon(FontAwesomeIcons.boxes),
|
leading: new FaIcon(FontAwesomeIcons.boxes),
|
||||||
onTap: _showStock,
|
onTap: _showStock,
|
||||||
),
|
),
|
||||||
@ -153,13 +154,13 @@ class InvenTreeDrawer extends StatelessWidget {
|
|||||||
onTap: _showCustomers,
|
onTap: _showCustomers,
|
||||||
),
|
),
|
||||||
*/
|
*/
|
||||||
new Divider(),
|
|
||||||
new ListTile(
|
new ListTile(
|
||||||
title: new Text("Settings"),
|
title: new Text(I18N.of(context).settings),
|
||||||
leading: new Icon(Icons.settings),
|
leading: new Icon(Icons.settings),
|
||||||
onTap: _settings,
|
onTap: _settings,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
).toList(),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
|
import 'package:InvenTree/user_profile.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/barcode.dart';
|
||||||
import 'package:InvenTree/api.dart';
|
import 'package:InvenTree/api.dart';
|
||||||
|
|
||||||
|
import 'package:InvenTree/settings/login.dart';
|
||||||
|
|
||||||
import 'package:InvenTree/widget/category_display.dart';
|
import 'package:InvenTree/widget/category_display.dart';
|
||||||
import 'package:InvenTree/widget/company_list.dart';
|
import 'package:InvenTree/widget/company_list.dart';
|
||||||
import 'package:InvenTree/widget/location_display.dart';
|
import 'package:InvenTree/widget/location_display.dart';
|
||||||
import 'package:InvenTree/widget/search.dart';
|
import 'package:InvenTree/widget/search.dart';
|
||||||
|
import 'package:InvenTree/widget/spinner.dart';
|
||||||
import 'package:InvenTree/widget/drawer.dart';
|
import 'package:InvenTree/widget/drawer.dart';
|
||||||
|
|
||||||
class InvenTreeHomePage extends StatefulWidget {
|
class InvenTreeHomePage extends StatefulWidget {
|
||||||
@ -24,70 +27,18 @@ class InvenTreeHomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||||
|
|
||||||
|
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
|
||||||
|
|
||||||
_InvenTreeHomePageState() : super() {
|
_InvenTreeHomePageState() : super() {
|
||||||
|
|
||||||
|
// Initially load the profile and attempt server connection
|
||||||
|
_loadProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _serverAddress = "";
|
// Selected user profile
|
||||||
|
UserProfile _profile;
|
||||||
|
|
||||||
String _serverStatus = "Connecting to server";
|
BuildContext _context;
|
||||||
|
|
||||||
String _serverMessage = "";
|
|
||||||
|
|
||||||
bool _serverConnection = false;
|
|
||||||
|
|
||||||
FaIcon _serverIcon = new FaIcon(FontAwesomeIcons.spinner);
|
|
||||||
|
|
||||||
Color _serverStatusColor = Color.fromARGB(255, 50, 50, 250);
|
|
||||||
|
|
||||||
void onConnectSuccess(String msg) {
|
|
||||||
_serverConnection = true;
|
|
||||||
_serverMessage = msg;
|
|
||||||
_serverStatus = "Connected to $_serverAddress";
|
|
||||||
_serverStatusColor = Color.fromARGB(255, 50, 250, 50);
|
|
||||||
_serverIcon = new FaIcon(FontAwesomeIcons.checkCircle, color: _serverStatusColor);
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onConnectFailure(String msg) {
|
|
||||||
_serverConnection = false;
|
|
||||||
_serverMessage = msg;
|
|
||||||
_serverStatus = "Could not connect to $_serverAddress";
|
|
||||||
_serverStatusColor = Color.fromARGB(255, 250, 50, 50);
|
|
||||||
_serverIcon = new FaIcon(FontAwesomeIcons.timesCircle, color: _serverStatusColor);
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Test the server connection
|
|
||||||
*/
|
|
||||||
void _checkServerConnection(BuildContext context) async {
|
|
||||||
|
|
||||||
var prefs = await SharedPreferences.getInstance();
|
|
||||||
|
|
||||||
_serverAddress = prefs.getString("server");
|
|
||||||
|
|
||||||
// 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(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _search() {
|
void _search() {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
@ -96,19 +47,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scan() {
|
void _scan(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
scanQrCode(context);
|
scanQrCode(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parts() {
|
void _parts(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stock() {
|
void _stock(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
|
||||||
@ -132,6 +83,15 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CustomerListWidget()));
|
Navigator.push(context, MaterialPageRoute(builder: (context) => CustomerListWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectProfile() {
|
||||||
|
Navigator.push(
|
||||||
|
context, MaterialPageRoute(builder: (context) => InvenTreeLoginSettingsWidget())
|
||||||
|
).then((context) {
|
||||||
|
// Once we return
|
||||||
|
_loadProfile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _unsupported() {
|
void _unsupported() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -147,8 +107,92 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _loadProfile() async {
|
||||||
|
|
||||||
|
_profile = await UserProfileDBManager().getSelectedProfile();
|
||||||
|
|
||||||
|
// A valid profile was loaded!
|
||||||
|
if (_profile != null) {
|
||||||
|
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
|
||||||
|
|
||||||
|
// Attempt server connection
|
||||||
|
InvenTreeAPI().connectToServer(_homeKey.currentContext).then((result) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Spinner(
|
||||||
|
icon: FontAwesomeIcons.spinner,
|
||||||
|
color: Color.fromRGBO(50, 50, 250, 1),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectProfile();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} 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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
_context = context;
|
||||||
|
|
||||||
// This method is rerun every time setState is called, for instance as done
|
// This method is rerun every time setState is called, for instance as done
|
||||||
// by the _incrementCounter method above.
|
// by the _incrementCounter method above.
|
||||||
//
|
//
|
||||||
@ -156,6 +200,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
// fast, so that you can just rebuild anything that needs updating rather
|
// fast, so that you can just rebuild anything that needs updating rather
|
||||||
// than having to individually change instances of widgets.
|
// than having to individually change instances of widgets.
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
key: _homeKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(I18N.of(context).appTitle),
|
title: Text(I18N.of(context).appTitle),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@ -197,7 +242,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: new FaIcon(FontAwesomeIcons.barcode),
|
icon: new FaIcon(FontAwesomeIcons.barcode),
|
||||||
tooltip: I18N.of(context).scanBarcode,
|
tooltip: I18N.of(context).scanBarcode,
|
||||||
onPressed: _scan,
|
onPressed: () { _scan(context); },
|
||||||
),
|
),
|
||||||
Text(I18N.of(context).scanBarcode),
|
Text(I18N.of(context).scanBarcode),
|
||||||
],
|
],
|
||||||
@ -213,7 +258,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: new FaIcon(FontAwesomeIcons.shapes),
|
icon: new FaIcon(FontAwesomeIcons.shapes),
|
||||||
tooltip: I18N.of(context).parts,
|
tooltip: I18N.of(context).parts,
|
||||||
onPressed: _parts,
|
onPressed: () { _parts(context); },
|
||||||
),
|
),
|
||||||
Text(I18N.of(context).parts),
|
Text(I18N.of(context).parts),
|
||||||
],
|
],
|
||||||
@ -223,7 +268,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: new FaIcon(FontAwesomeIcons.boxes),
|
icon: new FaIcon(FontAwesomeIcons.boxes),
|
||||||
tooltip: I18N.of(context).stock,
|
tooltip: I18N.of(context).stock,
|
||||||
onPressed: _stock,
|
onPressed: () { _stock(context); },
|
||||||
),
|
),
|
||||||
Text(I18N.of(context).stock),
|
Text(I18N.of(context).stock),
|
||||||
],
|
],
|
||||||
@ -306,25 +351,13 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
|||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
*/
|
*/
|
||||||
|
Spacer(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListTile(
|
child: _serverTile(),
|
||||||
title: Text("$_serverStatus",
|
|
||||||
style: TextStyle(color: _serverStatusColor),
|
|
||||||
),
|
|
||||||
subtitle: Text("$_serverMessage",
|
|
||||||
style: TextStyle(color: _serverStatusColor),
|
|
||||||
),
|
|
||||||
leading: _serverIcon,
|
|
||||||
onTap: () {
|
|
||||||
if (!_serverConnection) {
|
|
||||||
_checkServerConnection(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -6,7 +6,7 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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';
|
import 'package:InvenTree/widget/refreshable_state.dart';
|
||||||
|
|
||||||
class LocationDisplayWidget extends StatefulWidget {
|
class LocationDisplayWidget extends StatefulWidget {
|
||||||
@ -97,8 +97,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
if (location == null) {
|
if (location == null) {
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text("Stock Locations"),
|
title: Text(I18N.of(context).stockLocations),
|
||||||
subtitle: Text("Top level stock location")
|
subtitle: Text(I18N.of(context).stockTopLevel),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -135,14 +135,14 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
return BottomNavigationBar(
|
return BottomNavigationBar(
|
||||||
currentIndex: tabIndex,
|
currentIndex: tabIndex,
|
||||||
onTap: onTabSelectionChanged,
|
onTap: onTabSelectionChanged,
|
||||||
items: const <BottomNavigationBarItem> [
|
items: <BottomNavigationBarItem> [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.boxes),
|
icon: FaIcon(FontAwesomeIcons.boxes),
|
||||||
title: Text("Stock"),
|
title: Text(I18N.of(context).stock),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||||
title: Text("Actions"),
|
title: Text(I18N.of(context).actions),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -156,7 +156,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
|||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return ListView(
|
return ListView(
|
||||||
children: actionTiles(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: actionTiles()
|
||||||
|
).toList()
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@ -298,10 +301,11 @@ class SublocationList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: ClampingScrollPhysics(),
|
physics: ClampingScrollPhysics(),
|
||||||
itemBuilder: _build,
|
itemBuilder: _build,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||||
itemCount: _locations.length
|
itemCount: _locations.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -342,9 +346,10 @@ class StockList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: ClampingScrollPhysics(),
|
physics: ClampingScrollPhysics(),
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||||
itemBuilder: _build, itemCount: _items.length);
|
itemBuilder: _build, itemCount: _items.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:url_launcher/url_launcher.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/inventree/part.dart';
|
||||||
import 'package:InvenTree/widget/full_screen_image.dart';
|
import 'package:InvenTree/widget/full_screen_image.dart';
|
||||||
@ -82,6 +83,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
|
|
||||||
void _showStock(BuildContext context) async {
|
void _showStock(BuildContext context) async {
|
||||||
await part.getStockItems(context);
|
await part.getStockItems(context);
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => PartStockDetailWidget(part))
|
MaterialPageRoute(builder: (context) => PartStockDetailWidget(part))
|
||||||
@ -213,9 +215,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
// Stock information
|
// Stock information
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Stock"),
|
title: Text(I18N.of(context).stock),
|
||||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
leading: FaIcon(FontAwesomeIcons.boxes),
|
||||||
trailing: Text("${part.inStock}"),
|
trailing: Text("${part.inStockString}"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showStock(context);
|
_showStock(context);
|
||||||
},
|
},
|
||||||
@ -229,7 +231,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
title: Text("On Order"),
|
title: Text("On Order"),
|
||||||
leading: FaIcon(FontAwesomeIcons.shoppingCart),
|
leading: FaIcon(FontAwesomeIcons.shoppingCart),
|
||||||
trailing: Text("${part.onOrder}"),
|
trailing: Text("${part.onOrder}"),
|
||||||
onTap: null,
|
onTap: () {
|
||||||
|
// TODO: Click through to show items on order
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -325,7 +329,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Create Stock Item"),
|
title: Text(I18N.of(context).stockItemCreate),
|
||||||
leading: FaIcon(FontAwesomeIcons.box),
|
leading: FaIcon(FontAwesomeIcons.box),
|
||||||
onTap: null,
|
onTap: null,
|
||||||
)
|
)
|
||||||
@ -349,13 +353,19 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
case 0:
|
case 0:
|
||||||
return Center(
|
return Center(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: partTiles(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: partTiles()
|
||||||
|
).toList()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return Center(
|
return Center(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: actionTiles(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: actionTiles()
|
||||||
|
).toList()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@ -368,14 +378,14 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
return BottomNavigationBar(
|
return BottomNavigationBar(
|
||||||
currentIndex: tabIndex,
|
currentIndex: tabIndex,
|
||||||
onTap: onTabSelectionChanged,
|
onTap: onTabSelectionChanged,
|
||||||
items: const <BottomNavigationBarItem> [
|
items: <BottomNavigationBarItem> [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.infoCircle),
|
icon: FaIcon(FontAwesomeIcons.infoCircle),
|
||||||
title: Text("Details"),
|
title: Text(I18N.of(context).details),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||||
title: Text("Actions"),
|
title: Text(I18N.of(context).actions),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -38,7 +38,6 @@ class _PartStockDisplayState extends RefreshableState<PartStockDetailWidget> {
|
|||||||
@override
|
@override
|
||||||
Future<void> onBuild(BuildContext context) async {
|
Future<void> onBuild(BuildContext context) async {
|
||||||
refresh();
|
refresh();
|
||||||
print("onBuild");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -100,10 +99,11 @@ class PartStockList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: ClampingScrollPhysics(),
|
physics: ClampingScrollPhysics(),
|
||||||
itemBuilder: _build,
|
itemBuilder: _build,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 3),
|
||||||
itemCount: _items.length
|
itemCount: _items.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
54
lib/widget/spinner.dart
Normal file
54
lib/widget/spinner.dart
Normal file
@ -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<Spinner> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -303,6 +303,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
title: Text("${item.partName}"),
|
title: Text("${item.partName}"),
|
||||||
subtitle: Text("${item.partDescription}"),
|
subtitle: Text("${item.partDescription}"),
|
||||||
leading: InvenTreeAPI().getImage(item.partImage),
|
leading: InvenTreeAPI().getImage(item.partImage),
|
||||||
|
trailing: Text(item.serialOrQuantityDisplay()),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -365,7 +366,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
if (item.isSerialized()) {
|
if (item.isSerialized()) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Serial Number"),
|
title: Text(I18N.of(context).serialNumber),
|
||||||
leading: FaIcon(FontAwesomeIcons.hashtag),
|
leading: FaIcon(FontAwesomeIcons.hashtag),
|
||||||
trailing: Text("${item.serialNumber}"),
|
trailing: Text("${item.serialNumber}"),
|
||||||
)
|
)
|
||||||
@ -381,7 +382,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Supplier part?
|
// Supplier part?
|
||||||
if (item.supplierPartId > 0) {
|
// TODO: Display supplier part info page?
|
||||||
|
if (false && item.supplierPartId > 0) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${item.supplierName}"),
|
title: Text("${item.supplierName}"),
|
||||||
@ -410,7 +412,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
leading: FaIcon(FontAwesomeIcons.tasks),
|
leading: FaIcon(FontAwesomeIcons.tasks),
|
||||||
trailing: Text("${item.testResultCount}"),
|
trailing: Text("${item.testResultCount}"),
|
||||||
onTap: () {
|
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<StockDetailWidget> {
|
|||||||
title: Text(I18N.of(context).history),
|
title: Text(I18N.of(context).history),
|
||||||
leading: FaIcon(FontAwesomeIcons.history),
|
leading: FaIcon(FontAwesomeIcons.history),
|
||||||
trailing: Text("${item.trackingItemCount}"),
|
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<StockDetailWidget> {
|
|||||||
title: Text(I18N.of(context).notes),
|
title: Text(I18N.of(context).notes),
|
||||||
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
||||||
trailing: Text(""),
|
trailing: Text(""),
|
||||||
onTap: null,
|
onTap: () {
|
||||||
|
// TODO: Load notes in markdown viewer widget
|
||||||
|
// TODO: Make this widget editable?
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -579,11 +594,17 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
return ListView(
|
return ListView(
|
||||||
children: detailTiles(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: detailTiles()
|
||||||
|
).toList(),
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return ListView(
|
return ListView(
|
||||||
children: actionTiles(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: actionTiles()
|
||||||
|
).toList()
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
@ -250,7 +250,10 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
@override
|
@override
|
||||||
Widget getBody(BuildContext context) {
|
Widget getBody(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: resultsList(),
|
children: ListTile.divideTiles(
|
||||||
|
context: context,
|
||||||
|
tiles: resultsList()
|
||||||
|
).toList()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
64
pubspec.lock
64
pubspec.lock
@ -42,7 +42,7 @@ packages:
|
|||||||
name: camera
|
name: camera
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.4+5"
|
version: "0.7.0+2"
|
||||||
camera_platform_interface:
|
camera_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -106,6 +106,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -152,7 +166,7 @@ packages:
|
|||||||
name: flutter_keyboard_visibility
|
name: flutter_keyboard_visibility
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.3"
|
||||||
flutter_keyboard_visibility_platform_interface:
|
flutter_keyboard_visibility_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -251,14 +265,14 @@ packages:
|
|||||||
name: image_picker
|
name: image_picker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.7+21"
|
version: "0.6.7+22"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.6"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -300,7 +314,7 @@ packages:
|
|||||||
name: package_info
|
name: package_info
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3+2"
|
version: "0.4.3+4"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -371,13 +385,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
preferences:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: preferences
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.2.1"
|
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -392,6 +399,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.14"
|
version: "0.0.14"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -399,13 +413,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.25.0"
|
version: "0.25.0"
|
||||||
|
sembast:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sembast
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.9"
|
||||||
sentry:
|
sentry:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sentry
|
name: sentry
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "4.0.4"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -447,7 +468,7 @@ packages:
|
|||||||
name: shared_preferences_windows
|
name: shared_preferences_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1+3"
|
version: "0.0.2+3"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -466,14 +487,14 @@ packages:
|
|||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2+2"
|
version: "1.3.2+3"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.3+1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -564,7 +585,7 @@ packages:
|
|||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.5+1"
|
version: "0.1.5+3"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -572,13 +593,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1+3"
|
version: "0.0.1+3"
|
||||||
usage:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: usage
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.4.2"
|
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -599,7 +613,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.4"
|
version: "1.7.4+1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -11,7 +11,7 @@ description: InvenTree stock management
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.1.0+1
|
version: 0.1.0+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.1.0 <3.0.0"
|
sdk: ">=2.1.0 <3.0.0"
|
||||||
@ -30,18 +30,19 @@ dependencies:
|
|||||||
shared_preferences: ^0.5.7
|
shared_preferences: ^0.5.7
|
||||||
|
|
||||||
cached_network_image: ^2.5.0
|
cached_network_image: ^2.5.0
|
||||||
preferences: ^5.2.0 # Persistent settings storage
|
|
||||||
qr_code_scanner: ^0.0.13
|
qr_code_scanner: ^0.0.13
|
||||||
package_info: ^0.4.0 # App information introspection
|
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
|
font_awesome_flutter: ^8.8.1 # FontAwesome icon set
|
||||||
flutter_speed_dial: ^1.2.5 # FAB menu elements
|
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
|
flutter_typeahead: ^1.8.1 # Auto-complete input field
|
||||||
image_picker: ^0.6.6 # Select or take photos
|
image_picker: ^0.6.6 # Select or take photos
|
||||||
url_launcher: ^5.7.10 # Open link in system browser
|
url_launcher: ^5.7.10 # Open link in system browser
|
||||||
flutter_markdown: ^0.5.2 # Rendering markdown
|
flutter_markdown: ^0.5.2 # Rendering markdown
|
||||||
camera:
|
camera:
|
||||||
path_provider:
|
path_provider: ^1.6.27 # Local file storage
|
||||||
|
sembast: ^2.4.9 # NoSQL data storage
|
||||||
path:
|
path:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user