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
|
||||
---
|
||||
|
||||
### 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
|
||||
---
|
||||
|
||||
|
93
lib/api.dart
93
lib/api.dart
@ -42,6 +42,14 @@ class APIResponse {
|
||||
|
||||
// Request is "valid" if a statusCode was returned
|
||||
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 was invalid for some reason
|
||||
if (!response.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
if (!response.successful()) {
|
||||
showStatusCodeError(response.statusCode);
|
||||
return false;
|
||||
}
|
||||
@ -308,16 +311,10 @@ class InvenTreeAPI {
|
||||
// Clear the existing token value
|
||||
_token = "";
|
||||
|
||||
print("Requesting token from server");
|
||||
|
||||
response = await get(_URL_GET_TOKEN);
|
||||
|
||||
// Invalid response
|
||||
if (!response.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
if (!response.successful()) {
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 401:
|
||||
@ -413,7 +410,7 @@ class InvenTreeAPI {
|
||||
|
||||
var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
|
||||
|
||||
if (!response.isValid() || response.statusCode != 200) {
|
||||
if (!response.successful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -563,9 +560,8 @@ class InvenTreeAPI {
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
var _url = makeApiUrl(url);
|
||||
|
||||
var request = http.MultipartRequest(method, Uri.parse(_url));
|
||||
@ -582,25 +578,63 @@ class InvenTreeAPI {
|
||||
|
||||
request.files.add(_file);
|
||||
|
||||
var response = await request.send();
|
||||
APIResponse response = APIResponse(
|
||||
url: url,
|
||||
method: method,
|
||||
);
|
||||
|
||||
String jsondata = "";
|
||||
|
||||
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) {
|
||||
// Server error
|
||||
if (response.statusCode >= 500) {
|
||||
|
||||
var data = await response.stream.bytesToString();
|
||||
|
||||
sentryReportMessage(
|
||||
"Server error on file upload",
|
||||
"Server error in uploadFile()",
|
||||
context: {
|
||||
"url": _url,
|
||||
"statusCode": "${response.statusCode}",
|
||||
"response": response.toString(),
|
||||
"request": request.fields.toString(),
|
||||
"data": data,
|
||||
"url": url,
|
||||
"method": request.method,
|
||||
"name": name,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"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;
|
||||
@ -827,9 +861,6 @@ class InvenTreeAPI {
|
||||
return data ?? {};
|
||||
} on FormatException {
|
||||
|
||||
print("JSON format exception!");
|
||||
print("${body}");
|
||||
|
||||
sentryReportMessage(
|
||||
"Error decoding JSON response from server",
|
||||
context: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
@ -26,6 +28,9 @@ class APIFormField {
|
||||
// Constructor
|
||||
APIFormField(this.name, this.data);
|
||||
|
||||
// File to be uploaded for this filed
|
||||
File? attachedfile;
|
||||
|
||||
// Name of this field
|
||||
final String name;
|
||||
|
||||
@ -139,7 +144,7 @@ class APIFormField {
|
||||
params: filters,
|
||||
);
|
||||
|
||||
if (response.isValid()) {
|
||||
if (response.successful()) {
|
||||
initial_data = response.data;
|
||||
}
|
||||
}
|
||||
@ -159,6 +164,9 @@ class APIFormField {
|
||||
return _constructFloatField();
|
||||
case "choice":
|
||||
return _constructChoiceField();
|
||||
case "file upload":
|
||||
case "image upload":
|
||||
return _constructFileField();
|
||||
default:
|
||||
return ListTile(
|
||||
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() {
|
||||
|
||||
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)
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
@ -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
|
||||
if (!availableFields.containsKey(fieldName)) {
|
||||
print("Field '${fieldName}' not available at '${url}'");
|
||||
|
||||
sentryReportMessage(
|
||||
"API form called with unknown field '${fieldName}'",
|
||||
context: {
|
||||
"url": url.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -563,6 +617,7 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
||||
formFields,
|
||||
method,
|
||||
onSuccess: onSuccess,
|
||||
fileField: fileField,
|
||||
))
|
||||
);
|
||||
}
|
||||
@ -579,6 +634,8 @@ class APIFormWidget extends StatefulWidget {
|
||||
//! API method
|
||||
final String method;
|
||||
|
||||
final String fileField;
|
||||
|
||||
final List<APIFormField> fields;
|
||||
|
||||
Function(Map<String, dynamic>)? onSuccess;
|
||||
@ -591,11 +648,12 @@ class APIFormWidget extends StatefulWidget {
|
||||
{
|
||||
Key? key,
|
||||
this.onSuccess,
|
||||
this.fileField = "",
|
||||
}
|
||||
) : super(key: key);
|
||||
|
||||
@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 fileField;
|
||||
|
||||
List<APIFormField> fields;
|
||||
|
||||
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;
|
||||
|
||||
@ -678,6 +738,32 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
|
||||
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") {
|
||||
return await InvenTreeAPI().post(
|
||||
url,
|
||||
|
@ -73,7 +73,7 @@ class InvenTreeModel {
|
||||
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) {
|
||||
fields = formFields();
|
||||
@ -87,6 +87,7 @@ class InvenTreeModel {
|
||||
modelData: data,
|
||||
onSuccess: onSuccess,
|
||||
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 {
|
||||
|
||||
final http.StreamedResponse response = await InvenTreeAPI().uploadFile(
|
||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||
URL,
|
||||
attachment,
|
||||
method: 'POST',
|
||||
@ -519,11 +520,7 @@ class InvenTreeAttachment extends InvenTreeModel {
|
||||
fields: fields
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return response.successful();
|
||||
}
|
||||
|
||||
Future<void> downloadAttachment() async {
|
||||
|
@ -356,19 +356,14 @@ class InvenTreePart extends InvenTreeModel {
|
||||
|
||||
Future<bool> uploadImage(File image) async {
|
||||
// Upload file against this part
|
||||
final http.StreamedResponse response = await InvenTreeAPI().uploadFile(
|
||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||
url,
|
||||
image,
|
||||
method: 'PATCH',
|
||||
name: 'image',
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
print("uploadImage returned ${response.statusCode} at '${url}'");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return response.successful();
|
||||
}
|
||||
|
||||
// Return the "starred" status of this part
|
||||
|
@ -17,6 +17,20 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
@override
|
||||
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 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'] ?? '';
|
||||
|
||||
int get status => jsondata['status'] ?? -1;
|
||||
|
@ -51,104 +51,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
profile = userProfile;
|
||||
}
|
||||
|
||||
showFormDialog(
|
||||
createNew ? L10().profileAdd : L10().profileEdit,
|
||||
key: _addProfileKey,
|
||||
callback: () {
|
||||
if (createNew) {
|
||||
|
||||
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: 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,
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ProfileEditWidget(userProfile)
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
).then((context) {
|
||||
_reload();
|
||||
});
|
||||
}
|
||||
|
||||
void _selectProfile(BuildContext context, UserProfile profile) async {
|
||||
@ -191,8 +101,6 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
await UserProfileDBManager().updateProfile(profile);
|
||||
|
||||
_reload();
|
||||
|
||||
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) {
|
||||
|
||||
@ -275,14 +177,20 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
Navigator.of(context).pop();
|
||||
_selectProfile(context, profile);
|
||||
},
|
||||
child: Text(L10().profileConnect),
|
||||
child: ListTile(
|
||||
title: Text(L10().profileConnect),
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
)
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_editProfile(context, userProfile: profile);
|
||||
},
|
||||
child: Text(L10().profileEdit),
|
||||
child: ListTile(
|
||||
title: Text(L10().profileEdit),
|
||||
leading: FaIcon(FontAwesomeIcons.edit)
|
||||
)
|
||||
),
|
||||
SimpleDialogOption(
|
||||
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,
|
||||
appBar: AppBar(
|
||||
title: Text(L10().profileSelect),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||
onPressed: () {
|
||||
_editProfile(context, createNew: true);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
@ -327,12 +246,185 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
).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) {
|
||||
tiles.add(ListTile(
|
||||
title: Text(L10().noSubcategories),
|
||||
subtitle: Text(L10().noSubcategoriesAvailable)
|
||||
subtitle: Text(
|
||||
L10().noSubcategoriesAvailable,
|
||||
style: TextStyle(fontStyle: FontStyle.italic)
|
||||
)
|
||||
));
|
||||
} else {
|
||||
tiles.add(SubcategoryList(_subcategories));
|
||||
|
@ -1,94 +1,146 @@
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import 'dart:async';
|
||||
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) {
|
||||
_getImageFromGallery(field);
|
||||
}
|
||||
class FilePickerDialog {
|
||||
|
||||
static void _selectFromCamera(FormFieldState<File> field) {
|
||||
_getImageFromCamera(field);
|
||||
}
|
||||
|
||||
static Future<void> _getImageFromGallery(FormFieldState<File> field) async {
|
||||
static Future<File?> pickImageFromCamera() async {
|
||||
|
||||
final picker = ImagePicker();
|
||||
|
||||
final pickedImage = await picker.getImage(source: ImageSource.gallery);
|
||||
final pickedImage = await picker.pickImage(source: ImageSource.camera);
|
||||
|
||||
if (pickedImage != null)
|
||||
{
|
||||
field.didChange(File(pickedImage.path));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _getImageFromCamera(FormFieldState<File> field) async {
|
||||
|
||||
final picker = ImagePicker();
|
||||
|
||||
final pickedImage = await picker.getImage(source: ImageSource.camera);
|
||||
|
||||
if (pickedImage != null)
|
||||
{
|
||||
field.didChange(File(pickedImage.path));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ImagePickerField(BuildContext context, {String? label, Function(File?)? onSaved, bool required = false}) :
|
||||
super(
|
||||
onSaved: onSaved,
|
||||
validator: (File? img) {
|
||||
if (required && (img == null)) {
|
||||
return L10().required;
|
||||
if (pickedImage != null) {
|
||||
return File(pickedImage.path);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
builder: (FormFieldState<File> state) {
|
||||
}
|
||||
|
||||
String _label = label ?? L10().attachImage;
|
||||
static Future<File?> pickImageFromGallery() async {
|
||||
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
errorText: state.errorText,
|
||||
labelText: required ? _label + "*" : _label,
|
||||
final picker = ImagePicker();
|
||||
|
||||
final pickedImage = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
||||
if (pickedImage != null) {
|
||||
return File(pickedImage.path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<File?> pickFileFromDevice() async {
|
||||
|
||||
final FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
|
||||
if (result != null) {
|
||||
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),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
child: Text(L10().selectImage),
|
||||
onPressed: () {
|
||||
_selectFromGallery(state);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10().takePicture),
|
||||
onPressed: () {
|
||||
_selectFromCamera(state);
|
||||
onPressed: () async {
|
||||
|
||||
// Close the dialog
|
||||
OneContext().popDialog();
|
||||
|
||||
File? file;
|
||||
if (allowFiles) {
|
||||
file = await pickFileFromDevice();
|
||||
} else {
|
||||
file = await pickImageFromGallery();
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
if (onPicked != null) {
|
||||
onPicked(file);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (allowImages) {
|
||||
actions.add(
|
||||
SimpleDialogOption(
|
||||
child: ListTile(
|
||||
leading: FaIcon(FontAwesomeIcons.camera),
|
||||
title: Text(L10().takePicture),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Close the dialog
|
||||
OneContext().popDialog();
|
||||
|
||||
File? file = await pickImageFromCamera();
|
||||
|
||||
if (file != null) {
|
||||
if (onPicked != null) {
|
||||
onPicked(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
OneContext().showDialog(
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: Text(title),
|
||||
children: actions,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class CheckBoxField extends FormField<bool> {
|
||||
CheckBoxField({
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
@ -46,16 +47,14 @@ class _PartAttachmentDisplayState extends RefreshableState<PartAttachmentsWidget
|
||||
// File upload
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.fileUpload),
|
||||
onPressed: uploadFile,
|
||||
)
|
||||
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import 'package:inventree/api.dart';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/refreshable_state.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
@ -37,45 +38,6 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
||||
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
|
||||
String getAppBarTitle(BuildContext context) => part.fullname;
|
||||
|
||||
@ -89,16 +51,22 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
|
||||
// File upload
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.fileImage),
|
||||
onPressed: uploadFromGallery,
|
||||
)
|
||||
icon: FaIcon(FontAwesomeIcons.fileUpload),
|
||||
onPressed: () async {
|
||||
|
||||
FilePickerDialog.pickFile(
|
||||
onPicked: (File file) async {
|
||||
final result = await part.uploadImage(file);
|
||||
|
||||
if (!result) {
|
||||
showSnackIcon(L10().uploadFailed, success: false);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
);
|
||||
|
||||
// Camera upload
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
onPressed: uploadFromCamera,
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
return Scaffold(
|
||||
key: refreshableKey,
|
||||
appBar: getAppBar(context),
|
||||
drawer: null, // getDrawer(context),
|
||||
drawer: null,
|
||||
floatingActionButton: getFab(context),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
@ -36,6 +37,18 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().testResults;
|
||||
|
||||
@override
|
||||
List<Widget> getAppBarActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.plusCircle),
|
||||
onPressed: () {
|
||||
addTestResult(context);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request() async {
|
||||
await item.getTestTemplates();
|
||||
@ -46,73 +59,19 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
|
||||
_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(
|
||||
context, name, result,
|
||||
value: value,
|
||||
notes: notes,
|
||||
attachment: attachment
|
||||
);
|
||||
|
||||
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>[
|
||||
StringField(
|
||||
label: L10().testName,
|
||||
initial: name,
|
||||
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(
|
||||
InvenTreeStockItemTestResult().createForm(
|
||||
context,
|
||||
label: L10().attachImage,
|
||||
required: attachmentRequired,
|
||||
onSaved: (attachment) => _attachment = attachment,
|
||||
),
|
||||
StringField(
|
||||
allowEmpty: true,
|
||||
label: L10().notes,
|
||||
onSaved: (value) => _notes = value ?? '',
|
||||
),
|
||||
]
|
||||
L10().testResultAdd,
|
||||
data: {
|
||||
"stock_item": "${item.pk}",
|
||||
"test": "${name}",
|
||||
},
|
||||
onSuccess: (data) {
|
||||
refresh();
|
||||
},
|
||||
fileField: "attachment",
|
||||
);
|
||||
}
|
||||
|
||||
@ -244,6 +203,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
trailing: _icon,
|
||||
onLongPress: () {
|
||||
addTestResult(
|
||||
context,
|
||||
name: _test,
|
||||
nameIsEditable: !_required,
|
||||
valueRequired: _valueRequired,
|
||||
@ -272,31 +232,4 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
).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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -7,7 +7,7 @@ description: InvenTree stock management
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.4.5+26
|
||||
version: 0.4.6+27
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
@ -28,7 +28,6 @@ dependencies:
|
||||
package_info_plus: ^1.0.4 # App information introspection
|
||||
device_info_plus: ^2.1.0 # Information about the device
|
||||
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
|
||||
image_picker: ^0.8.3 # Select or take photos
|
||||
file_picker: ^4.0.0 # Select files from the device
|
||||
|
Loading…
x
Reference in New Issue
Block a user