2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 05:26:47 +00:00

UX Overhaul (#300)

* Add "global actions" to title bar

* Implement actions

* Add "speed dial" action buttons

* tweak global action icons

* Refactor actions for "stock item" display

* Refactor "part" detail

* part category

* SupplierPart

* More updates

* Add BottomAppBar

* Add a global bottom app bar

* Move "edit" buttons back to the app bar

* tweaks

* Updates to drawer navigation menu

* home screen improvements

* text tweaks

* Fix appBarTitle for notifications widget

* Update "tabs" for category display

* Fix for attachment widget

* Update tabs for purchaseorder view

* Update part display

* Cleanup

* Add "BOM" tab to part detail widget

* Paginated list search cleanup

* Update release notes

* Update old function

* linting

* linting

* Tweaks to bottomappbar

- Increase icon size slightly
- Adjust "actions" icon
This commit is contained in:
Oliver 2023-04-08 23:59:11 +10:00 committed by GitHub
parent 74176cdda8
commit a8f87e2f5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 979 additions and 1159 deletions

View File

@ -1,9 +1,12 @@
## InvenTree App Release Notes ## 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 - Support barcode scanning for purchase orders
### 0.10.2 - March 2023 ### 0.10.2 - March 2023

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>9.0</string> <string>11.0</string>
</dict> </dict>
</plist> </plist>

View File

@ -42,4 +42,4 @@ post_install do |installer|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end end
end end
end end

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 50; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -236,6 +236,7 @@
}; };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -250,6 +251,7 @@
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -276,17 +278,17 @@
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework", "${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework", "${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework",
"${BUILT_PRODUCTS_DIR}/camera/camera.framework", "${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
"${BUILT_PRODUCTS_DIR}/open_file/open_file.framework", "${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.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}/qr_code_scanner/qr_code_scanner.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.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}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.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}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.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}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.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}/image_picker_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.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}/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}/qr_code_scanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.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}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
); );
@ -392,7 +394,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -415,7 +417,7 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -476,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -523,7 +525,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -546,7 +548,7 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -577,7 +579,7 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@ -71,5 +71,9 @@
<array> <array>
<string>https</string> <string>https</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -1,12 +1,12 @@
import "dart:io"; import "dart:io";
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/purchase_order_detail.dart"; import "package:inventree/widget/purchase_order_detail.dart";
import "package:one_context/one_context.dart"; import "package:one_context/one_context.dart";
import "package:qr_code_scanner/qr_code_scanner.dart"; import "package:qr_code_scanner/qr_code_scanner.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
@ -805,17 +805,12 @@ Future<void> scanQrCode(BuildContext context) async {
} }
/* SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state, String barcode, String model, int pk) {
* 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) {
if (barcode.isEmpty) { if (barcode.isEmpty) {
return ListTile( return SpeedDialChild(
title: Text(L10().barcodeAssign), label: L10().barcodeAssign,
subtitle: Text(L10().barcodeAssignDetail), child: Icon(Icons.barcode_reader),
leading: Icon(Icons.qr_code, color: COLOR_CLICK),
trailing: Icon(Icons.qr_code_scanner),
onTap: () { onTap: () {
var handler = UniqueBarcodeHandler((String barcode) { var handler = UniqueBarcodeHandler((String barcode) {
InvenTreeAPI().linkBarcode({ InvenTreeAPI().linkBarcode({
@ -823,8 +818,8 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
"barcode": barcode, "barcode": barcode,
}).then((bool result) { }).then((bool result) {
showSnackIcon( showSnackIcon(
result ? L10().barcodeAssigned : L10().barcodeNotAssigned, result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
success: result success: result
); );
state.refresh(context); state.refresh(context);
@ -832,18 +827,18 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
}); });
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => InvenTreeQRView(handler) builder: (context) => InvenTreeQRView(handler)
) )
); );
} }
); );
} else { } else {
return ListTile( return SpeedDialChild(
title: Text(L10().barcodeUnassign), child: Icon(Icons.barcode_reader),
leading: Icon(Icons.qr_code, color: COLOR_CLICK), label: L10().barcodeUnassign,
onTap: () async { onTap: () {
InvenTreeAPI().unlinkBarcode({ InvenTreeAPI().unlinkBarcode({
model: pk.toString() model: pk.toString()
}).then((bool result) { }).then((bool result) {
@ -854,7 +849,7 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str
state.refresh(context); state.refresh(context);
}); });
}, }
); );
} }
} }

View File

@ -1,8 +1,10 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:package_info_plus/package_info_plus.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/settings/about.dart";
import "package:inventree/settings/app_settings.dart"; import "package:inventree/settings/app_settings.dart";
import "package:inventree/settings/home_settings.dart"; import "package:inventree/settings/home_settings.dart";
import "package:inventree/settings/login.dart"; import "package:inventree/settings/login.dart";
@ -22,6 +24,16 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
final _scaffoldKey = GlobalKey<ScaffoldState>(); final _scaffoldKey = GlobalKey<ScaffoldState>();
/*
* Load "About" widget
*/
Future<void> _about() async {
PackageInfo.fromPlatform().then((PackageInfo info) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)));
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -31,9 +43,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
), ),
body: Center( body: Center(
child: ListView( child: ListView(
children: ListTile.divideTiles( children: [
context: context,
tiles: <Widget>[
ListTile( ListTile(
title: Text(L10().server), title: Text(L10().server),
subtitle: Text(L10().configureServer), subtitle: Text(L10().configureServer),
@ -65,9 +75,14 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget())); Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget()));
} }
),
Divider(),
ListTile(
title: Text(L10().about),
leading: FaIcon(FontAwesomeIcons.circleInfo),
onTap: _about,
) )
] ]
).toList()
) )
) )
); );

