Merge branch 'main' into fix

This commit is contained in:
guozhigq
2024-11-12 14:13:58 +08:00
28 changed files with 847 additions and 192 deletions

View File

@ -15,5 +15,4 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端 // 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const List<int> publicFavFolder = <int>[0, 2, 22];
} }

View File

@ -3,7 +3,9 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/scrollview_observer.dart';
import '../models/common/video_episode_type.dart'; import '../models/common/video_episode_type.dart';
@ -20,6 +22,8 @@ class EpisodeBottomSheet {
final double? sheetHeight; final double? sheetHeight;
bool isFullScreen = false; bool isFullScreen = false;
final UgcSeason? ugcSeason; final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
EpisodeBottomSheet({ EpisodeBottomSheet({
required this.episodes, required this.episodes,
@ -30,6 +34,8 @@ class EpisodeBottomSheet {
this.sheetHeight, this.sheetHeight,
this.isFullScreen = false, this.isFullScreen = false,
this.ugcSeason, this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
}); });
Widget buildShowContent() { Widget buildShowContent() {
@ -42,6 +48,8 @@ class EpisodeBottomSheet {
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
isFullScreen: isFullScreen, isFullScreen: isFullScreen,
ugcSeason: ugcSeason, ugcSeason: ugcSeason,
currentEpisodeIndex: currentEpisodeIndex,
currentIndex: currentIndex,
); );
} }
@ -67,6 +75,8 @@ class PagesBottomSheet extends StatefulWidget {
this.sheetHeight, this.sheetHeight,
this.isFullScreen = false, this.isFullScreen = false,
this.ugcSeason, this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
}); });
final List<dynamic> episodes; final List<dynamic> episodes;
@ -77,41 +87,38 @@ class PagesBottomSheet extends StatefulWidget {
final double? sheetHeight; final double? sheetHeight;
final bool isFullScreen; final bool isFullScreen;
final UgcSeason? ugcSeason; final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
@override @override
State<PagesBottomSheet> createState() => _PagesBottomSheetState(); State<PagesBottomSheet> createState() => _PagesBottomSheetState();
} }
class _PagesBottomSheetState extends State<PagesBottomSheet> { class _PagesBottomSheetState extends State<PagesBottomSheet>
with TickerProviderStateMixin {
final ScrollController _listScrollController = ScrollController(); final ScrollController _listScrollController = ScrollController();
late ListObserverController _listObserverController; late ListObserverController _listObserverController;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
late int currentIndex; late int currentIndex;
TabController? tabController;
List<ListObserverController>? _listObserverControllerList;
List<ScrollController>? _listScrollControllerList;
final String heroTag = Get.arguments['heroTag'];
VideoDetailController? _videoDetailController;
RxInt isSubscribe = (-1).obs;
bool isVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
currentIndex = currentIndex = widget.currentIndex ??
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid); widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
_listObserverController = _scrollToInit();
ListObserverController(controller: _listScrollController); _scrollPositionInit();
if (widget.dataType == VideoEpidoesType.videoEpisode) { if (widget.dataType == VideoEpidoesType.videoEpisode) {
_listObserverController.initialIndexModel = ObserverIndexPositionModel( _videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
index: currentIndex, _getSubscribeStatus();
isFixedHeight: true,
);
} }
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.dataType != VideoEpidoesType.videoEpisode) {
double itemHeight = (widget.isFullScreen
? 400
: Get.size.width - 3 * StyleString.safeSpace) /
5.2;
double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
_scrollController.jumpTo(offset);
}
});
} }
String prefix() { String prefix() {
@ -126,9 +133,117 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
return '选集'; return '选集';
} }
// 滚动器初始化
void _scrollToInit() {
/// 单个
_listObserverController =
ListObserverController(controller: _listScrollController);
if (widget.dataType == VideoEpidoesType.videoEpisode &&
widget.ugcSeason?.sections != null &&
widget.ugcSeason!.sections!.length > 1) {
tabController = TabController(
length: widget.ugcSeason!.sections!.length,
vsync: this,
initialIndex: widget.currentEpisodeIndex ?? 0,
);
/// 多tab
_listScrollControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ScrollController();
},
);
_listObserverControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ListObserverController(
controller: _listScrollControllerList![index],
);
},
);
}
}
// 滚动器位置初始化
void _scrollPositionInit() {
if (widget.dataType == VideoEpidoesType.videoEpisode) {
// 单个 多tab
if (widget.ugcSeason?.sections != null) {
if (widget.ugcSeason!.sections!.length == 1) {
_listObserverController.initialIndexModel =
ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
} else {
_listObserverControllerList![widget.currentEpisodeIndex!]
.initialIndexModel = ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
}
}
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.dataType != VideoEpidoesType.videoEpisode) {
double itemHeight = (widget.isFullScreen
? 400
: Get.size.width - 3 * StyleString.safeSpace) /
5.2;
double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
_scrollController.jumpTo(offset);
}
});
}
// 获取订阅状态
void _getSubscribeStatus() async {
var res =
await VideoHttp.getSubscribeStatus(bvid: _videoDetailController!.bvid);
if (res['status']) {
isSubscribe.value = res['data']['season_fav'] ? 1 : 0;
}
}
// 更改订阅状态
void _changeSubscribeStatus() async {
if (isSubscribe.value == -1) {
return;
}
dynamic result = await VideoHttp.seasonFav(
isFav: isSubscribe.value == 1,
seasonId: widget.ugcSeason!.id,
);
if (result['status']) {
SmartDialog.showToast(isSubscribe.value == 1 ? '取消订阅成功' : '订阅成功');
isSubscribe.value = isSubscribe.value == 1 ? 0 : 1;
} else {
SmartDialog.showToast(result['msg']);
}
}
// 更改展开状态
void _changeVisible() {
setState(() {
isVisible = !isVisible;
});
}
@override @override
void dispose() { void dispose() {
try {
_listObserverController.controller?.dispose(); _listObserverController.controller?.dispose();
_listScrollController.dispose();
for (var element in _listObserverControllerList!) {
element.controller?.dispose();
}
for (var element in _listScrollControllerList!) {
element.dispose();
}
} catch (_) {}
super.dispose(); super.dispose();
} }
@ -145,23 +260,32 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
isFullScreen: widget.isFullScreen, isFullScreen: widget.isFullScreen,
), ),
if (widget.ugcSeason != null) ...[ if (widget.ugcSeason != null) ...[
UgcSeasonBuild(ugcSeason: widget.ugcSeason!), UgcSeasonBuild(
ugcSeason: widget.ugcSeason!,
isSubscribe: isSubscribe,
isVisible: isVisible,
changeFucCall: _changeSubscribeStatus,
changeVisible: _changeVisible,
),
], ],
Expanded( Expanded(
child: Material( child: Material(
child: widget.dataType == VideoEpidoesType.videoEpisode child: widget.dataType == VideoEpidoesType.videoEpisode
? (widget.ugcSeason!.sections!.length == 1
? ListViewObserver( ? ListViewObserver(
controller: _listObserverController, controller: _listObserverController,
child: ListView.builder( child: ListView.builder(
controller: _listScrollController, controller: _listScrollController,
itemCount: widget.episodes.length + 1, itemCount: widget.episodes.length + 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
bool isLastItem = index == widget.episodes.length; bool isLastItem =
index == widget.episodes.length;
bool isCurrentIndex = currentIndex == index; bool isCurrentIndex = currentIndex == index;
return isLastItem return isLastItem
? SizedBox( ? SizedBox(
height: height: MediaQuery.of(context)
MediaQuery.of(context).padding.bottom + .padding
.bottom +
20, 20,
) )
: EpisodeListItem( : EpisodeListItem(
@ -175,6 +299,7 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
}, },
), ),
) )
: buildTabBar())
: Padding( : Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0), // 设置左右间距为12 horizontal: 12.0), // 设置左右间距为12
@ -206,6 +331,65 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
); );
}); });
} }
Widget buildTabBar() {
return Column(
children: [
// 背景色
Container(
color: Theme.of(context).colorScheme.surface,
child: TabBar(
controller: tabController,
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
tabAlignment: TabAlignment.start,
splashBorderRadius: BorderRadius.circular(4),
tabs: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
return Tab(
text: section.title,
);
}).toList()
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
final int fIndex = widget.ugcSeason!.sections!.indexOf(section);
return ListViewObserver(
controller: _listObserverControllerList![fIndex],
child: ListView.builder(
controller: _listScrollControllerList![fIndex],
itemCount: section.episodes!.length + 1,
itemBuilder: (BuildContext context, int index) {
final bool isLastItem = index == section.episodes!.length;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom + 20,
)
: EpisodeListItem(
episode: section.episodes![index], // 调整索引
index: index, // 调整索引
isCurrentIndex: widget.currentCid ==
section.episodes![index].cid,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
);
}).toList()
],
),
),
],
);
}
} }
class TitleBar extends StatelessWidget { class TitleBar extends StatelessWidget {
@ -507,77 +691,134 @@ class EpisodeGridItem extends StatelessWidget {
class UgcSeasonBuild extends StatelessWidget { class UgcSeasonBuild extends StatelessWidget {
final UgcSeason ugcSeason; final UgcSeason ugcSeason;
final RxInt isSubscribe;
final bool isVisible;
final Function changeFucCall;
final Function changeVisible;
const UgcSeasonBuild({ const UgcSeasonBuild({
Key? key, Key? key,
required this.ugcSeason, required this.ugcSeason,
required this.isSubscribe,
required this.isVisible,
required this.changeFucCall,
required this.changeVisible,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( final ThemeData theme = Theme.of(context);
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), final Color outline = theme.colorScheme.outline;
color: Theme.of(context).colorScheme.surface, final Color surface = theme.colorScheme.surface;
final Color primary = theme.colorScheme.primary;
final Color onPrimary = theme.colorScheme.onPrimary;
final Color onInverseSurface = theme.colorScheme.onInverseSurface;
final TextStyle titleMedium = theme.textTheme.titleMedium!;
final TextStyle labelMedium = theme.textTheme.labelMedium!;
final Color dividerColor = theme.dividerColor.withOpacity(0.1);
return isVisible
? Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
color: surface,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Divider( Divider(height: 1, thickness: 1, color: dividerColor),
height: 1,
thickness: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
const SizedBox(height: 10), const SizedBox(height: 10),
Text(
'合集:${ugcSeason.title}',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 4),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Text(ugcSeason.intro ?? '', child: Text(
style: TextStyle( '合集:${ugcSeason.title}',
color: Theme.of(context).colorScheme.outline)), style: titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Obx(
() => isSubscribe.value == -1
? const SizedBox(height: 32)
: SizedBox(
height: 32,
child: FilledButton.tonal(
onPressed: () => changeFucCall.call(),
style: TextButton.styleFrom(
padding:
const EdgeInsets.only(left: 8, right: 8),
foregroundColor: isSubscribe.value == 1
? outline
: onPrimary,
backgroundColor: isSubscribe.value == 1
? onInverseSurface
: primary,
),
child:
Text(isSubscribe.value == 1 ? '已订阅' : '订阅'),
),
),
), ),
// SizedBox(
// height: 32,
// child: FilledButton.tonal(
// onPressed: () {},
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// child: const Text('订阅'),
// ),
// ),
// const SizedBox(width: 6),
], ],
), ),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 4),
Text(
ugcSeason.intro!,
style: TextStyle(color: outline, fontSize: 12),
),
], ],
const SizedBox(height: 4), const SizedBox(height: 4),
Text.rich( Text.rich(
TextSpan( TextSpan(
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, fontSize: labelMedium.fontSize, color: outline),
color: Theme.of(context).colorScheme.outline,
),
children: [ children: [
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'), TextSpan(
text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
const TextSpan(text: ' · '), const TextSpan(text: ' · '),
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'), TextSpan(
text:
'${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
], ],
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
Divider( Align(
height: 1, alignment: Alignment.center,
thickness: 1, child: Material(
color: Theme.of(context).dividerColor.withOpacity(0.1), color: surface,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 0),
child: Text(
'收起简介',
style: TextStyle(color: primary, fontSize: 12),
), ),
),
),
),
),
Divider(height: 1, thickness: 1, color: dividerColor),
], ],
), ),
)
: Align(
alignment: Alignment.center,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Text(
'展开简介',
style: TextStyle(color: primary, fontSize: 12),
),
),
),
); );
} }
} }

