Compare commits

...

8 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
a581945c9e feat: interactive video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-07 15:38:33 +08:00
bggRGjQaUbCoE
331fd0d619 mod: intro panel
opt: pgc page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-07 15:24:03 +08:00
bggRGjQaUbCoE
c6e229d571 fix: replay
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-06 12:09:29 +08:00
bggRGjQaUbCoE
b2c3b1ff95 fix: #199
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-06 12:09:08 +08:00
bggRGjQaUbCoE
3fc12fcc09 mod: widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-05 16:55:10 +08:00
bggRGjQaUbCoE
e098631553 mod: dyn square type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-05 13:49:37 +08:00
bggRGjQaUbCoE
0fcd55755e mod: handleWebview
Closes #194

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-05 13:36:44 +08:00
bggRGjQaUbCoE
65e7c0c4f4 opt: pages
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-05 12:19:45 +08:00
22 changed files with 676 additions and 535 deletions

View File

@@ -47,6 +47,9 @@
## feat
- [x] 互动视频
- [x] 发评反诈
- [x] 高能进度条
- [x] 滑动跳转预览视频缩略图
- [x] Live Photo
- [x] 复制/移动收藏夹/稍后再看视频

View File

@@ -12,6 +12,7 @@ class PBadge extends StatelessWidget {
final double? fs;
final String? semanticsLabel;
final bool bold;
final double? textScaleFactor;
const PBadge({
super.key,
@@ -26,6 +27,7 @@ class PBadge extends StatelessWidget {
this.fs = 11,
this.semanticsLabel,
this.bold = true,
this.textScaleFactor,
});
@override
@@ -71,6 +73,9 @@ class PBadge extends StatelessWidget {
),
child: Text(
text ?? "",
textScaler: textScaleFactor != null
? TextScaler.linear(textScaleFactor!)
: null,
style: TextStyle(
height: 1,
fontSize: fs ?? fontSize,

View File

@@ -977,7 +977,6 @@ class VideoHttp {
);
if (res.data['code'] == 0) {
dynamic data = res.data['data'];
List subtitlesJson = data['subtitle']['subtitles'];
/*
[
{
@@ -995,10 +994,11 @@ class VideoHttp {
*/
return {
'status': true,
'data': subtitlesJson,
'data': data['subtitle']['subtitles'],
'view_points': data['view_points'],
// 'last_play_time': data['last_play_time'],
'last_play_cid': data['last_play_cid'],
'interaction': data['interaction'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};

View File

@@ -265,7 +265,7 @@ class _BangumiInfoState extends State<BangumiInfo>
Expanded(
child: GestureDetector(
onTap: showIntroDetail,
behavior: HitTestBehavior.translucent,
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: isLandscape ? 115 : 115 / 0.75,
child: Column(

View File

@@ -82,48 +82,48 @@ class _BangumiPageState extends State<BangumiPage>
slivers: [
SliverToBoxAdapter(
child: Obx(
() => Visibility(
visible: _bangumiController.isLogin.value,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
'最近${widget.tabType == TabType.bangumi ? '追番' : '追剧'}${_bangumiController.followCount.value == -1 ? '' : ' ${_bangumiController.followCount.value}'}',
style: Theme.of(context).textTheme.titleMedium,
),
() => _bangumiController.isLogin.value
? Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
'最近${widget.tabType == TabType.bangumi ? '追番' : '追剧'}${_bangumiController.followCount.value == -1 ? '' : ' ${_bangumiController.followCount.value}'}',
style:
Theme.of(context).textTheme.titleMedium,
),
),
IconButton(
tooltip: '刷新',
onPressed: () {
_bangumiController
..followPage = 1
..followEnd = false
..queryBangumiFollow();
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
],
),
IconButton(
tooltip: '刷新',
onPressed: () {
_bangumiController
..followPage = 1
..followEnd = false
..queryBangumiFollow();
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
SizedBox(
height: Grid.smallCardWidth / 2 / 0.75 +
MediaQuery.textScalerOf(context).scale(50),
child: Obx(
() => _buildFollowBody(
_bangumiController.followState.value),
),
],
),
),
SizedBox(
height: Grid.smallCardWidth / 2 / 0.75 +
MediaQuery.textScalerOf(context).scale(50),
child: Obx(
() => _buildFollowBody(
_bangumiController.followState.value),
),
),
],
),
),
),
],
)
: const SizedBox.shrink(),
),
),
SliverToBoxAdapter(

View File

@@ -305,11 +305,16 @@ Widget forWard(item, context, source, callback, {floor = 1}) {
case 'DYNAMIC_TYPE_COMMON_SQUARE':
return InkWell(
onTap: () {
Get.toNamed('/webview', parameters: {
'url': item.modules.moduleDynamic.major.common['jump_url'],
'type': 'url',
'pageTitle': item.modules.moduleDynamic.major.common['title']
});
try {
String url = item.modules.moduleDynamic.major.common['jump_url'];
if (url.contains('bangumi/play') && Utils.viewPgcFromUri(url)) {
return;
}
Get.toNamed(
'/webview',
parameters: {'url': url},
);
} catch (_) {}
},
child: Container(
width: double.infinity,

View File

@@ -55,7 +55,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
_getInfo() async {
Map<String, String> data = {
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'],
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'',
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',
@@ -329,7 +330,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
}) async {
Map<String, String> data = {
'access_key': GStorage.localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'],
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'',
'appkey': Constants.appKey,
'build': '1462100',
'c_locale': 'zh_CN',

View File

@@ -79,11 +79,11 @@ class UserInfoCard extends StatelessWidget {
: images.nightImgurl?.http2https)
: images.imgUrl?.http2https;
return Hero(
tag: imgUrl ?? 'bgTag',
tag: imgUrl ?? '',
child: GestureDetector(
onTap: () {
context.imageView(
imgList: [SourceModel(url: imgUrl ?? 'bgTag')],
imgList: [SourceModel(url: imgUrl ?? '')],
);
},
child: CachedNetworkImage(
@@ -447,22 +447,22 @@ class UserInfoCard extends StatelessWidget {
),
);
_buildAvatar(BuildContext context) => Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.5,
color: Theme.of(context).colorScheme.surface,
),
shape: BoxShape.circle,
),
child: Hero(
tag: card.face ?? 'avatarTag',
child: GestureDetector(
onTap: () {
context.imageView(
imgList: [SourceModel(url: card.face ?? 'avatarTag')],
);
},
_buildAvatar(BuildContext context) => Hero(
tag: card.face ?? '',
child: GestureDetector(
onTap: () {
context.imageView(
imgList: [SourceModel(url: card.face ?? '')],
);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.5,
color: Theme.of(context).colorScheme.surface,
),
shape: BoxShape.circle,
),
child: NetworkImgLayer(
src: card.face,
type: 'avatar',

View File

@@ -127,7 +127,7 @@ class _MinePageState extends State<MinePage> {
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
behavior: HitTestBehavior.opaque,
onTap: _mineController.onLogin,
child: Row(
mainAxisSize: MainAxisSize.min,

View File

@@ -403,24 +403,18 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height:
MediaQuery.textScalerOf(context).scale(13),
width: MediaQuery.textScalerOf(context).scale(13),
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
),
style: TextStyle(fontSize: 13),
style: TextStyle(fontSize: 13, height: 1),
),
TextSpan(
text: ' ${_blockSettings[index].first.title}',
style: TextStyle(fontSize: 13),
style: TextStyle(fontSize: 13, height: 1),
),
],
),
@@ -451,23 +445,18 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: MediaQuery.textScalerOf(context).scale(15),
height: 10,
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
),
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
TextSpan(
text: ' ${_blockSettings[index].first.title}',
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
],
),

View File

@@ -425,24 +425,18 @@ class VideoDetailController extends GetxController
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height:
MediaQuery.textScalerOf(context).scale(14),
height: 10,
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item),
),
),
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
TextSpan(
text: ' ${item.title}',
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
],
),
@@ -528,24 +522,18 @@ class VideoDetailController extends GetxController
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height:
MediaQuery.textScalerOf(context).scale(14),
height: 10,
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item.segmentType),
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item.segmentType),
),
),
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
TextSpan(
text: ' ${item.segmentType.title}',
style: TextStyle(fontSize: 14),
style: TextStyle(fontSize: 14, height: 1),
),
],
),
@@ -1858,6 +1846,31 @@ class VideoDetailController extends GetxController
});
}
// interactive video
dynamic graphVersion;
Map? steinEdgeInfo;
late final RxBool showSteinEdgeInfo = false.obs;
void getSteinEdgeInfo([edgeId]) async {
steinEdgeInfo = null;
try {
dynamic res = await Request().get(
'https://api.bilibili.com/x/stein/edgeinfo_v2',
queryParameters: {
'bvid': bvid,
'graph_version': graphVersion,
if (edgeId != null) 'edge_id': edgeId,
},
);
if (res.data['code'] == 0) {
steinEdgeInfo = res.data['data'];
} else {
debugPrint('getSteinEdgeInfo error: ${res.data['message']}');
}
} catch (e) {
debugPrint('getSteinEdgeInfo: $e');
}
}
late bool continuePlayingPart = GStorage.continuePlayingPart;
Future _querySubtitles() async {
@@ -1866,6 +1879,19 @@ class VideoDetailController extends GetxController
// SmartDialog.showToast('查询字幕错误,${res["msg"]}');
// }
if (res['status']) {
// interactive video
if (graphVersion == null) {
try {
final introCtr = Get.find<VideoIntroController>(tag: heroTag);
if (introCtr.videoDetail.value.rights?['is_stein_gate'] == 1) {
graphVersion = res['interaction']?['graph_version'];
getSteinEdgeInfo();
}
} catch (e) {
debugPrint('handle stein: $e');
}
}
if (continuePlayingPart) {
continuePlayingPart = false;
try {
@@ -1957,7 +1983,7 @@ class VideoDetailController extends GetxController
super.onClose();
}
onReset() {
onReset([isStein]) {
playedTime = null;
videoUrl = null;
audioUrl = null;
@@ -1974,10 +2000,19 @@ class VideoDetailController extends GetxController
viewPointList.clear();
// sponsor block
positionSubscription?.cancel();
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
if (enableSponsorBlock) {
positionSubscription?.cancel();
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
}
// interactive video
if (isStein != true) {
graphVersion = null;
}
steinEdgeInfo = null;
showSteinEdgeInfo.value = false;
}
late final showDmChart = GStorage.showDmChart;

View File

@@ -578,13 +578,13 @@ class VideoIntroController extends GetxController
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(epid, bvid, cid, aid, cover) async {
Future changeSeasonOrbangu(epid, bvid, cid, aid, cover, [isStein]) async {
// 重新获取视频资源
final videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag)
..plPlayerController.pause()
..makeHeartBeat()
..updateMediaListHistory(aid)
..onReset()
..onReset(isStein)
..bvid = bvid
..oid.value = aid ?? IdUtils.bv2av(bvid)
..cid.value = cid

View File

@@ -308,223 +308,192 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
constraints.viewportMainAxisExtent * 1.25;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
top: 10),
left: StyleString.safeSpace,
right: StyleString.safeSpace,
top: 10,
),
sliver: SliverToBoxAdapter(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: showIntroDetail,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: videoItem['staff'] == null
? GestureDetector(
onTap: onPushMember,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
NetworkImgLayer(
type: 'avatar',
src: widget.loadingStatus
? videoItem['owner']?.face ?? ""
: videoDetail.owner!.face,
width: 35,
height: 35,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
widget.loadingStatus
? videoItem['owner']?.name ?? ""
: videoDetail.owner!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
// color: t.colorScheme.primary,
),
// semanticsLabel: "UP主${owner.name}",
),
const SizedBox(height: 0),
Obx(
() => Text(
'${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
],
),
),
followButton(context, t),
],
),
)
: SelfSizedHorizontalList(
gapSize: 25,
itemCount: videoItem['staff'].length,
childBuilder: (index) => GestureDetector(
onTap: () {
int? ownerMid = !widget.loadingStatus
? videoDetail.owner?.mid
: videoItem['owner']?.mid;
if (videoItem['staff'][index].mid == ownerMid &&
context.orientation ==
Orientation.landscape &&
_horizontalMemberPage) {
widget.onShowMemberPage(ownerMid);
} else {
Get.toNamed(
'/member?mid=${videoItem['staff'][index].mid}',
// arguments: {
// 'face':
// videoItem['staff'][index].face,
// 'heroTag': Utils.makeHeroTag(
// videoItem['staff'][index].mid),
// },
);
}
},
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: videoItem['staff'][index].face,
width: 35,
height: 35,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 5),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
videoItem['staff'][index].name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: videoItem['staff'][index]
.vip
.status >
0 &&
videoItem['staff'][index]
.vip
.type ==
2
? context.vipColor
: null,
),
),
Text(
videoItem['staff'][index].title,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
],
),
],
),
),
),
),
if (isHorizontal) ...[
const SizedBox(width: 10),
Expanded(child: actionGrid(context, videoIntroController)),
]
],
),
if (videoIntroController.videoDetail.value.argueMsg?.isNotEmpty ==
true &&
videoIntroController.showArgueMsg) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.secondaryContainer,
),
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Text.rich(
TextSpan(children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
size: 17,
Icons.warning_rounded,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
TextSpan(
text:
' ${videoIntroController.videoDetail.value.argueMsg}')
]),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
)
],
const SizedBox(height: 8),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: showIntroDetail,
child: ExpandablePanel(
controller: videoIntroController.expandableCtr,
collapsed: GestureDetector(
onLongPress: () {
feedBack();
Utils.copyText(
'${videoDetail.title ?? videoItem['title'] ?? ''}');
},
child: _buildVideoTitle(),
),
expanded: GestureDetector(
onLongPress: () {
feedBack();
Utils.copyText(
'${videoDetail.title ?? videoItem['title'] ?? ''}');
},
child: _buildVideoTitle(true),
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
),
Stack(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: showIntroDetail,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
behavior: HitTestBehavior.opaque,
onTap: () {},
child: Row(
children: [
Expanded(
child: videoItem['staff'] == null
? GestureDetector(
onTap: onPushMember,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
NetworkImgLayer(
type: 'avatar',
src: widget.loadingStatus
? videoItem['owner']?.face ?? ""
: videoDetail.owner!.face,
width: 35,
height: 35,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
widget.loadingStatus
? videoItem['owner']?.name ??
""
: videoDetail.owner!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
// color: t.colorScheme.primary,
),
// semanticsLabel: "UP主${owner.name}",
),
const SizedBox(height: 0),
Obx(
() => Text(
'${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
],
),
),
followButton(context, t),
],
),
)
: SelfSizedHorizontalList(
gapSize: 25,
itemCount: videoItem['staff'].length,
childBuilder: (index) => GestureDetector(
onTap: () {
int? ownerMid = !widget.loadingStatus
? videoDetail.owner?.mid
: videoItem['owner']?.mid;
if (videoItem['staff'][index].mid ==
ownerMid &&
context.orientation ==
Orientation.landscape &&
_horizontalMemberPage) {
widget.onShowMemberPage(ownerMid);
} else {
Get.toNamed(
'/member?mid=${videoItem['staff'][index].mid}',
// arguments: {
// 'face':
// videoItem['staff'][index].face,
// 'heroTag': Utils.makeHeroTag(
// videoItem['staff'][index].mid),
// },
);
}
},
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: videoItem['staff'][index].face,
width: 35,
height: 35,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 5),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
videoItem['staff'][index].name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: videoItem['staff'][index]
.vip
.status >
0 &&
videoItem['staff']
[index]
.vip
.type ==
2
? context.vipColor
: null,
),
),
Text(
videoItem['staff'][index].title,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
],
),
],
),
),
),
),
if (isHorizontal) ...[
const SizedBox(width: 10),
Expanded(
child: actionGrid(context, videoIntroController)),
]
],
),
),
const SizedBox(height: 8),
ExpandablePanel(
controller: videoIntroController.expandableCtr,
collapsed: GestureDetector(
onLongPress: () {
feedBack();
Utils.copyText(
'${videoDetail.title ?? videoItem['title'] ?? ''}');
},
child: _buildVideoTitle(),
),
expanded: GestureDetector(
onLongPress: () {
feedBack();
Utils.copyText(
'${videoDetail.title ?? videoItem['title'] ?? ''}');
},
child: _buildVideoTitle(true),
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
const SizedBox(height: 8),
Stack(
children: [
Row(
children: <Widget>[
statView(
context: context,
@@ -577,169 +546,195 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
),
],
),
),
),
if (videoIntroController.enableAi)
Positioned(
right: 10,
top: 0,
bottom: 0,
child: Center(
child: Semantics(
label: 'AI总结',
child: GestureDetector(
onTap: () async {
final res =
await videoIntroController.aiConclusion();
if (res['status']) {
widget.showAiBottomSheet();
}
},
child:
Image.asset('assets/images/ai.png', height: 22),
if (videoIntroController.enableAi)
Positioned(
right: 10,
top: 0,
bottom: 0,
child: Center(
child: Semantics(
label: 'AI总结',
child: GestureDetector(
onTap: () async {
final res =
await videoIntroController.aiConclusion();
if (res['status']) {
widget.showAiBottomSheet();
}
},
child: Image.asset('assets/images/ai.png',
height: 22),
),
),
),
),
),
)
],
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: showIntroDetail,
child: ExpandablePanel(
controller: videoIntroController.expandableCtr,
collapsed: const SizedBox.shrink(),
expanded: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
GestureDetector(
onTap: () {
Utils.copyText(
'${videoIntroController.videoDetail.value.bvid}');
},
child: Text(
videoIntroController.videoDetail.value.bvid ?? '',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
),
),
),
if (videoIntroController
.videoDetail.value.descV2.isNullOrEmpty.not) ...[
const SizedBox(height: 8),
SelectableText.rich(
style: const TextStyle(
height: 1.4,
// fontSize: 13,
),
TextSpan(
children: [
buildContent(context,
videoIntroController.videoDetail.value),
],
),
),
],
if (videoIntroController.videoTags is List &&
videoIntroController.videoTags.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: (videoIntroController.videoTags as List)
.map(
(item) => SearchText(
fontSize: 13,
text: item['tag_name'],
onTap: (_) => Get.toNamed('/searchResult',
parameters: {
'keyword': item['tag_name']
}),
onLongPress: (_) =>
Utils.copyText(item['tag_name']),
),
)
.toList(),
),
],
)
],
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
),
Obx(
() => videoIntroController.queryVideoIntroData.value["status"]
? const SizedBox.shrink()
: Center(
child: TextButton.icon(
icon: const Icon(Icons.refresh),
onPressed: () {
videoIntroController
.queryVideoIntroData.value["status"] = true;
videoIntroController.queryVideoIntro();
if (videoDetailCtr.videoUrl.isNullOrEmpty &&
videoDetailCtr.isQuerying.not) {
videoDetailCtr.queryVideoUrl();
}
},
label: const Text("点此重新加载"),
),
if (videoIntroController
.videoDetail.value.argueMsg?.isNotEmpty ==
true &&
videoIntroController.showArgueMsg) ...[
const SizedBox(height: 2),
Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
size: 13,
Icons.error_outline,
color: Theme.of(context).colorScheme.outline,
),
),
WidgetSpan(child: SizedBox(width: 2)),
TextSpan(
text:
'${videoIntroController.videoDetail.value.argueMsg}',
)
],
),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
ExpandablePanel(
controller: videoIntroController.expandableCtr,
collapsed: const SizedBox.shrink(),
expanded: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
GestureDetector(
onTap: () {
Utils.copyText(
'${videoIntroController.videoDetail.value.bvid}');
},
child: Text(
videoIntroController.videoDetail.value.bvid ?? '',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
),
),
),
if (videoIntroController
.videoDetail.value.descV2.isNullOrEmpty.not) ...[
const SizedBox(height: 8),
SelectableText.rich(
style: const TextStyle(
height: 1.4,
// fontSize: 13,
),
TextSpan(
children: [
buildContent(context,
videoIntroController.videoDetail.value),
],
),
),
],
if (videoIntroController.videoTags is List &&
videoIntroController.videoTags.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: (videoIntroController.videoTags as List)
.map(
(item) => SearchText(
fontSize: 13,
text: item['tag_name'],
onTap: (_) => Get.toNamed('/searchResult',
parameters: {
'keyword': item['tag_name']
}),
onLongPress: (_) =>
Utils.copyText(item['tag_name']),
),
)
.toList(),
),
],
],
),
theme: const ExpandableThemeData(
animationDuration: Duration(milliseconds: 300),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
),
),
Obx(
() =>
videoIntroController.queryVideoIntroData.value["status"]
? const SizedBox.shrink()
: Center(
child: TextButton.icon(
icon: const Icon(Icons.refresh),
onPressed: () {
videoIntroController.queryVideoIntroData
.value["status"] = true;
videoIntroController.queryVideoIntro();
if (videoDetailCtr.videoUrl.isNullOrEmpty &&
videoDetailCtr.isQuerying.not) {
videoDetailCtr.queryVideoUrl();
}
},
label: const Text("点此重新加载"),
),
),
),
// 点赞收藏转发 布局样式1
// SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: actionRow(
// context,
// videoIntroController,
// videoDetailCtr,
// ),
// ),
// 点赞收藏转发 布局样式2
if (!isHorizontal) ...[
const SizedBox(height: 8),
actionGrid(context, videoIntroController),
],
// 合集
if (!widget.loadingStatus &&
videoDetail.ugcSeason != null &&
(context.orientation != Orientation.landscape ||
(context.orientation == Orientation.landscape &&
videoDetailCtr.horizontalSeasonPanel.not)))
SeasonPanel(
heroTag: widget.heroTag,
ugcSeason: videoDetail.ugcSeason!,
changeFuc: videoIntroController.changeSeasonOrbangu,
showEpisodes: widget.showEpisodes,
pages: videoDetail.pages,
videoIntroController: videoIntroController,
),
if (!widget.loadingStatus &&
videoDetail.pages != null &&
videoDetail.pages!.length > 1 &&
(context.orientation != Orientation.landscape ||
(context.orientation == Orientation.landscape &&
videoDetailCtr.horizontalSeasonPanel.not))) ...[
PagesPanel(
heroTag: widget.heroTag,
videoIntroController: videoIntroController,
bvid: videoIntroController.bvid,
changeFuc: videoIntroController.changeSeasonOrbangu,
showEpisodes: widget.showEpisodes,
),
],
],
),
// 点赞收藏转发 布局样式1
// SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: actionRow(
// context,
// videoIntroController,
// videoDetailCtr,
// ),
// ),
// 点赞收藏转发 布局样式2
if (!isHorizontal) ...[
const SizedBox(height: 8),
actionGrid(context, videoIntroController),
],
// 合集
if (!widget.loadingStatus &&
videoDetail.ugcSeason != null &&
(context.orientation != Orientation.landscape ||
(context.orientation == Orientation.landscape &&
videoDetailCtr.horizontalSeasonPanel.not)))
SeasonPanel(
heroTag: widget.heroTag,
ugcSeason: videoDetail.ugcSeason!,
changeFuc: videoIntroController.changeSeasonOrbangu,
showEpisodes: widget.showEpisodes,
pages: videoDetail.pages,
videoIntroController: videoIntroController,
),
if (!widget.loadingStatus &&
videoDetail.pages != null &&
videoDetail.pages!.length > 1 &&
(context.orientation != Orientation.landscape ||
(context.orientation == Orientation.landscape &&
videoDetailCtr.horizontalSeasonPanel.not))) ...[
PagesPanel(
heroTag: widget.heroTag,
videoIntroController: videoIntroController,
bvid: videoIntroController.bvid,
changeFuc: videoIntroController.changeSeasonOrbangu,
showEpisodes: widget.showEpisodes,
),
],
],
)),
),
),
);
},
);