View File

@ -43,33 +43,35 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
List<InvenTreeAttachment> attachments = []; List<InvenTreeAttachment> attachments = [];
@override @override
String getAppBarTitle(BuildContext context) => L10().attachments; String getAppBarTitle() => L10().attachments;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
if (!widget.hasUploadPermission) return [];
List<Widget> actions = []; return [
IconButton(
if (widget.hasUploadPermission) { icon: FaIcon(FontAwesomeIcons.camera),
// File upload onPressed: () async {
actions.add( FilePickerDialog.pickImageFromCamera().then((File? file) {
IconButton( upload(context, file);
icon: FaIcon(FontAwesomeIcons.circlePlus), });
onPressed: () async { }
FilePickerDialog.pickFile( ),
onPicked: (File file) async { IconButton(
await upload(context, file); icon: FaIcon(FontAwesomeIcons.fileArrowUp),
} onPressed: () async {
); FilePickerDialog.pickFileFromDevice().then((File? file) {
}, upload(context, file);
) });
); }
} )
];
return actions;
} }
Future<void> upload(BuildContext context, File file) async { Future<void> upload(BuildContext context, File? file) async {
if (file == null) return;
showLoadingOverlay(context); showLoadingOverlay(context);
final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId);

View File

@ -37,7 +37,7 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) { String getAppBarTitle() {
if (widget.isParentComponent) { if (widget.isParentComponent) {
return L10().billOfMaterials; return L10().billOfMaterials;
} else { } else {
@ -46,7 +46,7 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
} }
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {

View File

@ -1,7 +1,7 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
@ -33,27 +33,56 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().partCategory; String getAppBarTitle() => L10().partCategory;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> 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<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> 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( actions.add(
IconButton( SpeedDialChild(
icon: FaIcon(FontAwesomeIcons.penToSquare), child: FaIcon(FontAwesomeIcons.sitemap),
tooltip: L10().edit, label: L10().categoryCreateDetail,
onPressed: () { onTap: () {
_editCategoryDialog(context); _newCategory(context);
}, }
) )
); );
} }
return actions; return actions;
} }
void _editCategoryDialog(BuildContext context) { void _editCategoryDialog(BuildContext context) {
@ -154,25 +183,20 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
@override @override
Widget getBottomNavBar(BuildContext context) { List<Widget> getTabIcons(BuildContext context) {
return BottomNavigationBar(
currentIndex: tabIndex, return [
onTap: onTabSelectionChanged, Tab(text: L10().details),
items: <BottomNavigationBarItem>[ Tab(text: L10().parts),
BottomNavigationBarItem( ];
icon: FaIcon(FontAwesomeIcons.sitemap), }
label: L10().details,
), @override
BottomNavigationBarItem( List<Widget> getTabs(BuildContext context) {
icon: FaIcon(FontAwesomeIcons.shapes), return [
label: L10().parts, Column(children: detailTiles()),
), Column(children: partsTiles()),
BottomNavigationBarItem( ];
icon: FaIcon(FontAwesomeIcons.wrench),
label: L10().actions
),
]
);
} }
// Construct the "details" panel // Construct the "details" panel
@ -216,7 +240,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
}; };
return [ return [
getCategoryDescriptionCard(extra: false),
ListTile( ListTile(
title: Text( title: Text(
L10().parts, L10().parts,
@ -298,74 +321,4 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
); );
} }
List<Widget> actionTiles(BuildContext context) {
List<Widget> 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();
}
}
} }

View File

@ -29,7 +29,7 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {
@ -41,7 +41,7 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> {
]; ];
@override @override
String getAppBarTitle(BuildContext context) => L10().partCategories; String getAppBarTitle() => L10().partCategories;
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
@ -40,34 +41,36 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
int attachmentCount = 0; int attachmentCount = 0;
@override @override
String getAppBarTitle(BuildContext context) => L10().company; String getAppBarTitle() => L10().company;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
actions.add( if (api.checkPermission("purchase_order", "change") ||
IconButton( api.checkPermission("sales_order", "change") ||
icon: FaIcon(FontAwesomeIcons.globe), api.checkPermission("return_order", "change")) {
onPressed: () async { actions.add(
widget.company.goToInvenTreePage(); IconButton(
}, icon: Icon(Icons.edit_square),
) tooltip: L10().companyEdit,
); onPressed: () {
editCompany(context);
}
)
);
}
return actions;
}
actions.add( @override
IconButton( List<SpeedDialChild> actionButtons(BuildContext context) {
icon: FaIcon(FontAwesomeIcons.penToSquare), List<SpeedDialChild> actions = [];
tooltip: L10().edit,
onPressed: () { // TODO
editCompany(context);
}
)
);
return actions; return actions;
} }
@override @override

View File

@ -32,7 +32,7 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
_CompanyListWidgetState(); _CompanyListWidgetState();
@override @override
String getAppBarTitle(BuildContext context) => widget.title; String getAppBarTitle() => widget.title;
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {

View File

@ -1,23 +1,17 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:package_info_plus/package_info_plus.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/barcode.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/settings/about.dart";
import "package:inventree/settings/settings.dart"; import "package:inventree/settings/settings.dart";
import "package:inventree/widget/category_display.dart";
import "package:inventree/widget/search.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. * 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 { class InvenTreeDrawer extends StatelessWidget {
@ -42,90 +36,126 @@ class InvenTreeDrawer extends StatelessWidget {
} }
} }
void _search() { // Load "parts" page
void _parts() {
if (!InvenTreeAPI().checkConnection()) return; _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(); _closeDrawer();
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SearchWidget(true) builder: (context) => PurchaseOrderListWidget(filters: {})
) )
); );
} }
/* // Load notifications screen
* Launch the camera to scan a QR code. void _notifications() {
* Upon successful scan, data are passed off to be decoded.
*/
Future <void> _scan() async {
if (!InvenTreeAPI().checkConnection()) return;
_closeDrawer(); _closeDrawer();
scanQrCode(context); Navigator.push(context, MaterialPageRoute(builder: (context) => NotificationWidget()));
} }
/* // Load settings widget
* Load settings widget
*/
void _settings() { void _settings() {
_closeDrawer(); _closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget())); Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
} }
/* // Construct list of tiles to display in the "drawer" menu
* Load "About" widget List<Widget> drawerTiles(BuildContext context) {
*/ List<Widget> tiles = [];
Future<void> _about() async {
_closeDrawer();
PackageInfo.fromPlatform().then((PackageInfo info) { // "Home" access
Navigator.push(context, tiles.add(ListTile(
MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info))); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return Drawer(
child: ListView( child: ListView(
children: ListTile.divideTiles( children: drawerTiles(context),
context: context,
tiles: <Widget>[
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(),
) )
); );
} }
} }

View File