View File

@ -609,4 +609,14 @@ class Api {
/// @我的 /// @我的
static const String messageAtAPi = '/x/msgfeed/at?'; static const String messageAtAPi = '/x/msgfeed/at?';
/// 订阅
static const String confirmSub = '/x/v3/fav/season/fav';
/// 订阅状态
static const String videoRelation = '/x/web-interface/archive/relation';
/// 获取空降区间
static const String getSkipSegments =
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments';
} }

View File

@ -1,3 +1,5 @@
import 'package:pilipala/models/sponsor_block/segment.dart';
import 'index.dart'; import 'index.dart';
class CommonHttp { class CommonHttp {
@ -14,4 +16,31 @@ class CommonHttp {
}; };
} }
} }
static Future querySkipSegments({required String bvid}) async {
var res = await Request().getWithoutCookie(Api.getSkipSegments, data: {
'videoID': bvid,
});
if (res.data is List && res.data.isNotEmpty) {
try {
return {
'status': true,
'data': res.data
.map<SegmentDataModel>((e) => SegmentDataModel.fromJson(e))
.toList(),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': 'sponsorBlock数据解析失败: $err',
};
}
} else {
return {
'status': false,
'data': [],
};
}
}
} }

View File

@ -7,6 +7,7 @@ class HttpString {
static const String passBaseUrl = 'https://passport.bilibili.com'; static const String passBaseUrl = 'https://passport.bilibili.com';
static const String messageBaseUrl = 'https://message.bilibili.com'; static const String messageBaseUrl = 'https://message.bilibili.com';
static const String bangumiBaseUrl = 'https://bili.meark.me'; static const String bangumiBaseUrl = 'https://bili.meark.me';
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
static const List<int> validateStatusCodes = [ static const List<int> validateStatusCodes = [
302, 302,
304, 304,

View File

@ -3,6 +3,7 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/utils/login.dart';
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@ -18,6 +19,9 @@ class ApiInterceptor extends Interceptor {
void onResponse(Response response, ResponseInterceptorHandler handler) { void onResponse(Response response, ResponseInterceptorHandler handler) {
try { try {
// 在响应之后处理数据 // 在响应之后处理数据
if (response.data is Map && response.data['code'] == -101) {
LoginUtils.loginOut();
}
} catch (err) { } catch (err) {
print('ApiInterceptor: $err'); print('ApiInterceptor: $err');
} }

View File

@ -516,4 +516,34 @@ class UserHttp {
}; };
} }
} }
// 解析up投稿
static Future parseUpArchiveVideo({
required int mid,
required int oid,
required String bvid,
String sortField = 'pubtime',
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/$mid',
data: {
'oid': oid,
'bvid': bvid,
'sort_field': sortField,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
} }

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../common/constants.dart'; import '../common/constants.dart';
import '../models/common/reply_type.dart'; import '../models/common/reply_type.dart';
import '../models/home/rcmd/result.dart'; import '../models/home/rcmd/result.dart';
@ -97,6 +98,8 @@ class VideoHttp {
for (var i in res.data['data']['items']) { for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户 // 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' && if (i['card_goto'] != 'ad_av' &&
i['card_goto'] != 'ad_web_s' &&
i['card_goto'] != 'ad_web' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null && (i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) { !blackMidsList.contains(i['args']['up_mid']))) {
@ -558,4 +561,50 @@ class VideoHttp {
final List body = res.data['body']; final List body = res.data['body'];
return {'content': content, 'body': body}; return {'content': content, 'body': body};
} }
static Future<Map<String, dynamic>> getSubscribeStatus(
{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 seasonFav({
required bool isFav,
required dynamic seasonId,
}) async {
var res = await Request().post(
isFav ? Api.cancelSub : Api.confirmSub,
data: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
} }

View File

@ -0,0 +1,26 @@
// 片段类型枚举
enum ActionType {
skip,
mute,
full,
poi,
chapter,
}
extension ActionTypeExtension on ActionType {
String get value => [
'skip',
'mute',
'full',
'poi',
'chapter',
][index];
String get label => [
'跳过',
'静音',
'完整观看',
'亮点',
'章节切换',
][index];
}

View File

@ -0,0 +1,43 @@
import 'action_type.dart';
import 'segment_type.dart';
class SegmentDataModel {
final SegmentType? category;
final ActionType? actionType;
final List? segment;
final String? uuid;
final num? videoDuration;
final int? locked;
final int? votes;
final String? description;
// 是否已经跳过
bool isSkip = false;
SegmentDataModel({
this.category,
this.actionType,
this.segment,
this.uuid,
this.videoDuration,
this.locked,
this.votes,
this.description,
});
factory SegmentDataModel.fromJson(Map<String, dynamic> json) {
return SegmentDataModel(
category: SegmentType.values.firstWhere(
(e) => e.value == json['category'],
orElse: () => SegmentType.sponsor),
actionType: ActionType.values.firstWhere(
(e) => e.value == json['actionType'],
orElse: () => ActionType.skip),
segment: json['segment'],
uuid: json['UUID'],
videoDuration: json['videoDuration'],
locked: json['locked'],
votes: json['votes'],
description: json['description'],
);
}
}

View File

@ -0,0 +1,46 @@
// 片段类型枚举
// ignore_for_file: constant_identifier_names
enum SegmentType {
sponsor,
intro,
outro,
interaction,
selfpromo,
music_offtopic,
preview,
poi_highlight,
filler,
exclusive_access,
chapter,
}
extension SegmentTypeExtension on SegmentType {
String get value => [
'sponsor',
'intro',
'outro',
'interaction',
'selfpromo',
'music_offtopic',
'preview',
'poi_highlight',
'filler',
'exclusive_access',
'chapter',
][index];
String get label => [
'赞助',
'开场介绍',
'片尾致谢',
'互动',
'自我推广',
'音乐',
'预览',
'亮点',
'无效填充',
'独家访问',
'章节',
][index];
}

View File

@ -641,6 +641,7 @@ class EpisodeItem {
this.page, this.page,
this.bvid, this.bvid,
this.cover, this.cover,
this.pages,
}); });
int? seasonId; int? seasonId;
int? sectionId; int? sectionId;
@ -655,6 +656,7 @@ class EpisodeItem {
int? pubdate; int? pubdate;
int? duration; int? duration;
Stat? stat; Stat? stat;
List<Page>? pages;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id']; seasonId = json['season_id'];
@ -670,6 +672,7 @@ class EpisodeItem {
pubdate = json['arc']['pubdate']; pubdate = json['arc']['pubdate'];
duration = json['arc']['duration']; duration = json['arc']['duration'];
stat = Stat.fromJson(json['arc']['stat']); stat = Stat.fromJson(json['arc']['stat']);
pages = json['pages'].map<Page>((e) => Page.fromJson(e)).toList();
} }
} }
@ -712,3 +715,18 @@ class Vip {
status = json['status']; status = json['status'];
} }
} }
class Page {
Page({
this.cid,
this.page,
});
int? cid;
int? page;
Page.fromJson(Map<String, dynamic> json) {
cid = json['cid'];
page = json['page'];
}
}

