mod: show fullscreen action item

Closes #367

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-03-05 21:49:57 +08:00
parent dc1451c3af
commit fac3c19d3f
8 changed files with 559 additions and 394 deletions

View File

@@ -2126,6 +2126,13 @@ List<SettingsModel> get extraSettings => [
GStorage.slideDismissReplyPage = value; GStorage.slideDismissReplyPage = value;
}, },
), ),
SettingsModel(
settingsType: SettingsType.sw1tch,
title: '全屏展示点赞/投币/收藏等操作按钮',
leading: Icon(MdiIcons.dotsHorizontalCircleOutline),
setKey: SettingBoxKey.showFSActionItem,
defaultVal: true,
),
SettingsModel( SettingsModel(
settingsType: SettingsType.sw1tch, settingsType: SettingsType.sw1tch,
enableFeedback: true, enableFeedback: true,

View File

@@ -861,4 +861,24 @@ class VideoIntroController extends GetxController
} }
return res; return res;
} }
// 收藏
showFavBottomSheet(BuildContext context, {type = 'tap'}) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
// 快速收藏 &
// 点按 收藏至默认文件夹
// 长按选择文件夹
if (enableQuickFav) {
if (type == 'tap') {
actionFavVideo(type: 'default');
} else {
Utils.showFavBottomSheet(context: context, ctr: this);
}
} else if (type != 'longPress') {
Utils.showFavBottomSheet(context: context, ctr: this);
}
}
} }

View File

@@ -232,26 +232,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
} }
} }
// 收藏
showFavBottomSheet({type = 'tap'}) {
if (videoIntroController.userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
// 快速收藏 &
// 点按 收藏至默认文件夹
// 长按选择文件夹
if (videoIntroController.enableQuickFav) {
if (type == 'tap') {
videoIntroController.actionFavVideo(type: 'default');
} else {
Utils.showFavBottomSheet(context: context, ctr: videoIntroController);
}
} else if (type != 'longPress') {
Utils.showFavBottomSheet(context: context, ctr: videoIntroController);
}
}
// 视频介绍 // 视频介绍
showIntroDetail() { showIntroDetail() {
if (widget.loadingStatus) { if (widget.loadingStatus) {
@@ -857,7 +837,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
} }
Widget actionGrid(BuildContext context, videoIntroController) { Widget actionGrid(
BuildContext context, VideoIntroController videoIntroController) {
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return Container( return Container(
@@ -903,7 +884,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
selectStatus: videoIntroController.hasDislike.value, selectStatus: videoIntroController.hasDislike.value,
loadingStatus: widget.loadingStatus, loadingStatus: widget.loadingStatus,
semanticsLabel: '点踩', semanticsLabel: '点踩',
text: "点踩"), text: "点踩",
),
), ),
// ActionItem( // ActionItem(
// icon: const Icon(FontAwesomeIcons.clock), // icon: const Icon(FontAwesomeIcons.clock),
@@ -931,8 +913,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
key: _favKey, key: _favKey,
icon: const Icon(FontAwesomeIcons.star), icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar), selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(), onTap: () => videoIntroController.showFavBottomSheet(context),
onLongPress: () => showFavBottomSheet(type: 'longPress'), onLongPress: () => videoIntroController
.showFavBottomSheet(context, type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus, loadingStatus: widget.loadingStatus,
semanticsLabel: '收藏', semanticsLabel: '收藏',
@@ -951,7 +934,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
semanticsLabel: '评论', semanticsLabel: '评论',
text: !widget.loadingStatus text: !widget.loadingStatus
? Utils.numFormat(videoDetail.stat!.reply!) ? Utils.numFormat(videoDetail.stat!.reply!)
: '评论'), : '评论',
),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),
@@ -960,14 +944,19 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
semanticsLabel: '分享', semanticsLabel: '分享',
text: !widget.loadingStatus text: !widget.loadingStatus
? Utils.numFormat(videoDetail.stat!.share!) ? Utils.numFormat(videoDetail.stat!.share!)
: '分享'), : '分享',
),
], ],
), ),
); );
}); });
} }
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { Widget actionRow(
BuildContext context,
VideoIntroController videoIntroController,
VideoDetailController videoDetailCtr,
) {
return Row(children: <Widget>[ return Row(children: <Widget>[
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
@@ -994,8 +983,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart), icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(), onTap: () => videoIntroController.showFavBottomSheet(context),
onLongPress: () => showFavBottomSheet(type: 'longPress'), onLongPress: () => videoIntroController.showFavBottomSheet(context,
type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus, loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus text: !widget.loadingStatus

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
class ActionItem extends StatefulWidget { class ActionItem extends StatefulWidget {
final Icon? icon; final Icon icon;
final Icon? selectIcon; final Icon? selectIcon;
final Function? onTap; final Function? onTap;
final Function? onLongPress; final Function? onLongPress;
@@ -16,10 +16,11 @@ class ActionItem extends StatefulWidget {
final bool needAnim; final bool needAnim;
final bool hasOneThree; final bool hasOneThree;
final Function? callBack; final Function? callBack;
final bool? expand;
const ActionItem({ const ActionItem({
super.key, super.key,
this.icon, required this.icon,
this.selectIcon, this.selectIcon,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
@@ -30,6 +31,7 @@ class ActionItem extends StatefulWidget {
this.hasOneThree = false, this.hasOneThree = false,
this.callBack, this.callBack,
required this.semanticsLabel, required this.semanticsLabel,
this.expand,
}); });
@override @override
@@ -113,8 +115,10 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return widget.expand == false ? _buildItem : Expanded(child: _buildItem);
child: Semantics( }
Widget get _buildItem => Semantics(
label: (widget.text ?? "") + label: (widget.text ?? "") +
(widget.selectStatus ? "" : "") + (widget.selectStatus ? "" : "") +
widget.semanticsLabel, widget.semanticsLabel,
@@ -155,14 +159,16 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
Icon( Icon(
widget.selectStatus widget.selectStatus
? widget.selectIcon!.icon! ? widget.selectIcon!.icon!
: widget.icon!.icon!, : widget.icon.icon,
size: 18, size: 18,
color: widget.selectStatus color: widget.selectStatus
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : widget.icon.color ??
Theme.of(context).colorScheme.outline,
), ),
], ],
), ),
if (widget.text != null)
AnimatedOpacity( AnimatedOpacity(
opacity: widget.loadingStatus! ? 0 : 1, opacity: widget.loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@@ -173,7 +179,7 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
return ScaleTransition(scale: animation, child: child); return ScaleTransition(scale: animation, child: child);
}, },
child: Text( child: Text(
widget.text ?? '', widget.text!,
key: ValueKey<String>(widget.text ?? ''), key: ValueKey<String>(widget.text ?? ''),
style: TextStyle( style: TextStyle(
color: widget.selectStatus color: widget.selectStatus
@@ -188,9 +194,7 @@ class ActionItemState extends State<ActionItem> with TickerProviderStateMixin {
], ],
), ),
), ),
),
); );
}
} }
class _ArcPainter extends CustomPainter { class _ArcPainter extends CustomPainter {

View File

@@ -5,6 +5,7 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart';
import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart';
import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/action_item.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
@@ -28,7 +29,6 @@ import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart';
import '../../../../models/video/play/CDN.dart'; import '../../../../models/video/play/CDN.dart';
import '../../../../models/video_detail_res.dart';
import '../../../setting/widgets/select_dialog.dart'; import '../../../setting/widgets/select_dialog.dart';
import '../introduction/index.dart'; import '../introduction/index.dart';
import 'package:marquee/marquee.dart'; import 'package:marquee/marquee.dart';
@@ -61,13 +61,14 @@ class _HeaderControlState extends State<HeaderControl> {
double buttonSpace = 8; double buttonSpace = 8;
String get heroTag => widget.heroTag; String get heroTag => widget.heroTag;
late VideoIntroController videoIntroController; late VideoIntroController videoIntroController;
late VideoDetailData videoDetail;
late bool horizontalScreen; late bool horizontalScreen;
RxString now = ''.obs; RxString now = ''.obs;
Timer? clock; Timer? clock;
late String defaultCDNService; late String defaultCDNService;
bool get isFullScreen => widget.controller.isFullScreen.value; bool get isFullScreen => widget.controller.isFullScreen.value;
Box get setting => GStorage.setting; Box get setting => GStorage.setting;
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
@override @override
void initState() { void initState() {
@@ -1746,22 +1747,17 @@ class _HeaderControlState extends State<HeaderControl> {
}); });
} }
@override Widget _buildHeader(bool showFSActionItem) => AppBar(
Widget build(BuildContext context) {
final plPlayerController = widget.controller;
// final bool isLandscape =
// MediaQuery.of(context).orientation == Orientation.landscape;
bool equivalentFullScreen = !isFullScreen &&
!horizontalScreen &&
MediaQuery.of(context).orientation == Orientation.landscape;
return LayoutBuilder(builder: (context, boxConstraints) {
return AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Colors.white, foregroundColor: Colors.white,
primary: false, primary: false,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Row( toolbarHeight: showFSActionItem && isFullScreen ? 112 : null,
flexibleSpace: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 11),
Row(
children: [ children: [
SizedBox( SizedBox(
width: 42, width: 42,
@@ -1799,23 +1795,30 @@ class _HeaderControlState extends State<HeaderControl> {
color: Colors.white, color: Colors.white,
), ),
onPressed: () { onPressed: () {
widget.videoDetailCtr.plPlayerController.backToHome = true; widget.videoDetailCtr.plPlayerController.backToHome =
true;
Get.until((route) => route.isFirst); Get.until((route) => route.isFirst);
}, },
), ),
), ),
if ((videoIntroController.videoDetail.value.title != null) && if ((videoIntroController.videoDetail.value.title != null) &&
(isFullScreen || equivalentFullScreen)) (isFullScreen ||
Column( (!isFullScreen &&
!horizontalScreen &&
MediaQuery.of(context).orientation ==
Orientation.landscape)))
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ConstrainedBox( Container(
constraints: BoxConstraints( padding: const EdgeInsets.symmetric(horizontal: 10),
maxWidth: boxConstraints.maxWidth / 2 - 60, constraints: BoxConstraints(maxHeight: 25),
maxHeight: 25),
child: Obx( child: Obx(
() => Marquee( () => Marquee(
text: videoIntroController.videoDetail.value.title!, text:
videoIntroController.videoDetail.value.title!,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 16, fontSize: 16,
@@ -1832,7 +1835,8 @@ class _HeaderControlState extends State<HeaderControl> {
startPadding: 0, startPadding: 0,
accelerationDuration: const Duration(seconds: 1), accelerationDuration: const Duration(seconds: 1),
accelerationCurve: Curves.linear, accelerationCurve: Curves.linear,
decelerationDuration: const Duration(milliseconds: 500), decelerationDuration:
const Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut, decelerationCurve: Curves.easeOut,
), ),
), ),
@@ -1849,8 +1853,11 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
], ],
), ),
)
else
const Spacer(), const Spacer(),
if (MediaQuery.of(context).orientation == Orientation.landscape && if (MediaQuery.of(context).orientation ==
Orientation.landscape &&
(isFullScreen || !horizontalScreen)) ...[ (isFullScreen || !horizontalScreen)) ...[
// const Spacer(), // const Spacer(),
// show current datetime // show current datetime
@@ -2007,8 +2014,8 @@ class _HeaderControlState extends State<HeaderControl> {
const Text( const Text(
'建议开启【后台音频服务】\n' '建议开启【后台音频服务】\n'
'避免画中画没有暂停按钮', '避免画中画没有暂停按钮',
style: style: TextStyle(
TextStyle(fontSize: 12.5, height: 1.5)), fontSize: 12.5, height: 1.5)),
Row(children: [ Row(children: [
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
@@ -2047,11 +2054,14 @@ class _HeaderControlState extends State<HeaderControl> {
showCloseIcon: true, showCloseIcon: true,
), ),
); );
await Future.delayed(const Duration(seconds: 3), () {}); await Future.delayed(
const Duration(seconds: 3), () {});
} }
final Rational aspectRatio = Rational( final Rational aspectRatio = Rational(
widget.videoDetailCtr.data.dash!.video!.first.width!, widget
widget.videoDetailCtr.data.dash!.video!.first.height!, .videoDetailCtr.data.dash!.video!.first.width!,
widget
.videoDetailCtr.data.dash!.video!.first.height!,
); );
if (!context.mounted) return; if (!context.mounted) return;
await widget.floating!.enable(EnableManual( await widget.floating!.enable(EnableManual(
@@ -2084,8 +2094,136 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
], ],
), ),
if (showFSActionItem)
isFullScreen
? Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
expand: false,
icon: const Icon(
FontAwesomeIcons.thumbsUp,
color: Colors.white,
),
selectIcon:
const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: videoIntroController.actionLikeVideo,
onLongPress: videoIntroController.actionOneThree,
selectStatus: videoIntroController.hasLike.value,
semanticsLabel: '点赞',
needAnim: true,
hasOneThree: videoIntroController.hasLike.value &&
videoIntroController.hasCoin.value &&
videoIntroController.hasFav.value,
callBack: (start) {
if (start) {
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
}
},
),
),
),
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
expand: false,
icon: const Icon(
FontAwesomeIcons.thumbsDown,
color: Colors.white,
),
selectIcon:
const Icon(FontAwesomeIcons.solidThumbsDown),
onTap: videoIntroController.actionDislikeVideo,
selectStatus:
videoIntroController.hasDislike.value,
semanticsLabel: '点踩',
),
),
),
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
key: _coinKey,
expand: false,
icon: const Icon(
FontAwesomeIcons.b,
color: Colors.white,
),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: videoIntroController.actionCoinVideo,
selectStatus: videoIntroController.hasCoin.value,
semanticsLabel: '投币',
needAnim: true,
),
),
),
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
key: _favKey,
expand: false,
icon: const Icon(
FontAwesomeIcons.star,
color: Colors.white,
),
selectIcon:
const Icon(FontAwesomeIcons.solidStar),
onTap: () => videoIntroController
.showFavBottomSheet(context),
onLongPress: () => videoIntroController
.showFavBottomSheet(context,
type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
semanticsLabel: '收藏',
needAnim: true,
),
),
),
SizedBox(
width: 42,
height: 34,
child: ActionItem(
expand: false,
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
color: Colors.white,
),
onTap: videoIntroController.actionShareVideo,
selectStatus: false,
semanticsLabel: '分享',
),
),
],
)
: const SizedBox.shrink(),
],
),
); );
});
PlPlayerController get plPlayerController => widget.controller;
@override
Widget build(BuildContext context) {
// final bool isLandscape =
// MediaQuery.of(context).orientation == Orientation.landscape;
return plPlayerController.showFSActionItem
? Obx(() => _buildHeader(true))
: _buildHeader(false);
} }
} }