@ -13,15 +13,12 @@ import "package:inventree/settings/login.dart";
import "package:inventree/settings/settings.dart"; import "package:inventree/settings/settings.dart";
import "package:inventree/user_profile.dart"; import "package:inventree/user_profile.dart";
import "package:inventree/inventree/notification.dart";
import "package:inventree/widget/category_display.dart"; import "package:inventree/widget/category_display.dart";
import "package:inventree/widget/drawer.dart"; import "package:inventree/widget/drawer.dart";
import "package:inventree/widget/location_display.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/part_list.dart";
import "package:inventree/widget/purchase_order_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/snacks.dart";
import "package:inventree/widget/spinner.dart"; import "package:inventree/widget/spinner.dart";
import "package:inventree/widget/company_list.dart"; import "package:inventree/widget/company_list.dart";
@ -35,7 +32,8 @@ class InvenTreeHomePage extends StatefulWidget {
_InvenTreeHomePageState createState() => _InvenTreeHomePageState(); _InvenTreeHomePageState createState() => _InvenTreeHomePageState();
} }
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetProperties {
_InvenTreeHomePageState() : super() { _InvenTreeHomePageState() : super() {
// Load display settings // Load display settings
@ -64,11 +62,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
}); });
} }
// Index of bottom navigation bar final homeKey = GlobalKey<ScaffoldState>();
int _tabIndex = 0;
// Number of outstanding notifications
int _notificationCounter = 0;
bool homeShowPo = false; bool homeShowPo = false;
bool homeShowSubscribed = false; bool homeShowSubscribed = false;
@ -76,8 +70,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
bool homeShowCustomers = false; bool homeShowCustomers = false;
bool homeShowSuppliers = false; bool homeShowSuppliers = false;
final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>();
// Selected user profile // Selected user profile
UserProfile? _profile; UserProfile? _profile;
@ -202,10 +194,10 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
return; return;
} }
final notifications = await InvenTreeNotification().list(); // final notifications = await InvenTreeNotification().list();
setState(() { setState(() {
_notificationCounter = notifications.length; // _notificationCounter = notifications.length;
}); });
} }
@ -416,78 +408,17 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
* Return the main body widget for display. * Return the main body widget for display.
* This depends on the current value of _tabIndex * This depends on the current value of _tabIndex
*/ */
@override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
if (!InvenTreeAPI().isConnected()) { if (!InvenTreeAPI().isConnected()) {
return _connectionStatusWidget(context); return _connectionStatusWidget(context);
} }
switch (_tabIndex) { return ListView(
case 1: // Search widget scrollDirection: Axis.vertical,
return SearchWidget(false); children: getListTiles(context),
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<BottomNavigationBarItem> getNavBarItems(BuildContext context) {
List<BottomNavigationBarItem> items = <BottomNavigationBarItem>[
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: <Widget>[
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;
} }
@override @override
@ -497,7 +428,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
var connecting = !connected && InvenTreeAPI().isConnecting(); var connecting = !connected && InvenTreeAPI().isConnecting();
return Scaffold( return Scaffold(
key: _homeKey, key: homeKey,
appBar: AppBar( appBar: AppBar(
title: Text(L10().appTitle), title: Text(L10().appTitle),
actions: [ actions: [
@ -512,17 +443,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
), ),
drawer: InvenTreeDrawer(context), drawer: InvenTreeDrawer(context),
body: getBody(context), body: getBody(context),
bottomNavigationBar: connected ? BottomNavigationBar( bottomNavigationBar: InvenTreeAPI().isConnected() ? buildBottomAppBar(context, homeKey) : null,
currentIndex: _tabIndex,
onTap: (int index) {
setState(() {
_tabIndex = index;
});
_refreshNotifications();
},
items: getNavBarItems(context),
) : null,
); );
} }
} }

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
@ -40,35 +41,96 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) { return L10().stockLocation; } String getAppBarTitle() {
return L10().stockLocation;
}
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (location != null) { // Add "locate" button
if (location != null && api.supportsMixin("locate")) {
// Add "locate" button actions.add(
if (api.supportsMixin("locate")) {
actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation), icon: Icon(Icons.travel_explore),
tooltip: L10().locateLocation, tooltip: L10().locateLocation,
onPressed: () async { onPressed: () async {
_locateStockLocation(context); 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<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> 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")) { if (api.checkPermission("stock_location", "change")) {
actions.add( actions.add(
IconButton( SpeedDialChild(
icon: FaIcon(FontAwesomeIcons.penToSquare), child: FaIcon(FontAwesomeIcons.qrcode),
tooltip: L10().edit, label: L10().transferStockLocation,
onPressed: () { _editLocationDialog(context); }, 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<LocationDisplayWidget> {
return actions; return actions;
} }
/* @override
* Request identification of this location List<SpeedDialChild> actionButtons(BuildContext context) {
*/ List<SpeedDialChild> actions = [];
Future<void> _locateStockLocation(BuildContext context) async {
final _loc = location; // Create new location
if (api.checkPermission("stock_location", "add")) {
if (_loc != null) { actions.add(
api.locateItemOrLocation(context, location: _loc.pk); 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 * Launch a dialog form to edit this stock location
*/ */
void _editLocationDialog(BuildContext context) { void _editLocationDialog(BuildContext context) {
final _loc = location; final _loc = location;
if (_loc == null) { if (_loc == null) {
@ -101,12 +183,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
} }
_loc.editForm( _loc.editForm(
context, context,
L10().editLocation, L10().editLocation,
onSuccess: (data) async { onSuccess: (data) async {
refresh(context); refresh(context);
showSnackIcon(L10().locationUpdated, success: true); showSnackIcon(L10().locationUpdated, success: true);
} }
); );
} }
@ -117,7 +199,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
@override @override
Future<void> request(BuildContext context) async { Future<void> request(BuildContext context) async {
// Reload location information // Reload location information
if (location != null) { if (location != null) {
final bool result = await location!.reload(); final bool result = await location!.reload();
@ -133,35 +214,32 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
} }
Future<void> _newLocation(BuildContext context) async { Future<void> _newLocation(BuildContext context) async {
int pk = location?.pk ?? -1; int pk = location?.pk ?? -1;
InvenTreeStockLocation().createForm( InvenTreeStockLocation().createForm(
context, context,
L10().locationCreate, L10().locationCreate,
data: { data: {
"parent": (pk > 0) ? pk : null, "parent": (pk > 0) ? pk : null,
}, },
onSuccess: (result) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
Map<String, dynamic> data = result as Map<String, dynamic>; if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data);
if (data.containsKey("pk")) { Navigator.push(
var loc = InvenTreeStockLocation.fromJson(data); context,
MaterialPageRoute(
Navigator.push( builder: (context) => LocationDisplayWidget(loc)
context, )
MaterialPageRoute( );
builder: (context) => LocationDisplayWidget(loc) }
)
);
} }
}
); );
} }
Future<void> _newStockItem(BuildContext context) async { Future<void> _newStockItem(BuildContext context) async {
int pk = location?.pk ?? -1; int pk = location?.pk ?? -1;
if (location != null && pk <= 0) { if (location != null && pk <= 0) {
@ -169,48 +247,46 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
} }
InvenTreeStockItem().createForm( InvenTreeStockItem().createForm(
context, context,
L10().stockItemCreate, L10().stockItemCreate,
data: { data: {
"location": location != null ? pk : null, "location": location != null ? pk : null,
}, },
onSuccess: (result) async { onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
Map<String, dynamic> data = result as Map<String, dynamic>; if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
if (data.containsKey("pk")) { Navigator.push(
var item = InvenTreeStockItem.fromJson(data); context,
MaterialPageRoute(
Navigator.push( builder: (context) => StockDetailWidget(item)
context, )
MaterialPageRoute( );
builder: (context) => StockDetailWidget(item) }
)
);
} }
}
); );
} }
Widget locationDescriptionCard({bool includeActions = true}) { Widget locationDescriptionCard({bool includeActions = true}) {
if (location == null) { if (location == null) {
return Card( return Card(
child: ListTile( child: ListTile(
title: Text( title: Text(
L10().stockTopLevel, L10().stockTopLevel,
style: TextStyle(fontStyle: FontStyle.italic) style: TextStyle(fontStyle: FontStyle.italic)
), ),
leading: FaIcon(FontAwesomeIcons.boxesStacked), leading: FaIcon(FontAwesomeIcons.boxesStacked),
) )
); );
} else { } else {
List<Widget> children = [ List<Widget> children = [
ListTile( ListTile(
title: Text("${location!.name}"), title: Text("${location!.name}"),
subtitle: Text("${location!.description}"), subtitle: Text("${location!.description}"),
leading: location!.customIcon ?? FaIcon(FontAwesomeIcons.boxesStacked), leading: location!.customIcon ??
FaIcon(FontAwesomeIcons.boxesStacked),
), ),
]; ];
@ -221,19 +297,19 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
subtitle: Text("${location!.parentPathString}"), subtitle: Text("${location!.parentPathString}"),
leading: FaIcon(FontAwesomeIcons.turnUp, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.turnUp, color: COLOR_CLICK),
onTap: () async { onTap: () async {
int parentId = location?.parentId ?? -1; int parentId = location?.parentId ?? -1;
if (parentId < 0) { if (parentId < 0) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(null)));
} else { } else {
showLoadingOverlay(context); showLoadingOverlay(context);
var loc = await InvenTreeStockLocation().get(parentId); var loc = await InvenTreeStockLocation().get(parentId);
hideLoadingOverlay(); hideLoadingOverlay();
if (loc is InvenTreeStockLocation) { 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<LocationDisplayWidget> {
} }
return Card( return Card(
child: Column( child: Column(
children: children, children: children,
) )
); );
} }
} }
@override @override
Widget getBottomNavBar(BuildContext context) { List<Widget> getTabIcons(BuildContext context) {
return BottomNavigationBar( return [
currentIndex: tabIndex, Tab(text: L10().details),
onTap: onTabSelectionChanged, Tab(text: L10().stockItems),
items: <BottomNavigationBarItem> [ ];
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();
}
} }
@override @override
Widget getBody(BuildContext context) { List<Widget> getTabs(BuildContext context) {
return getSelectedWidget(tabIndex); return [
Column(children: detailTiles()),
Column(children: stockTiles()),
];
} }
// Construct the "details" panel // Construct the "details" panel
@ -306,18 +346,18 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
List<Widget> tiles = [ List<Widget> tiles = [
locationDescriptionCard(), locationDescriptionCard(),
ListTile( ListTile(
title: Text( title: Text(
L10().sublocations, L10().sublocations,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
trailing: GestureDetector( trailing: GestureDetector(
child: FaIcon(FontAwesomeIcons.filter), child: FaIcon(FontAwesomeIcons.filter),
onTap: () async { onTap: () async {
setState(() { setState(() {
showFilterOptions = !showFilterOptions; showFilterOptions = !showFilterOptions;
}); });
}, },
) )
), ),
Expanded( Expanded(
child: PaginatedStockLocationList( child: PaginatedStockLocationList(
@ -335,13 +375,11 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
// Construct the "stock" panel // Construct the "stock" panel
List<Widget> stockTiles() { List<Widget> stockTiles() {
Map<String, String> filters = { Map<String, String> filters = {
"location": location?.pk.toString() ?? "null", "location": location?.pk.toString() ?? "null",
}; };
return [ return [
locationDescriptionCard(includeActions: false),
ListTile( ListTile(
title: Text( title: Text(
L10().stock, L10().stock,
@ -365,115 +403,4 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
) )
]; ];
} }
List<Widget> actionTiles() {
List<Widget> 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;
}
} }

View File

@ -30,7 +30,7 @@ class _StockLocationListState extends RefreshableState<StockLocationList> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {
@ -42,7 +42,7 @@ class _StockLocationListState extends RefreshableState<StockLocationList> {
]; ];
@override @override
String getAppBarTitle(BuildContext context) => L10().stockLocations; String getAppBarTitle() => L10().stockLocations;
@override @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {

View File

@ -24,10 +24,7 @@ class _NotificationState extends RefreshableState<NotificationWidget> {
List<InvenTreeNotification> notifications = []; List<InvenTreeNotification> notifications = [];
@override @override
AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { String getAppBarTitle() => L10().notifications;
// No app bar for the notification widget
return null;
}
@override @override
Future<void> request (BuildContext context) async { Future<void> request (BuildContext context) async {

View File

@ -409,19 +409,21 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
*/ */
Widget buildSearchInput(BuildContext context) { Widget buildSearchInput(BuildContext context) {
return ListTile( return ListTile(
leading: orderingOptions.isEmpty ? null : GestureDetector( trailing: orderingOptions.isEmpty ? null : GestureDetector(
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_CLICK), child: FaIcon(FontAwesomeIcons.sort, color: COLOR_CLICK),
onTap: () async { onTap: () async {
_saveOrderingOptions(context); _saveOrderingOptions(context);
}, },
), ),
trailing: GestureDetector( leading: GestureDetector(
child: FaIcon( child: FaIcon(
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft, searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_CLICK, color: searchController.text.isNotEmpty ? COLOR_DANGER : null,
), ),
onTap: () { onTap: () {
searchController.clear(); if (searchController.text.isEmpty) {
searchController.clear();
}
updateSearchTerm(); updateSearchTerm();
}, },
), ),

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
@ -65,39 +66,62 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
int variantCount = 0; int variantCount = 0;
@override @override
String getAppBarTitle(BuildContext context) => L10().partDetails; String getAppBarTitle() => L10().partDetails;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (api.checkPermission("part", "view")) {
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.globe),
onPressed: _openInvenTreePage,
),
);
}
if (api.checkPermission("part", "change")) { if (api.checkPermission("part", "change")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.penToSquare), icon: Icon(Icons.edit_square),
tooltip: L10().edit, tooltip: L10().editPart,
onPressed: () { onPressed: () {
_editPartDialog(context); _editPartDialog(context);
}, }
) )
); );
} }
return actions;
}
@override
List<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if (api.checkPermission("part", "change")) {
if (api.supportModernBarcodes) {
actions.add(
customBarcodeAction(
context, this,
widget.part.customBarcode, "part",
widget.part.pk
)
);
}
}
return actions; return actions;
} }
Future<void> _openInvenTreePage() async { @override
part.goToInvenTreePage(); List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if (api.checkPermission("stock", "add")) {
actions.add(
SpeedDialChild(
child: FaIcon(FontAwesomeIcons.box),
label: L10().stockItemCreate,
onTap: () {
_newStockItem(context);
}
)
);
}
return actions;
} }
@override @override
@ -144,20 +168,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
// Request the number of parameters for this part // Request the number of parameters for this part
if (api.supportsPartParameters) { if (api.supportsPartParameters) {
showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool;
} else {
InvenTreePartParameter().count( showParameters = false;
filters: {
"part": part.pk.toString(),
}
).then((int value) {
if (mounted) {
setState(() {
parameterCount = value;
});
}
});
} }
// Request the number of attachments // Request the number of attachments
@ -394,18 +407,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
ListTile( ListTile(
title: Text(L10().availableStock), title: Text(L10().availableStock),
subtitle: Text(L10().stockDetails), subtitle: Text(L10().stockDetails),
leading: FaIcon(FontAwesomeIcons.boxesStacked, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.boxesStacked),
trailing: Text( trailing: Text(
part.stockString(), part.stockString(),
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
onTap: () {
setState(() {
tabIndex = 1;
});
},
), ),
); );
@ -436,14 +444,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
title: Text(L10().billOfMaterials), title: Text(L10().billOfMaterials),
leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK),
trailing: Text(bomCount.toString()), trailing: Text(bomCount.toString()),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BillOfMaterialsWidget(part)
)
);
}
) )
); );
} }
@ -543,24 +543,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
} }
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 // Notes field
tiles.add( tiles.add(
ListTile( ListTile(
@ -682,89 +664,49 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
} }
); );
}
List<Widget> actionTiles(BuildContext context) {
List<Widget> 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 @override
Widget getBottomNavBar(BuildContext context) { List<Widget> getTabIcons(BuildContext context) {
return BottomNavigationBar( List<Widget> icons = [
currentIndex: tabIndex, Tab(text: L10().details),
onTap: onTabSelectionChanged, Tab(text: L10().stock)
items: <BottomNavigationBarItem> [ ];
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.circleInfo), if (showBom && part.isAssembly) {
label: L10().details, icons.add(Tab(text: L10().bom));
), }
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.boxesStacked), if (showParameters) {
label: L10().stock icons.add(Tab(text: L10().parameters));
), }
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench), return icons;
label: L10().actions,
),
]
);
} }
@override @override
Widget getBody(BuildContext context) { List<Widget> getTabs(BuildContext context) {
return getSelectedWidget(tabIndex); List<Widget> 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;
} }
} }

