feat: 直播列表

This commit is contained in:
guozhigq
2023-07-11 17:14:59 +08:00
parent e3c7719a05
commit 75d4e20d99
8 changed files with 455 additions and 11 deletions

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/item.dart';
class LiveController extends GetxController {
final ScrollController scrollController = ScrollController();
int count = 12;
int _currentPage = 1;
int crossAxisCount = 2;
RxList<LiveItemModel> liveList = [LiveItemModel()].obs;
bool isLoadingMore = false;
bool flag = false;
OverlayEntry? popupDialog;
// 获取推荐
Future queryLiveList(type) async {
if (type == 'init') {
_currentPage = 1;
}
var res = await LiveHttp.liveList(
pn: _currentPage,
);
if (res['status']) {
if (type == 'init') {
liveList.value = res['data'];
} else if (type == 'onLoad') {
liveList.addAll(res['data']);
}
_currentPage += 1;
}
isLoadingMore = false;
return res;
}
// 下拉刷新
Future onRefresh() async {
queryLiveList('init');
}
// 上拉加载
Future onLoad() async {
queryLiveList('onLoad');
}
// 返回顶部并刷新
void animateToTop() async {
if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) {
scrollController.jumpTo(0);
} else {
await scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
}

View File

@ -0,0 +1,4 @@
library live;
export './controller.dart';
export './view.dart';

149
lib/pages/live/view.dart Normal file
View File

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'controller.dart';
import 'widgets/live_item.dart';
class LivePage extends StatefulWidget {
const LivePage({super.key});
@override
State<LivePage> createState() => _LivePageState();
}
class _LivePageState extends State<LivePage> {
final LiveController _liveController = Get.put(LiveController());
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_liveController.scrollController.addListener(
() {
if (_liveController.scrollController.position.pixels >=
_liveController.scrollController.position.maxScrollExtent - 200) {
if (!_liveController.isLoadingMore) {
_liveController.isLoadingMore = true;
_liveController.onLoad();
}
}
},
);
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 0, StyleString.cardSpace, 8),
sliver: FutureBuilder(
future: _liveController.queryLiveList('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() =>
contentGrid(_liveController, _liveController.liveList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
},
),
),
const LoadingMore()
],
),
);
}
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: liveItem),
),
);
}
Widget contentGrid(ctr, liveList) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: ctr.crossAxisCount,
mainAxisExtent:
Get.size.width / ctr.crossAxisCount / StyleString.aspectRatio + 70,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return liveList!.isNotEmpty
? LiveCardV(
liveItem: liveList[index],
longPress: () {
_liveController.popupDialog =
_createPopupDialog(liveList[index]);
Overlay.of(context).insert(_liveController.popupDialog!);
},
longPressEnd: () {
_liveController.popupDialog?.remove();
},
)
: const VideoCardVSkeleton();
},
childCount: liveList!.isNotEmpty ? liveList!.length : 10,
),
);
}
}
class LoadingMore extends StatelessWidget {
const LoadingMore({super.key});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).padding.bottom + 80,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Text(
'加载中...',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
),
),
);
}
}

View File

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class LiveCardV extends StatelessWidget {
LiveItemModel liveItem;
Function()? longPress;
Function()? longPressEnd;
LiveCardV({
Key? key,
required this.liveItem,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
child: InkWell(
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
// Get.toNamed('/video?bvid=${liveItem.bvid}&cid=${liveItem.cid}',
// arguments: {'videoItem': liveItem, 'heroTag': heroTag});
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(StyleString.imgRadius),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: '${liveItem.cover!}@.webp',
width: maxWidth,
height: maxHeight,
),
),
],
);
}),
),
),
LiveContent(liveItem: liveItem)
],
),
),
),
);
}
}
class LiveContent extends StatelessWidget {
final liveItem;
const LiveContent({Key? key, required this.liveItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 8, 6, 7),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
Row(
children: [
UpTag(),
Expanded(
child: Text(
liveItem.uname,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
),
Row(
children: [
Text(
'${'[' + liveItem.areaName}]',
style: const TextStyle(fontSize: 11),
),
const Text(''),
Text(
liveItem.watchedShow['text_large'],
style: const TextStyle(fontSize: 11),
),
],
),
],
),
),
);
}
}