MediaQueue: QueueList widget
This commit is contained in:
parent
c0af101acf
commit
90ae50dd28
5 changed files with 151 additions and 118 deletions
|
|
@ -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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
lib/src/cast/widgets/queue_list/QueueListItemHolder.dart
Normal file
84
lib/src/cast/widgets/queue_list/QueueListItemHolder.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
6
lib/src/cast/widgets/queue_list/utils.dart
Normal file
6
lib/src/cast/widgets/queue_list/utils.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef EmptyStateBuilder = Widget Function(
|
||||
BuildContext context, bool isLoading, Object? error);
|
||||
|
||||
final defaultEmptyState = () => SizedBox.shrink();
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue