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/widgets.dart';
|
||||||
import 'package:flutter_cast_framework/cast.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 {
|
class QueueList extends StatelessWidget {
|
||||||
final FlutterCastFramework flutterCastFramework;
|
final FlutterCastFramework castFramework;
|
||||||
final ItemWidgetBuilder<MediaQueueItem>? widgetBuilder;
|
final ListItemBuilder listItemBuilder;
|
||||||
|
final EmptyStateBuilder? emptyListStateBuilder;
|
||||||
|
final EmptyStateBuilder? emptyItemStateBuilder;
|
||||||
|
|
||||||
const QueueList({
|
QueueList({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.flutterCastFramework,
|
required this.castFramework,
|
||||||
this.widgetBuilder,
|
required this.listItemBuilder,
|
||||||
|
this.emptyListStateBuilder,
|
||||||
|
this.emptyItemStateBuilder,
|
||||||
}) : super(key: key);
|
}) : 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MyPaginatedList<MediaQueueItem>(
|
final sessionManager = castFramework.castContext.sessionManager;
|
||||||
widgetBuilder: this.widget.widgetBuilder ?? widgetBuilder,
|
|
||||||
loadMore: loadMore,
|
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/expanded_controls/ExpandedControlsConnectedDeviceLabel.dart';
|
||||||
|
|
||||||
export 'src/cast/widgets/mini_controller/MiniController.dart';
|
export 'src/cast/widgets/mini_controller/MiniController.dart';
|
||||||
|
|
||||||
|
export 'src/cast/widgets/queue_list/QueueList.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue