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