mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-18 09:36:52 +08:00
feat: subsribe season
This commit is contained in:
@@ -41,6 +41,7 @@
|
|||||||
|
|
||||||
## feat
|
## feat
|
||||||
|
|
||||||
|
- [x] 取消/订阅合集
|
||||||
- [x] SponsorBlock
|
- [x] SponsorBlock
|
||||||
- [x] 显示视频完整合集
|
- [x] 显示视频完整合集
|
||||||
- [x] 三连动画
|
- [x] 三连动画
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:PiliPalaX/common/constants.dart';
|
import 'package:PiliPalaX/common/constants.dart';
|
||||||
import 'package:PiliPalaX/common/widgets/network_img_layer.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/bangumi/info.dart' as bangumi;
|
||||||
import 'package:PiliPalaX/models/video_detail_res.dart' as video;
|
import 'package:PiliPalaX/models/video_detail_res.dart' as video;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -15,7 +16,7 @@ import '../../utils/utils.dart';
|
|||||||
class ListSheet {
|
class ListSheet {
|
||||||
ListSheet({
|
ListSheet({
|
||||||
this.index,
|
this.index,
|
||||||
this.sections,
|
this.season,
|
||||||
required this.episodes,
|
required this.episodes,
|
||||||
this.bvid,
|
this.bvid,
|
||||||
this.aid,
|
this.aid,
|
||||||
@@ -26,7 +27,7 @@ class ListSheet {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final dynamic index;
|
final dynamic index;
|
||||||
final dynamic sections;
|
final dynamic season;
|
||||||
final dynamic episodes;
|
final dynamic episodes;
|
||||||
final String? bvid;
|
final String? bvid;
|
||||||
final int? aid;
|
final int? aid;
|
||||||
@@ -39,7 +40,7 @@ class ListSheet {
|
|||||||
|
|
||||||
Widget get listSheetContent => ListSheetContent(
|
Widget get listSheetContent => ListSheetContent(
|
||||||
index: index,
|
index: index,
|
||||||
sections: sections,
|
season: season,
|
||||||
episodes: episodes,
|
episodes: episodes,
|
||||||
bvid: bvid,
|
bvid: bvid,
|
||||||
aid: aid,
|
aid: aid,
|
||||||
@@ -62,8 +63,8 @@ class ListSheet {
|
|||||||
class ListSheetContent extends StatefulWidget {
|
class ListSheetContent extends StatefulWidget {
|
||||||
const ListSheetContent({
|
const ListSheetContent({
|
||||||
super.key,
|
super.key,
|
||||||
this.index = 0,
|
this.index,
|
||||||
this.sections,
|
this.season,
|
||||||
required this.episodes,
|
required this.episodes,
|
||||||
this.bvid,
|
this.bvid,
|
||||||
this.aid,
|
this.aid,
|
||||||
@@ -73,7 +74,7 @@ class ListSheetContent extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final dynamic index;
|
final dynamic index;
|
||||||
final dynamic sections;
|
final dynamic season;
|
||||||
final dynamic episodes;
|
final dynamic episodes;
|
||||||
final String? bvid;
|
final String? bvid;
|
||||||
final int? aid;
|
final int? aid;
|
||||||
@@ -94,9 +95,14 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
late List<bool> reverse;
|
late List<bool> reverse;
|
||||||
|
|
||||||
int get _index => widget.index ?? 0;
|
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;
|
TabController? _ctr;
|
||||||
StreamController? _indexStream;
|
StreamController? _indexStream;
|
||||||
|
int? _seasonFav;
|
||||||
|
StreamController? _favStream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -105,24 +111,37 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
_indexStream = StreamController<int>();
|
_indexStream = StreamController<int>();
|
||||||
_ctr = TabController(
|
_ctr = TabController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
length: widget.sections.length,
|
length: widget.season.sections.length,
|
||||||
initialIndex: _index,
|
initialIndex: _index,
|
||||||
)..addListener(() {
|
)..addListener(() {
|
||||||
_indexStream?.add(_ctr?.index);
|
_indexStream?.add(_ctr?.index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
itemScrollController = _isList
|
itemScrollController = _isList
|
||||||
? List.generate(widget.sections.length, (_) => ItemScrollController())
|
? List.generate(
|
||||||
|
widget.season.sections.length, (_) => ItemScrollController())
|
||||||
: [ItemScrollController()];
|
: [ItemScrollController()];
|
||||||
reverse =
|
reverse = _isList
|
||||||
_isList ? List.generate(widget.sections.length, (_) => false) : [false];
|
? List.generate(widget.season.sections.length, (_) => false)
|
||||||
|
: [false];
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
itemScrollController[_index].jumpTo(index: currentIndex);
|
itemScrollController[_index].jumpTo(index: currentIndex);
|
||||||
});
|
});
|
||||||
|
if (widget.bvid != null && widget.season != null) {
|
||||||
|
_favStream = StreamController<int>();
|
||||||
|
() async {
|
||||||
|
dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid);
|
||||||
|
if (result['status']) {
|
||||||
|
_seasonFav = result['data']['season_fav'] ? 1 : 0;
|
||||||
|
_favStream?.add(_seasonFav);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_favStream?.close();
|
||||||
_indexStream?.close();
|
_indexStream?.close();
|
||||||
_ctr?.removeListener(() {});
|
_ctr?.removeListener(() {});
|
||||||
_ctr?.dispose();
|
_ctr?.dispose();
|
||||||
@@ -261,40 +280,67 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
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,
|
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: '跳至顶部',
|
tooltip: '跳至顶部',
|
||||||
icon: const Icon(Icons.vertical_align_top),
|
icon: Icons.vertical_align_top,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||||
index: !reverse[_ctr?.index ?? 0]
|
index: !reverse[_ctr?.index ?? 0]
|
||||||
? 0
|
? 0
|
||||||
: _isList
|
: _isList
|
||||||
? widget.sections[_ctr?.index].episodes.length - 1
|
? widget.season.sections[_ctr?.index].episodes
|
||||||
|
.length -
|
||||||
|
1
|
||||||
: widget.episodes.length - 1,
|
: widget.episodes.length - 1,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
_mediumButton(
|
||||||
tooltip: '跳至底部',
|
tooltip: '跳至底部',
|
||||||
icon: const Icon(Icons.vertical_align_bottom),
|
icon: Icons.vertical_align_bottom,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||||
index: !reverse[_ctr?.index ?? 0]
|
index: !reverse[_ctr?.index ?? 0]
|
||||||
? _isList
|
? _isList
|
||||||
? widget.sections[_ctr?.index].episodes.length - 1
|
? widget.season.sections[_ctr?.index].episodes
|
||||||
|
.length -
|
||||||
|
1
|
||||||
: widget.episodes.length - 1
|
: widget.episodes.length - 1
|
||||||
: 0,
|
: 0,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
_mediumButton(
|
||||||
tooltip: '跳至当前',
|
tooltip: '跳至当前',
|
||||||
icon: const Icon(Icons.my_location),
|
icon: Icons.my_location,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (_ctr != null && _ctr?.index != (_index)) {
|
if (_ctr != null && _ctr?.index != (_index)) {
|
||||||
_ctr?.animateTo(_index);
|
_ctr?.animateTo(_index);
|
||||||
@@ -310,13 +356,11 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: _indexStream?.stream,
|
stream: _indexStream?.stream,
|
||||||
initialData: 0,
|
initialData: 0,
|
||||||
builder: (_, snapshot) => IconButton(
|
builder: (_, snapshot) => _mediumButton(
|
||||||
tooltip: reverse[snapshot.data] ? '正序' : '反序',
|
tooltip: reverse[snapshot.data] ? '正序' : '反序',
|
||||||
icon: Icon(
|
icon: !reverse[snapshot.data]
|
||||||
!reverse[snapshot.data]
|
? MdiIcons.sortAscending
|
||||||
? MdiIcons.sortAscending
|
: MdiIcons.sortDescending,
|
||||||
: MdiIcons.sortDescending,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
|
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
|
||||||
@@ -324,9 +368,9 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
_mediumButton(
|
||||||
tooltip: '关闭',
|
tooltip: '关闭',
|
||||||
icon: const Icon(Icons.close),
|
icon: Icons.close,
|
||||||
onPressed: widget.onClose,
|
onPressed: widget.onClose,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -340,7 +384,7 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
TabBar(
|
TabBar(
|
||||||
controller: _ctr,
|
controller: _ctr,
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabs: (widget.sections as List)
|
tabs: (widget.season.sections as List)
|
||||||
.map((item) => Tab(text: item.title))
|
.map((item) => Tab(text: item.title))
|
||||||
.toList(),
|
.toList(),
|
||||||
dividerHeight: 1,
|
dividerHeight: 1,
|
||||||
@@ -351,9 +395,9 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
? TabBarView(
|
? TabBarView(
|
||||||
controller: _ctr,
|
controller: _ctr,
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
widget.sections.length,
|
widget.season.sections.length,
|
||||||
(index) =>
|
(index) => _buildBody(
|
||||||
_buildBody(index, widget.sections[index].episodes),
|
index, widget.season.sections[index].episodes),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _buildBody(null, widget.episodes),
|
: _buildBody(null, widget.episodes),
|
||||||
@@ -363,6 +407,25 @@ class _ListSheetContentState extends State<ListSheetContent>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
Widget _buildBody(i, episodes) => Material(
|
||||||
child: ScrollablePositionedList.separated(
|
child: ScrollablePositionedList.separated(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
|
|||||||
@@ -633,4 +633,8 @@ class Api {
|
|||||||
static const String removeDynamic = '/dynamic_svr/v1/dynamic_svr/rm_dynamic';
|
static const String removeDynamic = '/dynamic_svr/v1/dynamic_svr/rm_dynamic';
|
||||||
|
|
||||||
static const String uploadBfs = '/x/dynamic/feed/draw/upload_bfs';
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<LoadingState> relatedVideoList({required String bvid}) async {
|
static Future<LoadingState> relatedVideoList({required String bvid}) async {
|
||||||
var res = await Request().get(Api.relatedList, data: {'bvid': bvid});
|
var res = await Request().get(Api.relatedList, data: {'bvid': bvid});
|
||||||
|
|||||||
@@ -659,6 +659,7 @@ class SectionItem {
|
|||||||
this.episodes,
|
this.episodes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
int? epCount;
|
||||||
int? seasonId;
|
int? seasonId;
|
||||||
int? id;
|
int? id;
|
||||||
String? title;
|
String? title;
|
||||||
@@ -666,6 +667,7 @@ class SectionItem {
|
|||||||
List<EpisodeItem>? episodes;
|
List<EpisodeItem>? episodes;
|
||||||
|
|
||||||
SectionItem.fromJson(Map<String, dynamic> json) {
|
SectionItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
epCount = json['ep_count'];
|
||||||
seasonId = json['season_id'];
|
seasonId = json['season_id'];
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
title = json['title'];
|
title = json['title'];
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => widget.showEpisodes(
|
onTap: () => widget.showEpisodes(
|
||||||
_index,
|
_index,
|
||||||
widget.ugcSeason.sections,
|
widget.ugcSeason,
|
||||||
episodes,
|
episodes,
|
||||||
null,
|
_videoDetailController.bvid,
|
||||||
null,
|
null,
|
||||||
cid,
|
cid,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1315,10 +1315,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showEpisodes(index, sections, episodes, bvid, aid, cid) {
|
showEpisodes(index, season, episodes, bvid, aid, cid) {
|
||||||
ListSheet(
|
ListSheet(
|
||||||
index: index,
|
index: index,
|
||||||
sections: sections,
|
season: season,
|
||||||
episodes: episodes,
|
episodes: episodes,
|
||||||
bvid: bvid,
|
bvid: bvid,
|
||||||
aid: aid,
|
aid: aid,
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
if (widget.showEpisodes != null) {
|
if (widget.showEpisodes != null) {
|
||||||
widget.showEpisodes!(
|
widget.showEpisodes!(
|
||||||
index,
|
index,
|
||||||
videoIntroController?.videoDetail.value.ugcSeason?.sections,
|
videoIntroController?.videoDetail.value.ugcSeason,
|
||||||
episodes,
|
episodes,
|
||||||
bvid,
|
bvid,
|
||||||
IdUtils.bv2av(bvid),
|
IdUtils.bv2av(bvid),
|
||||||
|
|||||||
Reference in New Issue
Block a user