View File

@@ -360,6 +360,7 @@ class ReplyItem extends StatelessWidget {
type: 'line',
fs: 9,
semanticsLabel: '置顶',
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),
@@ -550,12 +551,13 @@ class ReplyItem extends StatelessWidget {
if (replies![i].isUp!) ...[
const TextSpan(text: ' '),
const WidgetSpan(
alignment: PlaceholderAlignment.top,
alignment: PlaceholderAlignment.middle,
child: PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),

View File

@@ -375,6 +375,7 @@ class ReplyItemGrpc extends StatelessWidget {
type: 'line',
fs: 9,
semanticsLabel: '置顶',
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),
@@ -584,12 +585,13 @@ class ReplyItemGrpc extends StatelessWidget {
if (replyItem.replies[i].mid == upMid) ...[
const TextSpan(text: ' '),
const WidgetSpan(
alignment: PlaceholderAlignment.top,
alignment: PlaceholderAlignment.middle,
child: PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),

View File

@@ -240,6 +240,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 播放器状态监听
void playerListener(PlayerStatus? status) async {
if (status == PlayerStatus.completed) {
try {
if ((videoDetailController.steinEdgeInfo?['edges']['questions'][0]
['choices'] as List?)
?.isNotEmpty ==
true) {
videoDetailController.showSteinEdgeInfo.value = true;
return;
}
} catch (_) {}
shutdownTimerService.handleWaitingFinished();
bool notExitFlag = false;
@@ -417,6 +426,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return;
}
if (videoDetailController.plPlayerController.playerStatus.status.value ==
PlayerStatus.playing &&
videoDetailController.playerStatus != PlayerStatus.playing) {
videoDetailController.plPlayerController.pause();
}
isShowing = true;
PlPlayerController.setPlayCallBack(playCallBack);
videoIntroController.startTimer();
@@ -1415,6 +1430,69 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// child: Text('index'),
// ),
// ),
Obx(
() {
if (videoDetailController.showSteinEdgeInfo.value) {
try {
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: plPlayerController?.showControls.value == true
? 75
: 16,
),
child: Wrap(
spacing: 25,
runSpacing: 10,
children: (videoDetailController.steinEdgeInfo!['edges']
['questions'][0]['choices'] as List)
.map((item) {
return FilledButton.tonal(
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
backgroundColor: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.8),
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
visualDensity:
VisualDensity(horizontal: -2, vertical: -2),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
videoIntroController.changeSeasonOrbangu(
null,
videoDetailController.bvid,
item['cid'],
IdUtils.bv2av(videoDetailController.bvid),
null,
true,
);
videoDetailController
.getSteinEdgeInfo(item['id']);
},
child: Text(item['option']),
);
}).toList(),
),
),
);
} catch (e) {
debugPrint('build stein edges: $e');
return const SizedBox.shrink();
}
}
return const SizedBox.shrink();
},
),
],
);