View File

@ -35,10 +35,10 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
} }
@override @override
String getAppBarTitle(BuildContext context) => part.fullname; String getAppBarTitle() => part.fullname;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];

View File

@ -36,10 +36,10 @@ class _PartListState extends RefreshableState<PartList> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts; String getAppBarTitle() => title.isNotEmpty ? title : L10().parts;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {

View File

@ -30,10 +30,10 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> {
} }
@override @override
String getAppBarTitle(BuildContext context) => L10().partNotes; String getAppBarTitle() => L10().partNotes;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];

View File

@ -24,12 +24,12 @@ class _ParameterWidgetState extends RefreshableState<PartParameterWidget> {
_ParameterWidgetState(); _ParameterWidgetState();
@override @override
String getAppBarTitle(BuildContext context) { String getAppBarTitle() {
return L10().parameters; return L10().parameters;
} }
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
return []; return [];
} }

View File

@ -38,10 +38,10 @@ class _PartSupplierState extends RefreshableState<PartSupplierWidget> {
} }
@override @override
String getAppBarTitle(BuildContext context) => L10().partSuppliers; String getAppBarTitle() => L10().partSuppliers;
@override @override
List<Widget> getAppBarActions(BuildContext contexts) { List<Widget> appBarActions(BuildContext contexts) {
// TODO // TODO
return []; return [];
} }

View File

@ -1,5 +1,4 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart";
import "package:one_context/one_context.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/api_form.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/attachment_widget.dart"; import "package:inventree/widget/attachment_widget.dart";
import "package:inventree/widget/company_detail.dart"; import "package:inventree/widget/company_detail.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock_list.dart"; import "package:inventree/widget/stock_list.dart";
@ -41,17 +41,17 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
int attachmentCount = 0; int attachmentCount = 0;
@override @override
String getAppBarTitle(BuildContext context) => L10().purchaseOrder; String getAppBarTitle() => L10().purchaseOrder;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission("purchase_order", "change")) { if (InvenTreeAPI().checkPermission("purchase_order", "change")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.penToSquare), icon: Icon(Icons.edit_square),
tooltip: L10().edit, tooltip: L10().purchaseOrderEdit,
onPressed: () { onPressed: () {
editOrder(context); editOrder(context);
} }
@ -145,12 +145,6 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
title: Text(L10().lineItems), title: Text(L10().lineItems),
leading: FaIcon(FontAwesomeIcons.clipboardList, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.clipboardList, color: COLOR_CLICK),
trailing: Text("${order.lineItemCount}"), trailing: Text("${order.lineItemCount}"),
onTap: () {
setState(() {
// Switch to the "line items" tab
tabIndex = 1;
});
},
)); ));
tiles.add(ListTile( tiles.add(ListTile(
@ -165,12 +159,6 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
title: Text(L10().received), title: Text(L10().received),
leading: FaIcon(FontAwesomeIcons.clipboardCheck, color: COLOR_CLICK), leading: FaIcon(FontAwesomeIcons.clipboardCheck, color: COLOR_CLICK),
trailing: Text("${completedLines}"), trailing: Text("${completedLines}"),
onTap: () {
setState(() {
// Switch to the "received items" tab
tabIndex = 2;
});
},
)); ));
if (order.issueDate.isNotEmpty) { if (order.issueDate.isNotEmpty) {
@ -371,58 +359,23 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
return tiles; return tiles;
} }
@override @override
Widget getBody(BuildContext context) { List<Widget> getTabIcons(BuildContext context) {
return [
return Center( Tab(text: L10().details),
child: getSelectedWidget(context, tabIndex), 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<String, String> filters = {
"purchase_order": "${order.pk}"
};
return PaginatedStockItemList(filters, true);
default:
return ListView();
}
}
@override @override
Widget getBottomNavBar(BuildContext context) { List<Widget> getTabs(BuildContext context) {
return BottomNavigationBar( return [
currentIndex: tabIndex, ListView(children: orderTiles(context)),
onTap: onTabSelectionChanged, ListView(children: lineTiles(context)),
items: [ PaginatedStockItemList({"purchase_order": order.pk.toString()}, true),
BottomNavigationBarItem( ];
icon: FaIcon(FontAwesomeIcons.circleInfo),
label: L10().details
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.tableList),
label: L10().lineItems,
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.boxesStacked),
label: L10().stockItems
)
],
);
} }
} }

