feat: 直播列表
This commit is contained in:
@ -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
25
lib/http/live.dart
Normal 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
77
lib/models/live/item.dart
Normal 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'];
|
||||
}
|
||||
}
|
@ -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(),
|
||||
],
|
||||
|
56
lib/pages/live/controller.dart
Normal file
56
lib/pages/live/controller.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
4
lib/pages/live/index.dart
Normal file
4
lib/pages/live/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library live;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
149
lib/pages/live/view.dart
Normal file
149
lib/pages/live/view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
137
lib/pages/live/widgets/live_item.dart
Normal file
137
lib/pages/live/widgets/live_item.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user