View File

@@ -397,33 +397,7 @@ class _SendDanmakuPanelState extends CommonPublishPageState<SendDanmakuPanel> {
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
title: Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Color Picker ',
style: TextStyle(fontSize: 15),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: MediaQuery.textScalerOf(context).scale(13),
width: MediaQuery.textScalerOf(context).scale(13),
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _color.value,
),
),
),
style: TextStyle(fontSize: 13),
),
],
),
),
title: Text('Color Picker'),
content: SlideColorPicker(
showResetBtn: false,
color: _color.value,

View File

@@ -285,7 +285,7 @@ class PlPlayerController {
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
final GlobalKey<VideoState> key = GlobalKey<VideoState>();
GlobalKey<VideoState> key = GlobalKey<VideoState>();
TextStyle get subTitleStyle => TextStyle(
height: 1.5,
@@ -1496,7 +1496,9 @@ class PlPlayerController {
if (type == 'single' && playerCount.value > 1) {
_playerCount.value -= 1;
_heartDuration = 0;
pause();
if (!Get.previousRoute.startsWith('/video')) {
pause();
}
return;
}
_playerCount.value = 0;

View File

@@ -125,8 +125,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}
// 双击播放、暂停
void onDoubleTapCenter() {
plPlayerController.videoPlayerController!.playOrPause();
void onDoubleTapCenter() async {
if (plPlayerController.videoPlayerController!.state.completed) {
await plPlayerController.videoPlayerController!.seek(Duration.zero);
plPlayerController.videoPlayerController!.play();
} else {
plPlayerController.videoPlayerController!.playOrPause();
}
}
void doubleTapFuc(String type) {
@@ -154,6 +159,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
@override
void initState() {
super.initState();
plPlayerController.key = GlobalKey<VideoState>();
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 100));
videoController = plPlayerController.videoController!;
@@ -282,7 +288,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 播放暂停
BottomControlType.playOrPause: PlayOrPauseButton(
controller: plPlayerController,
plPlayerController: plPlayerController,
),
/// 下一集

View File

@@ -7,13 +7,13 @@ import 'package:PiliPlus/plugin/pl_player/index.dart';
class PlayOrPauseButton extends StatefulWidget {
final double? iconSize;
final Color? iconColor;
final PlPlayerController? controller;
final PlPlayerController plPlayerController;
const PlayOrPauseButton({
super.key,
this.iconSize,
this.iconColor,
this.controller,
required this.plPlayerController,
});
@override
@@ -28,10 +28,12 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
late Player player;
bool isOpacity = false;
PlPlayerController get plPlayerController => widget.plPlayerController;
@override
void initState() {
super.initState();
player = widget.controller!.videoPlayerController!;
player = plPlayerController.videoPlayerController!;
animation = AnimationController(
vsync: this,
value: player.state.playing ? 1 : 0,
@@ -67,13 +69,20 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
width: 42,
height: 34,
child: InkWell(
onTap: player.playOrPause,
onTap: () async {
if (player.state.completed) {
await player.seek(Duration.zero);
player.play();
} else {
player.playOrPause();
}
},
// iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,
// color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
child: Center(
child: AnimatedIcon(
semanticLabel:
widget.controller!.videoPlayerController!.state.playing
plPlayerController.videoPlayerController!.state.playing
? '暂停'
: '播放',
progress: animation,

View File

@@ -6,8 +6,10 @@ import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/dynamics_type.dart';
import 'package:PiliPlus/models/common/tab_type.dart' hide tabsConfig;
import 'package:PiliPlus/models/user/info.dart';
import 'package:PiliPlus/models/user/stat.dart';
import 'package:PiliPlus/pages/bangumi/controller.dart';
import 'package:PiliPlus/pages/dynamics/tab/controller.dart';
import 'package:PiliPlus/pages/live/controller.dart';
import 'package:PiliPlus/pages/main/controller.dart';
@@ -113,6 +115,18 @@ class LoginUtils {
..isLogin.value = true
..fetchLiveFollowing();
} catch (_) {}
try {
Get.find<BangumiController>(tag: TabType.bangumi.name)
..isLogin.value = true
..queryBangumiFollow();
} catch (_) {}
try {
Get.find<BangumiController>(tag: TabType.cinema.name)
..isLogin.value = true
..queryBangumiFollow();
} catch (_) {}
} else {
// 获取用户信息失败
SmartDialog.showNotify(
@@ -170,6 +184,18 @@ class LoginUtils {
Get.find<DynamicsTabController>(tag: tabsConfig[i]['tag']).onRefresh();
} catch (_) {}
}
try {
Get.find<BangumiController>(tag: TabType.bangumi.name)
..isLogin.value = false
..followState.value = LoadingState.loading();
} catch (_) {}
try {
Get.find<BangumiController>(tag: TabType.cinema.name)
..isLogin.value = false
..followState.value = LoadingState.loading();
} catch (_) {}
}
static String buvid() {

View File

@@ -373,7 +373,15 @@ class Utils {
bool inApp = false,
}) {
if (inApp.not && GStorage.openInBrowser) {
launchURL(url);
if (RegExp(r'^(https?://)?((www|m).)?(bilibili|bv23).com')
.hasMatch(url)) {
toDupNamed(
'/webview',
parameters: {'url': url},
);
} else {
launchURL(url);
}
} else {
if (off) {
Get.offNamed(