diff --git a/README.md b/README.md index 6a061c7b..066e5b6d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 取消/订阅合集 - [x] SponsorBlock - [x] 显示视频完整合集 - [x] 三连动画 diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 5360d17d..ae74b4b0 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; +import 'package:PiliPalaX/http/video.dart'; import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi; import 'package:PiliPalaX/models/video_detail_res.dart' as video; import 'package:flutter/material.dart'; @@ -15,7 +16,7 @@ import '../../utils/utils.dart'; class ListSheet { ListSheet({ this.index, - this.sections, + this.season, required this.episodes, this.bvid, this.aid, @@ -26,7 +27,7 @@ class ListSheet { }); final dynamic index; - final dynamic sections; + final dynamic season; final dynamic episodes; final String? bvid; final int? aid; @@ -39,7 +40,7 @@ class ListSheet { Widget get listSheetContent => ListSheetContent( index: index, - sections: sections, + season: season, episodes: episodes, bvid: bvid, aid: aid, @@ -62,8 +63,8 @@ class ListSheet { class ListSheetContent extends StatefulWidget { const ListSheetContent({ super.key, - this.index = 0, - this.sections, + this.index, + this.season, required this.episodes, this.bvid, this.aid, @@ -73,7 +74,7 @@ class ListSheetContent extends StatefulWidget { }); final dynamic index; - final dynamic sections; + final dynamic season; final dynamic episodes; final String? bvid; final int? aid; @@ -94,9 +95,14 @@ class _ListSheetContentState extends State late List reverse; int get _index => widget.index ?? 0; - bool get _isList => widget.sections is List && widget.sections.length > 1; + bool get _isList => + widget.season != null && + widget.season?.sections is List && + widget.season.sections.length > 1; TabController? _ctr; StreamController? _indexStream; + int? _seasonFav; + StreamController? _favStream; @override void initState() { @@ -105,24 +111,37 @@ class _ListSheetContentState extends State _indexStream = StreamController(); _ctr = TabController( vsync: this, - length: widget.sections.length, + length: widget.season.sections.length, initialIndex: _index, )..addListener(() { _indexStream?.add(_ctr?.index); }); } itemScrollController = _isList - ? List.generate(widget.sections.length, (_) => ItemScrollController()) + ? List.generate( + widget.season.sections.length, (_) => ItemScrollController()) : [ItemScrollController()]; - reverse = - _isList ? List.generate(widget.sections.length, (_) => false) : [false]; + reverse = _isList + ? List.generate(widget.season.sections.length, (_) => false) + : [false]; WidgetsBinding.instance.addPostFrameCallback((_) { itemScrollController[_index].jumpTo(index: currentIndex); }); + if (widget.bvid != null && widget.season != null) { + _favStream = StreamController(); + () async { + dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid); + if (result['status']) { + _seasonFav = result['data']['season_fav'] ? 1 : 0; + _favStream?.add(_seasonFav); + } + }(); + } } @override void dispose() { + _favStream?.close(); _indexStream?.close(); _ctr?.removeListener(() {}); _ctr?.dispose(); @@ -261,40 +280,67 @@ class _ListSheetContentState extends State child: Row( children: [ Text( - '合集(${_isList ? List.generate(widget.sections.length, (index) => widget.sections[index].episodes.length).reduce((value, element) => value + element) : widget.episodes!.length})', + '合集(${_isList ? widget.season.epCount : widget.episodes!.length})', style: Theme.of(context).textTheme.titleMedium, ), - IconButton( + StreamBuilder( + stream: _favStream?.stream, + builder: (_, snapshot) => snapshot.hasData + ? _mediumButton( + tooltip: _seasonFav == 1 ? '取消订阅' : '订阅', + icon: _seasonFav == 1 ? Icons.alarm_off : Icons.alarm, + onPressed: () async { + dynamic result = await VideoHttp.seasonFav( + isFav: _seasonFav == 1, + seasonId: widget.season.id, + ); + if (result['status']) { + SmartDialog.showToast( + '${_seasonFav == 1 ? '取消' : ''}订阅成功'); + _seasonFav = _seasonFav == 1 ? 0 : 1; + _favStream?.add(_seasonFav); + } else { + SmartDialog.showToast(result['msg']); + } + }, + ) + : const SizedBox.shrink(), + ), + _mediumButton( tooltip: '跳至顶部', - icon: const Icon(Icons.vertical_align_top), + icon: Icons.vertical_align_top, onPressed: () { itemScrollController[_ctr?.index ?? 0].scrollTo( index: !reverse[_ctr?.index ?? 0] ? 0 : _isList - ? widget.sections[_ctr?.index].episodes.length - 1 + ? widget.season.sections[_ctr?.index].episodes + .length - + 1 : widget.episodes.length - 1, duration: const Duration(milliseconds: 200), ); }, ), - IconButton( + _mediumButton( tooltip: '跳至底部', - icon: const Icon(Icons.vertical_align_bottom), + icon: Icons.vertical_align_bottom, onPressed: () { itemScrollController[_ctr?.index ?? 0].scrollTo( index: !reverse[_ctr?.index ?? 0] ? _isList - ? widget.sections[_ctr?.index].episodes.length - 1 + ? widget.season.sections[_ctr?.index].episodes + .length - + 1 : widget.episodes.length - 1 : 0, duration: const Duration(milliseconds: 200), ); }, ), - IconButton( + _mediumButton( tooltip: '跳至当前', - icon: const Icon(Icons.my_location), + icon: Icons.my_location, onPressed: () async { if (_ctr != null && _ctr?.index != (_index)) { _ctr?.animateTo(_index); @@ -310,13 +356,11 @@ class _ListSheetContentState extends State StreamBuilder( stream: _indexStream?.stream, initialData: 0, - builder: (_, snapshot) => IconButton( + builder: (_, snapshot) => _mediumButton( tooltip: reverse[snapshot.data] ? '正序' : '反序', - icon: Icon( - !reverse[snapshot.data] - ? MdiIcons.sortAscending - : MdiIcons.sortDescending, - ), + icon: !reverse[snapshot.data] + ? MdiIcons.sortAscending + : MdiIcons.sortDescending, onPressed: () { setState(() { reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0]; @@ -324,9 +368,9 @@ class _ListSheetContentState extends State }, ), ), - IconButton( + _mediumButton( tooltip: '关闭', - icon: const Icon(Icons.close), + icon: Icons.close, onPressed: widget.onClose, ), ], @@ -340,7 +384,7 @@ class _ListSheetContentState extends State TabBar( controller: _ctr, isScrollable: true, - tabs: (widget.sections as List) + tabs: (widget.season.sections as List) .map((item) => Tab(text: item.title)) .toList(), dividerHeight: 1, @@ -351,9 +395,9 @@ class _ListSheetContentState extends State ? TabBarView( controller: _ctr, children: List.generate( - widget.sections.length, - (index) => - _buildBody(index, widget.sections[index].episodes), + widget.season.sections.length, + (index) => _buildBody( + index, widget.season.sections[index].episodes), ), ) : _buildBody(null, widget.episodes), @@ -363,6 +407,25 @@ class _ListSheetContentState extends State ); } + Widget _mediumButton({ + String? tooltip, + IconData? icon, + VoidCallback? onPressed, + }) { + return SizedBox( + width: 34, + height: 34, + child: IconButton( + tooltip: tooltip, + icon: Icon(icon), + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: onPressed, + ), + ); + } + Widget _buildBody(i, episodes) => Material( child: ScrollablePositionedList.separated( padding: EdgeInsets.only( diff --git a/lib/http/api.dart b/lib/http/api.dart index b5a170fe..e14fa4b0 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -633,4 +633,8 @@ class Api { static const String removeDynamic = '/dynamic_svr/v1/dynamic_svr/rm_dynamic'; static const String uploadBfs = '/x/dynamic/feed/draw/upload_bfs'; + + static const String videoRelation = '/x/web-interface/archive/relation'; + + static const String seasonFav = '/x/v3/fav/season/'; // + fav unfav } diff --git a/lib/http/video.dart b/lib/http/video.dart index a885166b..e857ef3e 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -274,6 +274,54 @@ class VideoHttp { } } + static Future seasonFav({ + required bool isFav, + required dynamic seasonId, + }) async { + var res = await Request().post( + Api.seasonFav + (isFav ? 'unfav' : 'fav'), + data: { + 'platform': 'web', + 'season_id': seasonId, + 'csrf': await Request.getCsrf(), + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return { + 'status': true, + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } + + static Future videoRelation({required dynamic bvid}) async { + var res = await Request().get( + Api.videoRelation, + data: { + 'aid': IdUtils.bv2av(bvid), + 'bvid': bvid, + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } + // 相关视频 static Future relatedVideoList({required String bvid}) async { var res = await Request().get(Api.relatedList, data: {'bvid': bvid}); diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index bdb7476c..14b04d47 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -659,6 +659,7 @@ class SectionItem { this.episodes, }); + int? epCount; int? seasonId; int? id; String? title; @@ -666,6 +667,7 @@ class SectionItem { List? episodes; SectionItem.fromJson(Map json) { + epCount = json['ep_count']; seasonId = json['season_id']; id = json['id']; title = json['title']; diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 51cdb687..ed16faff 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -88,9 +88,9 @@ class _SeasonPanelState extends State { child: InkWell( onTap: () => widget.showEpisodes( _index, - widget.ugcSeason.sections, + widget.ugcSeason, episodes, - null, + _videoDetailController.bvid, null, cid, ), diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 3d115101..af653053 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1315,10 +1315,10 @@ class _VideoDetailPageState extends State ); } - showEpisodes(index, sections, episodes, bvid, aid, cid) { + showEpisodes(index, season, episodes, bvid, aid, cid) { ListSheet( index: index, - sections: sections, + season: season, episodes: episodes, bvid: bvid, aid: aid, diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 9c9b7919..a2808afe 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -374,7 +374,7 @@ class _PLVideoPlayerState extends State if (widget.showEpisodes != null) { widget.showEpisodes!( index, - videoIntroController?.videoDetail.value.ugcSeason?.sections, + videoIntroController?.videoDetail.value.ugcSeason, episodes, bvid, IdUtils.bv2av(bvid),