feat: 直播列表
This commit is contained in:
@ -185,4 +185,9 @@ class Api {
|
|||||||
// vmid 用户id pn 页码 ps 每页个数,最大50 order: desc
|
// vmid 用户id pn 页码 ps 每页个数,最大50 order: desc
|
||||||
// order_type 排序规则 最近访问传空,最常访问传 attention
|
// order_type 排序规则 最近访问传空,最常访问传 attention
|
||||||
static const String fans = 'https://api.bilibili.com/x/relation/fans';
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/pages/hot/index.dart';
|
import 'package:pilipala/pages/hot/index.dart';
|
||||||
|
import 'package:pilipala/pages/live/index.dart';
|
||||||
import 'package:pilipala/pages/rcmd/index.dart';
|
import 'package:pilipala/pages/rcmd/index.dart';
|
||||||
import './controller.dart';
|
import './controller.dart';
|
||||||
|
|
||||||
@ -21,16 +22,6 @@ class _HomePageState extends State<HomePage>
|
|||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// _tabController = TabController(
|
|
||||||
// initialIndex: _homeController.initialIndex,
|
|
||||||
// length: _homeController.tabs.length,
|
|
||||||
// vsync: this,
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@ -106,7 +97,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
controller: _homeController.tabController,
|
controller: _homeController.tabController,
|
||||||
children: const [
|
children: const [
|
||||||
SizedBox(),
|
LivePage(),
|
||||||
RcmdPage(),
|
RcmdPage(),
|
||||||
HotPage(),
|
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