feat: 首页番剧&样式

This commit is contained in:
guozhigq
2023-08-08 22:18:18 +08:00
9 changed files with 573 additions and 86 deletions

View File

@ -253,4 +253,25 @@ class Api {
// 取消追番
static const String bangumiDel = '/pgc/web/follow/del';
// 番剧列表
// https://api.bilibili.com/pgc/season/index/result?
// st=1&
// order=3
// season_version=-1 全部-1 正片1 电影2 其他3
// spoken_language_type=-1 全部-1 原生1 中文配音2
// area=-1&
// is_finish=-1&
// copyright=-1&
// season_status=-1&
// season_month=-1&
// year=-1&
// style_id=-1&
// sort=0&
// page=1&
// season_type=1&
// pagesize=20&
// type=1
static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
}

20
lib/http/bangumi.dart Normal file
View File

@ -0,0 +1,20 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/list.dart';
class BangumiHttp {
static Future bangumiList({int? page}) async {
var res = await Request().get(Api.bangumiList, data: {'page': page});
if (res.data['code'] == 0) {
return {
'status': true,
'data': BangumiListDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -0,0 +1,85 @@
class BangumiListDataModel {
BangumiListDataModel({
this.hasNext,
this.list,
this.num,
this.size,
this.total,
});
int? hasNext;
List? list;
int? num;
int? size;
int? total;
BangumiListDataModel.fromJson(Map<String, dynamic> json) {
hasNext = json['has_next'];
list = json['list'] != null
? json['list']
.map<BangumiListItemModel>((e) => BangumiListItemModel.fromJson(e))
.toList()
: [];
num = json['num'];
size = json['size'];
total = json['total'];
}
}
class BangumiListItemModel {
BangumiListItemModel({
this.badge,
this.badgeType,
this.cover,
// this.firstEp,
this.indexShow,
this.isFinish,
this.link,
this.mediaId,
this.order,
this.orderType,
this.score,
this.seasonId,
this.seaconStatus,
this.seasonType,
this.subTitle,
this.title,
this.titleIcon,
});
String? badge;
int? badgeType;
String? cover;
String? indexShow;
int? isFinish;
String? link;
int? mediaId;
String? order;
String? orderType;
String? score;
int? seasonId;
int? seaconStatus;
int? seasonType;
String? subTitle;
String? title;
String? titleIcon;
BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type'];
cover = json['cover'];
indexShow = json['index_show'];
isFinish = json['is_finish'];
link = json['link'];
mediaId = json['media_id'];
order = json['order'];
orderType = json['order_type'];
score = json['score'];
seasonId = json['season_id'];
seaconStatus = json['seacon_status'];
seasonType = json['season_type'];
subTitle = json['sub_title'];
title = json['title'];
titleIcon = json['title_icon'];
}
}

View File

@ -1,3 +1,30 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/bangumi.dart';
import 'package:pilipala/models/bangumi/list.dart';
class BangumiController extends GetxController {}
class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = [BangumiListItemModel()].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Future queryBangumiListFeed({type = 'init'}) async {
var result = await BangumiHttp.bangumiList(page: _currentPage);
if (result['status']) {
if (type == 'init') {
bangumiList.value = result['data'].list;
} else {
bangumiList.addAll(result['data'].list);
}
_currentPage += 1;
} else {}
isLoadingMore = false;
return result;
}
// 上拉加载
Future onLoad() async {
queryBangumiListFeed(type: 'onLoad');
}
}

View File

@ -1,4 +1,15 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'controller.dart';
import 'widgets/bangumu_card_v.dart';
class BangumiPage extends StatefulWidget {
const BangumiPage({super.key});
@ -7,12 +18,106 @@ class BangumiPage extends StatefulWidget {
State<BangumiPage> createState() => _BangumiPageState();
}
class _BangumiPageState extends State<BangumiPage> {
class _BangumiPageState extends State<BangumiPage>
with AutomaticKeepAliveClientMixin {
final BangumiController _bangumidController = Get.put(BangumiController());
late Future? _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
ScrollController scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_bangumidController.isLoadingMore) {
await _bangumidController.onLoad();
await Future.delayed(const Duration(milliseconds: 200));
_bangumidController.isLoadingMore = true;
}
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('还在开发中'),
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return Future.delayed(const Duration(seconds: 2));
},
child: CustomScrollView(
controller: _bangumidController.scrollController,
slivers: [
SliverPadding(
padding: EdgeInsets.zero,
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(_bangumidController,
_bangumidController.bangumiList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return contentGrid(_bangumidController, []);
}
},
),
),
const LoadingMore()
],
),
),
);
}
Widget contentGrid(ctr, bangumiList) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace - 2,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + 45,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return bangumiList!.isNotEmpty
? BangumiCardV(bangumiItem: bangumiList[index])
: const SizedBox();
},
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),
);
}

View File

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class BangumiCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
const BangumiCardV({
Key? key,
required this.bangumiItem,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
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 {
int seasonId = bangumiItem.seasonId;
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(seasonId: seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
});
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: 0.65,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: bangumiItem.cover,
width: maxWidth,
height: maxHeight,
),
),
if (bangumiItem.badge != null)
pBadge(bangumiItem.badge, context, 6, 6, null, null),
pBadge(bangumiItem.order, context, null, null, 6, 6,
type: 'gray'),
],
);
}),
),
),
BangumiContent(bangumiItem: bangumiItem)
],
),
),
),
);
}
}
class BangumiContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
const BangumiContent({Key? key, required this.bangumiItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Expanded(
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
return SizedBox(
width: constraints.maxWidth,
child: Text(
bangumiItem.indexShow,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
);
}),
),
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
],
),
// Row(
// children: [
// StatView(
// theme: 'black',
// view: videoItem.stat.view,
// ),
// const SizedBox(width: 6),
// StatDanMu(
// theme: 'black',
// danmu: videoItem.stat.danmaku,
// ),
// ],
// ),
],
),
),
);
}
}

View File

@ -41,10 +41,9 @@ class _HomePageState extends State<HomePage>
CustomAppBar(stream: stream, ctr: _homeController),
Container(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
child: Stack(
child: Row(
children: [
Align(
alignment: Alignment.center,
Expanded(
child: Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
@ -93,6 +92,23 @@ class _HomePageState extends State<HomePage>
),
),
),
SizedBox(
width: 30,
height: 30,
child: IconButton(
tooltip: '全部分类',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {},
icon: Icon(
Icons.dataset_outlined,
color: Theme.of(context).colorScheme.outline,
size: 19,
),
),
),
const SizedBox(width: 14)
],
),
),

View File

@ -54,47 +54,54 @@ class _LivePageState extends State<LivePage> {
@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.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() =>
contentGrid(_liveController, _liveController.liveList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: EdgeInsets.zero,
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 {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
} else {
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}

View File

@ -59,49 +59,56 @@ class _RcmdPageState extends State<RcmdPage>
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(0, 0, 0, 0),
sliver: FutureBuilder(
future: _rcmdController.queryRcmdFeed('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_rcmdController.videoList.length > 1) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
} else {
// 缓存数据
if (_rcmdController.videoList.length > 1) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}