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

@ -185,4 +185,9 @@ class Api {
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention
static const String fans = 'https://api.bilibili.com/x/relation/fans';
// 直播
// ?page=1&page_size=30&platform=web
static const String liveList =
'https://api.live.bilibili.com/xlive/web-interface/v1/second/getUserRecommend';
}

25
lib/http/live.dart Normal file
View File

@ -0,0 +1,25 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/live/item.dart';
class LiveHttp {
static Future liveList(
{int? vmid, int? pn, int? ps, String? orderType}) async {
var res = await Request().get(Api.liveList,
data: {'page': pn, 'page_size': 30, 'platform': 'web'});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['list']
.map<LiveItemModel>((e) => LiveItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

77
lib/models/live/item.dart Normal file
View File

@ -0,0 +1,77 @@
class LiveItemModel {
LiveItemModel({
this.roomId,
this.uid,
this.title,
this.uname,
this.online,
this.userCover,
this.userCoverFlag,
this.systemCover,
this.cover,
this.pic,
this.link,
this.face,
this.parentId,
this.parentName,
this.areaId,
this.areaName,
this.sessionId,
this.groupId,
this.pkId,
this.verify,
this.headBox,
this.headBoxType,
this.watchedShow,
});
int? roomId;
int? uid;
String? title;
String? uname;
int? online;
String? userCover;
int? userCoverFlag;
String? systemCover;
String? cover;
String? pic;
String? link;
String? face;
int? parentId;
String? parentName;
int? areaId;
String? areaName;
String? sessionId;
int? groupId;
int? pkId;
Map? verify;
Map? headBox;
int? headBoxType;
Map? watchedShow;
LiveItemModel.fromJson(Map<String, dynamic> json) {
roomId = json['room_id'];
uid = json['uid'];
title = json['title'];
uname = json['uname'];
online = json['online'];
userCover = json['user_cover'];
userCoverFlag = json['user_cover_flag'];
systemCover = json['system_cover'];
cover = json['cover'];
pic = json['cover'];
link = json['link'];
face = json['face'];
parentId = json['parent_id'];
parentName = json['parent_name'];
areaId = json['area_id'];
areaName = json['area_name'];
sessionId = json['session_id'];
groupId = json['group_id'];
pkId = json['pk_id'];
verify = json['verify'];
headBox = json['head_box'];
headBoxType = json['head_box_type'];
watchedShow = json['watched_show'];
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import './controller.dart';
@ -21,16 +22,6 @@ class _HomePageState extends State<HomePage>
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
// _tabController = TabController(
// initialIndex: _homeController.initialIndex,
// length: _homeController.tabs.length,
// vsync: this,
// );
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -106,7 +97,7 @@ class _HomePageState extends State<HomePage>
body: TabBarView(
controller: _homeController.tabController,
children: const [
SizedBox(),
LivePage(),
RcmdPage(),
HotPage(),
],

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),
),
],
),
],
),
),
);
}
}