View File

@ -24,7 +24,7 @@ class BangumiIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
var seasonId = Get.parameters['seasonId'] != null var seasonId = Get.parameters['seasonId'] != null
? int.parse(Get.parameters['seasonId']!) ? int.tryParse(Get.parameters['seasonId']!)
: null; : null;
var epId = Get.parameters['epId'] != null var epId = Get.parameters['epId'] != null
? int.tryParse(Get.parameters['epId']!) ? int.tryParse(Get.parameters['epId']!)
@ -69,6 +69,7 @@ class BangumiIntroController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
print('bangumi: ${Get.parameters.toString()}');
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null; userLogin = userInfo != null;
if (userLogin && seasonId != null) { if (userLogin && seasonId != null) {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/logic_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class FavItem extends StatelessWidget { class FavItem extends StatelessWidget {
@ -96,9 +97,7 @@ class VideoContent extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
Constants.publicFavFolder.contains(favFolderItem.attr) LogicUtils.isPublic(favFolderItem.attr) ? '公开' : '私密',
? '公开'
: '私密',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -1,14 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/utils/global_data_cache.dart'; import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart';
class MemberArchiveController extends GetxController { class MemberArchiveController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
late int mid; late int mid;
int pn = 1; int pn = 1;
int count = 0; RxInt count = 0.obs;
RxMap<String, String> currentOrder = <String, String>{}.obs; RxMap<String, String> currentOrder = <String, String>{}.obs;
RxList<Map<String, String>> orderList = [ RxList<Map<String, String>> orderList = [
{'type': 'pubdate', 'label': '最新发布'}, {'type': 'pubdate', 'label': '最新发布'},
@ -50,11 +52,11 @@ class MemberArchiveController extends GetxController {
if (res['status']) { if (res['status']) {
if (type == 'init') { if (type == 'init') {
archivesList.value = res['data'].list.vlist; archivesList.value = res['data'].list.vlist;
count.value = res['data'].page['count'];
} }
if (type == 'onLoad') { if (type == 'onLoad') {
archivesList.addAll(res['data'].list.vlist); archivesList.addAll(res['data'].list.vlist);
} }
count = res['data'].page['count'];
pn += 1; pn += 1;
} }
isLoading.value = false; isLoading.value = false;
@ -76,4 +78,29 @@ class MemberArchiveController extends GetxController {
Future onLoad() async { Future onLoad() async {
getMemberArchive('onLoad'); getMemberArchive('onLoad');
} }
Future toViewPlayAll() async {
final VListItemModel firstItem = archivesList.first;
final String bvid = firstItem.bvid!;
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
late Map sortFieldMap = {
'pubdate': 'pubtime',
'click': 'play',
'fav': 'fav',
};
Get.toNamed(
'/video?bvid=${firstItem.bvid}&cid=$cid',
arguments: {
'videoItem': firstItem,
'heroTag': heroTag,
'sourceType': 'up_archive',
'oid': firstItem.aid,
'favTitle': '${firstItem.owner!.name!} - ${currentOrder['label']!}',
'favInfo': firstItem,
'count': count.value,
'sortField': sortFieldMap[currentOrder['type']],
},
);
}
} }

View File

@ -135,6 +135,15 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
), ),
], ],
), ),
floatingActionButton: Obx(
() => _memberArchivesController.count > 0
? FloatingActionButton.extended(
onPressed: _memberArchivesController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
); );
} }
} }

