diff --git a/assets/release_notes.md b/assets/release_notes.md
index e1e9f5bf..1eed3e45 100644
--- a/assets/release_notes.md
+++ b/assets/release_notes.md
@@ -1,9 +1,12 @@
## InvenTree App Release Notes
---
-### 0.10.3 - April 2023
+### 0.11.0 - April 2023
---
+- Adds globally accessible action button for "search"
+- Adds globally accessible action button for "barcode scan"
+- Implement context actions using floating actions buttons
- Support barcode scanning for purchase orders
### 0.10.2 - March 2023
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 8d4492f9..9625e105 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 11.0
diff --git a/ios/Podfile b/ios/Podfile
index 0564dd31..74fd5d31 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -42,4 +42,4 @@ post_install do |installer|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
-end
\ No newline at end of file
+end
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 293e5b55..b35a1caa 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 50;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -236,6 +236,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -250,6 +251,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -276,17 +278,17 @@
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
- "${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
- "${BUILT_PRODUCTS_DIR}/camera/camera.framework",
+ "${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework",
+ "${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
- "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
- "${BUILT_PRODUCTS_DIR}/open_file/open_file.framework",
+ "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
- "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
- "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework",
+ "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
);
@@ -299,17 +301,17 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_filex.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
- "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
);
@@ -392,7 +394,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -415,7 +417,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -476,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -523,7 +525,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -546,7 +548,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -577,7 +579,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 74a9f38e..e3962ae9 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -71,5 +71,9 @@
https
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/lib/barcode.dart b/lib/barcode.dart
index 5c7d512d..5487e3ee 100644
--- a/lib/barcode.dart
+++ b/lib/barcode.dart
@@ -1,12 +1,12 @@
import "dart:io";
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/purchase_order_detail.dart";
import "package:one_context/one_context.dart";
import "package:qr_code_scanner/qr_code_scanner.dart";
-import "package:inventree/app_colors.dart";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
@@ -805,17 +805,12 @@ Future scanQrCode(BuildContext context) async {
}
-/*
- * Construct a generic ListTile widget to link or un-link a custom barcode from a model.
- */
-Widget customBarcodeActionTile(BuildContext context, RefreshableState state, String barcode, String model, int pk) {
+SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state, String barcode, String model, int pk) {
if (barcode.isEmpty) {
- return ListTile(
- title: Text(L10().barcodeAssign),
- subtitle: Text(L10().barcodeAssignDetail),
- leading: Icon(Icons.qr_code, color: COLOR_CLICK),
- trailing: Icon(Icons.qr_code_scanner),
+ return SpeedDialChild(
+ label: L10().barcodeAssign,
+ child: Icon(Icons.barcode_reader),
onTap: () {
var handler = UniqueBarcodeHandler((String barcode) {
InvenTreeAPI().linkBarcode({
@@ -823,8 +818,8 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
"barcode": barcode,
}).then((bool result) {
showSnackIcon(
- result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
- success: result
+ result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
+ success: result
);
state.refresh(context);
@@ -832,18 +827,18 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
});
Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => InvenTreeQRView(handler)
- )
+ context,
+ MaterialPageRoute(
+ builder: (context) => InvenTreeQRView(handler)
+ )
);
}
);
} else {
- return ListTile(
- title: Text(L10().barcodeUnassign),
- leading: Icon(Icons.qr_code, color: COLOR_CLICK),
- onTap: () async {
+ return SpeedDialChild(
+ child: Icon(Icons.barcode_reader),
+ label: L10().barcodeUnassign,
+ onTap: () {
InvenTreeAPI().unlinkBarcode({
model: pk.toString()
}).then((bool result) {
@@ -854,7 +849,7 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
state.refresh(context);
});
- },
+ }
);
}
}
diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart
index 4e8bf038..9c2c95cf 100644
--- a/lib/settings/settings.dart
+++ b/lib/settings/settings.dart
@@ -1,8 +1,10 @@
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
+import "package:package_info_plus/package_info_plus.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
+import "package:inventree/settings/about.dart";
import "package:inventree/settings/app_settings.dart";
import "package:inventree/settings/home_settings.dart";
import "package:inventree/settings/login.dart";
@@ -22,6 +24,16 @@ class _InvenTreeSettingsState extends State {
final _scaffoldKey = GlobalKey();
+ /*
+ * Load "About" widget
+ */
+ Future _about() async {
+ PackageInfo.fromPlatform().then((PackageInfo info) {
+ Navigator.push(context,
+ MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)));
+ });
+ }
+
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -31,9 +43,7 @@ class _InvenTreeSettingsState extends State {
),
body: Center(
child: ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: [
+ children: [
ListTile(
title: Text(L10().server),
subtitle: Text(L10().configureServer),
@@ -65,9 +75,14 @@ class _InvenTreeSettingsState extends State {
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget()));
}
+ ),
+ Divider(),
+ ListTile(
+ title: Text(L10().about),
+ leading: FaIcon(FontAwesomeIcons.circleInfo),
+ onTap: _about,
)
]
- ).toList()
)
)
);
diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart
index 6ee694b7..47bcc651 100644
--- a/lib/widget/attachment_widget.dart
+++ b/lib/widget/attachment_widget.dart
@@ -43,33 +43,35 @@ class _AttachmentWidgetState extends RefreshableState {
List attachments = [];
@override
- String getAppBarTitle(BuildContext context) => L10().attachments;
+ String getAppBarTitle() => L10().attachments;
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
+ if (!widget.hasUploadPermission) return [];
- List actions = [];
-
- if (widget.hasUploadPermission) {
- // File upload
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.circlePlus),
- onPressed: () async {
- FilePickerDialog.pickFile(
- onPicked: (File file) async {
- await upload(context, file);
- }
- );
- },
- )
- );
- }
-
- return actions;
+ return [
+ IconButton(
+ icon: FaIcon(FontAwesomeIcons.camera),
+ onPressed: () async {
+ FilePickerDialog.pickImageFromCamera().then((File? file) {
+ upload(context, file);
+ });
+ }
+ ),
+ IconButton(
+ icon: FaIcon(FontAwesomeIcons.fileArrowUp),
+ onPressed: () async {
+ FilePickerDialog.pickFileFromDevice().then((File? file) {
+ upload(context, file);
+ });
+ }
+ )
+ ];
}
- Future upload(BuildContext context, File file) async {
+ Future upload(BuildContext context, File? file) async {
+
+ if (file == null) return;
showLoadingOverlay(context);
final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId);
diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart
index 022fc38c..8504c75d 100644
--- a/lib/widget/bom_list.dart
+++ b/lib/widget/bom_list.dart
@@ -37,7 +37,7 @@ class _BillOfMaterialsState extends RefreshableState {
bool showFilterOptions = false;
@override
- String getAppBarTitle(BuildContext context) {
+ String getAppBarTitle() {
if (widget.isParentComponent) {
return L10().billOfMaterials;
} else {
@@ -46,7 +46,7 @@ class _BillOfMaterialsState extends RefreshableState {
}
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart
index e72ec2a7..20906871 100644
--- a/lib/widget/category_display.dart
+++ b/lib/widget/category_display.dart
@@ -1,7 +1,7 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
-import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
@@ -33,27 +33,56 @@ class _CategoryDisplayState extends RefreshableState {
bool showFilterOptions = false;
@override
- String getAppBarTitle(BuildContext context) => L10().partCategory;
+ String getAppBarTitle() => L10().partCategory;
@override
- List getAppBarActions(BuildContext context) {
-
+ List appBarActions(BuildContext context) {
List actions = [];
- if ((widget.category != null) && InvenTreeAPI().checkPermission("part_category", "change")) {
+ if (widget.category != null) {
+ if (api.checkPermission("part_category", "change")) {
+ actions.add(
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().editCategory,
+ onPressed: () {
+ _editCategoryDialog(context);
+ },
+ )
+ );
+ }
+ }
+
+ return actions;
+ }
+
+ @override
+ List actionButtons(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("part", "add")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.shapes),
+ label: L10().partCreateDetail,
+ onTap: _newPart,
+ )
+ );
+ }
+
+ if (api.checkPermission("part_category", "add")) {
actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () {
- _editCategoryDialog(context);
- },
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.sitemap),
+ label: L10().categoryCreateDetail,
+ onTap: () {
+ _newCategory(context);
+ }
)
);
}
return actions;
-
}
void _editCategoryDialog(BuildContext context) {
@@ -154,25 +183,20 @@ class _CategoryDisplayState extends RefreshableState {
}
@override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.sitemap),
- label: L10().details,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.shapes),
- label: L10().parts,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.wrench),
- label: L10().actions
- ),
- ]
- );
+ List getTabIcons(BuildContext context) {
+
+ return [
+ Tab(text: L10().details),
+ Tab(text: L10().parts),
+ ];
+ }
+
+ @override
+ List getTabs(BuildContext context) {
+ return [
+ Column(children: detailTiles()),
+ Column(children: partsTiles()),
+ ];
}
// Construct the "details" panel
@@ -216,7 +240,6 @@ class _CategoryDisplayState extends RefreshableState {
};
return [
- getCategoryDescriptionCard(extra: false),
ListTile(
title: Text(
L10().parts,
@@ -298,74 +321,4 @@ class _CategoryDisplayState extends RefreshableState {
}
);
}
-
- List actionTiles(BuildContext context) {
-
- List tiles = [
- getCategoryDescriptionCard(extra: false),
- ];
-
- if (InvenTreeAPI().checkPermission("part", "add")) {
- tiles.add(
- ListTile(
- title: Text(L10().categoryCreate),
- subtitle: Text(L10().categoryCreateDetail),
- leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
- onTap: () async {
- _newCategory(context);
- },
- )
- );
-
- if (widget.category != null) {
- tiles.add(
- ListTile(
- title: Text(L10().partCreate),
- subtitle: Text(L10().partCreateDetail),
- leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_CLICK),
- onTap: _newPart,
- )
- );
- }
- }
-
- if (tiles.isEmpty) {
- tiles.add(
- ListTile(
- title: Text(
- L10().actionsNone
- ),
- subtitle: Text(
- L10().permissionAccountDenied,
- ),
- leading: FaIcon(FontAwesomeIcons.userXmark),
- )
- );
- }
-
- return tiles;
- }
-
- int partCount = 0;
-
- @override
- Widget getBody(BuildContext context) {
-
- switch (tabIndex) {
- case 0:
- return Column(
- children: detailTiles()
- );
- case 1:
- return Column(
- children: partsTiles()
- );
- case 2:
- return ListView(
- children: actionTiles(context)
- );
- default:
- return ListView();
- }
- }
}
diff --git a/lib/widget/category_list.dart b/lib/widget/category_list.dart
index 3e6f5be3..0686faf9 100644
--- a/lib/widget/category_list.dart
+++ b/lib/widget/category_list.dart
@@ -29,7 +29,7 @@ class _PartCategoryListState extends RefreshableState {
bool showFilterOptions = false;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
@@ -41,7 +41,7 @@ class _PartCategoryListState extends RefreshableState {
];
@override
- String getAppBarTitle(BuildContext context) => L10().partCategories;
+ String getAppBarTitle() => L10().partCategories;
@override
Widget getBody(BuildContext context) {
diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart
index 7cc528fc..be5167a8 100644
--- a/lib/widget/company_detail.dart
+++ b/lib/widget/company_detail.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart";
@@ -40,34 +41,36 @@ class _CompanyDetailState extends RefreshableState {
int attachmentCount = 0;
@override
- String getAppBarTitle(BuildContext context) => L10().company;
+ String getAppBarTitle() => L10().company;
@override
- List getAppBarActions(BuildContext context) {
-
+ List appBarActions(BuildContext context) {
List actions = [];
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.globe),
- onPressed: () async {
- widget.company.goToInvenTreePage();
- },
- )
- );
+ if (api.checkPermission("purchase_order", "change") ||
+ api.checkPermission("sales_order", "change") ||
+ api.checkPermission("return_order", "change")) {
+ actions.add(
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().companyEdit,
+ onPressed: () {
+ editCompany(context);
+ }
+ )
+ );
+ }
+
+ return actions;
+ }
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () {
- editCompany(context);
- }
- )
- );
+ @override
+ List actionButtons(BuildContext context) {
+ List actions = [];
+
+ // TODO
return actions;
-
}
@override
diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart
index 033423de..de9a5395 100644
--- a/lib/widget/company_list.dart
+++ b/lib/widget/company_list.dart
@@ -32,7 +32,7 @@ class _CompanyListWidgetState extends RefreshableState {
_CompanyListWidgetState();
@override
- String getAppBarTitle(BuildContext context) => widget.title;
+ String getAppBarTitle() => widget.title;
@override
Widget getBody(BuildContext context) {
diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart
index 64b6c3b4..4da5c504 100644
--- a/lib/widget/drawer.dart
+++ b/lib/widget/drawer.dart
@@ -1,23 +1,17 @@
import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
-import "package:package_info_plus/package_info_plus.dart";
import "package:inventree/api.dart";
-import "package:inventree/barcode.dart";
import "package:inventree/l10.dart";
-
-import "package:inventree/settings/about.dart";
import "package:inventree/settings/settings.dart";
-
-import "package:inventree/widget/search.dart";
+import "package:inventree/widget/category_display.dart";
+import "package:inventree/widget/notifications.dart";
+import "package:inventree/widget/purchase_order_list.dart";
+import "package:inventree/widget/location_display.dart";
/*
* Custom "drawer" widget for the InvenTree app.
- *
- * - Provides a "home" button which completely unwinds the widget stack
- * - Global search
- * - Barcode scan
*/
class InvenTreeDrawer extends StatelessWidget {
@@ -42,90 +36,126 @@ class InvenTreeDrawer extends StatelessWidget {
}
}
- void _search() {
-
- if (!InvenTreeAPI().checkConnection()) return;
+ // Load "parts" page
+ void _parts() {
+ _closeDrawer();
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))
+ );
+ }
+ // Load "stock" page
+ void _stock() {
+ _closeDrawer();
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))
+ );
+ }
+
+ // Load "purchase orders" page
+ void _purchaseOrders() {
_closeDrawer();
Navigator.push(
context,
MaterialPageRoute(
- builder: (context) => SearchWidget(true)
+ builder: (context) => PurchaseOrderListWidget(filters: {})
)
);
}
- /*
- * Launch the camera to scan a QR code.
- * Upon successful scan, data are passed off to be decoded.
- */
- Future _scan() async {
- if (!InvenTreeAPI().checkConnection()) return;
-
+ // Load notifications screen
+ void _notifications() {
_closeDrawer();
- scanQrCode(context);
+ Navigator.push(context, MaterialPageRoute(builder: (context) => NotificationWidget()));
}
- /*
- * Load settings widget
- */
+ // Load settings widget
void _settings() {
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
}
- /*
- * Load "About" widget
- */
- Future _about() async {
- _closeDrawer();
+ // Construct list of tiles to display in the "drawer" menu
+ List drawerTiles(BuildContext context) {
+ List tiles = [];
- PackageInfo.fromPlatform().then((PackageInfo info) {
- Navigator.push(context,
- MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)));
- });
+ // "Home" access
+ tiles.add(ListTile(
+ leading: FaIcon(FontAwesomeIcons.house),
+ title: Text(
+ L10().appTitle,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ onTap: _home,
+ ));
+
+ tiles.add(Divider());
+
+ if (InvenTreeAPI().checkPermission("part_category", "view")) {
+ tiles.add(
+ ListTile(
+ title: Text(L10().parts),
+ leading: FaIcon(FontAwesomeIcons.shapes),
+ onTap: _parts,
+ )
+ );
+ }
+
+ if (InvenTreeAPI().checkPermission("stock_location", "view")) {
+ tiles.add(
+ ListTile(
+ title: Text(L10().stock),
+ leading: FaIcon(FontAwesomeIcons.boxesStacked),
+ onTap: _stock,
+ )
+ );
+ }
+
+ if (InvenTreeAPI().checkPermission("purchase_order", "view")) {
+ tiles.add(
+ ListTile(
+ title: Text(L10().purchaseOrders),
+ leading: FaIcon(FontAwesomeIcons.cartShopping),
+ onTap: _purchaseOrders,
+ )
+ );
+ }
+
+ if (tiles.length > 2) {
+ tiles.add(Divider());
+ }
+
+ if (InvenTreeAPI().supportsNotifications) {
+ tiles.add(
+ ListTile(
+ leading: FaIcon(FontAwesomeIcons.bell),
+ title: Text(L10().notifications),
+ onTap: _notifications,
+ )
+ );
+ }
+
+ tiles.add(
+ ListTile(
+ title: Text(L10().settings),
+ leading: Icon(Icons.settings),
+ onTap: _settings,
+ )
+ );
+
+ return tiles;
}
@override
Widget build(BuildContext context) {
- return Drawer(
+ return Drawer(
child: ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: [
- ListTile(
- leading: FaIcon(FontAwesomeIcons.house),
- title: Text(
- L10().appTitle,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- onTap: _home,
- ),
- ListTile(
- title: Text(L10().scanBarcode),
- onTap: _scan,
- leading: Icon(Icons.qr_code_scanner),
- ),
- ListTile(
- title: Text(L10().search),
- leading: FaIcon(FontAwesomeIcons.magnifyingGlass),
- onTap: _search,
- ),
- ListTile(
- title: Text(L10().settings),
- leading: Icon(Icons.settings),
- onTap: _settings,
- ),
- ListTile(
- title: Text(L10().about),
- leading: FaIcon(FontAwesomeIcons.circleInfo),
- onTap: _about,
- )
- ]
- ).toList(),
+ children: drawerTiles(context),
)
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/widget/home.dart b/lib/widget/home.dart
index 8307f157..39c74a86 100644
--- a/lib/widget/home.dart
+++ b/lib/widget/home.dart
@@ -13,15 +13,12 @@ import "package:inventree/settings/login.dart";
import "package:inventree/settings/settings.dart";
import "package:inventree/user_profile.dart";
-import "package:inventree/inventree/notification.dart";
-
import "package:inventree/widget/category_display.dart";
import "package:inventree/widget/drawer.dart";
import "package:inventree/widget/location_display.dart";
-import "package:inventree/widget/notifications.dart";
import "package:inventree/widget/part_list.dart";
import "package:inventree/widget/purchase_order_list.dart";
-import "package:inventree/widget/search.dart";
+import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/spinner.dart";
import "package:inventree/widget/company_list.dart";
@@ -35,7 +32,8 @@ class InvenTreeHomePage extends StatefulWidget {
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
}
-class _InvenTreeHomePageState extends State {
+
+class _InvenTreeHomePageState extends State with BaseWidgetProperties {
_InvenTreeHomePageState() : super() {
// Load display settings
@@ -64,11 +62,7 @@ class _InvenTreeHomePageState extends State {
});
}
- // Index of bottom navigation bar
- int _tabIndex = 0;
-
- // Number of outstanding notifications
- int _notificationCounter = 0;
+ final homeKey = GlobalKey();
bool homeShowPo = false;
bool homeShowSubscribed = false;
@@ -76,8 +70,6 @@ class _InvenTreeHomePageState extends State {
bool homeShowCustomers = false;
bool homeShowSuppliers = false;
- final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
-
// Selected user profile
UserProfile? _profile;
@@ -202,10 +194,10 @@ class _InvenTreeHomePageState extends State {
return;
}
- final notifications = await InvenTreeNotification().list();
+ // final notifications = await InvenTreeNotification().list();
setState(() {
- _notificationCounter = notifications.length;
+ // _notificationCounter = notifications.length;
});
}
@@ -416,78 +408,17 @@ class _InvenTreeHomePageState extends State {
* Return the main body widget for display.
* This depends on the current value of _tabIndex
*/
+ @override
Widget getBody(BuildContext context) {
if (!InvenTreeAPI().isConnected()) {
return _connectionStatusWidget(context);
}
- switch (_tabIndex) {
- case 1: // Search widget
- return SearchWidget(false);
- case 2: // Notification widget
- return NotificationWidget();
- case 0: // Home widget
- default:
- return ListView(
- scrollDirection: Axis.vertical,
- children: getListTiles(context),
- );
- }
- }
-
- /*
- * Construct the bottom navigation bar
- */
- List getNavBarItems(BuildContext context) {
-
- List items = [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.house),
- label: L10().home,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.magnifyingGlass),
- label: L10().search,
- ),
- ];
-
- if (InvenTreeAPI().supportsNotifications) {
- items.add(
- BottomNavigationBarItem(
- icon: _notificationCounter == 0 ? FaIcon(FontAwesomeIcons.bell) : Stack(
- children: [
- FaIcon(FontAwesomeIcons.bell),
- Positioned(
- right: 0,
- child: Container(
- padding: EdgeInsets.all(2),
- decoration: BoxDecoration(
- color: Colors.red,
- borderRadius: BorderRadius.circular(20),
- ),
- constraints: BoxConstraints(
- minWidth: 12,
- minHeight: 12,
- ),
- child: Text(
- "${_notificationCounter}",
- style: TextStyle(
- color: Colors.white,
- fontSize: 9,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- )
- ],
- ),
- label: L10().notifications,
- )
- );
- }
-
- return items;
+ return ListView(
+ scrollDirection: Axis.vertical,
+ children: getListTiles(context),
+ );
}
@override
@@ -497,7 +428,7 @@ class _InvenTreeHomePageState extends State {
var connecting = !connected && InvenTreeAPI().isConnecting();
return Scaffold(
- key: _homeKey,
+ key: homeKey,
appBar: AppBar(
title: Text(L10().appTitle),
actions: [
@@ -512,17 +443,7 @@ class _InvenTreeHomePageState extends State {
),
drawer: InvenTreeDrawer(context),
body: getBody(context),
- bottomNavigationBar: connected ? BottomNavigationBar(
- currentIndex: _tabIndex,
- onTap: (int index) {
- setState(() {
- _tabIndex = index;
- });
-
- _refreshNotifications();
- },
- items: getNavBarItems(context),
- ) : null,
+ bottomNavigationBar: InvenTreeAPI().isConnected() ? buildBottomAppBar(context, homeKey) : null,
);
}
}
diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart
index 7adcc450..f5518a3e 100644
--- a/lib/widget/location_display.dart
+++ b/lib/widget/location_display.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
@@ -40,35 +41,96 @@ class _LocationDisplayState extends RefreshableState {
bool showFilterOptions = false;
@override
- String getAppBarTitle(BuildContext context) { return L10().stockLocation; }
+ String getAppBarTitle() {
+ return L10().stockLocation;
+ }
@override
- List getAppBarActions(BuildContext context) {
-
+ List appBarActions(BuildContext context) {
List actions = [];
- if (location != null) {
-
- // Add "locate" button
- if (api.supportsMixin("locate")) {
- actions.add(
+ // Add "locate" button
+ if (location != null && api.supportsMixin("locate")) {
+ actions.add(
IconButton(
- icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation),
- tooltip: L10().locateLocation,
- onPressed: () async {
- _locateStockLocation(context);
- },
+ icon: Icon(Icons.travel_explore),
+ tooltip: L10().locateLocation,
+ onPressed: () async {
+ api.locateItemOrLocation(context, location: location!.pk);
+ }
)
+ );
+ }
+
+ // Add "edit" button
+ if (location != null && api.checkPermission("stock_location", "change")) {
+ actions.add(
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().editLocation,
+ onPressed: () {
+ _editLocationDialog(context);
+ }
+ )
+ );
+ }
+
+
+ return actions;
+ }
+
+ @override
+ List barcodeButtons(BuildContext context) {
+ List actions = [];
+
+ if (location != null) {
+ // Scan items into this location
+ if (api.checkPermission("stock", "change")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.qrcode),
+ label: L10().barcodeScanItem,
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) =>
+ InvenTreeQRView(
+ StockLocationScanInItemsHandler(location!)))
+ ).then((value) {
+ refresh(context);
+ });
+ }
+ )
);
}
- // Add "edit" button
+ // Scan this location into another one
if (api.checkPermission("stock_location", "change")) {
actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () { _editLocationDialog(context); },
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.qrcode),
+ label: L10().transferStockLocation,
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) =>
+ InvenTreeQRView(
+ ScanParentLocationHandler(location!)))
+ ).then((value) {
+ refresh(context);
+ });
+ }
+ )
+ );
+ }
+
+ // Assign or un-assign barcodes
+ if (api.supportModernBarcodes) {
+ actions.add(
+ customBarcodeAction(
+ context, this,
+ location!.customBarcode, "stocklocation",
+ location!.pk
)
);
}
@@ -77,23 +139,43 @@ class _LocationDisplayState extends RefreshableState {
return actions;
}
- /*
- * Request identification of this location
- */
- Future _locateStockLocation(BuildContext context) async {
+ @override
+ List actionButtons(BuildContext context) {
+ List actions = [];
- final _loc = location;
-
- if (_loc != null) {
- api.locateItemOrLocation(context, location: _loc.pk);
+ // Create new location
+ if (api.checkPermission("stock_location", "add")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.sitemap),
+ label: L10().locationCreate,
+ onTap: () async {
+ _newLocation(context);
+ }
+ )
+ );
}
+
+ // Create new item
+ if (location != null && api.checkPermission("stock", "add")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.boxesStacked),
+ label: L10().stockItemCreate,
+ onTap: () async {
+ _newStockItem(context);
+ }
+ )
+ );
+ }
+
+ return actions;
}
/*
* Launch a dialog form to edit this stock location
*/
void _editLocationDialog(BuildContext context) {
-
final _loc = location;
if (_loc == null) {
@@ -101,12 +183,12 @@ class _LocationDisplayState extends RefreshableState {
}
_loc.editForm(
- context,
- L10().editLocation,
- onSuccess: (data) async {
- refresh(context);
- showSnackIcon(L10().locationUpdated, success: true);
- }
+ context,
+ L10().editLocation,
+ onSuccess: (data) async {
+ refresh(context);
+ showSnackIcon(L10().locationUpdated, success: true);
+ }
);
}
@@ -117,7 +199,6 @@ class _LocationDisplayState extends RefreshableState {
@override
Future request(BuildContext context) async {
-
// Reload location information
if (location != null) {
final bool result = await location!.reload();
@@ -133,35 +214,32 @@ class _LocationDisplayState extends RefreshableState {
}
Future _newLocation(BuildContext context) async {
-
int pk = location?.pk ?? -1;
InvenTreeStockLocation().createForm(
- context,
- L10().locationCreate,
- data: {
- "parent": (pk > 0) ? pk : null,
- },
- onSuccess: (result) async {
+ context,
+ L10().locationCreate,
+ data: {
+ "parent": (pk > 0) ? pk : null,
+ },
+ onSuccess: (result) async {
+ Map data = result as Map;
- Map data = result as Map;
+ if (data.containsKey("pk")) {
+ var loc = InvenTreeStockLocation.fromJson(data);
- if (data.containsKey("pk")) {
- var loc = InvenTreeStockLocation.fromJson(data);
-
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => LocationDisplayWidget(loc)
- )
- );
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => LocationDisplayWidget(loc)
+ )
+ );
+ }
}
- }
);
}
Future _newStockItem(BuildContext context) async {
-
int pk = location?.pk ?? -1;
if (location != null && pk <= 0) {
@@ -169,48 +247,46 @@ class _LocationDisplayState extends RefreshableState {
}
InvenTreeStockItem().createForm(
- context,
- L10().stockItemCreate,
- data: {
- "location": location != null ? pk : null,
- },
- onSuccess: (result) async {
+ context,
+ L10().stockItemCreate,
+ data: {
+ "location": location != null ? pk : null,
+ },
+ onSuccess: (result) async {
+ Map data = result as Map;
- Map data = result as Map;
+ if (data.containsKey("pk")) {
+ var item = InvenTreeStockItem.fromJson(data);
- if (data.containsKey("pk")) {
- var item = InvenTreeStockItem.fromJson(data);
-
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => StockDetailWidget(item)
- )
- );
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => StockDetailWidget(item)
+ )
+ );
+ }
}
- }
);
-
}
Widget locationDescriptionCard({bool includeActions = true}) {
if (location == null) {
return Card(
- child: ListTile(
- title: Text(
- L10().stockTopLevel,
- style: TextStyle(fontStyle: FontStyle.italic)
- ),
- leading: FaIcon(FontAwesomeIcons.boxesStacked),
- )
+ child: ListTile(
+ title: Text(
+ L10().stockTopLevel,
+ style: TextStyle(fontStyle: FontStyle.italic)
+ ),
+ leading: FaIcon(FontAwesomeIcons.boxesStacked),
+ )
);
} else {
-
List children = [
ListTile(
title: Text("${location!.name}"),
subtitle: Text("${location!.description}"),
- leading: location!.customIcon ?? FaIcon(FontAwesomeIcons.boxesStacked),
+ leading: location!.customIcon ??
+ FaIcon(FontAwesomeIcons.boxesStacked),
),
];
@@ -221,19 +297,19 @@ class _LocationDisplayState extends RefreshableState {
subtitle: Text("${location!.parentPathString}"),
leading: FaIcon(FontAwesomeIcons.turnUp, color: COLOR_CLICK),
onTap: () async {
-
int parentId = location?.parentId ?? -1;
if (parentId < 0) {
- Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
+ Navigator.push(context, MaterialPageRoute(
+ builder: (context) => LocationDisplayWidget(null)));
} else {
-
showLoadingOverlay(context);
var loc = await InvenTreeStockLocation().get(parentId);
hideLoadingOverlay();
if (loc is InvenTreeStockLocation) {
- Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
+ Navigator.push(context, MaterialPageRoute(
+ builder: (context) => LocationDisplayWidget(loc)));
}
}
},
@@ -242,63 +318,27 @@ class _LocationDisplayState extends RefreshableState {
}
return Card(
- child: Column(
- children: children,
- )
+ child: Column(
+ children: children,
+ )
);
}
}
@override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.sitemap),
- label: L10().details,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.boxesStacked),
- label: L10().stock,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.wrench),
- label: L10().actions,
- )
- ]
- );
- }
-
- int stockItemCount = 0;
-
- Widget getSelectedWidget(int index) {
-
- switch (index) {
- case 0:
- return Column(
- children: detailTiles(),
- );
- case 1:
- return Column(
- children: stockTiles(),
- );
- case 2:
- return ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: actionTiles()
- ).toList()
- );
- default:
- return ListView();
- }
+ List getTabIcons(BuildContext context) {
+ return [
+ Tab(text: L10().details),
+ Tab(text: L10().stockItems),
+ ];
}
@override
- Widget getBody(BuildContext context) {
- return getSelectedWidget(tabIndex);
+ List getTabs(BuildContext context) {
+ return [
+ Column(children: detailTiles()),
+ Column(children: stockTiles()),
+ ];
}
// Construct the "details" panel
@@ -306,18 +346,18 @@ class _LocationDisplayState extends RefreshableState {
List tiles = [
locationDescriptionCard(),
ListTile(
- title: Text(
- L10().sublocations,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- trailing: GestureDetector(
- child: FaIcon(FontAwesomeIcons.filter),
- onTap: () async {
- setState(() {
- showFilterOptions = !showFilterOptions;
- });
- },
- )
+ title: Text(
+ L10().sublocations,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ trailing: GestureDetector(
+ child: FaIcon(FontAwesomeIcons.filter),
+ onTap: () async {
+ setState(() {
+ showFilterOptions = !showFilterOptions;
+ });
+ },
+ )
),
Expanded(
child: PaginatedStockLocationList(
@@ -335,13 +375,11 @@ class _LocationDisplayState extends RefreshableState {
// Construct the "stock" panel
List stockTiles() {
-
Map filters = {
"location": location?.pk.toString() ?? "null",
};
return [
- locationDescriptionCard(includeActions: false),
ListTile(
title: Text(
L10().stock,
@@ -365,115 +403,4 @@ class _LocationDisplayState extends RefreshableState {
)
];
}
-
- List actionTiles() {
- List tiles = [];
-
- tiles.add(locationDescriptionCard(includeActions: false));
-
- if (api.checkPermission("stock", "add")) {
-
- tiles.add(
- ListTile(
- title: Text(L10().locationCreate),
- subtitle: Text(L10().locationCreateDetail),
- leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
- trailing: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK),
- onTap: () async {
- _newLocation(context);
- },
- )
- );
-
- tiles.add(
- ListTile(
- title: Text(L10().stockItemCreate),
- subtitle: Text(L10().stockItemCreateDetail),
- leading: FaIcon(FontAwesomeIcons.boxesStacked, color: COLOR_CLICK),
- trailing: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK),
- onTap: () async {
- _newStockItem(context);
- },
- )
- );
-
- }
-
- if (location != null) {
-
- // Scan stock item into location
- if (api.checkPermission("stock", "change")) {
- tiles.add(
- ListTile(
- title: Text(L10().barcodeScanItem),
- subtitle: Text(L10().barcodeScanInItems),
- leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK),
- trailing: Icon(Icons.qr_code, color: COLOR_CLICK),
- onTap: () {
-
- var _loc = location;
-
- if (_loc != null) {
- Navigator.push(
- context,
- MaterialPageRoute(builder: (context) =>
- InvenTreeQRView(
- StockLocationScanInItemsHandler(_loc)))
- ).then((value) {
- refresh(context);
- });
- }
- },
- )
- );
-
- // Scan this location into another one
- if (api.checkPermission("stock_location", "change")) {
- tiles.add(
- ListTile(
- title: Text(L10().transferStockLocation),
- subtitle: Text(L10().transferStockLocationDetail),
- leading: FaIcon(FontAwesomeIcons.rightToBracket, color: COLOR_CLICK),
- trailing: Icon(Icons.qr_code, color: COLOR_CLICK),
- onTap: () {
- var _loc = location;
-
- if (_loc != null) {
- Navigator.push(
- context,
- MaterialPageRoute(builder: (context) =>
- InvenTreeQRView(
- ScanParentLocationHandler(_loc)))
- ).then((value) {
- refresh(context);
- });
- }
- }
- )
- );
- }
-
- if (api.supportModernBarcodes) {
- tiles.add(
- customBarcodeActionTile(context, this, location!.customBarcode, "stocklocation", location!.pk)
- );
- }
- }
- }
-
- if (tiles.length <= 1) {
- tiles.add(
- ListTile(
- title: Text(
- L10().actionsNone,
- style: TextStyle(
- fontStyle: FontStyle.italic
- ),
- )
- )
- );
- }
-
- return tiles;
- }
}
diff --git a/lib/widget/location_list.dart b/lib/widget/location_list.dart
index d2762fb8..73c992e6 100644
--- a/lib/widget/location_list.dart
+++ b/lib/widget/location_list.dart
@@ -30,7 +30,7 @@ class _StockLocationListState extends RefreshableState {
bool showFilterOptions = false;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
@@ -42,7 +42,7 @@ class _StockLocationListState extends RefreshableState {
];
@override
- String getAppBarTitle(BuildContext context) => L10().stockLocations;
+ String getAppBarTitle() => L10().stockLocations;
@override
Widget getBody(BuildContext context) {
diff --git a/lib/widget/notifications.dart b/lib/widget/notifications.dart
index 23b8bd65..6b37d831 100644
--- a/lib/widget/notifications.dart
+++ b/lib/widget/notifications.dart
@@ -24,10 +24,7 @@ class _NotificationState extends RefreshableState {
List notifications = [];
@override
- AppBar? buildAppBar(BuildContext context, GlobalKey key) {
- // No app bar for the notification widget
- return null;
- }
+ String getAppBarTitle() => L10().notifications;
@override
Future request (BuildContext context) async {
diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart
index 86b76c10..77f70f5f 100644
--- a/lib/widget/paginator.dart
+++ b/lib/widget/paginator.dart
@@ -409,19 +409,21 @@ abstract class PaginatedSearchState extends Sta
*/
Widget buildSearchInput(BuildContext context) {
return ListTile(
- leading: orderingOptions.isEmpty ? null : GestureDetector(
+ trailing: orderingOptions.isEmpty ? null : GestureDetector(
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_CLICK),
onTap: () async {
_saveOrderingOptions(context);
},
),
- trailing: GestureDetector(
+ leading: GestureDetector(
child: FaIcon(
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
- color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_CLICK,
+ color: searchController.text.isNotEmpty ? COLOR_DANGER : null,
),
onTap: () {
- searchController.clear();
+ if (searchController.text.isEmpty) {
+ searchController.clear();
+ }
updateSearchTerm();
},
),
diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart
index b410a27e..be397dc3 100644
--- a/lib/widget/part_detail.dart
+++ b/lib/widget/part_detail.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
@@ -65,39 +66,62 @@ class _PartDisplayState extends RefreshableState {
int variantCount = 0;
@override
- String getAppBarTitle(BuildContext context) => L10().partDetails;
+ String getAppBarTitle() => L10().partDetails;
@override
- List getAppBarActions(BuildContext context) {
-
+ List appBarActions(BuildContext context) {
List actions = [];
- if (api.checkPermission("part", "view")) {
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.globe),
- onPressed: _openInvenTreePage,
- ),
- );
- }
-
if (api.checkPermission("part", "change")) {
actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () {
- _editPartDialog(context);
- },
- )
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().editPart,
+ onPressed: () {
+ _editPartDialog(context);
+ }
+ )
);
}
+ return actions;
+ }
+
+ @override
+ List barcodeButtons(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("part", "change")) {
+ if (api.supportModernBarcodes) {
+ actions.add(
+ customBarcodeAction(
+ context, this,
+ widget.part.customBarcode, "part",
+ widget.part.pk
+ )
+ );
+ }
+ }
return actions;
}
- Future _openInvenTreePage() async {
- part.goToInvenTreePage();
+ @override
+ List actionButtons(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("stock", "add")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.box),
+ label: L10().stockItemCreate,
+ onTap: () {
+ _newStockItem(context);
+ }
+ )
+ );
+ }
+
+ return actions;
}
@override
@@ -144,20 +168,9 @@ class _PartDisplayState extends RefreshableState {
// Request the number of parameters for this part
if (api.supportsPartParameters) {
-
showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool;
-
- InvenTreePartParameter().count(
- filters: {
- "part": part.pk.toString(),
- }
- ).then((int value) {
- if (mounted) {
- setState(() {
- parameterCount = value;
- });
- }
- });
+ } else {
+ showParameters = false;
}
// Request the number of attachments
@@ -394,18 +407,13 @@ class _PartDisplayState extends RefreshableState {
ListTile(
title: Text(L10().availableStock),
subtitle: Text(L10().stockDetails),
- leading: FaIcon(FontAwesomeIcons.boxesStacked, color: COLOR_CLICK),
+ leading: FaIcon(FontAwesomeIcons.boxesStacked),
trailing: Text(
part.stockString(),
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
- onTap: () {
- setState(() {
- tabIndex = 1;
- });
- },
),
);
@@ -436,14 +444,6 @@ class _PartDisplayState extends RefreshableState {
title: Text(L10().billOfMaterials),
leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK),
trailing: Text(bomCount.toString()),
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => BillOfMaterialsWidget(part)
- )
- );
- }
)
);
}
@@ -543,24 +543,6 @@ class _PartDisplayState extends RefreshableState {
}
}
- if (showParameters) {
- tiles.add(
- ListTile(
- title: Text(L10().parameters),
- leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK),
- trailing: parameterCount > 0 ? Text(parameterCount.toString()) : null,
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => PartParameterWidget(part)
- )
- );
- }
- )
- );
- }
-
// Notes field
tiles.add(
ListTile(
@@ -682,89 +664,49 @@ class _PartDisplayState extends RefreshableState {
}
}
);
-
- }
-
- List actionTiles(BuildContext context) {
- List tiles = [];
-
- tiles.add(headerTile());
-
- tiles.add(
- ListTile(
- title: Text(L10().stockItemCreate),
- leading: FaIcon(FontAwesomeIcons.box),
- onTap: () {
- _newStockItem(context);
- },
- )
- );
-
- if (api.supportModernBarcodes) {
- tiles.add(
- customBarcodeActionTile(context, this, part.customBarcode, "part", part.pk)
- );
- }
-
- return tiles;
- }
-
- int stockItemCount = 0;
-
- Widget getSelectedWidget(int index) {
- switch (index) {
- case 0:
- return Center(
- child: ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: partTiles()
- ).toList()
- ),
- );
- case 1:
- return PaginatedStockItemList(
- {"part": "${part.pk}"},
- true,
- );
- case 2:
- return Center(
- child: ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: actionTiles(context)
- ).toList()
- )
- );
- default:
- return Center();
- }
}
@override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.circleInfo),
- label: L10().details,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.boxesStacked),
- label: L10().stock
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.wrench),
- label: L10().actions,
- ),
- ]
- );
+ List getTabIcons(BuildContext context) {
+ List icons = [
+ Tab(text: L10().details),
+ Tab(text: L10().stock)
+ ];
+
+ if (showBom && part.isAssembly) {
+ icons.add(Tab(text: L10().bom));
+ }
+
+ if (showParameters) {
+ icons.add(Tab(text: L10().parameters));
+ }
+
+ return icons;
}
@override
- Widget getBody(BuildContext context) {
- return getSelectedWidget(tabIndex);
+ List getTabs(BuildContext context) {
+ List tabs = [
+ Center(
+ child: ListView(
+ children: ListTile.divideTiles(
+ context: context,
+ tiles: partTiles()
+ ).toList()
+ )
+ ),
+ PaginatedStockItemList({"part": part.pk.toString()}, true)
+ ];
+
+ if (showBom && part.isAssembly) {
+ tabs.add(PaginatedBomList({"part": part.pk.toString()}, showSearch: true, isParentPart: true));
+ }
+
+ if (showParameters) {
+ tabs.add(PaginatedParameterList({"part": part.pk.toString()}, true));
+ }
+
+ return tabs;
}
+
}
diff --git a/lib/widget/part_image_widget.dart b/lib/widget/part_image_widget.dart
index fc4d0b93..c548e8e0 100644
--- a/lib/widget/part_image_widget.dart
+++ b/lib/widget/part_image_widget.dart
@@ -35,10 +35,10 @@ class _PartImageState extends RefreshableState {
}
@override
- String getAppBarTitle(BuildContext context) => part.fullname;
+ String getAppBarTitle() => part.fullname;
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
List actions = [];
diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart
index 4e2646b6..b6b7bc52 100644
--- a/lib/widget/part_list.dart
+++ b/lib/widget/part_list.dart
@@ -36,10 +36,10 @@ class _PartListState extends RefreshableState {
bool showFilterOptions = false;
@override
- String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts;
+ String getAppBarTitle() => title.isNotEmpty ? title : L10().parts;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
diff --git a/lib/widget/part_notes.dart b/lib/widget/part_notes.dart
index 32bffe93..15b9ee76 100644
--- a/lib/widget/part_notes.dart
+++ b/lib/widget/part_notes.dart
@@ -30,10 +30,10 @@ class _PartNotesState extends RefreshableState {
}
@override
- String getAppBarTitle(BuildContext context) => L10().partNotes;
+ String getAppBarTitle() => L10().partNotes;
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
List actions = [];
diff --git a/lib/widget/part_parameter_widget.dart b/lib/widget/part_parameter_widget.dart
index 50e8d8dc..1adc5235 100644
--- a/lib/widget/part_parameter_widget.dart
+++ b/lib/widget/part_parameter_widget.dart
@@ -24,12 +24,12 @@ class _ParameterWidgetState extends RefreshableState {
_ParameterWidgetState();
@override
- String getAppBarTitle(BuildContext context) {
+ String getAppBarTitle() {
return L10().parameters;
}
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
return [];
}
diff --git a/lib/widget/part_suppliers.dart b/lib/widget/part_suppliers.dart
index 9b1a2789..36b3900b 100644
--- a/lib/widget/part_suppliers.dart
+++ b/lib/widget/part_suppliers.dart
@@ -38,10 +38,10 @@ class _PartSupplierState extends RefreshableState {
}
@override
- String getAppBarTitle(BuildContext context) => L10().partSuppliers;
+ String getAppBarTitle() => L10().partSuppliers;
@override
- List getAppBarActions(BuildContext contexts) {
+ List appBarActions(BuildContext contexts) {
// TODO
return [];
}
diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/purchase_order_detail.dart
index fa1ecb11..f70d09a3 100644
--- a/lib/widget/purchase_order_detail.dart
+++ b/lib/widget/purchase_order_detail.dart
@@ -1,5 +1,4 @@
import "package:flutter/material.dart";
-
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.dart";
@@ -7,12 +6,13 @@ import "package:inventree/api.dart";
import "package:inventree/api_form.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
+import "package:inventree/l10.dart";
+
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/attachment_widget.dart";
import "package:inventree/widget/company_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/l10.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_list.dart";
@@ -41,17 +41,17 @@ class _PurchaseOrderDetailState extends RefreshableState L10().purchaseOrder;
+ String getAppBarTitle() => L10().purchaseOrder;
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
List actions = [];
if (InvenTreeAPI().checkPermission("purchase_order", "change")) {
actions.add(
IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().purchaseOrderEdit,
onPressed: () {
editOrder(context);
}
@@ -145,12 +145,6 @@ class _PurchaseOrderDetailState extends RefreshableState getTabIcons(BuildContext context) {
+ return [
+ Tab(text: L10().details),
+ Tab(text: L10().lineItems),
+ Tab(text: L10().received)
+ ];
}
-
- Widget getSelectedWidget(BuildContext context, int index) {
- switch (index) {
- case 0:
- return ListView(
- children: orderTiles(context)
- );
- case 1:
- return ListView(
- children: lineTiles(context)
- );
- case 2:
- // Stock items received against this order
- Map filters = {
- "purchase_order": "${order.pk}"
- };
-
- return PaginatedStockItemList(filters, true);
-
- default:
- return ListView();
- }
- }
-
+
@override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.circleInfo),
- label: L10().details
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.tableList),
- label: L10().lineItems,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.boxesStacked),
- label: L10().stockItems
- )
- ],
- );
+ List getTabs(BuildContext context) {
+ return [
+ ListView(children: orderTiles(context)),
+ ListView(children: lineTiles(context)),
+ PaginatedStockItemList({"purchase_order": order.pk.toString()}, true),
+ ];
}
}
\ No newline at end of file
diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/purchase_order_list.dart
index 57309c9f..5b9546ab 100644
--- a/lib/widget/purchase_order_list.dart
+++ b/lib/widget/purchase_order_list.dart
@@ -33,10 +33,10 @@ class _PurchaseOrderListWidgetState extends RefreshableState L10().purchaseOrders;
+ String getAppBarTitle() => L10().purchaseOrders;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart
index 184d8b52..1bd1fb36 100644
--- a/lib/widget/refreshable_state.dart
+++ b/lib/widget/refreshable_state.dart
@@ -1,9 +1,12 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:inventree/api.dart";
+import "package:inventree/barcode.dart";
import "package:inventree/widget/back.dart";
import "package:inventree/widget/drawer.dart";
+import "package:inventree/widget/search.dart";
/*
@@ -11,17 +14,22 @@ import "package:inventree/widget/drawer.dart";
*/
mixin BaseWidgetProperties {
- // Return a list of appBar actions (default = None)
- List getAppBarActions(BuildContext context) => [];
+ /*
+ * Return a list of appBar actions
+ * By default, no appBar actions are available
+ */
+ List appBarActions(BuildContext context) => [];
// Return a title for the appBar
- String getAppBarTitle(BuildContext context) { return "--- app bar ---"; }
+ String getAppBarTitle() { return "--- app bar ---"; }
// Function to construct a drawer (override if needed)
Widget getDrawer(BuildContext context) {
return InvenTreeDrawer(context);
}
+ List getTabs(BuildContext context) => [];
+
// Function to construct a body (MUST BE PROVIDED)
Widget getBody(BuildContext context) {
@@ -29,18 +37,145 @@ mixin BaseWidgetProperties {
return ListView();
}
- Widget? getBottomNavBar(BuildContext context) {
- return null;
- }
+ /*
+ * Construct the top AppBar for this view
+ */
AppBar? buildAppBar(BuildContext context, GlobalKey key) {
+
+ List tabs = getTabIcons(context);
+
return AppBar(
- title: Text(getAppBarTitle(context)),
- actions: getAppBarActions(context),
+ centerTitle: false,
+ bottom: tabs.isEmpty ? null : TabBar(tabs: tabs),
+ title: Text(getAppBarTitle()),
+ actions: appBarActions(context),
leading: backButton(context, key),
);
}
+ /*
+ * Construct a global navigation bar at the bottom of the screen
+ * - Button to access navigation menu
+ * - Button to access global search
+ * - Button to access barcode scan
+ */
+ BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey key) {
+
+ const double iconSize = 32;
+ const Color iconColor = Colors.blueGrey;
+
+ List icons = [
+ IconButton(
+ icon: Icon(Icons.menu, color: iconColor),
+ iconSize: iconSize,
+ onPressed: () {
+ if (key.currentState != null) {
+ key.currentState!.openDrawer();
+ }
+ },
+ ),
+ IconButton(
+ icon: Icon(Icons.search, color: iconColor),
+ iconSize: iconSize,
+ onPressed: () {
+ if (InvenTreeAPI().checkConnection()) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => SearchWidget(true)
+ )
+ );
+ }
+ },
+ ),
+ IconButton(
+ icon: Icon(Icons.qr_code_scanner, color: iconColor),
+ iconSize: iconSize,
+ onPressed: () {
+ if (InvenTreeAPI().checkConnection()) {
+ scanQrCode(context);
+ }
+ },
+ )
+ ];
+
+ return BottomAppBar(
+ shape: CircularNotchedRectangle(),
+ notchMargin: 20,
+ child: IconTheme(
+ data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: icons,
+ )
+ )
+ );
+ }
+
+ /*
+ * Build out a set of SpeedDialChild widgets, to serve as "actions" for this view
+ * Should be re-implemented by particular view with the required actions
+ * By default, returns an empty list, and thus nothing will be rendered
+ */
+ List actionButtons(BuildContext context) => [];
+
+ /*
+ * Build out a set of barcode actions available for this view
+ */
+ List barcodeButtons(BuildContext context) => [];
+
+ /*
+ * Build out action buttons for a given widget
+ */
+ Widget? buildSpeedDial(BuildContext context) {
+
+ final actions = actionButtons(context);
+ final barcodeActions = barcodeButtons(context);
+
+ if (actions.isEmpty && barcodeActions.isEmpty) {
+ return null;
+ }
+
+ List children = [];
+
+ if (barcodeActions.isNotEmpty) {
+ children.add(
+ SpeedDial(
+ icon: Icons.qr_code_scanner,
+ activeIcon: Icons.close,
+ children: barcodeActions,
+ spacing: 14,
+ childPadding: const EdgeInsets.all(5),
+ spaceBetweenChildren: 15,
+ )
+ );
+ }
+
+ if (actions.isNotEmpty) {
+ children.add(
+ SpeedDial(
+ icon: Icons.more_horiz,
+ activeIcon: Icons.close,
+ children: actions,
+ spacing: 14,
+ childPadding: const EdgeInsets.all(5),
+ spaceBetweenChildren: 15,
+ )
+ );
+ }
+
+ return Wrap(
+ direction: Axis.horizontal,
+ children: children,
+ spacing: 15,
+ );
+ }
+
+ // Return list of "tabs" for this widget
+ List getTabIcons(BuildContext context) => [];
+
}
@@ -57,9 +192,6 @@ abstract class RefreshableState extends State with
// Storage for context once "Build" is called
late BuildContext? _context;
- // Current tab index (used for widgets which display bottom tabs)
- int tabIndex = 0;
-
// Bool indicator
bool loading = false;
@@ -68,16 +200,6 @@ abstract class RefreshableState extends State with
// Helper function to return API instance
InvenTreeAPI get api => InvenTreeAPI();
- // Update current tab selection
- void onTabSelectionChanged(int index) {
-
- if (mounted) {
- setState(() {
- tabIndex = index;
- });
- }
- }
-
@override
void initState() {
super.initState();
@@ -124,21 +246,38 @@ abstract class RefreshableState extends State with
// Save the context for future use
_context = context;
- return Scaffold(
+ List tabs = getTabIcons(context);
+
+ Widget body = tabs.isEmpty ? getBody(context) : TabBarView(children: getTabs(context));
+
+ Scaffold view = Scaffold(
key: refreshableKey,
appBar: buildAppBar(context, refreshableKey),
drawer: getDrawer(context),
+ floatingActionButton: buildSpeedDial(context),
+ floatingActionButtonLocation: FloatingActionButtonLocation
+ .miniEndDocked,
body: Builder(
- builder: (BuildContext context) {
- return RefreshIndicator(
- onRefresh: () async {
- refresh(context);
- },
- child: getBody(context)
- );
- }
+ builder: (BuildContext context) {
+ return RefreshIndicator(
+ onRefresh: () async {
+ refresh(context);
+ },
+ child: body
+ );
+ }
),
- bottomNavigationBar: getBottomNavBar(context),
+ bottomNavigationBar: buildBottomAppBar(context, refreshableKey),
);
+
+ // Default implementation is *not* tabbed
+ if (tabs.isNotEmpty) {
+ return DefaultTabController(
+ length: tabs.length,
+ child: view,
+ );
+ } else {
+ return view;
+ }
}
}
\ No newline at end of file
diff --git a/lib/widget/search.dart b/lib/widget/search.dart
index 39741430..2f7ddb30 100644
--- a/lib/widget/search.dart
+++ b/lib/widget/search.dart
@@ -54,7 +54,7 @@ class _SearchDisplayState extends RefreshableState {
}
@override
- String getAppBarTitle(BuildContext context) => L10().search;
+ String getAppBarTitle() => L10().search;
@override
AppBar? buildAppBar(BuildContext context, GlobalKey key) {
diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart
index 950867cb..e274608b 100644
--- a/lib/widget/stock_detail.dart
+++ b/lib/widget/stock_detail.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
@@ -43,42 +44,34 @@ class _StockItemDisplayState extends RefreshableState {
_StockItemDisplayState();
@override
- String getAppBarTitle(BuildContext context) => L10().stockItem;
+ String getAppBarTitle() => L10().stockItem;
bool stockShowHistory = false;
@override
- List getAppBarActions(BuildContext context) {
-
+ List appBarActions(BuildContext context) {
List actions = [];
- if (InvenTreeAPI().checkPermission("stock", "view")) {
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.globe),
- onPressed: _openInvenTreePage,
- )
- );
- }
-
- if (InvenTreeAPI().supportsMixin("locate")) {
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation),
- tooltip: L10().locateItem,
- onPressed: () async {
- InvenTreeAPI().locateItemOrLocation(context, item: widget.item.pk);
- },
- )
- );
- }
-
- if (InvenTreeAPI().checkPermission("stock", "change")) {
+ if (api.supportsMixin("locate")) {
actions.add(
IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () { _editStockItem(context); },
+ icon: Icon(Icons.travel_explore),
+ tooltip: L10().locateItem,
+ onPressed: () async {
+ api.locateItemOrLocation(context, item: widget.item.pk);
+ }
+ )
+ );
+ }
+
+ if (api.checkPermission("stock", "change")) {
+ actions.add(
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().editItem,
+ onPressed: () {
+ _editStockItem(context);
+ }
)
);
}
@@ -86,8 +79,115 @@ class _StockItemDisplayState extends RefreshableState {
return actions;
}
- Future _openInvenTreePage() async {
- widget.item.goToInvenTreePage();
+ @override
+ List actionButtons(BuildContext context) {
+
+ List actions = [];
+
+ if (api.checkPermission("stock", "change")) {
+
+ // Stock adjustment actions available if item is *not* serialized
+ if (!widget.item.isSerialized()) {
+
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.circleCheck, color: Colors.blue),
+ label: L10().countStock,
+ onTap: _countStockDialog,
+ )
+ );
+
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.circleMinus, color: Colors.red),
+ label: L10().removeStock,
+ onTap: _removeStockDialog,
+ )
+ );
+
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.circlePlus, color: Colors.green),
+ label: L10().addStock,
+ onTap: _addStockDialog,
+ )
+ );
+ }
+
+ // Transfer item
+ actions.add(
+ SpeedDialChild(
+ child: Icon(Icons.trolley),
+ label: L10().transferStock,
+ onTap: () {
+ _transferStockDialog(context);
+ }
+ )
+ );
+ }
+
+ if (labels.isNotEmpty) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.print),
+ label: L10().printLabel,
+ onTap: () {
+ _printLabel(context);
+ }
+ )
+ );
+ }
+
+ if (api.checkPermission("stock", "delete")) {
+ actions.add(
+ SpeedDialChild(
+ child: FaIcon(FontAwesomeIcons.trashCan, color: Colors.red),
+ label: L10().stockItemDelete,
+ onTap: () {
+ _deleteItem(context);
+ }
+ )
+ );
+ }
+
+ return actions;
+ }
+
+ @override
+ List barcodeButtons(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("stock", "change")) {
+ // Scan item into location
+ actions.add(
+ SpeedDialChild(
+ child: Icon(Icons.qr_code_scanner),
+ label: L10().scanIntoLocation,
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) =>
+ InvenTreeQRView(
+ StockItemScanIntoLocationHandler(widget.item)))
+ ).then((ctx) {
+ refresh(context);
+ });
+ }
+ )
+ );
+
+ if (api.supportModernBarcodes) {
+ actions.add(
+ customBarcodeAction(
+ context, this,
+ widget.item.customBarcode,
+ "stockitem", widget.item.pk
+ )
+ );
+ }
+ }
+
+ return actions;
}
// Is label printing enabled for this StockItem?
@@ -740,170 +840,13 @@ class _StockItemDisplayState extends RefreshableState {
return tiles;
}
- List actionTiles(BuildContext context) {
- List tiles = [];
-
- tiles.add(headerTile());
-
- // First check that the user has the required permissions to adjust stock
- if (!InvenTreeAPI().checkPermission("stock", "change")) {
- tiles.add(
- ListTile(
- title: Text(L10().permissionRequired),
- leading: FaIcon(FontAwesomeIcons.userXmark)
- )
- );
-
- tiles.add(
- ListTile(
- subtitle: Text(L10().permissionAccountDenied),
- )
- );
-
- return tiles;
- }
-
- // "Count" is not available for serialized stock
- if (!widget.item.isSerialized()) {
- tiles.add(
- ListTile(
- title: Text(L10().countStock),
- leading: FaIcon(FontAwesomeIcons.circleCheck, color: COLOR_CLICK),
- onTap: _countStockDialog,
- trailing: Text(widget.item.quantityString(includeUnits: true)),
- )
- );
-
- tiles.add(
- ListTile(
- title: Text(L10().removeStock),
- leading: FaIcon(FontAwesomeIcons.circleMinus, color: COLOR_CLICK),
- onTap: _removeStockDialog,
- )
- );
-
- tiles.add(
- ListTile(
- title: Text(L10().addStock),
- leading: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK),
- onTap: _addStockDialog,
- )
- );
- }
-
- tiles.add(
- ListTile(
- title: Text(L10().transferStock),
- subtitle: Text(L10().transferStockDetail),
- leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK),
- onTap: () { _transferStockDialog(context); },
- )
- );
-
- // Scan item into a location
- tiles.add(
- ListTile(
- title: Text(L10().scanIntoLocation),
- subtitle: Text(L10().scanIntoLocationDetail),
- leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK),
- trailing: Icon(Icons.qr_code_scanner),
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(widget.item)))
- ).then((ctx) {
- refresh(context);
- });
- },
- )
- );
-
- if (InvenTreeAPI().supportModernBarcodes || widget.item.customBarcode.isEmpty) {
- tiles.add(customBarcodeActionTile(context, this, widget.item.customBarcode, "stockitem", widget.item.pk));
- } else {
- // Note: Custom legacy barcodes (only for StockItem model) are handled differently
- tiles.add(
- ListTile(
- title: Text(L10().barcodeUnassign),
- leading: Icon(Icons.qr_code, color: COLOR_CLICK),
- onTap: () async {
- await widget.item.update(values: {"uid": ""});
- refresh(context);
- }
- )
- );
- }
-
-
- // Print label (if label printing plugins exist)
- if (labels.isNotEmpty) {
- tiles.add(
- ListTile(
- title: Text(L10().printLabel),
- leading: FaIcon(FontAwesomeIcons.print, color: COLOR_CLICK),
- onTap: () {
- _printLabel(context);
- },
- ),
- );
- }
-
- // If the user has permission to delete this stock item
- if (InvenTreeAPI().checkPermission("stock", "delete")) {
- tiles.add(
- ListTile(
- title: Text("Delete Stock Item"),
- leading: FaIcon(FontAwesomeIcons.trashCan, color: COLOR_DANGER),
- onTap: () {
- _deleteItem(context);
- },
- )
- );
- }
-
- return tiles;
- }
-
- @override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.circleInfo),
- label: L10().details,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.wrench),
- label: L10().actions, ),
- ]
- );
- }
-
- Widget getSelectedWidget(int index) {
- switch (index) {
- case 0:
- return ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: detailTiles()
- ).toList(),
- );
- case 1:
- return ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: actionTiles(context)
- ).toList()
- );
- default:
- return ListView();
- }
- }
-
@override
Widget getBody(BuildContext context) {
- return getSelectedWidget(tabIndex);
+ return ListView(
+ children: ListTile.divideTiles(
+ context: context,
+ tiles: detailTiles()
+ ).toList()
+ );
}
}
\ No newline at end of file
diff --git a/lib/widget/stock_item_history.dart b/lib/widget/stock_item_history.dart
index c2284c0f..b03263ff 100644
--- a/lib/widget/stock_item_history.dart
+++ b/lib/widget/stock_item_history.dart
@@ -24,7 +24,7 @@ class _StockItemHistoryDisplayState extends RefreshableState L10().stockItemHistory;
+ String getAppBarTitle() => L10().stockItemHistory;
List history = [];
diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart
index 3d602a9a..817e7c37 100644
--- a/lib/widget/stock_item_test_results.dart
+++ b/lib/widget/stock_item_test_results.dart
@@ -28,10 +28,10 @@ class _StockItemTestResultDisplayState extends RefreshableState L10().testResults;
+ String getAppBarTitle() => L10().testResults;
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
return [
IconButton(
icon: FaIcon(FontAwesomeIcons.circlePlus),
diff --git a/lib/widget/stock_list.dart b/lib/widget/stock_list.dart
index 41c95f6c..70c3928a 100644
--- a/lib/widget/stock_list.dart
+++ b/lib/widget/stock_list.dart
@@ -30,10 +30,10 @@ class _StockListState extends RefreshableState {
bool showFilterOptions = false;
@override
- String getAppBarTitle(BuildContext context) => L10().stockItems;
+ String getAppBarTitle() => L10().stockItems;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
diff --git a/lib/widget/stock_notes.dart b/lib/widget/stock_notes.dart
index 82e5684b..c990d0ef 100644
--- a/lib/widget/stock_notes.dart
+++ b/lib/widget/stock_notes.dart
@@ -27,7 +27,7 @@ class _StockNotesState extends RefreshableState {
final InvenTreeStockItem item;
@override
- String getAppBarTitle(BuildContext context) => L10().stockItemNotes;
+ String getAppBarTitle() => L10().stockItemNotes;
@override
Future request(BuildContext context) async {
@@ -37,7 +37,7 @@ class _StockNotesState extends RefreshableState {
}
@override
- List getAppBarActions(BuildContext context) {
+ List appBarActions(BuildContext context) {
List actions = [];
if (InvenTreeAPI().checkPermission("stock", "change")) {
diff --git a/lib/widget/supplier_part_detail.dart b/lib/widget/supplier_part_detail.dart
index a2956584..9435e64e 100644
--- a/lib/widget/supplier_part_detail.dart
+++ b/lib/widget/supplier_part_detail.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
@@ -35,39 +36,64 @@ class _SupplierPartDisplayState extends RefreshableState L10().supplierPart;
-
- @override
- List getAppBarActions(BuildContext context) {
- List actions = [];
-
- actions.add(
- IconButton(
- icon: FaIcon(FontAwesomeIcons.penToSquare),
- tooltip: L10().edit,
- onPressed: () {
- editSupplierPart(context);
- },
- )
- );
-
- return actions;
- }
+ String getAppBarTitle() => L10().supplierPart;
/*
* Launch a form to edit the current SupplierPart instance
*/
Future editSupplierPart(BuildContext context) async {
widget.supplierPart.editForm(
- context,
- L10().supplierPartEdit,
- onSuccess: (data) async {
- refresh(context);
- showSnackIcon(L10().supplierPartUpdated, success: true);
- }
+ context,
+ L10().supplierPartEdit,
+ onSuccess: (data) async {
+ refresh(context);
+ showSnackIcon(L10().supplierPartUpdated, success: true);
+ }
);
}
+ @override
+ List barcodeButtons(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("purchase_order", "change") ||
+ api.checkPermission("sales_order", "change") ||
+ api.checkPermission("return_order", "change")) {
+
+ actions.add(
+ customBarcodeAction(
+ context, this,
+ widget.supplierPart.customBarcode,
+ "supplierpart",
+ widget.supplierPart.pk
+ )
+ );
+ }
+
+ return actions;
+ }
+
+ @override
+ List appBarActions(BuildContext context) {
+ List actions = [];
+
+ if (api.checkPermission("purchase_order", "change") ||
+ api.checkPermission("sales_order", "change") ||
+ api.checkPermission("return_order", "change")) {
+ actions.add(
+ IconButton(
+ icon: Icon(Icons.edit_square),
+ tooltip: L10().edit,
+ onPressed: () {
+ editSupplierPart(context);
+ }
+ )
+ );
+ }
+
+ return actions;
+ }
+
@override
Future request(BuildContext context) async {
final bool result = widget.supplierPart.pk > 0 && await widget.supplierPart.reload();
@@ -179,60 +205,14 @@ class _SupplierPartDisplayState extends RefreshableState actionTiles(BuildContext context) {
- List tiles = [];
-
- tiles.add(
- customBarcodeActionTile(context, this, widget.supplierPart.customBarcode, "supplierpart", widget.supplierPart.pk)
- );
-
- return tiles;
- }
-
- Widget getSelectedWidget(int index) {
- switch (index) {
- case 0:
- return ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: detailTiles(context),
- ).toList()
- );
- case 1:
- return ListView(
- children: ListTile.divideTiles(
- context: context,
- tiles: actionTiles(context)
- ).toList()
- );
- default:
- return ListView();
- }
- }
-
@override
Widget getBody(BuildContext context) {
- return getSelectedWidget(tabIndex);
- }
-
- @override
- Widget getBottomNavBar(BuildContext context) {
- return BottomNavigationBar(
- currentIndex: tabIndex,
- onTap: onTabSelectionChanged,
- items: [
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.circleInfo),
- label: L10().details,
- ),
- BottomNavigationBarItem(
- icon: FaIcon(FontAwesomeIcons.wrench),
- label: L10().actions
- )
- ]
+ return ListView(
+ children: ListTile.divideTiles(
+ context: context,
+ tiles: detailTiles(context),
+ ).toList()
);
}
+
}
\ No newline at end of file
diff --git a/lib/widget/supplier_part_list.dart b/lib/widget/supplier_part_list.dart
index 7a7ea8c1..a35c8ec4 100644
--- a/lib/widget/supplier_part_list.dart
+++ b/lib/widget/supplier_part_list.dart
@@ -29,12 +29,12 @@ class SupplierPartList extends StatefulWidget {
class _SupplierPartListState extends RefreshableState {
@override
- String getAppBarTitle(BuildContext context) => L10().supplierParts;
+ String getAppBarTitle() => L10().supplierParts;
bool showFilterOptions = false;
@override
- List getAppBarActions(BuildContext context) => [
+ List appBarActions(BuildContext context) => [
IconButton(
icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async {
diff --git a/pubspec.lock b/pubspec.lock
index efea81ba..336cbeb4 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -395,6 +395,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
+ flutter_speed_dial:
+ dependency: "direct main"
+ description:
+ name: flutter_speed_dial
+ sha256: "41d7ad0bc224248637b3a5e0b9083e912a75445bdb450cf82b8ed06d7af7c61d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
diff --git a/pubspec.yaml b/pubspec.yaml
index b0fd2da8..634682a0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -24,6 +24,7 @@ dependencies:
flutter_localized_locales: ^2.0.4
flutter_markdown: ^0.6.13+1 # Rendering markdown
flutter_overlay_loader: ^2.0.0 # Overlay screen support
+ flutter_speed_dial: ^6.2.0 # Speed dial / FAB implementation
font_awesome_flutter: ^10.3.0 # FontAwesome icon set
http: ^0.13.4
image_picker: ^0.8.6+1 # Select or take photos