diff --git a/lib/http/api.dart b/lib/http/api.dart index 288f0b2b..fdf94efe 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -306,4 +306,14 @@ class Api { static const String onlineTotal = '/x/player/online/total'; static const String webDanmaku = '/x/v2/dm/web/seg.so'; + + // up主分组 + static const String followUpTag = '/x/relation/tags'; + + // 设置Up主分组 + // 0 添加至默认分组 否则使用,分割tagid + static const String addUsers = '/x/relation/tags/addUsers'; + + // 获取指定分组下的up + static const String followUpGroup = '/x/relation/tag'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index a9c158da..a48dbffd 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,7 +1,9 @@ import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/dynamics/result.dart'; +import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/info.dart'; +import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/utils/wbi_sign.dart'; class MemberHttp { @@ -144,4 +146,73 @@ class MemberHttp { }; } } + + // 查询分组 + static Future followUpTags() async { + var res = await Request().get(Api.followUpTag); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'] + .map((e) => MemberTagItemModel.fromJson(e)) + .toList() + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 设置分组 + static Future addUsers(int? fids, String? tagids) async { + var res = await Request().post(Api.addUsers, queryParameters: { + 'fids': fids, + 'tagids': tagids ?? '0', + 'csrf': await Request.getCsrf(), + }, data: { + 'cross_domain': true + }); + if (res.data['code'] == 0) { + return {'status': true, 'data': [], 'msg': '操作成功'}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 获取某分组下的up + static Future followUpGroup( + int? mid, + int? tagid, + int? pn, + int? ps, + ) async { + var res = await Request().get(Api.followUpGroup, data: { + 'mid': mid, + 'tagid': tagid, + 'pn': pn, + 'ps': ps, + }); + if (res.data['code'] == 0) { + // FollowItemModel + return { + 'status': true, + 'data': res.data['data'] + .map((e) => FollowItemModel.fromJson(e)) + .toList() + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/search.dart b/lib/http/search.dart index 8583b271..7b21c2cd 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -1,13 +1,16 @@ import 'dart:convert'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/index.dart'; import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/suggest.dart'; +import 'package:pilipala/utils/storage.dart'; class SearchHttp { + static Box setting = GStrorage.setting; static Future hotSearchList() async { var res = await Request().get(Api.hotSearchList); if (res.data is String) { @@ -78,6 +81,12 @@ class SearchHttp { try { switch (searchType) { case SearchType.video: + List blackMidsList = + setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + for (var i in res.data['data']['result']) { + // 屏蔽推广和拉黑用户 + i['available'] = !blackMidsList.contains(i['mid']); + } data = SearchVideoModel.fromJson(res.data['data']); break; case SearchType.live_room: diff --git a/lib/models/follow/result.dart b/lib/models/follow/result.dart index c6656165..2f1cedf5 100644 --- a/lib/models/follow/result.dart +++ b/lib/models/follow/result.dart @@ -8,7 +8,7 @@ class FollowDataModel { List? list; FollowDataModel.fromJson(Map json) { - total = json['total']; + total = json['total'] ?? 0; list = json['list'] .map((e) => FollowItemModel.fromJson(e)) .toList(); @@ -19,7 +19,7 @@ class FollowItemModel { FollowItemModel({ this.mid, this.attribute, - this.mtime, + // this.mtime, this.tag, this.special, this.uname, @@ -30,7 +30,7 @@ class FollowItemModel { int? mid; int? attribute; - int? mtime; + // int? mtime; List? tag; int? special; String? uname; @@ -41,7 +41,7 @@ class FollowItemModel { FollowItemModel.fromJson(Map json) { mid = json['mid']; attribute = json['attribute']; - mtime = json['mtime']; + // mtime = json['mtime']; tag = json['tag']; special = json['special']; uname = json['uname']; diff --git a/lib/models/member/tags.dart b/lib/models/member/tags.dart new file mode 100644 index 00000000..33f7c1f8 --- /dev/null +++ b/lib/models/member/tags.dart @@ -0,0 +1,23 @@ +class MemberTagItemModel { + MemberTagItemModel({ + this.count, + this.name, + this.tagid, + this.tip, + this.checked, + }); + + int? count; + String? name; + int? tagid; + String? tip; + bool? checked; + + MemberTagItemModel.fromJson(Map json) { + count = json['count']; + name = json['name']; + tagid = json['tagid']; + tip = json['tip']; + checked = false; + } +} diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 7aec24dd..3d381ed9 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -6,6 +6,7 @@ class SearchVideoModel { List? list; SearchVideoModel.fromJson(Map json) { list = json['result'] + .where((e) => e['available'] == true) .map((e) => SearchVideoItemModel.fromJson(e)) .toList(); } @@ -17,7 +18,7 @@ class SearchVideoItemModel { this.id, this.cid, // this.author, - // this.mid, + this.mid, // this.typeid, // this.typename, this.arcurl, @@ -47,7 +48,7 @@ class SearchVideoItemModel { int? id; int? cid; // String? author; - // String? mid; + int? mid; // String? typeid; // String? typename; String? arcurl; @@ -80,6 +81,7 @@ class SearchVideoItemModel { arcurl = json['arcurl']; aid = json['aid']; bvid = json['bvid']; + mid = json['mid']; // title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); title = Em.regTitle(json['title']); description = json['description']; diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index c027f8af..f37a3310 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart'; +import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; @@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController { ? int.parse(Get.parameters['seasonId']!) : null; var epId = Get.parameters['epId'] != null - ? int.parse(Get.parameters['epId']!) + ? int.tryParse(Get.parameters['epId']!) : null; // 是否预渲染 骨架屏 @@ -257,7 +258,7 @@ class BangumiIntroController extends GetxController { VideoDetailController videoDetailCtr = Get.find(tag: Get.arguments['heroTag']); videoDetailCtr.bvid = bvid; - videoDetailCtr.cid = cid; + videoDetailCtr.cid.value = cid; videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.queryVideoUrl(); // 重新请求评论 @@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController { } return result; } + + /// 列表循环或者顺序播放时,自动播放下一个 + void nextPlay() { + late List episodes; + if (bangumiDetail.value.episodes != null) { + episodes = bangumiDetail.value.episodes!; + } + VideoDetailController videoDetailCtr = + Get.find(tag: Get.arguments['heroTag']); + int currentIndex = + episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value); + int nextIndex = currentIndex + 1; + PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat; + // 列表循环 + if (platRepeat == PlayRepeat.listCycle) { + if (nextIndex == episodes.length - 1) { + nextIndex = 0; + } + } + if (nextIndex <= episodes.length - 1 && + platRepeat == PlayRepeat.listOrder) {} + + int cid = episodes[nextIndex].cid!; + String bvid = episodes[nextIndex].bvid!; + int aid = episodes[nextIndex].aid!; + changeSeasonOrbangu(bvid, cid, aid); + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index 9ebd2558..af47d7da 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget { class _BangumiIntroPanelState extends State with AutomaticKeepAliveClientMixin { - final BangumiIntroController bangumiIntroController = - Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']); + late BangumiIntroController bangumiIntroController; + late VideoDetailController videoDetailCtr; BangumiInfoModel? bangumiDetail; late Future _futureBuilderFuture; + late int cid; + late String heroTag; // 添加页面缓存 @override @@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State @override void initState() { super.initState(); + heroTag = Get.arguments['heroTag']; + cid = widget.cid!; + bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); + videoDetailCtr = Get.find(tag: heroTag); bangumiIntroController.bangumiDetail.listen((value) { bangumiDetail = value; }); _futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); + videoDetailCtr.cid.listen((p0) { + print('🐶🐶$p0'); + cid = p0; + setState(() {}); + }); } @override @@ -61,9 +72,11 @@ class _BangumiIntroPanelState extends State if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data['status']) { // 请求成功 + return BangumiInfo( loadingStatus: false, bangumiDetail: bangumiDetail, + cid: cid, ); } else { // 请求错误 @@ -77,7 +90,7 @@ class _BangumiIntroPanelState extends State return BangumiInfo( loadingStatus: true, bangumiDetail: bangumiDetail, - cid: widget.cid, + cid: cid, ); } }, @@ -118,6 +131,12 @@ class _BangumiInfoState extends State { bangumiItem = bangumiIntroController.bangumiItem; sheetHeight = localCache.get('sheetHeight'); cid = widget.cid!; + print('cid: $cid'); + videoDetailCtr.cid.listen((p0) { + cid = p0; + print('cid: $cid'); + setState(() {}); + }); } // 收藏 diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 9c55448d..bb27a38a 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -1,7 +1,9 @@ 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/models/bangumi/info.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/utils/storage.dart'; class BangumiPanel extends StatefulWidget { @@ -30,16 +32,28 @@ class _BangumiPanelState extends State { dynamic userInfo; // 默认未开通 int vipStatus = 0; + late int cid; + String heroTag = Get.arguments['heroTag']; + late final VideoDetailController videoDetailCtr; @override void initState() { super.initState(); - currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!); + cid = widget.cid!; + currentIndex = widget.pages.indexWhere((e) => e.cid == cid); scrollToIndex(); userInfo = userInfoCache.get('userInfoCache'); if (userInfo != null) { vipStatus = userInfo.vipStatus; } + videoDetailCtr = Get.find(tag: heroTag); + + videoDetailCtr.cid.listen((p0) { + cid = p0; + setState(() {}); + currentIndex = widget.pages.indexWhere((e) => e.cid == cid); + scrollToIndex(); + }); } @override diff --git a/lib/pages/dynamics/deatil/view.dart b/lib/pages/dynamics/deatil/view.dart index 4c49ecc7..116e0d27 100644 --- a/lib/pages/dynamics/deatil/view.dart +++ b/lib/pages/dynamics/deatil/view.dart @@ -177,7 +177,7 @@ class _DynamicDetailPageState extends State return AnimatedOpacity( opacity: snapshot.data ? 1 : 0, duration: const Duration(milliseconds: 300), - child: author(_dynamicDetailController!.item, context), + child: AuthorPanel(item: _dynamicDetailController.item), ); }, ), diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 67a21371..a30d666e 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -1,65 +1,159 @@ import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/http/user.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/utils.dart'; -Widget author(item, context) { - String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid); - return Row( - children: [ - GestureDetector( - onTap: () { - feedBack(); - Get.toNamed( - '/member?mid=${item.modules.moduleAuthor.mid}', - arguments: { - 'face': item.modules.moduleAuthor.face, - 'heroTag': heroTag - }, - ); - }, - child: Hero( - tag: heroTag, - child: NetworkImgLayer( - width: 40, - height: 40, - type: 'avatar', - src: item.modules.moduleAuthor.face, +class AuthorPanel extends StatelessWidget { + final dynamic item; + const AuthorPanel({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid); + return Row( + children: [ + GestureDetector( + onTap: () { + feedBack(); + Get.toNamed( + '/member?mid=${item.modules.moduleAuthor.mid}', + arguments: { + 'face': item.modules.moduleAuthor.face, + 'heroTag': heroTag + }, + ); + }, + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 40, + height: 40, + type: 'avatar', + src: item.modules.moduleAuthor.face, + ), ), ), - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.modules.moduleAuthor.name, - style: TextStyle( - color: item.modules.moduleAuthor!.vip != null && - item.modules.moduleAuthor!.vip['status'] > 0 - ? const Color.fromARGB(255, 251, 100, 163) - : Theme.of(context).colorScheme.onBackground, - fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.modules.moduleAuthor.name, + style: TextStyle( + color: item.modules.moduleAuthor!.vip != null && + item.modules.moduleAuthor!.vip['status'] > 0 + ? const Color.fromARGB(255, 251, 100, 163) + : Theme.of(context).colorScheme.onBackground, + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + ), + ), + DefaultTextStyle.merge( + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + ), + child: Row( + children: [ + Text(item.modules.moduleAuthor.pubTime), + if (item.modules.moduleAuthor.pubTime != '' && + item.modules.moduleAuthor.pubAction != '') + const Text(' '), + Text(item.modules.moduleAuthor.pubAction), + ], + ), + ) + ], + ), + const Spacer(), + if (item.type == 'DYNAMIC_TYPE_AV') + SizedBox( + width: 32, + height: 32, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return MorePanel(item: item); + }, + ); + }, + icon: const Icon(Icons.more_vert_outlined, size: 18), ), ), - DefaultTextStyle.merge( - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + ], + ); + } +} + +class MorePanel extends StatelessWidget { + final dynamic item; + const MorePanel({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + // clipBehavior: Clip.hardEdge, + 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))), + ), + ), ), - child: Row( - children: [ - Text(item.modules.moduleAuthor.pubTime), - if (item.modules.moduleAuthor.pubTime != '' && - item.modules.moduleAuthor.pubAction != '') - const Text(' '), - Text(item.modules.moduleAuthor.pubAction), - ], + ), + ListTile( + onTap: () async { + try { + String bvid = item.modules.moduleDynamic.major.archive.bvid; + var res = await UserHttp.toViewLater(bvid: bvid); + SmartDialog.showToast(res['msg']); + Get.back(); + } catch (err) { + SmartDialog.showToast('出错了:${err.toString()}'); + } + }, + minLeadingWidth: 0, + // dense: true, + leading: const Icon(Icons.watch_later_outlined, size: 19), + title: Text( + '稍后再看', + style: Theme.of(context).textTheme.titleSmall, ), - ) + ), + const Divider(thickness: 0.1, height: 1), + ListTile( + onTap: () => Get.back(), + minLeadingWidth: 0, + dense: true, + title: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + textAlign: TextAlign.center, + ), + ), ], ), - ], - ); + ); + } } diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index 42670ac1..366f7ffe 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -39,7 +39,7 @@ class DynamicPanel extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), - child: author(item, context), + child: AuthorPanel(item: item), ), if (item!.modules!.moduleDynamic!.desc != null || item!.modules!.moduleDynamic!.major != null) diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index fe1bfabc..fe4b6100 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -1,20 +1,28 @@ +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/follow.dart'; +import 'package:pilipala/http/member.dart'; import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/utils/storage.dart'; -class FollowController extends GetxController { +/// 查看自己的关注时,可以查看分类 +/// 查看其他人的关注时,只可以看全部 +class FollowController extends GetxController with GetTickerProviderStateMixin { Box userInfoCache = GStrorage.userInfo; int pn = 1; int ps = 20; int total = 0; - RxList followList = [FollowItemModel()].obs; + RxList followList = [].obs; late int mid; late String name; var userInfo; RxString loadingText = '加载中...'.obs; + RxBool isOwner = false.obs; + late List followTags; + late TabController tabController; @override void onInit() { @@ -23,6 +31,7 @@ class FollowController extends GetxController { mid = Get.parameters['mid'] != null ? int.parse(Get.parameters['mid']!) : userInfo.mid; + isOwner.value = mid == userInfo.mid; name = Get.parameters['name'] ?? userInfo.uname; } @@ -56,4 +65,20 @@ class FollowController extends GetxController { } return res; } + + // 当查看当前用户的关注时,请求关注分组 + Future followUpTags() async { + if (userInfo != null && mid == userInfo.mid) { + var res = await MemberHttp.followUpTags(); + if (res['status']) { + followTags = res['data']; + tabController = TabController( + initialIndex: 0, + length: res['data'].length, + vsync: this, + ); + } + return res; + } + } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 0a8cc0ef..a4f1011b 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,12 +1,8 @@ -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/no_data.dart'; -import 'package:pilipala/models/follow/result.dart'; - import 'controller.dart'; -import 'widgets/follow_item.dart'; +import 'widgets/follow_list.dart'; +import 'widgets/owner_follow_list.dart'; class FollowPage extends StatefulWidget { const FollowPage({super.key}); @@ -19,30 +15,12 @@ class _FollowPageState extends State { late String mid; late FollowController _followController; final ScrollController scrollController = ScrollController(); - Future? _futureBuilderFuture; @override void initState() { super.initState(); mid = Get.parameters['mid']!; _followController = Get.put(FollowController(), tag: mid); - _futureBuilderFuture = _followController.queryFollowings('init'); - scrollController.addListener( - () async { - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('follow', const Duration(seconds: 1), () { - _followController.queryFollowings('onLoad'); - }); - } - }, - ); - } - - @override - void dispose() { - scrollController.removeListener(() {}); - super.dispose(); } @override @@ -54,73 +32,57 @@ class _FollowPageState extends State { titleSpacing: 0, centerTitle: false, title: Text( - '${_followController.name}的关注', + _followController.isOwner.value + ? '我的关注' + : '${_followController.name}的关注', style: Theme.of(context).textTheme.titleMedium, ), ), - body: RefreshIndicator( - onRefresh: () async => - await _followController.queryFollowings('init'), - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - List list = _followController.followList; - return Obx( - () => list.isNotEmpty - ? ListView.builder( - controller: scrollController, - itemCount: list.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == list.length) { - return Container( - height: - MediaQuery.of(context).padding.bottom + - 60, - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .padding - .bottom), - child: Center( - child: Obx( - () => Text( - _followController.loadingText.value, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline, - fontSize: 13), - ), - ), - ), - ); - } else { - return followItem(item: list[index]); - } - }, - ) - : const CustomScrollView( - slivers: [NoData()], + body: Obx( + () => !_followController.isOwner.value + ? FollowList(ctr: _followController) + : FutureBuilder( + future: _followController.followUpTags(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + return Column( + children: [ + TabBar( + controller: _followController.tabController, + isScrollable: true, + tabs: [ + for (var i in data['data']) ...[ + Tab(text: i.name), + ] + ]), + Expanded( + child: TabBarView( + controller: _followController.tabController, + children: [ + for (var i = 0; + i < _followController.tabController.length; + i++) ...[ + OwnerFollowList( + ctr: _followController, + tagItem: _followController.followTags[i], + ) + ] + ], + ), ), - ); - } else { - return CustomScrollView( - slivers: [ - HttpError( - errMsg: data['msg'], - fn: () => _followController.queryFollowings('init'), - ) - ], - ); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - )), + ], + ); + } else { + return const SizedBox(); + } + } else { + return const SizedBox(); + } + }, + ), + ), ); } } diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index d9b2617b..cae72f4c 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -1,38 +1,45 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/utils.dart'; -Widget followItem({item}) { - String heroTag = Utils.makeHeroTag(item!.mid); - return ListTile( - onTap: () { - feedBack(); - Get.toNamed('/member?mid=${item.mid}', - arguments: {'face': item.face, 'heroTag': heroTag}); - }, - leading: Hero( - tag: heroTag, - child: NetworkImgLayer( - width: 45, - height: 45, - type: 'avatar', - src: item.face, +class FollowItem extends StatelessWidget { + final FollowItemModel item; + const FollowItem({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(item!.mid); + return ListTile( + onTap: () { + feedBack(); + Get.toNamed('/member?mid=${item.mid}', + arguments: {'face': item.face, 'heroTag': heroTag}); + }, + leading: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: item.face, + ), ), - ), - title: Text( - item.uname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - item.sign, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - dense: true, - trailing: const SizedBox(width: 6), - ); + title: Text( + item.uname!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + item.sign!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + dense: true, + trailing: const SizedBox(width: 6), + ); + } } diff --git a/lib/pages/follow/widgets/follow_list.dart b/lib/pages/follow/widgets/follow_list.dart new file mode 100644 index 00000000..73535e2a --- /dev/null +++ b/lib/pages/follow/widgets/follow_list.dart @@ -0,0 +1,111 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; +import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/pages/follow/index.dart'; + +import 'follow_item.dart'; + +class FollowList extends StatefulWidget { + final FollowController ctr; + const FollowList({ + super.key, + required this.ctr, + }); + + @override + State createState() => _FollowListState(); +} + +class _FollowListState extends State { + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _futureBuilderFuture = widget.ctr.queryFollowings('init'); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + widget.ctr.queryFollowings('onLoad'); + }); + } + }, + ); + } + + @override + void dispose() { + scrollController.removeListener(() {}); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async => await widget.ctr.queryFollowings('init'), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + List list = widget.ctr.followList; + return Obx( + () => list.isNotEmpty + ? ListView.builder( + controller: scrollController, + itemCount: list.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == list.length) { + return Container( + height: + MediaQuery.of(context).padding.bottom + 60, + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + child: Center( + child: Obx( + () => Text( + widget.ctr.loadingText.value, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: 13), + ), + ), + ), + ); + } else { + return FollowItem(item: list[index]); + } + }, + ) + : const CustomScrollView(slivers: [NoData()]), + ); + } else { + return CustomScrollView( + slivers: [ + HttpError( + errMsg: data['msg'], + fn: () => widget.ctr.queryFollowings('init'), + ) + ], + ); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/follow/widgets/owner_follow_list.dart b/lib/pages/follow/widgets/owner_follow_list.dart new file mode 100644 index 00000000..13a1d0b3 --- /dev/null +++ b/lib/pages/follow/widgets/owner_follow_list.dart @@ -0,0 +1,128 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/follow/result.dart'; +import 'package:pilipala/models/member/tags.dart'; +import 'package:pilipala/pages/follow/index.dart'; +import 'follow_item.dart'; + +class OwnerFollowList extends StatefulWidget { + final FollowController ctr; + final MemberTagItemModel? tagItem; + const OwnerFollowList({super.key, required this.ctr, this.tagItem}); + + @override + State createState() => _OwnerFollowListState(); +} + +class _OwnerFollowListState extends State + with AutomaticKeepAliveClientMixin { + late int mid; + late Future _futureBuilderFuture; + final ScrollController scrollController = ScrollController(); + int pn = 1; + int ps = 20; + late MemberTagItemModel tagItem; + RxList followList = [].obs; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + mid = widget.ctr.mid; + tagItem = widget.tagItem!; + _futureBuilderFuture = followUpGroup('init'); + scrollController.addListener( + () async { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('follow', const Duration(seconds: 1), () { + followUpGroup('onLoad'); + }); + } + }, + ); + } + + // 获取分组下up + Future followUpGroup(type) async { + if (type == 'init') { + pn = 1; + } + var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps); + if (res['status']) { + if (res['data'].isNotEmpty) { + if (type == 'init') { + followList.value = res['data']; + } else { + followList.addAll(res['data']); + } + pn += 1; + } + } + return res; + } + + @override + void dispose() { + scrollController.removeListener(() {}); + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return RefreshIndicator( + onRefresh: () async => await followUpGroup('init'), + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data['status']) { + return Obx( + () => followList.isNotEmpty + ? ListView.builder( + controller: scrollController, + itemCount: followList.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == followList.length) { + return Container( + height: + MediaQuery.of(context).padding.bottom + 60, + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + ); + } else { + return FollowItem(item: followList[index]); + } + }, + ) + : const CustomScrollView(slivers: [NoData()]), + ); + } else { + return CustomScrollView( + slivers: [ + HttpError( + errMsg: data['msg'], + fn: () => widget.ctr.queryFollowings('init'), + ) + ], + ); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + ); + } +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index d6795c52..bc525fa3 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -24,7 +24,7 @@ class VideoDetailController extends GetxController with GetSingleTickerProviderStateMixin { /// 路由传参 String bvid = Get.parameters['bvid']!; - int cid = int.parse(Get.parameters['cid']!); + RxInt cid = int.parse(Get.parameters['cid']!).obs; RxInt danmakuCid = 0.obs; String heroTag = Get.arguments['heroTag']; // 视频详情 @@ -109,7 +109,7 @@ class VideoDetailController extends GetxController localCache.get(LocalCacheKey.historyPause) == true) { enableHeart = false; } - danmakuCid.value = cid; + danmakuCid.value = cid.value; /// if (Platform.isAndroid) { @@ -218,7 +218,7 @@ class VideoDetailController extends GetxController // 默认1倍速 speed: 1.0, bvid: bvid, - cid: cid, + cid: cid.value, enableHeart: enableHeart, isFirstTime: isFirstTime, autoplay: autoplay, @@ -230,7 +230,7 @@ class VideoDetailController extends GetxController // 视频链接 Future queryVideoUrl() async { - var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); + var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); if (result['status']) { data = result['data']; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 71b63c71..6c32dc33 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -11,11 +11,14 @@ import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart'; +import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; +import 'widgets/group_panel.dart'; + class VideoIntroController extends GetxController { // 视频bvid String bvid = Get.parameters['bvid']!; @@ -58,6 +61,7 @@ class VideoIntroController extends GetxController { RxString total = '1'.obs; Timer? timer; bool isPaused = false; + String heroTag = Get.arguments['heroTag']; @override void onInit() { @@ -102,9 +106,10 @@ class VideoIntroController extends GetxController { if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { lastPlayCid.value = videoDetail.value.pages!.first.cid!; } - Get.find(tag: Get.arguments['heroTag']) - .tabs - .value = ['简介', '评论 ${result['data']!.stat!.reply}']; + // Get.find(tag: heroTag).tabs.value = [ + // '简介', + // '评论 ${result['data']!.stat!.reply}' + // ]; // 获取到粉丝数再返回 await queryUserStat(); } @@ -425,6 +430,20 @@ class VideoIntroController extends GetxController { } followStatus['attribute'] = actionStatus; followStatus.refresh(); + if (actionStatus == 2) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('关注成功'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + label: '设置分组', + onPressed: setFollowGroup, + ), + ), + ); + } + } } SmartDialog.dismiss(); }, @@ -440,16 +459,16 @@ class VideoIntroController extends GetxController { Future changeSeasonOrbangu(bvid, cid, aid) async { // 重新获取视频资源 VideoDetailController videoDetailCtr = - Get.find(tag: Get.arguments['heroTag']); + Get.find(tag: heroTag); videoDetailCtr.bvid = bvid; - videoDetailCtr.cid = cid; + videoDetailCtr.cid.value = cid; videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.queryVideoUrl(); // 重新请求评论 try { /// 未渲染回复组件时可能异常 VideoReplyController videoReplyCtr = - Get.find(tag: Get.arguments['heroTag']); + Get.find(tag: heroTag); videoReplyCtr.aid = aid; videoReplyCtr.queryReplyList(type: 'init'); } catch (_) {} @@ -486,4 +505,60 @@ class VideoIntroController extends GetxController { } super.onClose(); } + + /// 列表循环或者顺序播放时,自动播放下一个 + void nextPlay() { + late List episodes; + // if (videoDetail.value.ugcSeason != null) { + // UgcSeason ugcSeason = videoDetail.value.ugcSeason!; + // List sections = ugcSeason.sections!; + // for (int i = 0; i < sections.length; i++) { + // List episodesList = sections[i].episodes!; + // for (int j = 0; j < episodesList.length; j++) { + // if (episodesList[j].cid == lastPlayCid.value) { + // episodes = episodesList; + // continue; + // } + // } + // } + // } + if (videoDetail.value.ugcSeason != null) { + UgcSeason ugcSeason = videoDetail.value.ugcSeason!; + List sections = ugcSeason.sections!; + episodes = []; + + for (int i = 0; i < sections.length; i++) { + List episodesList = sections[i].episodes!; + episodes.addAll(episodesList); + } + } + + int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value); + int nextIndex = currentIndex + 1; + VideoDetailController videoDetailCtr = + Get.find(tag: heroTag); + PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat; + + // 列表循环 + if (nextIndex >= episodes.length) { + if (platRepeat == PlayRepeat.listCycle) { + nextIndex = 0; + } + if (platRepeat == PlayRepeat.listOrder) { + return; + } + } + int cid = episodes[nextIndex].cid!; + String bvid = episodes[nextIndex].bvid!; + int aid = episodes[nextIndex].aid!; + changeSeasonOrbangu(bvid, cid, aid); + } + + // 设置关注分组 + void setFollowGroup() { + Get.bottomSheet( + GroupPanel(mid: videoDetail.value.owner!.mid!), + isScrollControlled: true, + ); + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index e961f135..1c9520a8 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -330,17 +330,17 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), const SizedBox(height: 7), // 点赞收藏转发 布局样式1 - SingleChildScrollView( - padding: const EdgeInsets.only(top: 7, bottom: 7), - scrollDirection: Axis.horizontal, - child: actionRow( - context, - videoIntroController, - videoDetailCtr, - ), - ), + // SingleChildScrollView( + // padding: const EdgeInsets.only(top: 7, bottom: 7), + // scrollDirection: Axis.horizontal, + // child: actionRow( + // context, + // videoIntroController, + // videoDetailCtr, + // ), + // ), // 点赞收藏转发 布局样式2 - // actionGrid(context, videoIntroController), + actionGrid(context, videoIntroController), // 合集 if (!loadingStatus && widget.videoDetail!.ugcSeason != null) ...[ @@ -458,7 +458,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Widget actionGrid(BuildContext context, videoIntroController) { return LayoutBuilder(builder: (context, constraints) { return Container( - padding: const EdgeInsets.only(top: 6, bottom: 10), + margin: const EdgeInsets.only(top: 6, bottom: 4), height: constraints.maxWidth / 5 * 0.8, child: GridView.count( primary: false, @@ -477,12 +477,12 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ? widget.videoDetail!.stat!.like!.toString() : '-'), ), - ActionItem( - icon: const Icon(FontAwesomeIcons.clock), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: loadingStatus, - text: '稍后再看'), + // ActionItem( + // icon: const Icon(FontAwesomeIcons.clock), + // onTap: () => videoIntroController.actionShareVideo(), + // selectStatus: false, + // loadingStatus: loadingStatus, + // text: '稍后再看'), Obx( () => ActionItem( icon: const Icon(FontAwesomeIcons.b), @@ -498,22 +498,28 @@ class _VideoInfoState extends State with TickerProviderStateMixin { () => ActionItem( icon: const Icon(FontAwesomeIcons.star), selectIcon: const Icon(FontAwesomeIcons.solidStar), - // onTap: () => videoIntroController.actionFavVideo(), onTap: () => showFavBottomSheet(), + onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, loadingStatus: loadingStatus, text: !loadingStatus ? widget.videoDetail!.stat!.favorite!.toString() : '-'), ), + ActionItem( + icon: const Icon(FontAwesomeIcons.comment), + onTap: () => videoDetailCtr.tabCtr.animateTo(1), + selectStatus: false, + loadingStatus: loadingStatus, + text: !loadingStatus + ? widget.videoDetail!.stat!.reply!.toString() + : '评论'), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.share!.toString() - : '-'), + text: '分享'), ], ), ); diff --git a/lib/pages/video/detail/introduction/widgets/action_item.dart b/lib/pages/video/detail/introduction/widgets/action_item.dart index a2e54b33..95ac103b 100644 --- a/lib/pages/video/detail/introduction/widgets/action_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_item.dart @@ -6,6 +6,7 @@ class ActionItem extends StatelessWidget { final Icon? icon; final Icon? selectIcon; final Function? onTap; + final Function? onLongPress; final bool? loadingStatus; final String? text; final bool selectStatus; @@ -15,6 +16,7 @@ class ActionItem extends StatelessWidget { this.icon, this.selectIcon, this.onTap, + this.onLongPress, this.loadingStatus, this.text, this.selectStatus = false, @@ -27,6 +29,9 @@ class ActionItem extends StatelessWidget { feedBack(), onTap!(), }, + onLongPress: () => { + if (onLongPress != null) {onLongPress!()} + }, borderRadius: StyleString.mdRadius, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/pages/video/detail/introduction/widgets/group_panel.dart b/lib/pages/video/detail/introduction/widgets/group_panel.dart new file mode 100644 index 00000000..0a105f9d --- /dev/null +++ b/lib/pages/video/detail/introduction/widgets/group_panel.dart @@ -0,0 +1,156 @@ +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/common/widgets/http_error.dart'; +import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/member/tags.dart'; +import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/storage.dart'; + +class GroupPanel extends StatefulWidget { + final int? mid; + const GroupPanel({super.key, this.mid}); + + @override + State createState() => _GroupPanelState(); +} + +class _GroupPanelState extends State { + Box localCache = GStrorage.localCache; + late double sheetHeight; + late Future _futureBuilderFuture; + late List tagsList; + bool showDefault = true; + + @override + void initState() { + super.initState(); + sheetHeight = localCache.get('sheetHeight'); + _futureBuilderFuture = MemberHttp.followUpTags(); + } + + void onSave() async { + feedBack(); + // 是否有选中的 有选中的带id,没选使用默认0 + bool anyHasChecked = tagsList.any((e) => e.checked == true); + late String tagids; + if (anyHasChecked) { + List checkedList = tagsList.where((e) => e.checked == true).toList(); + List tagidList = checkedList.map((e) => e.tagid).toList(); + tagids = tagidList.join(','); + } else { + tagids = '0'; + } + // 保存 + var res = await MemberHttp.addUsers(widget.mid, tagids); + SmartDialog.showToast(res['msg']); + if (res['status']) { + Get.back(); + } + } + + @override + Widget build(BuildContext context) { + return Container( + height: sheetHeight, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + centerTitle: false, + elevation: 0, + leading: IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close_outlined)), + title: + Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium), + ), + Expanded( + child: Material( + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + tagsList = data['data']; + return ListView.builder( + itemCount: data['data'].length, + itemBuilder: (context, index) { + return ListTile( + onTap: () { + data['data'][index].checked = + !data['data'][index].checked; + showDefault = + !data['data'].any((e) => e.checked == true); + setState(() {}); + }, + dense: true, + leading: const Icon(Icons.group_outlined), + minLeadingWidth: 0, + title: Text(data['data'][index].name), + subtitle: data['data'][index].tip != '' + ? Text(data['data'][index].tip) + : null, + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: data['data'][index].checked, + onChanged: (bool? checkValue) { + data['data'][index].checked = checkValue; + showDefault = !data['data'] + .any((e) => e.checked == true); + setState(() {}); + }, + ), + ), + ); + }, + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return const Text('请求中'); + } + }, + ), + ), + ), + Divider( + height: 1, + color: Theme.of(context).disabledColor.withOpacity(0.08), + ), + Padding( + padding: EdgeInsets.only( + left: 20, + right: 20, + top: 12, + bottom: MediaQuery.of(context).padding.bottom + 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => onSave(), + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 30, right: 30), + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: + Theme.of(context).colorScheme.primary, // 设置按钮背景色 + ), + child: Text(showDefault ? '保存至默认分组' : '保存'), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart index 261b6227..b3f1f7a0 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; class PagesPanel extends StatefulWidget { final List pages; @@ -22,13 +23,23 @@ class PagesPanel extends StatefulWidget { class _PagesPanelState extends State { late List episodes; + late int cid; late int currentIndex; + String heroTag = Get.arguments['heroTag']; + late VideoDetailController _videoDetailController; @override void initState() { super.initState(); + cid = widget.cid!; episodes = widget.pages; - currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); + _videoDetailController = Get.find(tag: heroTag); + currentIndex = episodes.indexWhere((e) => e.cid == cid); + _videoDetailController.cid.listen((p0) { + cid = p0; + setState(() {}); + currentIndex = episodes.indexWhere((e) => e.cid == cid); + }); } void changeFucCall(item, i) async { diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 3f3a1475..0be22757 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/utils/id_utils.dart'; class SeasonPanel extends StatefulWidget { @@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget { class _SeasonPanelState extends State { late List episodes; + late int cid; late int currentIndex; + String heroTag = Get.arguments['heroTag']; + late VideoDetailController _videoDetailController; @override void initState() { super.initState(); + cid = widget.cid!; + _videoDetailController = Get.find(tag: heroTag); /// 根据 cid 找到对应集,找到对应 episodes /// 有多个episodes时,只显示其中一个 @@ -36,7 +42,7 @@ class _SeasonPanelState extends State { for (int i = 0; i < sections.length; i++) { List episodesList = sections[i].episodes!; for (int j = 0; j < episodesList.length; j++) { - if (episodesList[j].cid == widget.cid) { + if (episodesList[j].cid == cid) { episodes = episodesList; continue; } @@ -47,7 +53,12 @@ class _SeasonPanelState extends State { // episodes = widget.ugcSeason.sections! // .firstWhere((e) => e.seasonId == widget.ugcSeason.id) // .episodes!; - currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); + currentIndex = episodes.indexWhere((e) => e.cid == cid); + _videoDetailController.cid.listen((p0) { + cid = p0; + setState(() {}); + currentIndex = episodes.indexWhere((e) => e.cid == cid); + }); } void changeFucCall(item, i) async { @@ -57,6 +68,7 @@ class _SeasonPanelState extends State { item.aid, ); currentIndex = i; + setState(() {}); Get.back(); } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 048876d6..3902a682 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -20,6 +20,7 @@ import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; import 'widgets/app_bar.dart'; @@ -41,6 +42,7 @@ class _VideoDetailPageState extends State final ScrollController _extendNestCtr = ScrollController(); late StreamController appbarStream; late VideoIntroController videoIntroController; + late BangumiIntroController bangumiIntroController; late String heroTag; PlayerStatus playerStatus = PlayerStatus.playing; @@ -61,6 +63,7 @@ class _VideoDetailPageState extends State heroTag = Get.arguments['heroTag']; videoDetailController = Get.put(VideoDetailController(), tag: heroTag); videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); statusBarHeight = localCache.get('statusBarHeight'); autoExitFullcreen = setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); @@ -98,6 +101,23 @@ class _VideoDetailPageState extends State if (autoExitFullcreen) { plPlayerController!.triggerFullScreen(status: false); } + + /// 顺序播放 列表循环 + if (plPlayerController!.playRepeat != PlayRepeat.pause && + plPlayerController!.playRepeat != PlayRepeat.singleCycle) { + if (videoDetailController.videoType == SearchType.video) { + videoIntroController.nextPlay(); + } + if (videoDetailController.videoType == SearchType.media_bangumi) { + bangumiIntroController.nextPlay(); + } + } + + /// 单个循环 + if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) { + plPlayerController!.seekTo(Duration.zero); + plPlayerController!.play(); + } // 播放完展示控制栏 try { PiPStatus currentStatus = @@ -385,8 +405,8 @@ class _VideoDetailPageState extends State const VideoIntroPanel(), ] else if (videoDetailController.videoType == SearchType.media_bangumi) ...[ - BangumiIntroPanel( - cid: videoDetailController.cid) + Obx(() => BangumiIntroPanel( + cid: videoDetailController.cid.value)), ], // if (videoDetailController.videoType == // SearchType.video) ...[ diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 1697b4d4..35c54b07 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -13,6 +13,7 @@ import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { @@ -56,7 +57,7 @@ class _HeaderControlState extends State { builder: (_) { return Container( width: double.infinity, - height: 400, + height: 440, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, @@ -149,13 +150,14 @@ class _HeaderControlState extends State { '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', style: subTitleStyle), ), - // ListTile( - // onTap: () {}, - // dense: true, - // enabled: false, - // leading: const Icon(Icons.play_circle_outline, size: 20), - // title: Text('播放设置', style: titleStyle), - // ), + ListTile( + onTap: () => {Get.back(), showSetRepeat()}, + dense: true, + leading: const Icon(Icons.repeat, size: 20), + title: Text('播放顺序', style: titleStyle), + subtitle: Text(widget.controller!.playRepeat.description, + style: subTitleStyle), + ), ListTile( onTap: () => {Get.back(), showSetDanmaku()}, dense: true, @@ -704,6 +706,60 @@ class _HeaderControlState extends State { ); } + /// 播放顺序 + void showSetRepeat() async { + showModalBottomSheet( + context: context, + elevation: 0, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + width: double.infinity, + height: 250, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + child: Column( + children: [ + SizedBox( + height: 45, + child: Center(child: Text('选择播放顺序', style: titleStyle))), + Expanded( + child: Material( + child: ListView( + children: [ + for (var i in PlayRepeat.values) ...[ + ListTile( + onTap: () { + widget.controller!.setPlayRepeat(i); + Get.back(); + }, + dense: true, + contentPadding: + const EdgeInsets.only(left: 20, right: 20), + title: Text(i.description), + trailing: widget.controller!.playRepeat == i + ? Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ) + : const SizedBox(), + ) + ], + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final _ = widget.controller!; diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 01476ade..5d7a8332 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -13,6 +13,7 @@ import 'package:media_kit_video/media_kit_video.dart'; import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; @@ -209,6 +210,9 @@ class PlPlayerController { late double fontSizeVal; late double danmakuSpeedVal; + // 播放顺序相关 + PlayRepeat playRepeat = PlayRepeat.pause; + // 添加一个私有构造函数 PlPlayerController._() { _videoType = videoType; @@ -226,6 +230,12 @@ class PlPlayerController { // 弹幕速度 danmakuSpeedVal = localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0); + playRepeat = PlayRepeat.values.toList().firstWhere( + (e) => + e.value == + videoStorage.get(VideoBoxKey.playRepeat, + defaultValue: PlayRepeat.pause.value), + ); // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // if (status == PlayerStatus.playing) { // WakelockPlus.enable(); @@ -910,6 +920,11 @@ class PlPlayerController { } } + setPlayRepeat(PlayRepeat type) { + playRepeat = type; + videoStorage.put(VideoBoxKey.playRepeat, type.value); + } + Future dispose({String type = 'single'}) async { // 每次减1,最后销毁 if (type == 'single' && playerCount.value > 1) { diff --git a/lib/plugin/pl_player/models/play_repeat.dart b/lib/plugin/pl_player/models/play_repeat.dart new file mode 100644 index 00000000..e68196c7 --- /dev/null +++ b/lib/plugin/pl_player/models/play_repeat.dart @@ -0,0 +1,25 @@ +enum PlayRepeat { + pause, + listOrder, + singleCycle, + listCycle, +} + +extension PlayRepeatExtension on PlayRepeat { + static final List _descList = [ + '播完暂停', + '顺序播放', + '单个循环', + '列表循环', + ]; + get description => _descList[index]; + + static final List _valueList = [ + 1, + 2, + 3, + 4, + ]; + get value => _valueList[index]; + get defaultValue => _valueList[1]; +} diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 2b1f4969..017abcbf 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -158,4 +158,6 @@ class VideoBoxKey { static const String videoBrightness = 'videoBrightness'; // 倍速 static const String videoSpeed = 'videoSpeed'; + // 播放顺序 + static const String playRepeat = 'playRepeat'; }