MediaQueue: QueueList widget

This commit is contained in:
gianlucaparadise 2022-07-21 06:16:56 +02:00
parent c0af101acf
commit 90ae50dd28
5 changed files with 151 additions and 118 deletions

View file

@ -1,87 +0,0 @@
import 'package:flutter/widgets.dart';
extension MyExtendedList<T> on List<T> {
T? lastOrNull() {
return this.isNotEmpty ? this.last : null;
}
}
typedef ItemWidgetBuilder<Item> = Widget Function(Item item);
/// When this return null, the pagination stops
typedef FutureItemsCallback<Item> = Future<List<Item>> Function(
Item? lastLoadedItem);
typedef ItemCallback<Item> = void Function(Item item);
class MyPaginatedList<Item> extends StatefulWidget {
final ItemWidgetBuilder<Item> widgetBuilder;
final FutureItemsCallback<Item> loadMore;
const MyPaginatedList({
Key? key,
required this.widgetBuilder,
required this.loadMore,
}) : super(key: key);
@override
_MyPaginatedListState createState() => _MyPaginatedListState();
}
class _MyPaginatedListState<Item> extends State<MyPaginatedList<Item>> {
List<Item> items = [];
bool shouldTryToLoadMore = true;
@override
void initState() {
super.initState();
waitOnItems(); // FIXME: this should be before initstate
}
void waitOnItems() async {
try {
final items = await widget.loadMore(this.items.lastOrNull());
this.shouldTryToLoadMore = items.isNotEmpty;
setState(() {
this.items.addAll(items);
});
} catch (error) {
print(error); // FIXME: this should call a callback
}
}
@override
Widget build(BuildContext context) {
if (items.isEmpty) {
return buildLoadingProgress();
} else {
//TODO: show progress bar at the bottom if loading more
return buildList();
}
}
Widget buildLoadingProgress() {
return Center(
child: Text("Loading..."),
);
}
Widget buildList() {
return ListView.builder(
itemCount: shouldTryToLoadMore ? null : items.length,
itemBuilder: (context, index) {
if (shouldTryToLoadMore && index == items.length - 1) {
waitOnItems();
return SizedBox.shrink();
} else if (index >= items.length) {
return SizedBox.shrink();
// } else if (widget.onItemSelected != null) {
// return InkWell(
// onTap: () => {widget.onItemSelected(items[index])},
// child: widget.widgetBuilder(items[index]),
// );
} else {
return widget.widgetBuilder(items[index]);
}
});
}
}

View file

