Implementation
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: widget.appBarLeading == null
? null
: widget.appBarLeading!(context),
leadingWidth: widget.leadingWidth,
title: _getScaffoldTitle(context),
actions: <Widget>[
/// Select All Button
if (widget.selection &&
widget.multipleSelection &&
widget.invertSelection)
IconButton(
tooltip: widget.invertSelectionText,
icon: const Icon(Icons.select_all),
onPressed: () {
for (final T model in _globalItems) {
if (_selections.containsKey(model.id)) {
_selections.remove(model.id);
} else {
_selections[model.id!] = model;
}
}
_streamController.add(AbstractListStateEnum.finishLoading);
},
),
/// Search Button
if (widget.showSearchButton)
IconButton(
tooltip: sprintf(
widget.searchButtonText,
<dynamic>[widget.builder.superSingle(context)],
),
icon: const Icon(Icons.search),
onPressed: _search,
),
/// Refresh Button
if (widget.showRefreshButton)
IconButton(
tooltip: widget.refreshButtonText,
icon: const Icon(FontAwesomeIcons.arrowsRotate),
onPressed: () => _loadData(context),
),
/// Selection Confirm Button
if (widget.selection && widget.multipleSelection)
IconButton(
tooltip: sprintf(
widget.selectionText,
<dynamic>[widget.builder.superPlural(context)],
),
icon: const FaIcon(FontAwesomeIcons.check),
onPressed: () =>
Navigator.of(context).pop(List<T>.of(_selections.values)),
),
/// Actions
if (!widget.selection && widget.actions != null)
...widget.actions!(
context,
widget.builder,
widget.consumer,
_qsParam,
selection: widget.selection,
),
/// Add Button
if (!FollyFields().isMobile && !widget.selection)
ValueListenableBuilder<bool>(
valueListenable: _insertNotifier,
builder: (BuildContext context, bool insert, _) {
return insert
? IconButton(
tooltip: sprintf(
widget.addText,
<dynamic>[widget.builder.superSingle(context)],
),
icon: const FaIcon(FontAwesomeIcons.plus),
onPressed: _addEntity,
)
: const SizedBox.shrink();
},
),
/// Legend Button
if (!widget.selection &&
widget.builder.listLegend(context).isNotEmpty)
IconButton(
tooltip: widget.builder.listLegendTitle(context),
icon: FaIcon(widget.builder.listLegendIcon(context)),
onPressed: _showListLegend,
),
],
),
floatingActionButton: FollyFields().isMobile && !widget.selection
? ValueListenableBuilder<bool>(
valueListenable: _insertNotifier,
builder: (BuildContext context, bool insert, _) {
return insert
? FloatingActionButton(
tooltip: sprintf(
widget.addText,
<dynamic>[widget.builder.superSingle(context)],
),
onPressed: _addEntity,
child: const FaIcon(FontAwesomeIcons.plus),
)
: const SizedBox.shrink();
},
)
: null,
bottomNavigationBar: widget.builder.buildBottomNavigationBar(context),
body: widget.builder.buildListBody(
context,
SafeFutureBuilder<bool>(
future: _loadPermissions(context),
waitingMessage: widget.waitingText,
builder: (BuildContext context, bool value, _) {
return SafeStreamBuilder<AbstractListStateEnum>(
stream: _streamController.stream,
waitingMessage: widget.waitingText,
builder: (BuildContext context, AbstractListStateEnum event, _) {
if (event == AbstractListStateEnum.loadingMessage) {
return WaitingMessage(message: widget.waitingText);
}
/// CircularProgressIndicator will be at the list bottom,
/// so we make space here with an extra index if
/// event is incrementalLoading
int itemCount = _globalItems.length;
if (event == AbstractListStateEnum.incrementalLoading) {
itemCount++;
}
/// If this is the first 'finishLoading' event and the
/// scrollbar hasn't even appeared yet, we won't be able to
/// scroll further.
/// This callback will be called after building this widget.
if (event == AbstractListStateEnum.finishLoading &&
!_initiallyFilled) {
WidgetsBinding.instance.addPostFrameCallback(
(_) async {
if (_scrollController.positions.isNotEmpty &&
_scrollController.position.hasContentDimensions &&
_scrollController.position.maxScrollExtent == 0) {
int extraAmount =
await _loadData(context, clear: false);
if (extraAmount == 0) {
// This flags that we won't try further '_loadData'
// calls
_initiallyFilled = true;
}
}
},
);
}
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () => _loadData(context),
child: _globalItems.isEmpty
? TextMessage(
sprintf(
widget.listEmpty,
<dynamic>[
widget.builder.superPlural(context).toLowerCase(),
],
),
)
: KeyboardListener(
autofocus: true,
focusNode: keyboardFocusNode,
onKeyEvent: (KeyEvent event) {
if (widget.showSearchButton &&
event.character != null) {
_search(event.character);
}
},
child: Scrollbar(
controller: _scrollController,
// isAlwaysShown: FollyFields().isWeb,
thumbVisibility: true,
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
controller: _scrollController,
itemBuilder: (BuildContext context, int index) {
/// Updating...
if (index >= _globalItems.length) {
return const SizedBox(
height: 80,
child: Center(
child: CircularProgressIndicator(),
),
);
}
T model = _globalItems[index];
return _delete &&
FollyFields().isMobile &&
widget.canDelete(model)
? Dismissible(
key: Key('key_${model.id}'),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(
right: 16,
),
child: const FaIcon(
FontAwesomeIcons.trashCan,
color: Colors.white,
),
),
confirmDismiss:
(DismissDirection direction) =>
_askDelete(),
onDismissed:
(DismissDirection direction) =>
_deleteEntity(model),
child: _buildResultItem(
model: model,
selected:
_selections.containsKey(model.id),
canDelete: false,
),
)
: _buildResultItem(
model: model,
selected:
_selections.containsKey(model.id),
canDelete: _delete &&
FollyFields().isNotMobile &&
widget.canDelete(model),
);
},
separatorBuilder: (_, _) => const FollyDivider(),
itemCount: itemCount,
),
),
),
);
},
);
},
),
),
);
}