mirror of
https://github.com/inventree/inventree-app.git
synced 2025-06-14 11:15:26 +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:
@ -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<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
|
||||
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<Widget> 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<ScaffoldState> key) {
|
||||
|
||||
List<Widget> 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<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
|
||||
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<T extends StatefulWidget> extends State<T> 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<T extends StatefulWidget> extends State<T> with
|
||||
// Save the context for future use
|
||||
_context = context;
|
||||
|
||||
return Scaffold(
|
||||
List<Widget> 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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user