@ -1,43 +1,71 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_cast_framework/cast.dart';
import 'package:flutter_cast_framework/src/cast/widgets/queue_list/MyPaginatedList.dart';
import 'package:flutter_cast_framework/src/cast/widgets/queue_list/QueueListItemHolder.dart';
import 'package:flutter_cast_framework/src/cast/widgets/queue_list/utils.dart';
class QueueList extends StatefulWidget {
final FlutterCastFramework flutterCastFramework;
final ItemWidgetBuilder<MediaQueueItem>? widgetBuilder;
class QueueList extends StatelessWidget {
final FlutterCastFramework castFramework;
final ListItemBuilder listItemBuilder;
final EmptyStateBuilder? emptyListStateBuilder;
final EmptyStateBuilder? emptyItemStateBuilder;
const QueueList({
QueueList({
Key? key,
required this.flutterCastFramework,
this.widgetBuilder,
required this.castFramework,
required this.listItemBuilder,
this.emptyListStateBuilder,
this.emptyItemStateBuilder,
}) : super(key: key);
@override
_QueueListState createState() => _QueueListState();
}
class _QueueListState extends State<QueueList> {
Widget widgetBuilder(MediaQueueItem item) {
// TODO complete this method
return SizedBox.shrink();
}
Future<List<MediaQueueItem>> loadMore(MediaQueueItem? lastLoadedItem) async {
// TODO complete this method
if (lastLoadedItem == null) {
//first load request
return [];
} else {
//subsequent load request(s)
return [];
}
}
@override
Widget build(BuildContext context) {
return MyPaginatedList<MediaQueueItem>(
widgetBuilder: this.widget.widgetBuilder ?? widgetBuilder,
loadMore: loadMore,
final sessionManager = castFramework.castContext.sessionManager;
Widget _getEmptyState(BuildContext context) {
if (emptyListStateBuilder == null) return defaultEmptyState();
return emptyListStateBuilder!(context, false, null);
}
Widget _getErrorState(BuildContext context, Object? error) {
if (emptyListStateBuilder == null) return defaultEmptyState();
return emptyListStateBuilder!(context, false, error);
}
Widget _getLoadingState(BuildContext context) {
if (emptyListStateBuilder == null) return defaultEmptyState();
return emptyListStateBuilder!(context, true, null);
}
Widget _getList(int count) {
return ListView.builder(
itemCount: count,
itemBuilder: (context, index) {
return QueueListItemHolder(
castFramework: this.castFramework,
index: index,
listItemBuilder: listItemBuilder,
emptyItemStateBuilder: emptyItemStateBuilder,
);
},
);
}
return FutureBuilder<int>(
future: sessionManager.remoteMediaClient.mediaQueue.getItemCount(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final count = snapshot.data;
if (count == null || count < 0) {
return _getEmptyState(context);
}
return _getList(count);
} else if (snapshot.hasError) {
return _getErrorState(context, snapshot.error);
} else {
return _getLoadingState(context);
}
},
);
}
}

View file

@ -0,0 +1,84 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_cast_framework/cast.dart';
import 'package:flutter_cast_framework/src/cast/widgets/queue_list/utils.dart';
typedef ListItemBuilder = Widget Function(
BuildContext context,
MediaQueueItem item,
);
class QueueListItemHolder extends StatefulWidget {
final FlutterCastFramework castFramework;
final ListItemBuilder listItemBuilder;
final EmptyStateBuilder? emptyItemStateBuilder;
final int index;
const QueueListItemHolder({
Key? key,
required this.castFramework,
required this.listItemBuilder,
required this.index,
this.emptyItemStateBuilder,
}) : super(key: key);
Widget _getEmptyState(BuildContext context) {
if (emptyItemStateBuilder == null) return defaultEmptyState();
return emptyItemStateBuilder!(context, false, null);
}
Widget _getErrorState(BuildContext context, Object? error) {
if (emptyItemStateBuilder == null) return defaultEmptyState();
return emptyItemStateBuilder!(context, false, error);
}
Widget _getLoadingState(BuildContext context) {
if (emptyItemStateBuilder == null) return defaultEmptyState();
return emptyItemStateBuilder!(context, true, null);
}
@override
State<QueueListItemHolder> createState() => _QueueListItemHolderState();
}
class _QueueListItemHolderState extends State<QueueListItemHolder> {
bool _hasChanged = false;
@override
Widget build(BuildContext context) {
final sessionManager = widget.castFramework.castContext.sessionManager;
final mediaQueue = sessionManager.remoteMediaClient.mediaQueue;
return FutureBuilder<MediaQueueItem>(
future: mediaQueue.getItemAtIndex(widget.index),
builder: (context, snapshot) {
if (snapshot.hasData) {
final item = snapshot.data;
if (item == null || item.itemId == null) {
// When itemId is null, the item is ready and needs to be
// re-requested once it's updated
final sub = mediaQueue.itemUpdatedAtIndexStream.listen(null);
sub.onData((i) {
final isUpdated = i == widget.index;
if (isUpdated) {
sub.cancel();
setState(() {
// FIXME: I don't like how the refresh is triggered
_hasChanged = true;
});
}
});
return widget._getLoadingState(context);
}
return widget.listItemBuilder(context, item);
} else if (snapshot.hasError) {
return widget._getErrorState(context, snapshot.error);
} else {
return widget._getLoadingState(context);
}
},
);
}
}

View file

@ -0,0 +1,6 @@
import 'package:flutter/widgets.dart';
typedef EmptyStateBuilder = Widget Function(
BuildContext context, bool isLoading, Object? error);
final defaultEmptyState = () => SizedBox.shrink();

View file

@ -9,3 +9,5 @@ export 'src/cast/widgets/expanded_controls/ExpandedControlsToolbar.dart';
export 'src/cast/widgets/expanded_controls/ExpandedControlsConnectedDeviceLabel.dart';
export 'src/cast/widgets/mini_controller/MiniController.dart';
export 'src/cast/widgets/queue_list/QueueList.dart';