mirror of
https://github.com/inventree/inventree-app.git
synced 2025-05-16 05:53:10 +00:00
Merge branch 'master' of github.com:SchrodingersGat/inventreeapp
This commit is contained in:
commit
9f811f1dbf
@ -1,6 +1,14 @@
|
|||||||
## InvenTree App Release Notes
|
## InvenTree App Release Notes
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 0.4.6 - August 2021
|
||||||
|
---
|
||||||
|
|
||||||
|
- Improved profile selection screen
|
||||||
|
- Fixed a number of incorrect labels
|
||||||
|
- Refactor test result upload functionality
|
||||||
|
- Refactor file selection and upload functions
|
||||||
|
|
||||||
### 0.4.5 - August 2021
|
### 0.4.5 - August 2021
|
||||||
---
|
---
|
||||||
|
|
||||||
|
93
lib/api.dart
93
lib/api.dart
@ -42,6 +42,14 @@ class APIResponse {
|
|||||||
|
|
||||||
// Request is "valid" if a statusCode was returned
|
// Request is "valid" if a statusCode was returned
|
||||||
bool isValid() => (statusCode >= 0) && (statusCode < 500);
|
bool isValid() => (statusCode >= 0) && (statusCode < 500);
|
||||||
|
|
||||||
|
bool successful() => (statusCode >= 200) && (statusCode < 300);
|
||||||
|
|
||||||
|
bool redirected() => (statusCode >= 300) && (statusCode < 400);
|
||||||
|
|
||||||
|
bool clientError() => (statusCode >= 400) && (statusCode < 500);
|
||||||
|
|
||||||
|
bool serverError() => (statusCode >= 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -254,12 +262,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
response = await get("", expectedStatusCode: 200);
|
response = await get("", expectedStatusCode: 200);
|
||||||
|
|
||||||
// Response was invalid for some reason
|
if (!response.successful()) {
|
||||||
if (!response.isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
showStatusCodeError(response.statusCode);
|
showStatusCodeError(response.statusCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -308,16 +311,10 @@ class InvenTreeAPI {
|
|||||||
// Clear the existing token value
|
// Clear the existing token value
|
||||||
_token = "";
|
_token = "";
|
||||||
|
|
||||||
print("Requesting token from server");
|
|
||||||
|
|
||||||
response = await get(_URL_GET_TOKEN);
|
response = await get(_URL_GET_TOKEN);
|
||||||
|
|
||||||
// Invalid response
|
// Invalid response
|
||||||
if (!response.isValid()) {
|
if (!response.successful()) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
|
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 401:
|
case 401:
|
||||||
@ -413,7 +410,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
||||||
|
|
||||||
if (!response.isValid() || response.statusCode != 200) {
|
if (!response.successful()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,9 +560,8 @@ class InvenTreeAPI {
|
|||||||
/*
|
/*
|
||||||
* Upload a file to the given URL
|
* Upload a file to the given URL
|
||||||
*/
|
*/
|
||||||
Future<http.StreamedResponse> uploadFile(String url, File f,
|
Future<APIResponse> uploadFile(String url, File f,
|
||||||
{String name = "attachment", String method="POST", Map<String, String>? fields}) async {
|
{String name = "attachment", String method="POST", Map<String, String>? fields}) async {
|
||||||
|
|
||||||
var _url = makeApiUrl(url);
|
var _url = makeApiUrl(url);
|
||||||
|
|
||||||
var request = http.MultipartRequest(method, Uri.parse(_url));
|
var request = http.MultipartRequest(method, Uri.parse(_url));
|
||||||
@ -582,25 +578,63 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
request.files.add(_file);
|
request.files.add(_file);
|
||||||
|
|
||||||
var response = await request.send();
|
APIResponse response = APIResponse(
|
||||||
|
url: url,
|
||||||
|
method: method,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode >= 500) {
|
String jsondata = "";
|
||||||
// Server error
|
|
||||||
|
try {
|
||||||
|
var httpResponse = await request.send().timeout(Duration(seconds: 120));
|
||||||
|
|
||||||
|
response.statusCode = httpResponse.statusCode;
|
||||||
|
|
||||||
|
jsondata = await httpResponse.stream.bytesToString();
|
||||||
|
|
||||||
|
response.data = json.decode(jsondata);
|
||||||
|
|
||||||
|
// Report a server-side error
|
||||||
if (response.statusCode >= 500) {
|
if (response.statusCode >= 500) {
|
||||||
|
|
||||||
var data = await response.stream.bytesToString();
|
|
||||||
|
|
||||||
sentryReportMessage(
|
sentryReportMessage(
|
||||||
"Server error on file upload",
|
"Server error in uploadFile()",
|
||||||
context: {
|
context: {
|
||||||
"url": _url,
|
"url": url,
|
||||||
"statusCode": "${response.statusCode}",
|
"method": request.method,
|
||||||
"response": response.toString(),
|
"name": name,
|
||||||
"request": request.fields.toString(),
|
"statusCode": response.statusCode.toString(),
|
||||||
"data": data,
|
"requestHeaders": request.headers.toString(),
|
||||||
|
"responseHeaders": httpResponse.headers.toString(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} on SocketException catch (error) {
|
||||||
|
showServerError(L10().connectionRefused, error.toString());
|
||||||
|
response.error = "SocketException";
|
||||||
|
response.errorDetail = error.toString();
|
||||||
|
} on FormatException {
|
||||||
|
showServerError(
|
||||||
|
L10().formatException,
|
||||||
|
L10().formatExceptionJson + ":\n${jsondata}"
|
||||||
|
);
|
||||||
|
|
||||||
|
sentryReportMessage(
|
||||||
|
"Error decoding JSON response from server",
|
||||||
|
context: {
|
||||||
|
"url": url,
|
||||||
|
"statusCode": response.statusCode.toString(),
|
||||||
|
"data": jsondata,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} on TimeoutException {
|
||||||
|
showTimeoutError();
|
||||||
|
response.error = "TimeoutException";
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
showServerError(L10().serverError, error.toString());
|
||||||
|
sentryReportError(error, stackTrace);
|
||||||
|
response.error = "UnknownError";
|
||||||
|
response.errorDetail = error.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -827,9 +861,6 @@ class InvenTreeAPI {
|
|||||||
return data ?? {};
|
return data ?? {};
|
||||||
} on FormatException {
|
} on FormatException {
|
||||||
|
|
||||||
print("JSON format exception!");
|
|
||||||
print("${body}");
|
|
||||||
|
|
||||||
sentryReportMessage(
|
sentryReportMessage(
|
||||||
"Error decoding JSON response from server",
|
"Error decoding JSON response from server",
|
||||||
context: {
|
context: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:dropdown_search/dropdown_search.dart';
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
@ -6,6 +7,7 @@ import 'package:dropdown_search/dropdown_search.dart';
|
|||||||
import 'package:inventree/api.dart';
|
import 'package:inventree/api.dart';
|
||||||
import 'package:inventree/app_colors.dart';
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
|
import 'package:inventree/inventree/sentry.dart';
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import 'package:inventree/inventree/stock.dart';
|
||||||
import 'package:inventree/widget/dialogs.dart';
|
import 'package:inventree/widget/dialogs.dart';
|
||||||
import 'package:inventree/widget/fields.dart';
|
import 'package:inventree/widget/fields.dart';
|
||||||
@ -26,6 +28,9 @@ class APIFormField {
|
|||||||
// Constructor
|
// Constructor
|
||||||
APIFormField(this.name, this.data);
|
APIFormField(this.name, this.data);
|
||||||
|
|
||||||
|
// File to be uploaded for this filed
|
||||||
|
File? attachedfile;
|
||||||
|
|
||||||
// Name of this field
|
// Name of this field
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
@ -139,7 +144,7 @@ class APIFormField {
|
|||||||
params: filters,
|
params: filters,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.isValid()) {
|
if (response.successful()) {
|
||||||
initial_data = response.data;
|
initial_data = response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,6 +164,9 @@ class APIFormField {
|
|||||||
return _constructFloatField();
|
return _constructFloatField();
|
||||||
case "choice":
|
case "choice":
|
||||||
return _constructChoiceField();
|
return _constructChoiceField();
|
||||||
|
case "file upload":
|
||||||
|
case "image upload":
|
||||||
|
return _constructFileField();
|
||||||
default:
|
default:
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -171,6 +179,44 @@ class APIFormField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field for selecting and uploading files
|
||||||
|
Widget _constructFileField() {
|
||||||
|
|
||||||
|
TextEditingController controller = new TextEditingController();
|
||||||
|
|
||||||
|
controller.text = (attachedfile?.path ?? L10().attachmentSelect).split("/").last;
|
||||||
|
|
||||||
|
return InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: TextField(
|
||||||
|
readOnly: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||||
|
onPressed: () async {
|
||||||
|
FilePickerDialog.pickFile(
|
||||||
|
message: L10().attachmentSelect,
|
||||||
|
onPicked: (file) {
|
||||||
|
print("${file.path}");
|
||||||
|
// Display the filename
|
||||||
|
controller.text = file.path.split("/").last;
|
||||||
|
|
||||||
|
// Save the file
|
||||||
|
attachedfile = file;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field for selecting from multiple choice options
|
||||||
Widget _constructChoiceField() {
|
Widget _constructChoiceField() {
|
||||||
|
|
||||||
dynamic _initial;
|
dynamic _initial;
|
||||||
@ -478,7 +524,7 @@ Map<String, dynamic> extractFields(APIResponse response) {
|
|||||||
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
|
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function(Map<String, dynamic>)? onSuccess, Function? onCancel}) async {
|
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {String fileField = "", Map<String, dynamic> modelData = const {}, String method = "PATCH", Function(Map<String, dynamic>)? onSuccess, Function? onCancel}) async {
|
||||||
|
|
||||||
var options = await InvenTreeAPI().options(url);
|
var options = await InvenTreeAPI().options(url);
|
||||||
|
|
||||||
@ -508,6 +554,14 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
|||||||
// Check that the field is actually available at the API endpoint
|
// Check that the field is actually available at the API endpoint
|
||||||
if (!availableFields.containsKey(fieldName)) {
|
if (!availableFields.containsKey(fieldName)) {
|
||||||
print("Field '${fieldName}' not available at '${url}'");
|
print("Field '${fieldName}' not available at '${url}'");
|
||||||
|
|
||||||
|
sentryReportMessage(
|
||||||
|
"API form called with unknown field '${fieldName}'",
|
||||||
|
context: {
|
||||||
|
"url": url.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,6 +617,7 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
|||||||
formFields,
|
formFields,
|
||||||
method,
|
method,
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
|
fileField: fileField,
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -579,6 +634,8 @@ class APIFormWidget extends StatefulWidget {
|
|||||||
//! API method
|
//! API method
|
||||||
final String method;
|
final String method;
|
||||||
|
|
||||||
|
final String fileField;
|
||||||
|
|
||||||
final List<APIFormField> fields;
|
final List<APIFormField> fields;
|
||||||
|
|
||||||
Function(Map<String, dynamic>)? onSuccess;
|
Function(Map<String, dynamic>)? onSuccess;
|
||||||
@ -591,11 +648,12 @@ class APIFormWidget extends StatefulWidget {
|
|||||||
{
|
{
|
||||||
Key? key,
|
Key? key,
|
||||||
this.onSuccess,
|
this.onSuccess,
|
||||||
|
this.fileField = "",
|
||||||
}
|
}
|
||||||
) : super(key: key);
|
) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess);
|
_APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess, fileField);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,11 +668,13 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
|
|
||||||
String method;
|
String method;
|
||||||
|
|
||||||
|
String fileField;
|
||||||
|
|
||||||
List<APIFormField> fields;
|
List<APIFormField> fields;
|
||||||
|
|
||||||
Function(Map<String, dynamic>)? onSuccess;
|
Function(Map<String, dynamic>)? onSuccess;
|
||||||
|
|
||||||
_APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess) : super();
|
_APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess, this.fileField) : super();
|
||||||
|
|
||||||
bool spacerRequired = false;
|
bool spacerRequired = false;
|
||||||
|
|
||||||
@ -678,6 +738,32 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
|
|
||||||
Future<APIResponse> _submit(Map<String, String> data) async {
|
Future<APIResponse> _submit(Map<String, String> data) async {
|
||||||
|
|
||||||
|
|
||||||
|
// If a file upload is required, we have to handle the submission differently
|
||||||
|
if (fileField.isNotEmpty) {
|
||||||
|
|
||||||
|
// Pop the "file" field
|
||||||
|
data.remove(fileField);
|
||||||
|
|
||||||
|
for (var field in fields) {
|
||||||
|
if (field.name == fileField) {
|
||||||
|
|
||||||
|
File? file = field.attachedfile;
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
|
||||||
|
// A valid file has been supplied
|
||||||
|
return await InvenTreeAPI().uploadFile(
|
||||||
|
url,
|
||||||
|
file,
|
||||||
|
name: fileField,
|
||||||
|
fields: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (method == "POST") {
|
if (method == "POST") {
|
||||||
return await InvenTreeAPI().post(
|
return await InvenTreeAPI().post(
|
||||||
url,
|
url,
|
||||||
|
@ -73,7 +73,7 @@ class InvenTreeModel {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createForm(BuildContext context, String title, {Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async {
|
Future<void> createForm(BuildContext context, String title, {String fileField = "", Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async {
|
||||||
|
|
||||||
if (fields.isEmpty) {
|
if (fields.isEmpty) {
|
||||||
fields = formFields();
|
fields = formFields();
|
||||||
@ -87,6 +87,7 @@ class InvenTreeModel {
|
|||||||
modelData: data,
|
modelData: data,
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
fileField: fileField,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -511,7 +512,7 @@ class InvenTreeAttachment extends InvenTreeModel {
|
|||||||
|
|
||||||
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
|
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
|
||||||
|
|
||||||
final http.StreamedResponse response = await InvenTreeAPI().uploadFile(
|
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||||
URL,
|
URL,
|
||||||
attachment,
|
attachment,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -519,11 +520,7 @@ class InvenTreeAttachment extends InvenTreeModel {
|
|||||||
fields: fields
|
fields: fields
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
return response.successful();
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadAttachment() async {
|
Future<void> downloadAttachment() async {
|
||||||
|
@ -356,19 +356,14 @@ class InvenTreePart extends InvenTreeModel {
|
|||||||
|
|
||||||
Future<bool> uploadImage(File image) async {
|
Future<bool> uploadImage(File image) async {
|
||||||
// Upload file against this part
|
// Upload file against this part
|
||||||
final http.StreamedResponse response = await InvenTreeAPI().uploadFile(
|
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||||
url,
|
url,
|
||||||
image,
|
image,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
name: 'image',
|
name: 'image',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
return response.successful();
|
||||||
print("uploadImage returned ${response.statusCode} at '${url}'");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the "starred" status of this part
|
// Return the "starred" status of this part
|
||||||
|
@ -17,6 +17,20 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
|||||||
@override
|
@override
|
||||||
String get URL => "stock/test/";
|
String get URL => "stock/test/";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> formFields() {
|
||||||
|
return {
|
||||||
|
"stock_item": {
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
"test": {},
|
||||||
|
"result": {},
|
||||||
|
"value": {},
|
||||||
|
"notes": {},
|
||||||
|
"attachment": {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
String get key => jsondata['key'] ?? '';
|
String get key => jsondata['key'] ?? '';
|
||||||
|
|
||||||
String get testName => jsondata['test'] ?? '';
|
String get testName => jsondata['test'] ?? '';
|
||||||
@ -190,39 +204,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> uploadTestResult(BuildContext context, String testName, bool result, {String? value, String? notes, File? attachment}) async {
|
|
||||||
|
|
||||||
Map<String, String> data = {
|
|
||||||
"stock_item": pk.toString(),
|
|
||||||
"test": testName,
|
|
||||||
"result": result.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (value != null && value.isNotEmpty) {
|
|
||||||
data["value"] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notes != null && notes.isNotEmpty) {
|
|
||||||
data["notes"] = notes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Upload is performed in different ways, depending if an attachment is provided.
|
|
||||||
* TODO: Is there a nice way to refactor this one?
|
|
||||||
*/
|
|
||||||
if (attachment == null) {
|
|
||||||
var _result = await InvenTreeStockItemTestResult().create(data);
|
|
||||||
|
|
||||||
return (_result != null) && (_result is InvenTreeStockItemTestResult);
|
|
||||||
} else {
|
|
||||||
var url = InvenTreeStockItemTestResult().URL;
|
|
||||||
http.StreamedResponse _uploadResponse = await InvenTreeAPI().uploadFile(url, attachment, fields: data);
|
|
||||||
|
|
||||||
// Check that the HTTP status code is HTTP_201_CREATED
|
|
||||||
return _uploadResponse.statusCode == 201;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get uid => jsondata['uid'] ?? '';
|
String get uid => jsondata['uid'] ?? '';
|
||||||
|
|
||||||
int get status => jsondata['status'] ?? -1;
|
int get status => jsondata['status'] ?? -1;
|
||||||
|
@ -51,104 +51,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
profile = userProfile;
|
profile = userProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
showFormDialog(
|
Navigator.push(
|
||||||
createNew ? L10().profileAdd : L10().profileEdit,
|
context,
|
||||||
key: _addProfileKey,
|
MaterialPageRoute(
|
||||||
callback: () {
|
builder: (context) => ProfileEditWidget(userProfile)
|
||||||
if (createNew) {
|
)
|
||||||
|
).then((context) {
|
||||||
UserProfile profile = UserProfile(
|
_reload();
|
||||||
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: L10().name,
|
|
||||||
hint: "Enter profile name",
|
|
||||||
initial: createNew ? '' : profile?.name ?? '',
|
|
||||||
onSaved: (value) => _name = value,
|
|
||||||
validator: _validateProfileName,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().server,
|
|
||||||
hint: "http[s]://<server>:<port>",
|
|
||||||
initial: createNew ? '' : profile?.server ?? '',
|
|
||||||
validator: _validateServer,
|
|
||||||
onSaved: (value) => _server = value,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().username,
|
|
||||||
hint: L10().enterPassword,
|
|
||||||
initial: createNew ? '' : profile?.username ?? '',
|
|
||||||
onSaved: (value) => _username = value,
|
|
||||||
validator: _validateUsername,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().password,
|
|
||||||
hint: L10().enterUsername,
|
|
||||||
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) {
|
|
||||||
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return L10().serverEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value.startsWith("http:") && !value.startsWith("https:")) {
|
|
||||||
return L10().serverStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: URL validator
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validateUsername(String value) {
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return L10().usernameEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validatePassword(String value) {
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return L10().passwordEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectProfile(BuildContext context, UserProfile profile) async {
|
void _selectProfile(BuildContext context, UserProfile profile) async {
|
||||||
@ -191,8 +101,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserProfileDBManager().updateProfile(profile);
|
|
||||||
|
|
||||||
_reload();
|
_reload();
|
||||||
|
|
||||||
if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
|
||||||
@ -204,12 +112,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addProfile(UserProfile profile) async {
|
|
||||||
|
|
||||||
await UserProfileDBManager().addProfile(profile);
|
|
||||||
|
|
||||||
_reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? _getProfileIcon(UserProfile profile) {
|
Widget? _getProfileIcon(UserProfile profile) {
|
||||||
|
|
||||||
@ -275,14 +177,20 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_selectProfile(context, profile);
|
_selectProfile(context, profile);
|
||||||
},
|
},
|
||||||
child: Text(L10().profileConnect),
|
child: ListTile(
|
||||||
|
title: Text(L10().profileConnect),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.server),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
SimpleDialogOption(
|
SimpleDialogOption(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_editProfile(context, userProfile: profile);
|
_editProfile(context, userProfile: profile);
|
||||||
},
|
},
|
||||||
child: Text(L10().profileEdit),
|
child: ListTile(
|
||||||
|
title: Text(L10().profileEdit),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.edit)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
SimpleDialogOption(
|
SimpleDialogOption(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -296,7 +204,10 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(L10().profileDelete),
|
child: ListTile(
|
||||||
|
title: Text(L10().profileDelete),
|
||||||
|
leading: FaIcon(FontAwesomeIcons.trashAlt),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -318,6 +229,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
key: _loginKey,
|
key: _loginKey,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(L10().profileSelect),
|
title: Text(L10().profileSelect),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||||
|
onPressed: () {
|
||||||
|
_editProfile(context, createNew: true);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@ -327,12 +246,185 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
|||||||
).toList(),
|
).toList(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
);
|
||||||
child: Icon(FontAwesomeIcons.plus),
|
}
|
||||||
onPressed: () {
|
}
|
||||||
_editProfile(context, createNew: true);
|
|
||||||
},
|
|
||||||
|
class ProfileEditWidget extends StatefulWidget {
|
||||||
|
|
||||||
|
UserProfile? profile;
|
||||||
|
|
||||||
|
ProfileEditWidget(this.profile) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ProfileEditState createState() => _ProfileEditState(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileEditState extends State<ProfileEditWidget> {
|
||||||
|
|
||||||
|
UserProfile? profile;
|
||||||
|
|
||||||
|
_ProfileEditState(this.profile) : super();
|
||||||
|
|
||||||
|
final formKey = new GlobalKey<FormState>();
|
||||||
|
|
||||||
|
String name = "";
|
||||||
|
String server = "";
|
||||||
|
String username = "";
|
||||||
|
String password = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(profile == null ? L10().profileAdd : L10().profileEdit),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.save),
|
||||||
|
onPressed: () async {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
formKey.currentState!.save();
|
||||||
|
|
||||||
|
UserProfile? prf = profile;
|
||||||
|
|
||||||
|
if (prf == null) {
|
||||||
|
UserProfile profile = UserProfile(
|
||||||
|
name: name,
|
||||||
|
server: server,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
await UserProfileDBManager().addProfile(profile);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
prf.name = name;
|
||||||
|
prf.server = server;
|
||||||
|
prf.username = username;
|
||||||
|
prf.password = password;
|
||||||
|
|
||||||
|
await UserProfileDBManager().updateProfile(prf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the window
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10().profileName,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
initialValue: profile?.name ?? "",
|
||||||
|
maxLines: 1,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
onSaved: (value) {
|
||||||
|
name = value?.trim() ?? "";
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return L10().valueCannotBeEmpty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10().server,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
hintText: "http[s]://<server>:<port>",
|
||||||
|
),
|
||||||
|
initialValue: profile?.server ?? "",
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
onSaved: (value) {
|
||||||
|
server = value?.trim() ?? "";
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return L10().serverEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
// Spaces are bad
|
||||||
|
if (value.contains(" ")) {
|
||||||
|
return L10().invalidHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.startsWith("http:") && !value.startsWith("https:")) {
|
||||||
|
// return L10().serverStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri? _uri = Uri.tryParse(value);
|
||||||
|
|
||||||
|
if (_uri == null || _uri.host.isEmpty) {
|
||||||
|
return L10().invalidHost;
|
||||||
|
} else {
|
||||||
|
Uri uri = Uri.parse(value);
|
||||||
|
|
||||||
|
if (uri.hasScheme) {
|
||||||
|
print("Scheme: ${uri.scheme}");
|
||||||
|
if (!(["http", "https"].contains(uri.scheme.toLowerCase()))) {
|
||||||
|
return L10().serverStart;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return L10().invalidHost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10().username,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
hintText: L10().enterUsername
|
||||||
|
),
|
||||||
|
initialValue: profile?.username ?? "",
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
onSaved: (value) {
|
||||||
|
username = value?.trim() ?? "";
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return L10().usernameEmpty;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: L10().password,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
hintText: L10().enterPassword,
|
||||||
|
),
|
||||||
|
initialValue: profile?.password ?? "",
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
obscureText: true,
|
||||||
|
onSaved: (value) {
|
||||||
|
password = value ?? "";
|
||||||
|
},
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return L10().passwordEmpty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -202,7 +202,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
} else if (_subcategories.length == 0) {
|
} else if (_subcategories.length == 0) {
|
||||||
tiles.add(ListTile(
|
tiles.add(ListTile(
|
||||||
title: Text(L10().noSubcategories),
|
title: Text(L10().noSubcategories),
|
||||||
subtitle: Text(L10().noSubcategoriesAvailable)
|
subtitle: Text(
|
||||||
|
L10().noSubcategoriesAvailable,
|
||||||
|
style: TextStyle(fontStyle: FontStyle.italic)
|
||||||
|
)
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
tiles.add(SubcategoryList(_subcategories));
|
tiles.add(SubcategoryList(_subcategories));
|
||||||
|
@ -1,92 +1,144 @@
|
|||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:inventree/l10.dart';
|
import 'package:inventree/l10.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
// TODO - Perhaps refactor all this using flutter_form_builder - https://pub.dev/packages/flutter_form_builder
|
import 'package:one_context/one_context.dart';
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Form field for selecting an image file,
|
|
||||||
* either from the gallery, or from the camera.
|
|
||||||
*/
|
|
||||||
class ImagePickerField extends FormField<File> {
|
|
||||||
|
|
||||||
static void _selectFromGallery(FormFieldState<File> field) {
|
class FilePickerDialog {
|
||||||
_getImageFromGallery(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _selectFromCamera(FormFieldState<File> field) {
|
static Future<File?> pickImageFromCamera() async {
|
||||||
_getImageFromCamera(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _getImageFromGallery(FormFieldState<File> field) async {
|
|
||||||
|
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
|
|
||||||
final pickedImage = await picker.getImage(source: ImageSource.gallery);
|
final pickedImage = await picker.pickImage(source: ImageSource.camera);
|
||||||
|
|
||||||
if (pickedImage != null)
|
if (pickedImage != null) {
|
||||||
{
|
return File(pickedImage.path);
|
||||||
field.didChange(File(pickedImage.path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _getImageFromCamera(FormFieldState<File> field) async {
|
static Future<File?> pickImageFromGallery() async {
|
||||||
|
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
|
|
||||||
final pickedImage = await picker.getImage(source: ImageSource.camera);
|
final pickedImage = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
|
||||||
if (pickedImage != null)
|
if (pickedImage != null) {
|
||||||
{
|
return File(pickedImage.path);
|
||||||
field.didChange(File(pickedImage.path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImagePickerField(BuildContext context, {String? label, Function(File?)? onSaved, bool required = false}) :
|
static Future<File?> pickFileFromDevice() async {
|
||||||
super(
|
|
||||||
onSaved: onSaved,
|
final FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||||
validator: (File? img) {
|
|
||||||
if (required && (img == null)) {
|
if (result != null) {
|
||||||
return L10().required;
|
String? path = result.files.single.path;
|
||||||
|
|
||||||
|
if (path != null) {
|
||||||
|
return File(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present a dialog to pick a file, either from local file system or from camera
|
||||||
|
static Future<void> pickFile({String message = "", bool allowImages = true, bool allowFiles = true, Function(File)? onPicked}) async {
|
||||||
|
|
||||||
|
String title = "";
|
||||||
|
|
||||||
|
if (allowImages && !allowFiles) {
|
||||||
|
title = L10().selectImage;
|
||||||
|
} else {
|
||||||
|
title = L10().selectFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct actions
|
||||||
|
List<Widget> actions = [
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
if (message.isNotEmpty) {
|
||||||
|
actions.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(message)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.add(
|
||||||
|
SimpleDialogOption(
|
||||||
|
child: ListTile(
|
||||||
|
leading: FaIcon(FontAwesomeIcons.fileUpload),
|
||||||
|
title: Text(allowFiles ? L10().selectFile : L10().selectImage),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
|
||||||
|
// Close the dialog
|
||||||
|
OneContext().popDialog();
|
||||||
|
|
||||||
|
File? file;
|
||||||
|
if (allowFiles) {
|
||||||
|
file = await pickFileFromDevice();
|
||||||
|
} else {
|
||||||
|
file = await pickImageFromGallery();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (file != null) {
|
||||||
|
if (onPicked != null) {
|
||||||
|
onPicked(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
builder: (FormFieldState<File> state) {
|
)
|
||||||
|
);
|
||||||
|
|
||||||
String _label = label ?? L10().attachImage;
|
if (allowImages) {
|
||||||
|
actions.add(
|
||||||
|
SimpleDialogOption(
|
||||||
|
child: ListTile(
|
||||||
|
leading: FaIcon(FontAwesomeIcons.camera),
|
||||||
|
title: Text(L10().takePicture),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
// Close the dialog
|
||||||
|
OneContext().popDialog();
|
||||||
|
|
||||||
return InputDecorator(
|
File? file = await pickImageFromCamera();
|
||||||
decoration: InputDecoration(
|
|
||||||
errorText: state.errorText,
|
if (file != null) {
|
||||||
labelText: required ? _label + "*" : _label,
|
if (onPicked != null) {
|
||||||
),
|
onPicked(file);
|
||||||
child: Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
}
|
||||||
children: <Widget>[
|
}
|
||||||
TextButton(
|
)
|
||||||
child: Text(L10().selectImage),
|
|
||||||
onPressed: () {
|
|
||||||
_selectFromGallery(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: Text(L10().takePicture),
|
|
||||||
onPressed: () {
|
|
||||||
_selectFromCamera(state);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OneContext().showDialog(
|
||||||
|
builder: (context) {
|
||||||
|
return SimpleDialog(
|
||||||
|
title: Text(title),
|
||||||
|
children: actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ 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:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
|
import 'package:inventree/widget/fields.dart';
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import 'package:inventree/widget/refreshable_state.dart';
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import 'package:inventree/widget/snacks.dart';
|
||||||
|
|
||||||
@ -46,16 +47,14 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
|||||||
// File upload
|
// File upload
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.fileUpload),
|
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||||
onPressed: uploadFile,
|
onPressed: () async {
|
||||||
)
|
FilePickerDialog.pickFile(
|
||||||
);
|
onPicked: (File file) {
|
||||||
|
upload(file);
|
||||||
// Upload from camera
|
}
|
||||||
actions.add(
|
);
|
||||||
IconButton(
|
},
|
||||||
icon: FaIcon(FontAwesomeIcons.camera),
|
|
||||||
onPressed: uploadFromCamera,
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -77,45 +76,6 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
|||||||
showSnackIcon(L10().uploadFailed, success: false);
|
showSnackIcon(L10().uploadFailed, success: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Select a file from the device to upload
|
|
||||||
*/
|
|
||||||
Future<void> uploadFile() async {
|
|
||||||
|
|
||||||
final FilePickerResult? result = await FilePicker.platform.pickFiles();
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
|
|
||||||
String? path = result.files.single.path;
|
|
||||||
|
|
||||||
if (path != null) {
|
|
||||||
File attachment = File(path);
|
|
||||||
|
|
||||||
upload(attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Upload an attachment by taking a new picture with the built in device camera
|
|
||||||
*/
|
|
||||||
Future<void> uploadFromCamera() async {
|
|
||||||
|
|
||||||
|
|
||||||
final picker = ImagePicker();
|
|
||||||
|
|
||||||
final pickedImage = await picker.getImage(source: ImageSource.camera);
|
|
||||||
|
|
||||||
if (pickedImage != null) {
|
|
||||||
File? attachment = File(pickedImage.path);
|
|
||||||
upload(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import 'package:inventree/api.dart';
|
|||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
|
import 'package:inventree/widget/fields.dart';
|
||||||
import 'package:inventree/widget/refreshable_state.dart';
|
import 'package:inventree/widget/refreshable_state.dart';
|
||||||
import 'package:inventree/widget/snacks.dart';
|
import 'package:inventree/widget/snacks.dart';
|
||||||
|
|
||||||
@ -37,45 +38,6 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
|||||||
await part.reload();
|
await part.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
void uploadFromGallery() async {
|
|
||||||
|
|
||||||
final picker = ImagePicker();
|
|
||||||
|
|
||||||
final pickedImage = await picker.getImage(source: ImageSource.gallery);
|
|
||||||
|
|
||||||
if (pickedImage != null) {
|
|
||||||
File? img = File(pickedImage.path);
|
|
||||||
|
|
||||||
final result = await part.uploadImage(img);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
showSnackIcon(L10().uploadFailed, success: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void uploadFromCamera() async {
|
|
||||||
|
|
||||||
final picker = ImagePicker();
|
|
||||||
|
|
||||||
final pickedImage = await picker.getImage(source: ImageSource.camera);
|
|
||||||
|
|
||||||
if (pickedImage != null) {
|
|
||||||
File? img = File(pickedImage.path);
|
|
||||||
|
|
||||||
final result = await part.uploadImage(img);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
showSnackIcon(L10().uploadFailed, success: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => part.fullname;
|
String getAppBarTitle(BuildContext context) => part.fullname;
|
||||||
|
|
||||||
@ -89,16 +51,22 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
|||||||
// File upload
|
// File upload
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.fileImage),
|
icon: FaIcon(FontAwesomeIcons.fileUpload),
|
||||||
onPressed: uploadFromGallery,
|
onPressed: () async {
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Camera upload
|
FilePickerDialog.pickFile(
|
||||||
actions.add(
|
onPicked: (File file) async {
|
||||||
IconButton(
|
final result = await part.uploadImage(file);
|
||||||
icon: FaIcon(FontAwesomeIcons.camera),
|
|
||||||
onPressed: uploadFromCamera,
|
if (!result) {
|
||||||
|
showSnackIcon(L10().uploadFailed, success: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: refreshableKey,
|
key: refreshableKey,
|
||||||
appBar: getAppBar(context),
|
appBar: getAppBar(context),
|
||||||
drawer: null, // getDrawer(context),
|
drawer: null,
|
||||||
floatingActionButton: getFab(context),
|
floatingActionButton: getFab(context),
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:inventree/api_form.dart';
|
||||||
import 'package:inventree/app_colors.dart';
|
import 'package:inventree/app_colors.dart';
|
||||||
import 'package:inventree/inventree/part.dart';
|
import 'package:inventree/inventree/part.dart';
|
||||||
import 'package:inventree/inventree/stock.dart';
|
import 'package:inventree/inventree/stock.dart';
|
||||||
@ -36,6 +37,18 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().testResults;
|
String getAppBarTitle(BuildContext context) => L10().testResults;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> getAppBarActions(BuildContext context) {
|
||||||
|
return [
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||||
|
onPressed: () {
|
||||||
|
addTestResult(context);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> request() async {
|
Future<void> request() async {
|
||||||
await item.getTestTemplates();
|
await item.getTestTemplates();
|
||||||
@ -46,73 +59,19 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
|
|
||||||
_StockItemTestResultDisplayState(this.item);
|
_StockItemTestResultDisplayState(this.item);
|
||||||
|
|
||||||
void uploadTestResult(String name, bool result, String value, String notes, File? attachment) async {
|
void addTestResult(BuildContext context, {String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
|
||||||
|
|
||||||
final success = await item.uploadTestResult(
|
InvenTreeStockItemTestResult().createForm(
|
||||||
context, name, result,
|
context,
|
||||||
value: value,
|
L10().testResultAdd,
|
||||||
notes: notes,
|
data: {
|
||||||
attachment: attachment
|
"stock_item": "${item.pk}",
|
||||||
);
|
"test": "${name}",
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
success ? L10().testResultUploadPass : L10().testResultUploadFail,
|
|
||||||
success: success
|
|
||||||
);
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
|
|
||||||
|
|
||||||
String _name = "";
|
|
||||||
bool _result = false;
|
|
||||||
String _value = "";
|
|
||||||
String _notes = "";
|
|
||||||
File? _attachment;
|
|
||||||
|
|
||||||
showFormDialog(L10().testResultAdd,
|
|
||||||
key: _addResultKey,
|
|
||||||
callback: () {
|
|
||||||
uploadTestResult(_name, _result, _value, _notes, _attachment);
|
|
||||||
},
|
},
|
||||||
fields: <Widget>[
|
onSuccess: (data) {
|
||||||
StringField(
|
refresh();
|
||||||
label: L10().testName,
|
},
|
||||||
initial: name,
|
fileField: "attachment",
|
||||||
isEnabled: nameIsEditable,
|
|
||||||
onSaved: (value) => _name = value ?? '',
|
|
||||||
),
|
|
||||||
CheckBoxField(
|
|
||||||
label: L10().result,
|
|
||||||
helperText: L10().testPassedOrFailed,
|
|
||||||
initial: true,
|
|
||||||
onSaved: (value) => _result = value ?? false,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().value,
|
|
||||||
initial: value,
|
|
||||||
allowEmpty: true,
|
|
||||||
onSaved: (value) => _value = value ?? '',
|
|
||||||
validator: (String value) {
|
|
||||||
if (valueRequired && value.isEmpty) {
|
|
||||||
return L10().valueRequired;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ImagePickerField(
|
|
||||||
context,
|
|
||||||
label: L10().attachImage,
|
|
||||||
required: attachmentRequired,
|
|
||||||
onSaved: (attachment) => _attachment = attachment,
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
allowEmpty: true,
|
|
||||||
label: L10().notes,
|
|
||||||
onSaved: (value) => _notes = value ?? '',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +203,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
trailing: _icon,
|
trailing: _icon,
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
addTestResult(
|
addTestResult(
|
||||||
|
context,
|
||||||
name: _test,
|
name: _test,
|
||||||
nameIsEditable: !_required,
|
nameIsEditable: !_required,
|
||||||
valueRequired: _valueRequired,
|
valueRequired: _valueRequired,
|
||||||
@ -272,31 +232,4 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
|||||||
).toList()
|
).toList()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
List<SpeedDialChild> actionButtons() {
|
|
||||||
|
|
||||||
var buttons = List<SpeedDialChild>();
|
|
||||||
|
|
||||||
buttons.add(SpeedDialChild(
|
|
||||||
child: Icon(FontAwesomeIcons.plusCircle),
|
|
||||||
label: L10().testResultAdd,
|
|
||||||
onTap: () {
|
|
||||||
addTestResult();
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget getFab(BuildContext context) {
|
|
||||||
return FloatingActionButton(
|
|
||||||
child: Icon(FontAwesomeIcons.plus),
|
|
||||||
onPressed: () {
|
|
||||||
addTestResult();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -235,13 +235,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
flutter_speed_dial:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_speed_dial
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.5"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -7,7 +7,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.4.5+26
|
version: 0.4.6+27
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
@ -28,7 +28,6 @@ dependencies:
|
|||||||
package_info_plus: ^1.0.4 # App information introspection
|
package_info_plus: ^1.0.4 # App information introspection
|
||||||
device_info_plus: ^2.1.0 # Information about the device
|
device_info_plus: ^2.1.0 # Information about the device
|
||||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||||
flutter_speed_dial: ^3.0.5 # FAB menu elements
|
|
||||||
sentry_flutter: 5.0.0 # Error reporting
|
sentry_flutter: 5.0.0 # Error reporting
|
||||||
image_picker: ^0.8.3 # Select or take photos
|
image_picker: ^0.8.3 # Select or take photos
|
||||||
file_picker: ^4.0.0 # Select files from the device
|
file_picker: ^4.0.0 # Select files from the device
|
||||||
|
Loading…
x
Reference in New Issue
Block a user