mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-27 21:16:48 +00:00
300 lines
7.8 KiB
Dart
300 lines
7.8 KiB
Dart
import "package:flutter/material.dart";
|
|
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
|
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
|
|
|
import "package:inventree/api.dart";
|
|
import "package:inventree/app_colors.dart";
|
|
import "package:inventree/barcode/barcode.dart";
|
|
|
|
import "package:inventree/widget/back.dart";
|
|
import "package:inventree/widget/drawer.dart";
|
|
import "package:inventree/widget/search.dart";
|
|
|
|
|
|
/*
|
|
* Simple mixin class which defines simple methods for defining widget properties
|
|
*/
|
|
mixin BaseWidgetProperties {
|
|
|
|
/*
|
|
* Return a list of appBar actions
|
|
* By default, no appBar actions are available
|
|
*/
|
|
List<Widget> appBarActions(BuildContext context) => [];
|
|
|
|
// Return a title for the appBar (placeholder)
|
|
String getAppBarTitle() { return "--- app bar ---"; }
|
|
|
|
// Function to construct a drawer (override if needed)
|
|
Widget getDrawer(BuildContext context) {
|
|
return InvenTreeDrawer(context);
|
|
}
|
|
|
|
// Function to construct a set of tabs for this widget (override if needed)
|
|
List<Widget> getTabs(BuildContext context) => [];
|
|
|
|
// Function to construct a set of tiles for this widget (override if needed)
|
|
List<Widget> getTiles(BuildContext context) => [];
|
|
|
|
// Function to construct a body
|
|
Widget getBody(BuildContext context) {
|
|
|
|
// Default implementation is to return a ListView
|
|
// Override getTiles to replace the internal context
|
|
return ListView(
|
|
physics: AlwaysScrollableScrollPhysics(),
|
|
children: getTiles(context),
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Construct the top AppBar for this view
|
|
*/
|
|
AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
|
|
|
|
List<Widget> tabs = getTabIcons(context);
|
|
|
|
return AppBar(
|
|
centerTitle: false,
|
|
bottom: tabs.isEmpty ? null : TabBar(tabs: tabs),
|
|
title: Text(getAppBarTitle()),
|
|
backgroundColor: COLOR_APP_BAR,
|
|
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 = 40;
|
|
|
|
List<Widget> icons = [
|
|
IconButton(
|
|
icon: Icon(Icons.menu, color: COLOR_ACTION),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
if (key.currentState != null) {
|
|
key.currentState!.openDrawer();
|
|
}
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: Icon(TablerIcons.search, color: COLOR_ACTION),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
if (InvenTreeAPI().checkConnection()) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => SearchWidget(true)
|
|
)
|
|
);
|
|
}
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: Icon(TablerIcons.barcode, color: COLOR_ACTION),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
if (InvenTreeAPI().checkConnection()) {
|
|
scanBarcode(context);
|
|
}
|
|
},
|
|
)
|
|
];
|
|
|
|
return BottomAppBar(
|
|
shape: AutomaticNotchedShape(
|
|
RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
|
),
|
|
RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(40)),
|
|
),
|
|
),
|
|
notchMargin: 10,
|
|
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) => [];
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* Abstract base class which provides generic "refresh" functionality.
|
|
*
|
|
* - Drag down and release to 'refresh' the widget
|
|
* - Define some method which runs to 'refresh' the widget state
|
|
*/
|
|
abstract class RefreshableState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties {
|
|
|
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
|
final refreshKey = GlobalKey<RefreshIndicatorState>();
|
|
|
|
// Storage for context once "Build" is called
|
|
late BuildContext? _context;
|
|
|
|
// Bool indicator
|
|
bool loading = false;
|
|
|
|
bool get loaded => !loading;
|
|
|
|
// Helper function to return API instance
|
|
InvenTreeAPI get api => InvenTreeAPI();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => onBuild(_context!));
|
|
}
|
|
|
|
// Function called after the widget is first build
|
|
Future<void> onBuild(BuildContext context) async {
|
|
refresh(context);
|
|
}
|
|
|
|
// Function to request data for this page
|
|
Future<void> request(BuildContext context) async {
|
|
return;
|
|
}
|
|
|
|
// Refresh the widget - handler for custom request() method
|
|
Future<void> refresh(BuildContext context) async {
|
|
|
|
// Escape if the widget is no longer loaded
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
loading = true;
|
|
});
|
|
|
|
await request(context);
|
|
|
|
// Escape if the widget is no longer loaded
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
loading = false;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
// Save the context for future use
|
|
_context = context;
|
|
|
|
List<Widget> tabs = getTabIcons(context);
|
|
|
|
Widget body = tabs.isEmpty ? getBody(context) : TabBarView(children: getTabs(context));
|
|
|
|
Scaffold view = Scaffold(
|
|
key: scaffoldKey,
|
|
appBar: buildAppBar(context, scaffoldKey),
|
|
drawer: getDrawer(context),
|
|
floatingActionButton: buildSpeedDial(context),
|
|
floatingActionButtonLocation: FloatingActionButtonLocation.miniEndDocked,
|
|
body: RefreshIndicator(
|
|
key: refreshKey,
|
|
notificationPredicate: (ScrollNotification notification) {
|
|
return true;
|
|
},
|
|
|
|
onRefresh: () async {
|
|
refresh(context);
|
|
},
|
|
child: body
|
|
),
|
|
bottomNavigationBar: buildBottomAppBar(context, scaffoldKey),
|
|
);
|
|
|
|
// Default implementation is *not* tabbed
|
|
if (tabs.isNotEmpty) {
|
|
return DefaultTabController(
|
|
length: tabs.length,
|
|
child: view,
|
|
);
|
|
} else {
|
|
return view;
|
|
}
|
|
}
|
|
} |