View File

@ -33,10 +33,10 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().purchaseOrders; String getAppBarTitle() => L10().purchaseOrders;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {

View File

@ -1,9 +1,12 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
import "package:inventree/barcode.dart";
import "package:inventree/widget/back.dart"; import "package:inventree/widget/back.dart";
import "package:inventree/widget/drawer.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 { mixin BaseWidgetProperties {
// Return a list of appBar actions (default = None) /*
List<Widget> getAppBarActions(BuildContext context) => []; * Return a list of appBar actions
* By default, no appBar actions are available
*/
List<Widget> appBarActions(BuildContext context) => [];
// Return a title for the appBar // 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) // Function to construct a drawer (override if needed)
Widget getDrawer(BuildContext context) { Widget getDrawer(BuildContext context) {
return InvenTreeDrawer(context); return InvenTreeDrawer(context);
} }
List<Widget> getTabs(BuildContext context) => [];
// Function to construct a body (MUST BE PROVIDED) // Function to construct a body (MUST BE PROVIDED)
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
@ -29,18 +37,145 @@ mixin BaseWidgetProperties {
return ListView(); return ListView();
} }
Widget? getBottomNavBar(BuildContext context) {
return null;
}
/*
* Construct the top AppBar for this view
*/
AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
List<Widget> tabs = getTabIcons(context);
return AppBar( return AppBar(
title: Text(getAppBarTitle(context)), centerTitle: false,
actions: getAppBarActions(context), bottom: tabs.isEmpty ? null : TabBar(tabs: tabs),
title: Text(getAppBarTitle()),
actions: appBarActions(context),
leading: backButton(context, key), 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<ScaffoldState> key) {
const double iconSize = 32;
const Color iconColor = Colors.blueGrey;
List<Widget> 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<SpeedDialChild> actionButtons(BuildContext context) => [];
/*
* Build out a set of barcode actions available for this view
*/
List<SpeedDialChild> 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<Widget> 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<Widget> getTabIcons(BuildContext context) => [];
} }
@ -57,9 +192,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
// Storage for context once "Build" is called // Storage for context once "Build" is called
late BuildContext? _context; late BuildContext? _context;
// Current tab index (used for widgets which display bottom tabs)
int tabIndex = 0;
// Bool indicator // Bool indicator
bool loading = false; bool loading = false;
@ -68,16 +200,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
// Helper function to return API instance // Helper function to return API instance
InvenTreeAPI get api => InvenTreeAPI(); InvenTreeAPI get api => InvenTreeAPI();
// Update current tab selection
void onTabSelectionChanged(int index) {
if (mounted) {
setState(() {
tabIndex = index;
});
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -124,21 +246,38 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
// Save the context for future use // Save the context for future use
_context = context; _context = context;
return Scaffold( List<Widget> tabs = getTabIcons(context);
Widget body = tabs.isEmpty ? getBody(context) : TabBarView(children: getTabs(context));
Scaffold view = Scaffold(
key: refreshableKey, key: refreshableKey,
appBar: buildAppBar(context, refreshableKey), appBar: buildAppBar(context, refreshableKey),
drawer: getDrawer(context), drawer: getDrawer(context),
floatingActionButton: buildSpeedDial(context),
floatingActionButtonLocation: FloatingActionButtonLocation
.miniEndDocked,
body: Builder( body: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
refresh(context); refresh(context);
}, },
child: getBody(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;
}
} }
} }

View File

@ -54,7 +54,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
} }
@override @override
String getAppBarTitle(BuildContext context) => L10().search; String getAppBarTitle() => L10().search;
@override @override
AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
@ -43,42 +44,34 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
_StockItemDisplayState(); _StockItemDisplayState();
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItem; String getAppBarTitle() => L10().stockItem;
bool stockShowHistory = false; bool stockShowHistory = false;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission("stock", "view")) { if (api.supportsMixin("locate")) {
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")) {
actions.add( actions.add(
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.penToSquare), icon: Icon(Icons.travel_explore),
tooltip: L10().edit, tooltip: L10().locateItem,
onPressed: () { _editStockItem(context); }, 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<StockDetailWidget> {
return actions; return actions;
} }
Future<void> _openInvenTreePage() async { @override
widget.item.goToInvenTreePage(); List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> 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<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> 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? // Is label printing enabled for this StockItem?
@ -740,170 +840,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
return tiles; return tiles;
} }
List<Widget> actionTiles(BuildContext context) {
List<Widget> 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> [
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 @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
return getSelectedWidget(tabIndex); return ListView(
children: ListTile.divideTiles(
context: context,
tiles: detailTiles()
).toList()
);
} }
} }