View File

@@ -254,6 +254,8 @@ class PlPlayerController {
/// 弹幕开关 /// 弹幕开关
Rx<bool> isOpenDanmu = false.obs; Rx<bool> isOpenDanmu = false.obs;
late final showFSActionItem = GStorage.showFSActionItem;
/// 弹幕权重 /// 弹幕权重
int danmakuWeight = 0; int danmakuWeight = 0;
int filterCount = 0; int filterCount = 0;

View File

@@ -28,7 +28,7 @@ class AppBarAni extends StatelessWidget implements PreferredSizeWidget {
parent: controller, parent: controller,
curve: Curves.linear, curve: Curves.linear,
)), )),
child: Container( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: position! == 'top' gradient: position! == 'top'
? const LinearGradient( ? const LinearGradient(

View File

@@ -394,6 +394,9 @@ class GStorage {
static bool slideDismissReplyPage = GStorage.setting static bool slideDismissReplyPage = GStorage.setting
.get(SettingBoxKey.slideDismissReplyPage, defaultValue: Platform.isIOS); .get(SettingBoxKey.slideDismissReplyPage, defaultValue: Platform.isIOS);
static bool get showFSActionItem =>
GStorage.setting.get(SettingBoxKey.showFSActionItem, defaultValue: true);
static List<double> get dynamicDetailRatio => List<double>.from(setting static List<double> get dynamicDetailRatio => List<double>.from(setting
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0])); .get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
@@ -649,6 +652,7 @@ class SettingBoxKey {
collapsibleVideoPage = 'collapsibleVideoPage', collapsibleVideoPage = 'collapsibleVideoPage',
enableHttp2 = 'enableHttp2', enableHttp2 = 'enableHttp2',
slideDismissReplyPage = 'slideDismissReplyPage', slideDismissReplyPage = 'slideDismissReplyPage',
showFSActionItem = 'showFSActionItem',
// Sponsor Block // Sponsor Block
enableSponsorBlock = 'enableSponsorBlock', enableSponsorBlock = 'enableSponsorBlock',