diff --git a/lib/src/cast/widgets/queue_list/MyPaginatedList.dart b/lib/src/cast/widgets/queue_list/MyPaginatedList.dart deleted file mode 100644 index 67a4a33..0000000 --- a/lib/src/cast/widgets/queue_list/MyPaginatedList.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/widgets.dart'; - -extension MyExtendedList on List { - T? lastOrNull() { - return this.isNotEmpty ? this.last : null; - } -} - -typedef ItemWidgetBuilder = Widget Function(Item item); - -/// When this return null, the pagination stops -typedef FutureItemsCallback = Future> Function( - Item? lastLoadedItem); -typedef ItemCallback = void Function(Item item); - -class MyPaginatedList extends StatefulWidget { - final ItemWidgetBuilder widgetBuilder; - final FutureItemsCallback loadMore; - - const MyPaginatedList({ - Key? key, - required this.widgetBuilder, - required this.loadMore, - }) : super(key: key); - - @override - _MyPaginatedListState createState() => _MyPaginatedListState(); -} - -class _MyPaginatedListState extends State> { - List 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]); - } - }); - } -} diff --git a/lib/src/cast/widgets/queue_list/QueueList.dart b/lib/src/cast/widgets/queue_list/QueueList.dart index 3e9400e..f0240a3 100644 --- a/lib/src/cast/widgets/queue_list/QueueList.dart +++ b/lib/src/cast/widgets/queue_list/QueueList.dart @@ -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? 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 { - Widget widgetBuilder(MediaQueueItem item) { - // TODO complete this method - return SizedBox.shrink(); - } - - Future> 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( - 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( + 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); + } + }, ); } } diff --git a/lib/src/cast/widgets/queue_list/QueueListItemHolder.dart b/lib/src/cast/widgets/queue_list/QueueListItemHolder.dart new file mode 100644 index 0000000..c0d5744 --- /dev/null +++ b/lib/src/cast/widgets/queue_list/QueueListItemHolder.dart @@ -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 createState() => _QueueListItemHolderState(); +} + +class _QueueListItemHolderState extends State { + bool _hasChanged = false; + + @override + Widget build(BuildContext context) { + final sessionManager = widget.castFramework.castContext.sessionManager; + final mediaQueue = sessionManager.remoteMediaClient.mediaQueue; + + return FutureBuilder( + 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); + } + }, + ); + } +} diff --git a/lib/src/cast/widgets/queue_list/utils.dart b/lib/src/cast/widgets/queue_list/utils.dart new file mode 100644 index 0000000..f08f5cf --- /dev/null +++ b/lib/src/cast/widgets/queue_list/utils.dart @@ -0,0 +1,6 @@ +import 'package:flutter/widgets.dart'; + +typedef EmptyStateBuilder = Widget Function( + BuildContext context, bool isLoading, Object? error); + +final defaultEmptyState = () => SizedBox.shrink(); diff --git a/lib/widgets.dart b/lib/widgets.dart index 2cedb8d..d3bd6ec 100644 --- a/lib/widgets.dart +++ b/lib/widgets.dart @@ -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';