View File

@ -24,7 +24,7 @@ class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWid
final InvenTreeStockItem item; final InvenTreeStockItem item;
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItemHistory; String getAppBarTitle() => L10().stockItemHistory;
List<InvenTreeStockItemHistory> history = []; List<InvenTreeStockItemHistory> history = [];

View File

@ -28,10 +28,10 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_StockItemTestResultDisplayState(this.item); _StockItemTestResultDisplayState(this.item);
@override @override
String getAppBarTitle(BuildContext context) => L10().testResults; String getAppBarTitle() => L10().testResults;
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
return [ return [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.circlePlus), icon: FaIcon(FontAwesomeIcons.circlePlus),

View File

@ -30,10 +30,10 @@ class _StockListState extends RefreshableState<StockItemList> {
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItems; String getAppBarTitle() => L10().stockItems;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {

View File

@ -27,7 +27,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
final InvenTreeStockItem item; final InvenTreeStockItem item;
@override @override
String getAppBarTitle(BuildContext context) => L10().stockItemNotes; String getAppBarTitle() => L10().stockItemNotes;
@override @override
Future<void> request(BuildContext context) async { Future<void> request(BuildContext context) async {
@ -37,7 +37,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> {
} }
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = []; List<Widget> actions = [];
if (InvenTreeAPI().checkPermission("stock", "change")) { if (InvenTreeAPI().checkPermission("stock", "change")) {

View File

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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:font_awesome_flutter/font_awesome_flutter.dart";
import "package:inventree/api.dart"; import "package:inventree/api.dart";
@ -35,39 +36,64 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
_SupplierPartDisplayState(); _SupplierPartDisplayState();
@override @override
String getAppBarTitle(BuildContext context) => L10().supplierPart; String getAppBarTitle() => L10().supplierPart;
@override
List<Widget> getAppBarActions(BuildContext context) {
List<Widget> actions = [];
actions.add(
IconButton(
icon: FaIcon(FontAwesomeIcons.penToSquare),
tooltip: L10().edit,
onPressed: () {
editSupplierPart(context);
},
)
);
return actions;
}
/* /*
* Launch a form to edit the current SupplierPart instance * Launch a form to edit the current SupplierPart instance
*/ */
Future<void> editSupplierPart(BuildContext context) async { Future<void> editSupplierPart(BuildContext context) async {
widget.supplierPart.editForm( widget.supplierPart.editForm(
context, context,
L10().supplierPartEdit, L10().supplierPartEdit,
onSuccess: (data) async { onSuccess: (data) async {
refresh(context); refresh(context);
showSnackIcon(L10().supplierPartUpdated, success: true); showSnackIcon(L10().supplierPartUpdated, success: true);
} }
); );
} }
@override
List<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> 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<Widget> appBarActions(BuildContext context) {
List<Widget> 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 @override
Future<void> request(BuildContext context) async { Future<void> request(BuildContext context) async {
final bool result = widget.supplierPart.pk > 0 && await widget.supplierPart.reload(); final bool result = widget.supplierPart.pk > 0 && await widget.supplierPart.reload();
@ -179,60 +205,14 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
return tiles; return tiles;
} }
/*
* Return a list of actions which can be performed for this SupplierPart
*/
List<Widget> actionTiles(BuildContext context) {
List<Widget> 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 @override
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
return getSelectedWidget(tabIndex); return ListView(
} children: ListTile.divideTiles(
context: context,
@override tiles: detailTiles(context),
Widget getBottomNavBar(BuildContext context) { ).toList()
return BottomNavigationBar(
currentIndex: tabIndex,
onTap: onTabSelectionChanged,
items: [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.circleInfo),
label: L10().details,
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.wrench),
label: L10().actions
)
]
); );
} }
} }

View File

@ -29,12 +29,12 @@ class SupplierPartList extends StatefulWidget {
class _SupplierPartListState extends RefreshableState<SupplierPartList> { class _SupplierPartListState extends RefreshableState<SupplierPartList> {
@override @override
String getAppBarTitle(BuildContext context) => L10().supplierParts; String getAppBarTitle() => L10().supplierParts;
bool showFilterOptions = false; bool showFilterOptions = false;
@override @override
List<Widget> getAppBarActions(BuildContext context) => [ List<Widget> appBarActions(BuildContext context) => [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.filter), icon: FaIcon(FontAwesomeIcons.filter),
onPressed: () async { onPressed: () async {

View File

@ -395,6 +395,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View File

@ -24,6 +24,7 @@ dependencies:
flutter_localized_locales: ^2.0.4 flutter_localized_locales: ^2.0.4
flutter_markdown: ^0.6.13+1 # Rendering markdown flutter_markdown: ^0.6.13+1 # Rendering markdown
flutter_overlay_loader: ^2.0.0 # Overlay screen support 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 font_awesome_flutter: ^10.3.0 # FontAwesome icon set
http: ^0.13.4 http: ^0.13.4
image_picker: ^0.8.6+1 # Select or take photos image_picker: ^0.8.6+1 # Select or take photos