diff --git a/lib/http/api.dart b/lib/http/api.dart index d1dc350a..d8158b54 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart new file mode 100644 index 00000000..6793dd83 --- /dev/null +++ b/lib/http/bangumi.dart @@ -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'], + }; + } + } +} diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart new file mode 100644 index 00000000..3971f3c7 --- /dev/null +++ b/lib/models/bangumi/list.dart @@ -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 json) { + hasNext = json['has_next']; + list = json['list'] != null + ? json['list'] + .map((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 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']; + } +} diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index cb02a3f7..796a541e 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -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 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'); + } +} diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index cb1b1ddb..9c7145e8 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -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 createState() => _BangumiPageState(); } -class _BangumiPageState extends State { +class _BangumiPageState extends State + 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 mainStream = + Get.find().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, ), ); } diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart new file mode 100644 index 00000000..3f5eda22 --- /dev/null +++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart @@ -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, + // ), + // ], + // ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 6a71d664..28610a35 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -41,10 +41,9 @@ class _HomePageState extends State 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 ), ), ), + 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) ], ), ), diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 93fe276f..20eeb09c 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -54,47 +54,54 @@ class _LivePageState extends State { @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() + ], + ), ), ); } diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index 41987f8c..8fea2f79 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -59,49 +59,56 @@ class _RcmdPageState extends State @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() + ], + ), ), ); }