Merge branch 'main' into fix
This commit is contained in:
@ -15,5 +15,4 @@ class Constants {
|
||||
// 59b43e04ad6965f34319062b478f83dd TV端
|
||||
static const String appSec = '59b43e04ad6965f34319062b478f83dd';
|
||||
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||
static const List<int> publicFavFolder = <int>[0, 2, 22];
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.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/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:scrollview_observer/scrollview_observer.dart';
|
||||
import '../models/common/video_episode_type.dart';
|
||||
@ -20,6 +22,8 @@ class EpisodeBottomSheet {
|
||||
final double? sheetHeight;
|
||||
bool isFullScreen = false;
|
||||
final UgcSeason? ugcSeason;
|
||||
final int? currentEpisodeIndex;
|
||||
final int? currentIndex;
|
||||
|
||||
EpisodeBottomSheet({
|
||||
required this.episodes,
|
||||
@ -30,6 +34,8 @@ class EpisodeBottomSheet {
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
this.currentEpisodeIndex,
|
||||
this.currentIndex,
|
||||
});
|
||||
|
||||
Widget buildShowContent() {
|
||||
@ -42,6 +48,8 @@ class EpisodeBottomSheet {
|
||||
sheetHeight: sheetHeight,
|
||||
isFullScreen: isFullScreen,
|
||||
ugcSeason: ugcSeason,
|
||||
currentEpisodeIndex: currentEpisodeIndex,
|
||||
currentIndex: currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,6 +75,8 @@ class PagesBottomSheet extends StatefulWidget {
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
this.currentEpisodeIndex,
|
||||
this.currentIndex,
|
||||
});
|
||||
|
||||
final List<dynamic> episodes;
|
||||
@ -77,41 +87,38 @@ class PagesBottomSheet extends StatefulWidget {
|
||||
final double? sheetHeight;
|
||||
final bool isFullScreen;
|
||||
final UgcSeason? ugcSeason;
|
||||
final int? currentEpisodeIndex;
|
||||
final int? currentIndex;
|
||||
|
||||
@override
|
||||
State<PagesBottomSheet> createState() => _PagesBottomSheetState();
|
||||
}
|
||||
|
||||
class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
class _PagesBottomSheetState extends State<PagesBottomSheet>
|
||||
with TickerProviderStateMixin {
|
||||
final ScrollController _listScrollController = ScrollController();
|
||||
late ListObserverController _listObserverController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex =
|
||||
currentIndex = widget.currentIndex ??
|
||||
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
|
||||
_listObserverController =
|
||||
ListObserverController(controller: _listScrollController);
|
||||
_scrollToInit();
|
||||
_scrollPositionInit();
|
||||
if (widget.dataType == VideoEpidoesType.videoEpisode) {
|
||||
_listObserverController.initialIndexModel = ObserverIndexPositionModel(
|
||||
index: currentIndex,
|
||||
isFixedHeight: true,
|
||||
);
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
_getSubscribeStatus();
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -126,9 +133,117 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
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
|
||||
void dispose() {
|
||||
try {
|
||||
_listObserverController.controller?.dispose();
|
||||
_listScrollController.dispose();
|
||||
for (var element in _listObserverControllerList!) {
|
||||
element.controller?.dispose();
|
||||
}
|
||||
for (var element in _listScrollControllerList!) {
|
||||
element.dispose();
|
||||
}
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -145,23 +260,32 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
isFullScreen: widget.isFullScreen,
|
||||
),
|
||||
if (widget.ugcSeason != null) ...[
|
||||
UgcSeasonBuild(ugcSeason: widget.ugcSeason!),
|
||||
UgcSeasonBuild(
|
||||
ugcSeason: widget.ugcSeason!,
|
||||
isSubscribe: isSubscribe,
|
||||
isVisible: isVisible,
|
||||
changeFucCall: _changeSubscribeStatus,
|
||||
changeVisible: _changeVisible,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: widget.dataType == VideoEpidoesType.videoEpisode
|
||||
? (widget.ugcSeason!.sections!.length == 1
|
||||
? ListViewObserver(
|
||||
controller: _listObserverController,
|
||||
child: ListView.builder(
|
||||
controller: _listScrollController,
|
||||
itemCount: widget.episodes.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool isLastItem = index == widget.episodes.length;
|
||||
bool isLastItem =
|
||||
index == widget.episodes.length;
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return isLastItem
|
||||
? SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
20,
|
||||
)
|
||||
: EpisodeListItem(
|
||||
@ -175,6 +299,7 @@ class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
},
|
||||
),
|
||||
)
|
||||
: buildTabBar())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
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 {
|
||||
@ -507,77 +691,134 @@ class EpisodeGridItem extends StatelessWidget {
|
||||
|
||||
class UgcSeasonBuild extends StatelessWidget {
|
||||
final UgcSeason ugcSeason;
|
||||
final RxInt isSubscribe;
|
||||
final bool isVisible;
|
||||
final Function changeFucCall;
|
||||
final Function changeVisible;
|
||||
|
||||
const UgcSeasonBuild({
|
||||
Key? key,
|
||||
required this.ugcSeason,
|
||||
required this.isSubscribe,
|
||||
required this.isVisible,
|
||||
required this.changeFucCall,
|
||||
required this.changeVisible,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final Color outline = theme.colorScheme.outline;
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
Divider(height: 1, thickness: 1, color: dividerColor),
|
||||
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(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(ugcSeason.intro ?? '',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline)),
|
||||
child: Text(
|
||||
'合集:${ugcSeason.title}',
|
||||
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),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
fontSize: labelMedium.fontSize, color: outline),
|
||||
children: [
|
||||
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
|
||||
TextSpan(
|
||||
text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
|
||||
const TextSpan(text: ' · '),
|
||||
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
|
||||
TextSpan(
|
||||
text:
|
||||
'${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -609,4 +609,14 @@ class Api {
|
||||
|
||||
/// @我的
|
||||
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';
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'package:pilipala/models/sponsor_block/segment.dart';
|
||||
|
||||
import 'index.dart';
|
||||
|
||||
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': [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ class HttpString {
|
||||
static const String passBaseUrl = 'https://passport.bilibili.com';
|
||||
static const String messageBaseUrl = 'https://message.bilibili.com';
|
||||
static const String bangumiBaseUrl = 'https://bili.meark.me';
|
||||
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
|
||||
static const List<int> validateStatusCodes = [
|
||||
302,
|
||||
304,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:pilipala/utils/login.dart';
|
||||
|
||||
class ApiInterceptor extends Interceptor {
|
||||
@override
|
||||
@ -18,6 +19,9 @@ class ApiInterceptor extends Interceptor {
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
try {
|
||||
// 在响应之后处理数据
|
||||
if (response.data is Map && response.data['code'] == -101) {
|
||||
LoginUtils.loginOut();
|
||||
}
|
||||
} catch (err) {
|
||||
print('ApiInterceptor: $err');
|
||||
}
|
||||
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import '../common/constants.dart';
|
||||
import '../models/common/reply_type.dart';
|
||||
import '../models/home/rcmd/result.dart';
|
||||
@ -97,6 +98,8 @@ class VideoHttp {
|
||||
for (var i in res.data['data']['items']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
if (i['card_goto'] != 'ad_av' &&
|
||||
i['card_goto'] != 'ad_web_s' &&
|
||||
i['card_goto'] != 'ad_web' &&
|
||||
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
|
||||
(i['args'] != null &&
|
||||
!blackMidsList.contains(i['args']['up_mid']))) {
|
||||
@ -558,4 +561,50 @@ class VideoHttp {
|
||||
final List body = res.data['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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
lib/models/sponsor_block/action_type.dart
Normal file
26
lib/models/sponsor_block/action_type.dart
Normal 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];
|
||||
}
|
||||
43
lib/models/sponsor_block/segment.dart
Normal file
43
lib/models/sponsor_block/segment.dart
Normal 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/models/sponsor_block/segment_type.dart
Normal file
46
lib/models/sponsor_block/segment_type.dart
Normal 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];
|
||||
}
|
||||
@ -641,6 +641,7 @@ class EpisodeItem {
|
||||
this.page,
|
||||
this.bvid,
|
||||
this.cover,
|
||||
this.pages,
|
||||
});
|
||||
int? seasonId;
|
||||
int? sectionId;
|
||||
@ -655,6 +656,7 @@ class EpisodeItem {
|
||||
int? pubdate;
|
||||
int? duration;
|
||||
Stat? stat;
|
||||
List<Page>? pages;
|
||||
|
||||
EpisodeItem.fromJson(Map<String, dynamic> json) {
|
||||
seasonId = json['season_id'];
|
||||
@ -670,6 +672,7 @@ class EpisodeItem {
|
||||
pubdate = json['arc']['pubdate'];
|
||||
duration = json['arc']['duration'];
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
||||
class Page {
|
||||
Page({
|
||||
this.cid,
|
||||
this.page,
|
||||
});
|
||||
|
||||
int? cid;
|
||||
int? page;
|
||||
|
||||
Page.fromJson(Map<String, dynamic> json) {
|
||||
cid = json['cid'];
|
||||
page = json['page'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ class BangumiIntroController extends GetxController {
|
||||
// 视频bvid
|
||||
String bvid = Get.parameters['bvid']!;
|
||||
var seasonId = Get.parameters['seasonId'] != null
|
||||
? int.parse(Get.parameters['seasonId']!)
|
||||
? int.tryParse(Get.parameters['seasonId']!)
|
||||
: null;
|
||||
var epId = Get.parameters['epId'] != null
|
||||
? int.tryParse(Get.parameters['epId']!)
|
||||
@ -69,6 +69,7 @@ class BangumiIntroController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
print('bangumi: ${Get.parameters.toString()}');
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin = userInfo != null;
|
||||
if (userLogin && seasonId != null) {
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/logic_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class FavItem extends StatelessWidget {
|
||||
@ -96,9 +97,7 @@ class VideoContent extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
Constants.publicFavFolder.contains(favFolderItem.attr)
|
||||
? '公开'
|
||||
: '私密',
|
||||
LogicUtils.isPublic(favFolderItem.attr) ? '公开' : '私密',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/member.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/models/member/archive.dart';
|
||||
import 'package:pilipala/utils/global_data_cache.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class MemberArchiveController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late int mid;
|
||||
int pn = 1;
|
||||
int count = 0;
|
||||
RxInt count = 0.obs;
|
||||
RxMap<String, String> currentOrder = <String, String>{}.obs;
|
||||
RxList<Map<String, String>> orderList = [
|
||||
{'type': 'pubdate', 'label': '最新发布'},
|
||||
@ -50,11 +52,11 @@ class MemberArchiveController extends GetxController {
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
archivesList.value = res['data'].list.vlist;
|
||||
count.value = res['data'].page['count'];
|
||||
}
|
||||
if (type == 'onLoad') {
|
||||
archivesList.addAll(res['data'].list.vlist);
|
||||
}
|
||||
count = res['data'].page['count'];
|
||||
pn += 1;
|
||||
}
|
||||
isLoading.value = false;
|
||||
@ -76,4 +78,29 @@ class MemberArchiveController extends GetxController {
|
||||
Future onLoad() async {
|
||||
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']],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,16 +61,7 @@ class SettingController extends GetxController {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// 清空cookie
|
||||
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);
|
||||
await LoginUtils.loginOut();
|
||||
SmartDialog.dismiss().then((value) => Get.back());
|
||||
},
|
||||
child: const Text('确认'),
|
||||
|
||||
@ -6,11 +6,14 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/http/common.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/common/reply_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/play/quality.dart';
|
||||
import 'package:pilipala/models/video/play/url.dart';
|
||||
@ -119,6 +122,9 @@ class VideoDetailController extends GetxController
|
||||
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
|
||||
RxBool isWatchLaterVisible = false.obs;
|
||||
RxString watchLaterTitle = ''.obs;
|
||||
RxInt watchLaterCount = 0.obs;
|
||||
List<SegmentDataModel> skipSegments = <SegmentDataModel>[];
|
||||
int? lastPosition;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -170,7 +176,7 @@ class VideoDetailController extends GetxController
|
||||
|
||||
sourceType.value = argMap['sourceType'] ?? 'normal';
|
||||
isWatchLaterVisible.value =
|
||||
sourceType.value == 'watchLater' || sourceType.value == 'fav';
|
||||
['watchLater', 'fav', 'up_archive'].contains(sourceType.value);
|
||||
if (sourceType.value == 'watchLater') {
|
||||
watchLaterTitle.value = '稍后再看';
|
||||
fetchMediaList();
|
||||
@ -179,9 +185,19 @@ class VideoDetailController extends GetxController
|
||||
watchLaterTitle.value = argMap['favTitle'];
|
||||
queryFavVideoList();
|
||||
}
|
||||
if (sourceType.value == 'up_archive') {
|
||||
watchLaterTitle.value = argMap['favTitle'];
|
||||
watchLaterCount.value = argMap['count'];
|
||||
queryArchiveVideoList();
|
||||
}
|
||||
tabCtr.addListener(() {
|
||||
onTabChanged();
|
||||
});
|
||||
|
||||
/// 仅投稿视频skip
|
||||
if (videoType == SearchType.video) {
|
||||
querySkipSegments();
|
||||
}
|
||||
}
|
||||
|
||||
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
|
||||
@ -299,6 +315,7 @@ class VideoDetailController extends GetxController
|
||||
plPlayerController.headerControl = headerControl;
|
||||
|
||||
plPlayerController.subtitles.value = subtitles;
|
||||
onPositionChanged();
|
||||
}
|
||||
|
||||
// 视频链接
|
||||
@ -585,7 +602,9 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -616,8 +635,19 @@ class VideoDetailController extends GetxController
|
||||
changeMediaList: changeMediaList,
|
||||
panelTitle: watchLaterTitle.value,
|
||||
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'],
|
||||
type: [
|
||||
'watchLater',
|
||||
'fav',
|
||||
].contains(sourceType.value)
|
||||
? 3
|
||||
: 1,
|
||||
);
|
||||
});
|
||||
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切换
|
||||
void onTabChanged() {
|
||||
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
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
|
||||
@ -63,6 +63,7 @@ class VideoIntroController extends GetxController {
|
||||
PersistentBottomSheetController? bottomSheetController;
|
||||
late bool enableRelatedVideo;
|
||||
UgcSeason? ugcSeason;
|
||||
RxList<Part> pages = <Part>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -84,18 +85,20 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
|
||||
// 获取视频简介&分p
|
||||
Future queryVideoIntro() async {
|
||||
Future queryVideoIntro({cover}) async {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
videoDetail.value = result['data']!;
|
||||
ugcSeason = result['data']!.ugcSeason;
|
||||
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
|
||||
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
|
||||
pages.value = result['data']!.pages!;
|
||||
lastPlayCid.value = videoDetail.value.cid!;
|
||||
if (pages.isNotEmpty) {
|
||||
lastPlayCid.value = pages.first.cid!;
|
||||
}
|
||||
final VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}'];
|
||||
videoDetailCtr.cover.value = result['data'].pic ?? '';
|
||||
videoDetailCtr.cover.value = cover ?? result['data'].pic ?? '';
|
||||
// 获取到粉丝数再返回
|
||||
await queryUserStat();
|
||||
}
|
||||
@ -470,8 +473,7 @@ class VideoIntroController extends GetxController {
|
||||
videoReplyCtr.queryReplyList(type: 'init');
|
||||
} catch (_) {}
|
||||
this.bvid = bvid;
|
||||
lastPlayCid.value = cid;
|
||||
await queryVideoIntro();
|
||||
await queryVideoIntro(cover: cover);
|
||||
}
|
||||
|
||||
void startTimer() {
|
||||
@ -521,9 +523,8 @@ class VideoIntroController extends GetxController {
|
||||
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
episodes.addAll(episodesList);
|
||||
}
|
||||
} else if (videoDetail.value.pages != null) {
|
||||
} else if (pages.isNotEmpty) {
|
||||
isPages = true;
|
||||
final List<Part> pages = videoDetail.value.pages!;
|
||||
episodes.addAll(pages);
|
||||
}
|
||||
|
||||
@ -621,10 +622,9 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (videoDetail.value.pages != null &&
|
||||
videoDetail.value.pages!.length > 1) {
|
||||
if (pages.length > 1) {
|
||||
dataType = VideoEpidoesType.videoPart;
|
||||
episodes = videoDetail.value.pages!;
|
||||
episodes = pages;
|
||||
}
|
||||
|
||||
DrawerUtils.showRightDialog(
|
||||
|
||||
@ -404,27 +404,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
Obx(
|
||||
() => SeasonPanel(
|
||||
ugcSeason: widget.videoDetail!.ugcSeason!,
|
||||
cid: videoIntroController.lastPlayCid.value != 0
|
||||
? videoIntroController.lastPlayCid.value
|
||||
: widget.videoDetail!.pages!.first.cid,
|
||||
cid: videoIntroController.lastPlayCid.value,
|
||||
sheetHeight: videoDetailCtr.sheetHeight.value,
|
||||
changeFuc: (bvid, cid, aid, cover) =>
|
||||
videoIntroController.changeSeasonOrbangu(
|
||||
bvid,
|
||||
cid,
|
||||
aid,
|
||||
cover,
|
||||
),
|
||||
changeFuc: videoIntroController.changeSeasonOrbangu,
|
||||
videoIntroCtr: videoIntroController,
|
||||
),
|
||||
)
|
||||
],
|
||||
// 合集 videoEpisode
|
||||
if (widget.videoDetail!.pages != null &&
|
||||
widget.videoDetail!.pages!.length > 1) ...[
|
||||
if (videoIntroController.pages.length > 1) ...[
|
||||
Obx(
|
||||
() => PagesPanel(
|
||||
pages: widget.videoDetail!.pages!,
|
||||
pages: videoIntroController.pages,
|
||||
cid: videoIntroController.lastPlayCid.value,
|
||||
sheetHeight: videoDetailCtr.sheetHeight.value,
|
||||
changeFuc: (cid, cover) =>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/logic_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FavPanel extends StatefulWidget {
|
||||
@ -67,14 +67,13 @@ class _FavPanelState extends State<FavPanel> {
|
||||
onTap: () =>
|
||||
widget.ctr!.onChoose(item.favState != 1, index),
|
||||
dense: true,
|
||||
leading: Icon(
|
||||
Constants.publicFavFolder.contains(item.attr)
|
||||
leading: Icon(LogicUtils.isPublic(item.attr)
|
||||
? Icons.folder_outlined
|
||||
: Icons.lock_outline),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(item.title!),
|
||||
subtitle: Text(
|
||||
'${item.mediaCount}个内容 - ${Constants.publicFavFolder.contains(item.attr) ? '公开' : '私密'}',
|
||||
'${item.mediaCount}个内容 - ${LogicUtils.isPublic(item.attr) ? '公开' : '私密'}',
|
||||
),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
|
||||
@ -3,7 +3,6 @@ import 'dart:math';
|
||||
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/pages/video/detail/introduction/index.dart';
|
||||
import '../../../../../common/pages_bottom_sheet.dart';
|
||||
import '../../../../../models/common/video_episode_type.dart';
|
||||
@ -32,25 +31,26 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
late int cid;
|
||||
late RxInt currentIndex = (-1).obs;
|
||||
final String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
final ScrollController listViewScrollCtr = ScrollController();
|
||||
late PersistentBottomSheetController? _bottomSheetController;
|
||||
PersistentBottomSheetController? _bottomSheetController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cid = widget.cid;
|
||||
episodes = widget.pages;
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
_videoDetailController.cid.listen((int p0) {
|
||||
updateCurrentIndexAndScroll();
|
||||
widget.videoIntroCtr.lastPlayCid.listen((int p0) {
|
||||
cid = p0;
|
||||
currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
updateCurrentIndexAndScroll();
|
||||
});
|
||||
}
|
||||
|
||||
void updateCurrentIndexAndScroll() {
|
||||
currentIndex.value = widget.pages.indexWhere((Part e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
listViewScrollCtr.dispose();
|
||||
@ -60,7 +60,10 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
void changeFucCall(item, i) async {
|
||||
widget.changeFuc?.call(item.cid, item.cover);
|
||||
currentIndex.value = i;
|
||||
cid = item.cid;
|
||||
if (_bottomSheetController != null) {
|
||||
_bottomSheetController?.close();
|
||||
}
|
||||
scrollToIndex();
|
||||
}
|
||||
|
||||
@ -112,7 +115,7 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
widget.videoIntroCtr.bottomSheetController =
|
||||
_bottomSheetController = EpisodeBottomSheet(
|
||||
currentCid: cid,
|
||||
episodes: episodes,
|
||||
episodes: widget.pages,
|
||||
changeFucCall: changeFucCall,
|
||||
sheetHeight: widget.sheetHeight,
|
||||
dataType: VideoEpidoesType.videoPart,
|
||||
|
||||
@ -33,6 +33,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
final String heroTag = Get.arguments['heroTag'];
|
||||
late VideoDetailController _videoDetailController;
|
||||
late PersistentBottomSheetController? _bottomSheetController;
|
||||
int currentEpisodeIndex = -1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -41,13 +42,12 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
/// 根据 cid 找到对应集,找到对应 episodes
|
||||
/// 有多个episodes时,只显示其中一个
|
||||
/// TODO 同时显示多个合集
|
||||
final List<SectionItem> sections = widget.ugcSeason.sections!;
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
final List<EpisodeItem> episodesList = sections[i].episodes!;
|
||||
for (int j = 0; j < episodesList.length; j++) {
|
||||
if (episodesList[j].cid == cid) {
|
||||
currentEpisodeIndex = i;
|
||||
episodes = episodesList;
|
||||
continue;
|
||||
}
|
||||
@ -55,10 +55,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
}
|
||||
|
||||
/// 取对应 season_id 的 episodes
|
||||
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
|
||||
getCurrentIndex();
|
||||
_videoDetailController.cid.listen((int p0) {
|
||||
cid = p0;
|
||||
currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
|
||||
getCurrentIndex();
|
||||
});
|
||||
}
|
||||
|
||||
@ -73,6 +73,23 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
_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(
|
||||
EpisodeItem episode,
|
||||
int index,
|
||||
@ -125,6 +142,8 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
sheetHeight: widget.sheetHeight,
|
||||
dataType: VideoEpidoesType.videoEpisode,
|
||||
ugcSeason: widget.ugcSeason,
|
||||
currentEpisodeIndex: currentEpisodeIndex,
|
||||
currentIndex: currentIndex.value,
|
||||
).show(context);
|
||||
},
|
||||
child: Padding(
|
||||
|
||||
@ -786,7 +786,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: vdCtr.sourceType.value == 'watchLater' ||
|
||||
vdCtr.sourceType.value == 'fav',
|
||||
vdCtr.sourceType.value == 'fav' ||
|
||||
vdCtr.sourceType.value == 'up_archive',
|
||||
child: AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
@ -818,8 +819,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
child: Row(children: [
|
||||
const Icon(Icons.playlist_play, size: 24),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Expanded(
|
||||
child: Text(
|
||||
vdCtr.watchLaterTitle.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
@ -828,7 +832,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
),
|
||||
const SizedBox(width: 50),
|
||||
const Icon(Icons.keyboard_arrow_up_rounded, size: 26),
|
||||
]),
|
||||
),
|
||||
|
||||
@ -19,8 +19,9 @@ class MediaListPanel extends StatefulWidget {
|
||||
this.changeMediaList,
|
||||
this.panelTitle,
|
||||
this.bvid,
|
||||
this.mediaId,
|
||||
required this.mediaId,
|
||||
this.hasMore = false,
|
||||
required this.type,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -29,8 +30,9 @@ class MediaListPanel extends StatefulWidget {
|
||||
final Function? changeMediaList;
|
||||
final String? panelTitle;
|
||||
final String? bvid;
|
||||
final int? mediaId;
|
||||
final int mediaId;
|
||||
final bool hasMore;
|
||||
final int type;
|
||||
|
||||
@override
|
||||
State<MediaListPanel> createState() => _MediaListPanelState();
|
||||
@ -59,8 +61,8 @@ class _MediaListPanelState extends State<MediaListPanel> {
|
||||
|
||||
void loadMore() async {
|
||||
var res = await UserHttp.getMediaList(
|
||||
type: 3,
|
||||
bizId: widget.mediaId!,
|
||||
type: widget.type,
|
||||
bizId: widget.mediaId,
|
||||
ps: 20,
|
||||
oid: mediaList.last.id,
|
||||
);
|
||||
|
||||
6
lib/utils/logic_utils.dart
Normal file
6
lib/utils/logic_utils.dart
Normal file
@ -0,0 +1,6 @@
|
||||
class LogicUtils {
|
||||
// 收藏夹是否公开
|
||||
static bool isPublic(int attr) {
|
||||
return (attr & 1) == 0;
|
||||
}
|
||||
}
|
||||
@ -7,15 +7,20 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/pages/dynamics/index.dart';
|
||||
import 'package:pilipala/pages/home/index.dart';
|
||||
import 'package:pilipala/pages/mine/index.dart';
|
||||
import 'package:pilipala/utils/cookie.dart';
|
||||
import 'package:pilipala/utils/global_data_cache.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class LoginUtils {
|
||||
static Box userInfoCache = GStrorage.userInfo;
|
||||
static Box localCache = GStrorage.localCache;
|
||||
|
||||
static Future refreshLoginStatus(bool status) async {
|
||||
try {
|
||||
// 更改我的页面登录状态
|
||||
@ -109,4 +114,14 @@ class LoginUtils {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
20
pubspec.lock
20
pubspec.lock
@ -533,18 +533,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
|
||||
sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.9.2+1"
|
||||
version: "0.9.3"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.9.4+1"
|
||||
version: "0.9.4+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -557,10 +557,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
|
||||
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
version: "0.9.3+3"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -842,10 +842,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447"
|
||||
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.8.12"
|
||||
version: "0.8.12+1"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1438,10 +1438,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scrollview_observer
|
||||
sha256: fa408bcfd41e19da841eb53fc471f8f952d5ef818b854d2505c4bb3f0c876381
|
||||
sha256: "8537ba32e5a15ade301e5c77ae858fd8591695defaad1821eca9eeb4ac28a157"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.22.0"
|
||||
version: "1.23.0"
|
||||
sentry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user