View File

@ -61,16 +61,7 @@ class SettingController extends GetxController {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
// 清空cookie await LoginUtils.loginOut();
await Request.cookieManager.cookieJar.deleteAll();
Request.dio.options.headers['cookie'] = '';
// 清空本地存储的用户标识
userInfoCache.put('userInfoCache', null);
localCache
.put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''});
await LoginUtils.refreshLoginStatus(false);
SmartDialog.dismiss().then((value) => Get.back()); SmartDialog.dismiss().then((value) => Get.back());
}, },
child: const Text('确认'), child: const Text('确认'),

View File

@ -6,11 +6,14 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/common.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/sponsor_block/segment.dart';
import 'package:pilipala/models/sponsor_block/segment_type.dart';
import 'package:pilipala/models/video/later.dart'; import 'package:pilipala/models/video/later.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
@ -119,6 +122,9 @@ class VideoDetailController extends GetxController
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[]; List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
RxBool isWatchLaterVisible = false.obs; RxBool isWatchLaterVisible = false.obs;
RxString watchLaterTitle = ''.obs; RxString watchLaterTitle = ''.obs;
RxInt watchLaterCount = 0.obs;
List<SegmentDataModel> skipSegments = <SegmentDataModel>[];
int? lastPosition;
@override @override
void onInit() { void onInit() {
@ -170,7 +176,7 @@ class VideoDetailController extends GetxController
sourceType.value = argMap['sourceType'] ?? 'normal'; sourceType.value = argMap['sourceType'] ?? 'normal';
isWatchLaterVisible.value = isWatchLaterVisible.value =
sourceType.value == 'watchLater' || sourceType.value == 'fav'; ['watchLater', 'fav', 'up_archive'].contains(sourceType.value);
if (sourceType.value == 'watchLater') { if (sourceType.value == 'watchLater') {
watchLaterTitle.value = '稍后再看'; watchLaterTitle.value = '稍后再看';
fetchMediaList(); fetchMediaList();
@ -179,9 +185,19 @@ class VideoDetailController extends GetxController
watchLaterTitle.value = argMap['favTitle']; watchLaterTitle.value = argMap['favTitle'];
queryFavVideoList(); queryFavVideoList();
} }
if (sourceType.value == 'up_archive') {
watchLaterTitle.value = argMap['favTitle'];
watchLaterCount.value = argMap['count'];
queryArchiveVideoList();
}
tabCtr.addListener(() { tabCtr.addListener(() {
onTabChanged(); onTabChanged();
}); });
/// 仅投稿视频skip
if (videoType == SearchType.video) {
querySkipSegments();
}
} }
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) { showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
@ -299,6 +315,7 @@ class VideoDetailController extends GetxController
plPlayerController.headerControl = headerControl; plPlayerController.headerControl = headerControl;
plPlayerController.subtitles.value = subtitles; plPlayerController.subtitles.value = subtitles;
onPositionChanged();
} }
// 视频链接 // 视频链接
@ -585,7 +602,9 @@ class VideoDetailController extends GetxController
} }
void toggeleWatchLaterVisible(bool val) { void toggeleWatchLaterVisible(bool val) {
if (sourceType.value == 'watchLater' || sourceType.value == 'fav') { if (sourceType.value == 'watchLater' ||
sourceType.value == 'fav' ||
sourceType.value == 'up_archive') {
isWatchLaterVisible.value = !isWatchLaterVisible.value; isWatchLaterVisible.value = !isWatchLaterVisible.value;
} }
} }
@ -616,8 +635,19 @@ class VideoDetailController extends GetxController
changeMediaList: changeMediaList, changeMediaList: changeMediaList,
panelTitle: watchLaterTitle.value, panelTitle: watchLaterTitle.value,
bvid: bvid, bvid: bvid,
mediaId: Get.arguments['mediaId'], mediaId: [
'watchLater',
'fav',
].contains(sourceType.value)
? Get.arguments['mediaId']
: Get.arguments['favInfo'].owner.mid,
hasMore: mediaList.length != Get.arguments['count'], hasMore: mediaList.length != Get.arguments['count'],
type: [
'watchLater',
'fav',
].contains(sourceType.value)
? 3
: 1,
); );
}); });
replyReplyBottomSheetCtr?.closed.then((value) { replyReplyBottomSheetCtr?.closed.then((value) {
@ -667,11 +697,73 @@ class VideoDetailController extends GetxController
} }
} }
Future queryArchiveVideoList() async {
final Map argMap = Get.arguments;
var favInfo = argMap['favInfo'];
var sortField = argMap['sortField'];
var res = await UserHttp.parseUpArchiveVideo(
mid: favInfo.owner.mid,
oid: oid.value,
bvid: bvid,
sortField: sortField,
);
if (res['status']) {
mediaList = res['data'];
}
}
// 监听tabBarView切换 // 监听tabBarView切换
void onTabChanged() { void onTabChanged() {
isWatchLaterVisible.value = tabCtr.index == 0; isWatchLaterVisible.value = tabCtr.index == 0;
} }
// 获取sponsorBlock数据
Future querySkipSegments() async {
var res = await CommonHttp.querySkipSegments(bvid: bvid);
if (res['status']) {
/// TODO 根据segmentType过滤数据
skipSegments = res['data'] ?? [];
}
}
// 监听视频进度
void onPositionChanged() async {
final List<SegmentDataModel> sponsorSkipSegments = skipSegments
.where((e) => e.category!.value == SegmentType.sponsor.value)
.toList();
if (sponsorSkipSegments.isEmpty) {
return;
}
plPlayerController.videoPlayerController?.stream.position
.listen((Duration position) async {
final int positionMs = position.inSeconds;
// 如果当前秒与上次处理的秒相同,则直接返回
if (lastPosition != null && lastPosition! == positionMs) {
return;
}
lastPosition = positionMs;
for (SegmentDataModel segment in sponsorSkipSegments) {
try {
final segmentStart = segment.segment!.first.toInt();
final segmentEnd = segment.segment!.last.toInt();
/// 只有顺序播放时才skip跳转时间点不会skip
if (positionMs == segmentStart && !segment.isSkip) {
await plPlayerController.videoPlayerController
?.seek(Duration(seconds: segmentEnd));
segment.isSkip = true;
SmartDialog.showToast('已跳过${segment.category!.label}片段');
}
} catch (err) {
SmartDialog.showToast('skipSegments error: $err');
}
}
});
}
@override @override
void onClose() { void onClose() {
super.onClose(); super.onClose();

View File

@ -63,6 +63,7 @@ class VideoIntroController extends GetxController {
PersistentBottomSheetController? bottomSheetController; PersistentBottomSheetController? bottomSheetController;
late bool enableRelatedVideo; late bool enableRelatedVideo;
UgcSeason? ugcSeason; UgcSeason? ugcSeason;
RxList<Part> pages = <Part>[].obs;
@override @override
void onInit() { void onInit() {
@ -84,18 +85,20 @@ class VideoIntroController extends GetxController {
} }
// 获取视频简介&分p // 获取视频简介&分p
Future queryVideoIntro() async { Future queryVideoIntro({cover}) async {
var result = await VideoHttp.videoIntro(bvid: bvid); var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) { if (result['status']) {
videoDetail.value = result['data']!; videoDetail.value = result['data']!;
ugcSeason = result['data']!.ugcSeason; ugcSeason = result['data']!.ugcSeason;
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { pages.value = result['data']!.pages!;
lastPlayCid.value = videoDetail.value.pages!.first.cid!; lastPlayCid.value = videoDetail.value.cid!;
if (pages.isNotEmpty) {
lastPlayCid.value = pages.first.cid!;
} }
final VideoDetailController videoDetailCtr = final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}']; videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}'];
videoDetailCtr.cover.value = result['data'].pic ?? ''; videoDetailCtr.cover.value = cover ?? result['data'].pic ?? '';
// 获取到粉丝数再返回 // 获取到粉丝数再返回
await queryUserStat(); await queryUserStat();
} }
@ -470,8 +473,7 @@ class VideoIntroController extends GetxController {
videoReplyCtr.queryReplyList(type: 'init'); videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {} } catch (_) {}
this.bvid = bvid; this.bvid = bvid;
lastPlayCid.value = cid; await queryVideoIntro(cover: cover);
await queryVideoIntro();
} }
void startTimer() { void startTimer() {
@ -521,9 +523,8 @@ class VideoIntroController extends GetxController {
final List<EpisodeItem> episodesList = sections[i].episodes!; final List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList); episodes.addAll(episodesList);
} }
} else if (videoDetail.value.pages != null) { } else if (pages.isNotEmpty) {
isPages = true; isPages = true;
final List<Part> pages = videoDetail.value.pages!;
episodes.addAll(pages); episodes.addAll(pages);
} }
@ -621,10 +622,9 @@ class VideoIntroController extends GetxController {
} }
} }
} }
if (videoDetail.value.pages != null && if (pages.length > 1) {
videoDetail.value.pages!.length > 1) {
dataType = VideoEpidoesType.videoPart; dataType = VideoEpidoesType.videoPart;
episodes = videoDetail.value.pages!; episodes = pages;
} }
DrawerUtils.showRightDialog( DrawerUtils.showRightDialog(

View File

@ -404,27 +404,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!, ugcSeason: widget.videoDetail!.ugcSeason!,
cid: videoIntroController.lastPlayCid.value != 0 cid: videoIntroController.lastPlayCid.value,
? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid,
sheetHeight: videoDetailCtr.sheetHeight.value, sheetHeight: videoDetailCtr.sheetHeight.value,
changeFuc: (bvid, cid, aid, cover) => changeFuc: videoIntroController.changeSeasonOrbangu,
videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
cover,
),
videoIntroCtr: videoIntroController, videoIntroCtr: videoIntroController,
), ),
) )
], ],
// 合集 videoEpisode // 合集 videoEpisode
if (widget.videoDetail!.pages != null && if (videoIntroController.pages.length > 1) ...[
widget.videoDetail!.pages!.length > 1) ...[
Obx( Obx(
() => PagesPanel( () => PagesPanel(
pages: widget.videoDetail!.pages!, pages: videoIntroController.pages,
cid: videoIntroController.lastPlayCid.value, cid: videoIntroController.lastPlayCid.value,
sheetHeight: videoDetailCtr.sheetHeight.value, sheetHeight: videoDetailCtr.sheetHeight.value,
changeFuc: (cid, cover) => changeFuc: (cid, cover) =>

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/logic_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class FavPanel extends StatefulWidget { class FavPanel extends StatefulWidget {
@ -67,14 +67,13 @@ class _FavPanelState extends State<FavPanel> {
onTap: () => onTap: () =>
widget.ctr!.onChoose(item.favState != 1, index), widget.ctr!.onChoose(item.favState != 1, index),
dense: true, dense: true,
leading: Icon( leading: Icon(LogicUtils.isPublic(item.attr)
Constants.publicFavFolder.contains(item.attr)
? Icons.folder_outlined ? Icons.folder_outlined
: Icons.lock_outline), : Icons.lock_outline),
minLeadingWidth: 0, minLeadingWidth: 0,
title: Text(item.title!), title: Text(item.title!),
subtitle: Text( subtitle: Text(
'${item.mediaCount}个内容 - ${Constants.publicFavFolder.contains(item.attr) ? '公开' : '私密'}', '${item.mediaCount}个内容 - ${LogicUtils.isPublic(item.attr) ? '公开' : '私密'}',
), ),
trailing: Transform.scale( trailing: Transform.scale(
scale: 0.9, scale: 0.9,

View File

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import '../../../../../common/pages_bottom_sheet.dart'; import '../../../../../common/pages_bottom_sheet.dart';
import '../../../../../models/common/video_episode_type.dart'; import '../../../../../models/common/video_episode_type.dart';
@ -32,25 +31,26 @@ class _PagesPanelState extends State<PagesPanel> {
late int cid; late int cid;
late RxInt currentIndex = (-1).obs; late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag']; final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
final ScrollController listViewScrollCtr = ScrollController(); final ScrollController listViewScrollCtr = ScrollController();
late PersistentBottomSheetController? _bottomSheetController; PersistentBottomSheetController? _bottomSheetController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid; cid = widget.cid;
episodes = widget.pages; episodes = widget.pages;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag); updateCurrentIndexAndScroll();
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid); widget.videoIntroCtr.lastPlayCid.listen((int p0) {
scrollToIndex();
_videoDetailController.cid.listen((int p0) {
cid = p0; cid = p0;
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid); updateCurrentIndexAndScroll();
scrollToIndex();
}); });
} }
void updateCurrentIndexAndScroll() {
currentIndex.value = widget.pages.indexWhere((Part e) => e.cid == cid);
scrollToIndex();
}
@override @override
void dispose() { void dispose() {
listViewScrollCtr.dispose(); listViewScrollCtr.dispose();
@ -60,7 +60,10 @@ class _PagesPanelState extends State<PagesPanel> {
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
widget.changeFuc?.call(item.cid, item.cover); widget.changeFuc?.call(item.cid, item.cover);
currentIndex.value = i; currentIndex.value = i;
cid = item.cid;
if (_bottomSheetController != null) {
_bottomSheetController?.close(); _bottomSheetController?.close();
}
scrollToIndex(); scrollToIndex();
} }
@ -112,7 +115,7 @@ class _PagesPanelState extends State<PagesPanel> {
widget.videoIntroCtr.bottomSheetController = widget.videoIntroCtr.bottomSheetController =
_bottomSheetController = EpisodeBottomSheet( _bottomSheetController = EpisodeBottomSheet(
currentCid: cid, currentCid: cid,
episodes: episodes, episodes: widget.pages,
changeFucCall: changeFucCall, changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight, sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoPart, dataType: VideoEpidoesType.videoPart,

View File

@ -33,6 +33,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
final String heroTag = Get.arguments['heroTag']; final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController; late VideoDetailController _videoDetailController;
late PersistentBottomSheetController? _bottomSheetController; late PersistentBottomSheetController? _bottomSheetController;
int currentEpisodeIndex = -1;
@override @override
void initState() { void initState() {
@ -41,13 +42,12 @@ class _SeasonPanelState extends State<SeasonPanel> {
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag); _videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes /// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个
/// TODO 同时显示多个合集
final List<SectionItem> sections = widget.ugcSeason.sections!; final List<SectionItem> sections = widget.ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) { for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!; final List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) { for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == cid) { if (episodesList[j].cid == cid) {
currentEpisodeIndex = i;
episodes = episodesList; episodes = episodesList;
continue; continue;
} }
@ -55,10 +55,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
} }
/// 取对应 season_id 的 episodes /// 取对应 season_id 的 episodes
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid); getCurrentIndex();
_videoDetailController.cid.listen((int p0) { _videoDetailController.cid.listen((int p0) {
cid = p0; cid = p0;
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid); getCurrentIndex();
}); });
} }
@ -73,6 +73,23 @@ class _SeasonPanelState extends State<SeasonPanel> {
_bottomSheetController?.close(); _bottomSheetController?.close();
} }
// 获取currentIndex
void getCurrentIndex() {
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
final List<SectionItem> sections = widget.ugcSeason.sections!;
if (sections.length == 1 && sections.first.type == 1) {
final List<EpisodeItem> episodesList = sections.first.episodes!;
for (int i = 0; i < episodesList.length; i++) {
for (int j = 0; j < episodesList[i].pages!.length; j++) {
if (episodesList[i].pages![j].cid == cid) {
currentIndex.value = i;
continue;
}
}
}
}
}
Widget buildEpisodeListItem( Widget buildEpisodeListItem(
EpisodeItem episode, EpisodeItem episode,
int index, int index,
@ -125,6 +142,8 @@ class _SeasonPanelState extends State<SeasonPanel> {
sheetHeight: widget.sheetHeight, sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoEpisode, dataType: VideoEpidoesType.videoEpisode,
ugcSeason: widget.ugcSeason, ugcSeason: widget.ugcSeason,
currentEpisodeIndex: currentEpisodeIndex,
currentIndex: currentIndex.value,
).show(context); ).show(context);
}, },
child: Padding( child: Padding(

View File

@ -786,7 +786,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
Obx( Obx(
() => Visibility( () => Visibility(
visible: vdCtr.sourceType.value == 'watchLater' || visible: vdCtr.sourceType.value == 'watchLater' ||
vdCtr.sourceType.value == 'fav', vdCtr.sourceType.value == 'fav' ||
vdCtr.sourceType.value == 'up_archive',
child: AnimatedPositioned( child: AnimatedPositioned(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -818,8 +819,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: Row(children: [ child: Row(children: [
const Icon(Icons.playlist_play, size: 24), const Icon(Icons.playlist_play, size: 24),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Expanded(
child: Text(
vdCtr.watchLaterTitle.value, vdCtr.watchLaterTitle.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
@ -828,7 +832,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
letterSpacing: 0.2, letterSpacing: 0.2,
), ),
), ),
const Spacer(), ),
const SizedBox(width: 50),
const Icon(Icons.keyboard_arrow_up_rounded, size: 26), const Icon(Icons.keyboard_arrow_up_rounded, size: 26),
]), ]),
), ),

View File

@ -19,8 +19,9 @@ class MediaListPanel extends StatefulWidget {
this.changeMediaList, this.changeMediaList,
this.panelTitle, this.panelTitle,
this.bvid, this.bvid,
this.mediaId, required this.mediaId,
this.hasMore = false, this.hasMore = false,
required this.type,
super.key, super.key,
}); });
@ -29,8 +30,9 @@ class MediaListPanel extends StatefulWidget {
final Function? changeMediaList; final Function? changeMediaList;
final String? panelTitle; final String? panelTitle;
final String? bvid; final String? bvid;
final int? mediaId; final int mediaId;
final bool hasMore; final bool hasMore;
final int type;
@override @override
State<MediaListPanel> createState() => _MediaListPanelState(); State<MediaListPanel> createState() => _MediaListPanelState();
@ -59,8 +61,8 @@ class _MediaListPanelState extends State<MediaListPanel> {
void loadMore() async { void loadMore() async {
var res = await UserHttp.getMediaList( var res = await UserHttp.getMediaList(
type: 3, type: widget.type,
bizId: widget.mediaId!, bizId: widget.mediaId,
ps: 20, ps: 20,
oid: mediaList.last.id, oid: mediaList.last.id,
); );

View File

@ -0,0 +1,6 @@
class LogicUtils {
// 收藏夹是否公开
static bool isPublic(int attr) {
return (attr & 1) == 0;
}
}

View File

@ -7,15 +7,20 @@ import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/utils/cookie.dart'; import 'package:pilipala/utils/cookie.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class LoginUtils { class LoginUtils {
static Box userInfoCache = GStrorage.userInfo;
static Box localCache = GStrorage.localCache;
static Future refreshLoginStatus(bool status) async { static Future refreshLoginStatus(bool status) async {
try { try {
// 更改我的页面登录状态 // 更改我的页面登录状态
@ -109,4 +114,14 @@ class LoginUtils {
Clipboard.setData(ClipboardData(text: content)); Clipboard.setData(ClipboardData(text: content));
} }
} }
// 退出登录
static loginOut() async {
await Request.cookieManager.cookieJar.deleteAll();
Request.dio.options.headers['cookie'] = '';
userInfoCache.put('userInfoCache', null);
localCache.put(LocalCacheKey.accessKey, {'mid': -1, 'value': ''});
GlobalDataCache().userInfo = null;
await refreshLoginStatus(false);
}
} }

View File

@ -533,18 +533,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_linux name: file_selector_linux
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.2+1" version: "0.9.3"
file_selector_macos: file_selector_macos:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.4+1" version: "0.9.4+2"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -557,10 +557,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_windows name: file_selector_windows
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.3+2" version: "0.9.3+3"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -842,10 +842,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.8.12" version: "0.8.12+1"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
@ -1438,10 +1438,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: scrollview_observer name: scrollview_observer
sha256: fa408bcfd41e19da841eb53fc471f8f952d5ef818b854d2505c4bb3f0c876381 sha256: "8537ba32e5a15ade301e5c77ae858fd8591695defaad1821eca9eeb4ac28a157"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.22.0" version: "1.23.0"
sentry: sentry:
dependency: transitive dependency: transitive
description: description: