diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart index d7eea2ca..afc74bd2 100644 --- a/lib/common/pages_bottom_sheet.dart +++ b/lib/common/pages_bottom_sheet.dart @@ -430,7 +430,7 @@ class EpisodeGridItem extends StatelessWidget { decoration: BoxDecoration( color: isCurrentIndex ? colorScheme.primaryContainer.withOpacity(0.6) - : colorScheme.secondaryContainer.withOpacity(0.4), + : colorScheme.onInverseSurface.withOpacity(0.6), borderRadius: BorderRadius.circular(8), border: Border.all( color: isCurrentIndex diff --git a/lib/http/api.dart b/lib/http/api.dart index 787b036b..2bd83d69 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -301,10 +301,6 @@ class Api { 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'; - // 我的订阅 - static const String bangumiFollow = - '/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969'; - // 黑名单 static const String blackLst = '/x/relation/blacks'; @@ -604,4 +600,10 @@ class Api { /// 图片上传 static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs'; + + /// 更新追番状态 + static const String updateBangumiStatus = '/pgc/web/follow/status/update'; + + /// 番剧点赞投币收藏状态 + static const String bangumiActionStatus = '/pgc/season/episode/community'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index 91508682..d0c052d6 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; import '../models/bangumi/list.dart'; import 'index.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:html/dom.dart' as html_dom; class BangumiHttp { static Future bangumiList({int? page}) async { @@ -18,8 +21,19 @@ class BangumiHttp { } } - static Future bangumiFollow({int? mid}) async { - var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); + static Future getRecentBangumi({ + int? mid, + int type = 1, + int pn = 1, + int ps = 20, + }) async { + var res = await Request().get(Api.getRecentBangumiApi, data: { + 'vmid': mid, + 'type': type, + 'follow_status': 0, + 'pn': pn, + 'ps': ps, + }); if (res.data['code'] == 0) { return { 'status': true, @@ -33,4 +47,62 @@ class BangumiHttp { }; } } + + // 获取追番状态 + static Future bangumiStatus({required int seasonId}) async { + var res = await Request() + .get('https://www.bilibili.com/bangumi/play/ss$seasonId'); + html_dom.Document document = html_parser.parse(res.data); + // 查找 id 为 __NEXT_DATA__ 的 script 元素 + html_dom.Element? scriptElement = + document.querySelector('script#\\__NEXT_DATA__'); + if (scriptElement != null) { + // 提取 script 元素的内容 + String scriptContent = scriptElement.text; + final dynamic scriptContentJson = jsonDecode(scriptContent); + Map followState = scriptContentJson['props']['pageProps']['followState']; + return { + 'status': true, + 'data': { + 'isFollowed': followState['isFollowed'], + 'followStatus': followState['followStatus'] + } + }; + } else { + print('Script element with id "__NEXT_DATA__" not found.'); + } + } + + // 更新追番状态 + static Future updateBangumiStatus({ + required int seasonId, + required int status, + }) async { + var res = await Request().post(Api.updateBangumiStatus, data: { + 'season_id': seasonId, + 'status': status, + }); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 获取番剧点赞投币收藏状态 + static Future bangumiActionStatus({required int epId}) async { + var res = await Request().get( + Api.bangumiActionStatus, + data: {'ep_id': epId}, + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': 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 index fe71bb61..df20fa3b 100644 --- a/lib/models/bangumi/list.dart +++ b/lib/models/bangumi/list.dart @@ -47,6 +47,7 @@ class BangumiListItemModel { this.title, this.titleIcon, this.progress, + this.progressIndex, }); String? badge; @@ -66,8 +67,8 @@ class BangumiListItemModel { String? subTitle; String? title; String? titleIcon; - String? progress; + int? progressIndex; BangumiListItemModel.fromJson(Map json) { badge = json['badge'] == '' ? null : json['badge']; @@ -87,7 +88,9 @@ class BangumiListItemModel { subTitle = json['sub_title']; title = json['title']; titleIcon = json['title_icon']; - progress = json['progress']; + progressIndex = int.parse( + RegExp(r'第(\d+)话').firstMatch(json['progress'] ?? '第1话')?.group(1) ?? + '0'); } } diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index e5748d6c..29dd15d1 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -9,6 +9,7 @@ class BangumiController extends GetxController { final ScrollController scrollController = ScrollController(); RxList bangumiList = [].obs; RxList bangumiFollowList = [].obs; + RxInt total = 0.obs; int _currentPage = 1; bool isLoadingMore = true; Box userInfoCache = GStrorage.userInfo; @@ -54,9 +55,10 @@ class BangumiController extends GetxController { if (userInfo == null) { return; } - var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid); + var result = await BangumiHttp.getRecentBangumi(mid: userInfo.mid); if (result['status']) { bangumiFollowList.value = result['data'].list; + total.value = result['data'].total; } else {} return result; } diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index cf428c28..dc173a57 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/http/bangumi.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/video.dart'; @@ -52,28 +53,34 @@ class BangumiIntroController extends GetxController { Rx favFolderData = FavFolderData().obs; List addMediaIdsNew = []; List delMediaIdsNew = []; - // 关注状态 默认未关注 - RxMap followStatus = {}.obs; + // 追番状态 1想看 2在看 3已看 + RxBool isFollowed = false.obs; + RxInt followStatus = 1.obs; int _tempThemeValue = -1; var userInfo; PersistentBottomSheetController? bottomSheetController; + List> followStatusList = [ + {'title': '标记为 「想看」', 'status': 1}, + {'title': '标记为 「在看」', 'status': 2}, + {'title': '标记为 「已看」', 'status': 3}, + {'title': '取消追番', 'status': -1}, + ]; @override void onInit() { super.onInit(); userInfo = userInfoCache.get('userInfoCache'); userLogin = userInfo != null; + if (userLogin && seasonId != null) { + bangumiStatus(); + } } // 获取番剧简介&选集 Future queryBangumiIntro() async { if (userLogin) { - // 获取点赞状态 - queryHasLikeVideo(); - // 获取投币状态 - queryHasCoinVideo(); - // 获取收藏状态 - queryHasFavVideo(); + // 获取点赞投币收藏状态 + bangumiActionStatus(); } var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); if (result['status']) { @@ -83,26 +90,15 @@ class BangumiIntroController extends GetxController { return result; } - // 获取点赞状态 - Future queryHasLikeVideo() async { - var result = await VideoHttp.hasLikeVideo(bvid: bvid); - // data num 被点赞标志 0:未点赞 1:已点赞 - hasLike.value = result["data"] == 1 ? true : false; - } - - // 获取投币状态 - Future queryHasCoinVideo() async { - var result = await VideoHttp.hasCoinVideo(bvid: bvid); - hasCoin.value = result["data"]['multiply'] == 0 ? false : true; - } - - // 获取收藏状态 - Future queryHasFavVideo() async { - var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid)); + // 获取番剧点赞投币收藏状态 + Future bangumiActionStatus() async { + var result = await BangumiHttp.bangumiActionStatus(epId: epId!); if (result['status']) { - hasFav.value = result["data"]['favoured']; + hasLike.value = result['data']['like'] == 1; + hasCoin.value = result['data']['coin_number'] != 0; + hasFav.value = result['data']['favorite'] == 1; } else { - hasFav.value = false; + SmartDialog.showToast(result['msg']); } } @@ -110,7 +106,7 @@ class BangumiIntroController extends GetxController { Future actionLikeVideo() async { var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); if (result['status']) { - SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞'); + SmartDialog.showToast(!hasLike.value ? '点赞成功' : '取消赞'); hasLike.value = !hasLike.value; bangumiDetail.value.stat!['likes'] = bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1); @@ -147,7 +143,7 @@ class BangumiIntroController extends GetxController { var res = await VideoHttp.coinVideo( bvid: bvid, multiply: _tempThemeValue); if (res['status']) { - SmartDialog.showToast('投币成功 👏'); + SmartDialog.showToast('投币成功'); hasCoin.value = true; bangumiDetail.value.stat!['coins'] = bangumiDetail.value.stat!['coins'] + @@ -185,9 +181,11 @@ class BangumiIntroController extends GetxController { addMediaIdsNew = []; delMediaIdsNew = []; // 重新获取收藏状态 - queryHasFavVideo(); - SmartDialog.showToast('✅ 操作成功'); + bangumiActionStatus(); + SmartDialog.showToast('操作成功'); Get.back(); + } else { + SmartDialog.showToast(result['msg']); } } @@ -239,15 +237,22 @@ class BangumiIntroController extends GetxController { // 追番 Future bangumiAdd() async { - var result = - await VideoHttp.bangumiAdd(seasonId: bangumiDetail.value.seasonId); + var result = await VideoHttp.bangumiAdd( + seasonId: seasonId ?? bangumiDetail.value.seasonId); + if (result['status']) { + followStatus.value = 2; + isFollowed.value = true; + } SmartDialog.showToast(result['msg']); } // 取消追番 Future bangumiDel() async { - var result = - await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId); + var result = await VideoHttp.bangumiDel( + seasonId: seasonId ?? bangumiDetail.value.seasonId); + if (result['status']) { + isFollowed.value = false; + } SmartDialog.showToast(result['msg']); } @@ -315,4 +320,35 @@ class BangumiIntroController extends GetxController { hiddenEpisodeBottomSheet() { bottomSheetController?.close(); } + + // 获取追番状态 + Future bangumiStatus() async { + var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!); + if (result['status']) { + followStatus.value = result['data']['followStatus']; + isFollowed.value = result['data']['isFollowed']; + } + return result; + } + + // 更新追番状态 + Future updateBangumiStatus(int status) async { + Get.back(); + if (status == -1) { + bangumiDel(); + } else { + var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!); + if (result['status']) { + followStatus.value = status; + final title = followStatusList.firstWhere( + (e) => e['status'] == status, + orElse: () => {'title': '未知状态'}, + )['title']; + SmartDialog.showToast('追番状态$title'); + } else { + SmartDialog.showToast(result['msg']); + } + return result; + } + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index b79b37b8..429b109a 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -239,104 +239,65 @@ class _BangumiInfoState extends State { Expanded( child: InkWell( onTap: () => showIntroDetail(), + borderRadius: BorderRadius.circular(8), child: SizedBox( height: 115 / 0.75, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Text( - widget.bangumiDetail!.title!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 20), - SizedBox( - width: 34, - height: 34, - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith( - (Set states) { - return t.colorScheme.primaryContainer - .withOpacity(0.7); - }), - ), - onPressed: () => - bangumiIntroController.bangumiAdd(), - icon: Icon( - Icons.favorite_border_rounded, - color: t.colorScheme.primary, - size: 22, + child: Padding( + padding: const EdgeInsets.fromLTRB(6, 4, 6, 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + widget.bangumiDetail!.title!, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), - ), - ], - ), - Row( - children: [ - StatView( - view: widget.bangumiDetail!.stat!['views'], - size: 'medium', - ), - const SizedBox(width: 6), - StatDanMu( - danmu: widget.bangumiDetail!.stat!['danmakus'], - size: 'medium', - ), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Text( - (widget.bangumiDetail!.areas!.isNotEmpty - ? widget.bangumiDetail!.areas!.first['name'] - : ''), - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, + const SizedBox(width: 20), + Obx( + () => BangumiStatusWidget( + ctr: bangumiIntroController, + isFollowed: + bangumiIntroController.isFollowed.value, + ), ), - ), - const SizedBox(width: 6), - Text( - widget.bangumiDetail!.publish!['pub_time_show'], - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - ], - ), - Text( - widget.bangumiDetail!.newEp!['desc'], - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, + ], ), - ), - const Spacer(), - Text( - '简介:${widget.bangumiDetail!.evaluate!}', - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - color: t.colorScheme.outline, + const SizedBox(height: 4), + Row( + children: [ + StatView( + view: widget.bangumiDetail!.stat!['views'], + size: 'medium', + ), + const SizedBox(width: 6), + StatDanMu( + danmu: widget.bangumiDetail!.stat!['danmakus'], + size: 'medium', + ), + ], ), - ), - ], + const SizedBox(height: 10), + Text( + '简介:${widget.bangumiDetail!.evaluate!}', + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: t.colorScheme.outline, + ), + ), + ], + ), ), ), ), @@ -426,3 +387,97 @@ class _BangumiInfoState extends State { }); } } + +// 追番状态 +class BangumiStatusWidget extends StatelessWidget { + final BangumiIntroController ctr; + final bool isFollowed; + + const BangumiStatusWidget({ + Key? key, + required this.ctr, + required this.isFollowed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + ColorScheme colorScheme = Theme.of(context).colorScheme; + + void updateFollowStatus() { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return morePanel(context, ctr); + }, + ); + } + + return Obx( + () => SizedBox( + width: 34, + height: 34, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: + MaterialStateProperty.resolveWith((Set states) { + return ctr.isFollowed.value + ? colorScheme.primaryContainer.withOpacity(0.7) + : colorScheme.outlineVariant.withOpacity(0.7); + }), + ), + onPressed: + isFollowed ? () => updateFollowStatus() : () => ctr.bangumiAdd(), + icon: Icon( + ctr.isFollowed.value + ? Icons.favorite + : Icons.favorite_border_rounded, + color: ctr.isFollowed.value + ? colorScheme.primary + : colorScheme.outline, + size: 22, + ), + ), + ), + ); + } + + Widget morePanel(BuildContext context, BangumiIntroController ctr) { + return Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: const BorderRadius.all(Radius.circular(3))), + ), + ), + ), + ), + ...ctr.followStatusList + .map( + (e) => ListTile( + onTap: () => ctr.updateBangumiStatus(e['status']), + selected: ctr.followStatus == e['status'], + title: Text(e['title']), + ), + ) + .toList(), + const SizedBox(height: 20), + ], + ), + ); + } +} diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 4092943b..f1f8d279 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -76,9 +76,14 @@ class _BangumiPageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '最近追番', - style: Theme.of(context).textTheme.titleMedium, + Obx( + () => 0 != _bangumidController.total.value + ? Text( + '我的追番(${_bangumidController.total.value})', + style: + Theme.of(context).textTheme.titleMedium, + ) + : const SizedBox(), ), IconButton( onPressed: () { diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 3cb9abc0..db29d8e2 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -175,59 +175,60 @@ class _BangumiPanelState extends State { return Container( width: 150, margin: const EdgeInsets.only(right: 10), - child: Material( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( color: Theme.of(context).colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () => changeFucCall(page, i), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 10, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (isSelected) ...[ - Image.asset('assets/images/live.png', - color: primary, height: 12), - const SizedBox(width: 6) - ], + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => changeFucCall(page, i), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isSelected) ...[ + Image.asset('assets/images/live.png', + color: primary, height: 12), + const SizedBox(width: 6) + ], + Text( + '第${i + 1}话', + style: TextStyle( + fontSize: 13, + color: isSelected ? primary : onSurface, + ), + ), + const SizedBox(width: 2), + if (page.badge != null) ...[ + const Spacer(), Text( - '第${i + 1}话', + page.badge!, style: TextStyle( - fontSize: 13, - color: isSelected ? primary : onSurface, + fontSize: 12, + color: primary, ), ), - const SizedBox(width: 2), - if (page.badge != null) ...[ - const Spacer(), - Text( - page.badge!, - style: TextStyle( - fontSize: 12, - color: primary, - ), - ), - ] - ], + ] + ], + ), + const SizedBox(height: 3), + Text( + page.longTitle!, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: isSelected ? primary : onSurface, ), - const SizedBox(height: 3), - Text( - page.longTitle!, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: isSelected ? primary : onSurface, - ), - overflow: TextOverflow.ellipsis, - ) - ], - ), + overflow: TextOverflow.ellipsis, + ) + ], ), ), ), diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart index 10d95a1c..19aa4d88 100644 --- a/lib/pages/bangumi/widgets/bangumu_card_v.dart +++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart @@ -25,6 +25,7 @@ class BangumiCardV extends StatelessWidget { RoutePush.bangumiPush( bangumiItem.seasonId, null, + progressIndex: bangumiItem.progressIndex, heroTag: heroTag, ); }, diff --git a/lib/utils/route_push.dart b/lib/utils/route_push.dart index 9ee28846..9254da2a 100644 --- a/lib/utils/route_push.dart +++ b/lib/utils/route_push.dart @@ -8,7 +8,7 @@ import 'package:pilipala/utils/utils.dart'; class RoutePush { // 番剧跳转 static Future bangumiPush(int? seasonId, int? epId, - {String? heroTag}) async { + {String? heroTag, int? progressIndex}) async { SmartDialog.showLoading(msg: '获取中...'); try { var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); @@ -19,7 +19,10 @@ class RoutePush { return; } final BangumiInfoModel bangumiDetail = result['data']; - final EpisodeItem episode = bangumiDetail.episodes!.first; + EpisodeItem episode = bangumiDetail.episodes!.first; + if (progressIndex != null && progressIndex >= 1) { + episode = bangumiDetail.episodes![progressIndex - 1]; + } final int epId = episode.id!; final int cid = episode.cid!; final String bvid = episode.bvid!; @@ -31,7 +34,7 @@ class RoutePush { }; arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid); Get.toNamed( - '/video?bvid=$bvid&cid=$cid&epId=$epId', + '/video?bvid=$bvid&cid=$cid&epId=$epId&seasonId=$seasonId', arguments: arguments, ); } else {