refa video action item

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-08-09 19:01:18 +08:00
parent 27c9c266c1
commit 85c72731f6
7 changed files with 252 additions and 329 deletions

View File

@@ -54,6 +54,11 @@ abstract class CommonIntroController extends GetxController {
bool prevPlay();
bool nextPlay();
void actionLikeVideo();
void actionCoinVideo();
void actionTriple();
void actionShareVideo(BuildContext context);
// 同时观看
final bool isShowOnlineTotal = Pref.enableOnlineTotal;
late final RxString total = '1'.obs;

View File

@@ -98,6 +98,7 @@ class PgcIntroController extends CommonIntroController {
}
// (取消)点赞
@override
Future<void> actionLikeVideo() async {
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');
@@ -115,6 +116,7 @@ class PgcIntroController extends CommonIntroController {
}
// 投币
@override
void actionCoinVideo() {
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');
@@ -138,6 +140,7 @@ class PgcIntroController extends CommonIntroController {
}
// 分享视频
@override
void actionShareVideo(BuildContext context) {
showDialog(
context: context,
@@ -408,7 +411,8 @@ class PgcIntroController extends CommonIntroController {
}
// 一键三连
Future<void> actionOneThree() async {
@override
Future<void> actionTriple() async {
feedBack();
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');

View File

@@ -18,7 +18,6 @@ import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
import 'package:PiliPlus/utils/num_util.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@@ -41,13 +40,13 @@ class PgcIntroPage extends StatefulWidget {
}
class _PgcIntroPageState extends State<PgcIntroPage>
with AutomaticKeepAliveClientMixin {
with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
TripleAnimMixin {
late PgcIntroController pgcIntroController;
late VideoDetailController videoDetailCtr;
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
bool isProcessing = false;
Future<void> handleState(FutureOr Function() action) async {
if (!isProcessing) {
@@ -65,6 +64,13 @@ class _PgcIntroPageState extends State<PgcIntroPage>
super.initState();
pgcIntroController = Get.put(PgcIntroController(), tag: widget.heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: widget.heroTag);
initTriple();
}
@override
void dispose() {
disposeTriple();
super.dispose();
}
@override
@@ -425,42 +431,30 @@ class _PgcIntroPageState extends State<PgcIntroPage>
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => handleState(pgcIntroController.actionLikeVideo),
onLongPress: pgcIntroController.actionOneThree,
onLongPress: () => handleState(pgcIntroController.actionTriple),
selectStatus: pgcIntroController.hasLike.value,
semanticsLabel: '点赞',
text: NumUtil.numFormat(item.stat!.like),
needAnim: true,
hasTriple:
pgcIntroController.hasLike.value &&
pgcIntroController.hasCoin &&
pgcIntroController.hasFav.value,
callBack: (start) {
if (start) {
HapticFeedback.lightImpact();
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
}
},
controller: animController,
animation: animation,
onStartTriple: onStartTriple,
onCancelTriple: onCancelTriple,
),
),
Obx(
() => ActionItem(
key: _coinKey,
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => handleState(pgcIntroController.actionCoinVideo),
selectStatus: pgcIntroController.hasCoin,
semanticsLabel: '投币',
text: NumUtil.numFormat(item.stat!.coin),
needAnim: true,
controller: animController,
animation: animation,
),
),
Obx(
() => ActionItem(
key: _favKey,
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => pgcIntroController.showFavBottomSheet(context),
@@ -471,7 +465,8 @@ class _PgcIntroPageState extends State<PgcIntroPage>
selectStatus: pgcIntroController.hasFav.value,
semanticsLabel: '收藏',
text: NumUtil.numFormat(item.stat!.favorite),
needAnim: true,
controller: animController,
animation: animation,
),
),
Obx(
@@ -495,4 +490,16 @@ class _PgcIntroPageState extends State<PgcIntroPage>
),
);
}
@override
bool get hasTriple =>
pgcIntroController.hasLike.value &&
pgcIntroController.hasCoin &&
pgcIntroController.hasFav.value;
@override
void onLike() => handleState(pgcIntroController.actionLikeVideo);
@override
void onTriple() => handleState(pgcIntroController.actionTriple);
}

View File

@@ -181,7 +181,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
}
// 一键三连
Future<void> actionOneThree() async {
@override
Future<void> actionTriple() async {
feedBack();
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');
@@ -217,6 +218,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
}
// (取消)点赞
@override
Future<void> actionLikeVideo() async {
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');
@@ -266,7 +268,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
}
// 投币
Future<void> actionCoinVideo() async {
@override
void actionCoinVideo() {
if (!accountService.isLogin.value) {
SmartDialog.showToast('账号未登录');
return;
@@ -297,6 +300,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
StatDetail? getStat() => videoDetail.value.stat;
// 分享视频
@override
void actionShareVideo(BuildContext context) {
showDialog(
context: context,

View File

@@ -30,7 +30,6 @@ import 'package:PiliPlus/utils/utils.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart' hide ContextExtensionss;
@@ -54,20 +53,27 @@ class UgcIntroPanel extends StatefulWidget {
}
class _UgcIntroPanelState extends State<UgcIntroPanel>
with AutomaticKeepAliveClientMixin {
with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
TripleAnimMixin {
late UgcIntroController ugcIntroController;
late final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: widget.heroTag);
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
bool isProcessing = false;
@override
void initState() {
super.initState();
ugcIntroController = Get.put(UgcIntroController(), tag: widget.heroTag);
initTriple();
}
@override
void dispose() {
disposeTriple();
super.dispose();
}
@override
@@ -530,27 +536,16 @@ class _UgcIntroPanelState extends State<UgcIntroPanel>
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => handleState(ugcIntroController.actionLikeVideo),
onLongPress: () => handleState(ugcIntroController.actionOneThree),
onLongPress: () => handleState(ugcIntroController.actionTriple),
selectStatus: ugcIntroController.hasLike.value,
semanticsLabel: '点赞',
text: !isLoading
? NumUtil.numFormat(videoDetail.stat!.like)
: null,
needAnim: true,
hasTriple:
ugcIntroController.hasLike.value &&
ugcIntroController.hasCoin &&
ugcIntroController.hasFav.value,
callBack: (start) {
if (start) {
HapticFeedback.lightImpact();
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
}
},
controller: animController,
animation: animation,
onStartTriple: onStartTriple,
onCancelTriple: onCancelTriple,
),
),
Obx(
@@ -565,7 +560,6 @@ class _UgcIntroPanelState extends State<UgcIntroPanel>
),
Obx(
() => ActionItem(
key: _coinKey,
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => handleState(ugcIntroController.actionCoinVideo),
@@ -574,12 +568,12 @@ class _UgcIntroPanelState extends State<UgcIntroPanel>
text: !isLoading
? NumUtil.numFormat(videoDetail.stat!.coin)
: null,
needAnim: true,
controller: animController,
animation: animation,
),
),
Obx(
() => ActionItem(
key: _favKey,
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => ugcIntroController.showFavBottomSheet(context),
@@ -592,7 +586,8 @@ class _UgcIntroPanelState extends State<UgcIntroPanel>
text: !isLoading
? NumUtil.numFormat(videoDetail.stat!.favorite)
: null,
needAnim: true,
controller: animController,
animation: animation,
),
),
Obx(
@@ -980,4 +975,16 @@ class _UgcIntroPanelState extends State<UgcIntroPanel>
@override
bool get wantKeepAlive => true;
@override
bool get hasTriple =>
ugcIntroController.hasLike.value &&
ugcIntroController.hasCoin &&
ugcIntroController.hasFav.value;
@override
void onLike() => handleState(ugcIntroController.actionLikeVideo);
@override
void onTriple() => handleState(ugcIntroController.actionTriple);
}

View File

@@ -1,106 +1,65 @@
import 'dart:async';
import 'dart:math';
import 'dart:async' show Timer;
import 'dart:math' show pi;
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class ActionItem extends StatefulWidget {
final Icon icon;
final Icon? selectIcon;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final String? text;
final bool selectStatus;
final String semanticsLabel;
final bool needAnim;
final bool hasTriple;
final ValueChanged<bool>? callBack;
final bool expand;
mixin TripleAnimMixin<T extends StatefulWidget>
on SingleTickerProviderStateMixin<T> {
late AnimationController animController;
late Animation<double> animation;
const ActionItem({
super.key,
required this.icon,
this.selectIcon,
this.onTap,
this.onLongPress,
this.text,
this.selectStatus = false,
this.needAnim = false,
this.hasTriple = false,
this.callBack,
required this.semanticsLabel,
this.expand = true,
});
@override
State<ActionItem> createState() => ActionItemState();
}
class ActionItemState extends State<ActionItem>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation<double>? _animation;
late final _isThumbsUp = widget.semanticsLabel == '点赞';
late int _lastTime;
Timer? _timer;
void _startLongPress() {
bool get hasTriple;
void onTriple();
void onLike();
void initTriple() {
animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
reverseDuration: const Duration(milliseconds: 400),
);
animation = Tween<double>(begin: 0, end: -2 * pi).animate(
CurvedAnimation(
parent: animController,
curve: Curves.easeInOut,
),
);
}
void onStartTriple() {
_lastTime = DateTime.now().millisecondsSinceEpoch;
_timer ??= Timer(const Duration(milliseconds: 200), () {
if (widget.hasTriple) {
if (hasTriple) {
HapticFeedback.lightImpact();
SmartDialog.showToast('已经完成三连');
} else {
controller?.forward();
widget.callBack?.call(true);
animController.forward().whenComplete(() {
animController.reset();
onTriple();
});
}
cancelTimer();
});
}
void _cancelLongPress([bool isCancel = false]) {
void onCancelTriple(bool isCancel) {
int duration = DateTime.now().millisecondsSinceEpoch - _lastTime;
if (duration >= 200 && duration < 1500) {
if (!widget.hasTriple) {
controller?.reverse();
widget.callBack?.call(false);
if (!hasTriple) {
animController.reverse();
}
} else if (duration < 200) {
cancelTimer();
if (!isCancel) {
feedBack();
widget.onTap?.call();
}
}
}
@override
void initState() {
super.initState();
if (widget.needAnim) {
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
reverseDuration: const Duration(milliseconds: 400),
)..addListener(listener);
_animation = Tween<double>(begin: 0, end: -2 * pi).animate(
CurvedAnimation(
parent: controller!,
curve: Curves.easeInOut,
),
);
}
}
void listener() {
if (controller!.value == 1) {
controller!.reset();
if (_isThumbsUp) {
widget.onLongPress?.call();
onLike();
}
}
}
@@ -110,13 +69,49 @@ class ActionItemState extends State<ActionItem>
_timer = null;
}
@override
void dispose() {
void disposeTriple() {
cancelTimer();
controller?.removeListener(listener);
controller?.dispose();
super.dispose();
animController.dispose();
}
}
class ActionItem extends StatefulWidget {
const ActionItem({
super.key,
required this.icon,
this.selectIcon,
this.onTap,
this.onLongPress,
this.text,
this.selectStatus = false,
required this.semanticsLabel,
this.expand = true,
this.controller,
this.animation,
this.onStartTriple,
this.onCancelTriple,
});
final Icon icon;
final Icon? selectIcon;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final String? text;
final bool selectStatus;
final String semanticsLabel;
final bool expand;
final AnimationController? controller;
final Animation<double>? animation;
final VoidCallback? onStartTriple;
final ValueChanged<bool>? onCancelTriple;
@override
State<ActionItem> createState() => ActionItemState();
}
class ActionItemState extends State<ActionItem>
with SingleTickerProviderStateMixin {
late final _isThumbsUp = widget.onStartTriple != null;
@override
Widget build(BuildContext context) {
@@ -155,9 +150,11 @@ class ActionItemState extends State<ActionItem>
widget.onTap?.call();
},
onLongPress: _isThumbsUp ? null : widget.onLongPress,
onTapDown: _isThumbsUp ? (details) => _startLongPress() : null,
onTapUp: _isThumbsUp ? (details) => _cancelLongPress() : null,
onTapCancel: _isThumbsUp ? () => _cancelLongPress(true) : null,
onTapDown: _isThumbsUp ? (details) => widget.onStartTriple!() : null,
onTapUp: _isThumbsUp
? (details) => widget.onCancelTriple!(false)
: null,
onTapCancel: _isThumbsUp ? () => widget.onCancelTriple!(true) : null,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -165,14 +162,14 @@ class ActionItemState extends State<ActionItem>
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
if (widget.needAnim)
if (widget.animation != null)
AnimatedBuilder(
animation: _animation!,
animation: widget.animation!,
builder: (context, child) => CustomPaint(
size: const Size(28, 28),
painter: _ArcPainter(
color: theme.colorScheme.primary,
sweepAngle: _animation!.value,
sweepAngle: widget.animation!.value,
),
),
)

View File

@@ -37,7 +37,6 @@ import 'package:dio/dio.dart';
import 'package:document_file_save_plus/document_file_save_plus_platform_interface.dart';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@@ -63,7 +62,8 @@ class HeaderControl extends StatefulWidget {
State<HeaderControl> createState() => HeaderControlState();
}
class HeaderControlState extends State<HeaderControl> {
class HeaderControlState extends State<HeaderControl>
with SingleTickerProviderStateMixin, TripleAnimMixin {
late final PlPlayerController plPlayerController = widget.controller;
late final VideoDetailController videoDetailCtr = widget.videoDetailCtr;
late final PlayUrlModel videoInfo = videoDetailCtr.data;
@@ -80,8 +80,6 @@ class HeaderControlState extends State<HeaderControl> {
Timer? clock;
bool get isFullScreen => widget.controller.isFullScreen.value;
Box setting = GStorage.setting;
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
@override
void initState() {
@@ -91,11 +89,13 @@ class HeaderControlState extends State<HeaderControl> {
} else {
pgcIntroController = Get.find<PgcIntroController>(tag: heroTag);
}
initTriple();
}
@override
void dispose() {
clock?.cancel();
disposeTriple();
super.dispose();
}
@@ -2224,7 +2224,6 @@ class HeaderControlState extends State<HeaderControl> {
? Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (videoDetailCtr.isUgc) ...[
SizedBox(
width: 42,
height: 34,
@@ -2238,37 +2237,25 @@ class HeaderControlState extends State<HeaderControl> {
selectIcon: const Icon(
FontAwesomeIcons.solidThumbsUp,
),
onTap: ugcIntroController.actionLikeVideo,
onLongPress: () {
ugcIntroController.actionOneThree();
plPlayerController
..isTriple = null
..hideTaskControls();
},
selectStatus: ugcIntroController.hasLike.value,
onTap: introController.actionLikeVideo,
selectStatus: introController.hasLike.value,
semanticsLabel: '点赞',
needAnim: true,
hasTriple:
ugcIntroController.hasLike.value &&
ugcIntroController.hasCoin &&
ugcIntroController.hasFav.value,
callBack: (start) {
if (start) {
HapticFeedback.lightImpact();
controller: animController,
animation: animation,
onStartTriple: () {
plPlayerController.isTriple = true;
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
onStartTriple();
},
onCancelTriple: (value) {
plPlayerController
..isTriple = null
..hideTaskControls();
}
onCancelTriple(value);
},
),
),
),
if (videoDetailCtr.isUgc)
SizedBox(
width: 42,
height: 34,
@@ -2293,17 +2280,17 @@ class HeaderControlState extends State<HeaderControl> {
height: 34,
child: Obx(
() => ActionItem(
key: _coinKey,
expand: false,
icon: const Icon(
FontAwesomeIcons.b,
color: Colors.white,
),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: ugcIntroController.actionCoinVideo,
selectStatus: ugcIntroController.hasCoin,
onTap: introController.actionCoinVideo,
selectStatus: introController.hasCoin,
semanticsLabel: '投币',
needAnim: true,
controller: animController,
animation: animation,
),
),
),
@@ -2312,7 +2299,6 @@ class HeaderControlState extends State<HeaderControl> {
height: 34,
child: Obx(
() => ActionItem(
key: _favKey,
expand: false,
icon: const Icon(
FontAwesomeIcons.star,
@@ -2320,12 +2306,15 @@ class HeaderControlState extends State<HeaderControl> {
),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () =>
ugcIntroController.showFavBottomSheet(context),
onLongPress: () => ugcIntroController
.showFavBottomSheet(context, isLongPress: true),
selectStatus: ugcIntroController.hasFav.value,
introController.showFavBottomSheet(context),
onLongPress: () => introController.showFavBottomSheet(
context,
isLongPress: true,
),
selectStatus: introController.hasFav.value,
semanticsLabel: '收藏',
needAnim: true,
controller: animController,
animation: animation,
),
),
),
@@ -2338,112 +2327,10 @@ class HeaderControlState extends State<HeaderControl> {
FontAwesomeIcons.shareFromSquare,
color: Colors.white,
),
onTap: () =>
ugcIntroController.actionShareVideo(context),
onTap: () => introController.actionShareVideo(context),
semanticsLabel: '分享',
),
),
] else ...[
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
expand: false,
icon: const Icon(
FontAwesomeIcons.thumbsUp,
color: Colors.white,
),
selectIcon: const Icon(
FontAwesomeIcons.solidThumbsUp,
),
onTap: pgcIntroController.actionLikeVideo,
onLongPress: () {
pgcIntroController.actionOneThree();
plPlayerController
..isTriple = null
..hideTaskControls();
},
selectStatus: pgcIntroController.hasLike.value,
semanticsLabel: '点赞',
needAnim: true,
hasTriple:
pgcIntroController.hasLike.value &&
pgcIntroController.hasCoin &&
pgcIntroController.hasFav.value,
callBack: (start) {
if (start) {
HapticFeedback.lightImpact();
plPlayerController.isTriple = true;
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
plPlayerController
..isTriple = null
..hideTaskControls();
}
},
),
),
),
SizedBox(
width: 42,
height: 34,
child: Obx(
() => ActionItem(
expand: false,
key: _coinKey,
icon: const Icon(
FontAwesomeIcons.b,
color: Colors.white,
),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: pgcIntroController.actionCoinVideo,
selectStatus: pgcIntroController.hasCoin,
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: () =>
pgcIntroController.showFavBottomSheet(context),
onLongPress: () => pgcIntroController
.showFavBottomSheet(context, isLongPress: true),
selectStatus: pgcIntroController.hasFav.value,
semanticsLabel: '收藏',
needAnim: true,
),
),
),
SizedBox(
width: 42,
height: 34,
child: ActionItem(
expand: false,
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
color: Colors.white,
),
onTap: () =>
pgcIntroController.actionShareVideo(context),
semanticsLabel: '转发',
),
),
],
],
)
: const SizedBox.shrink(),
@@ -2457,6 +2344,18 @@ class HeaderControlState extends State<HeaderControl> {
? Obx(() => _buildHeader(true))
: _buildHeader(false);
}
@override
bool get hasTriple =>
introController.hasLike.value &&
introController.hasCoin &&
introController.hasFav.value;
@override
void onLike() => introController.actionLikeVideo();
@override
void onTriple() => introController.actionTriple();
}
class MSliderTrackShape extends RoundedRectSliderTrackShape {