diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 56e5ebcb..f9eea235 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -417,4 +417,27 @@ class BangumiIntroController extends CommonController { SmartDialog.showToast('番剧暂无相关视频'); return false; } + + // 一键三连 + Future actionOneThree() async { + feedBack(); + if (userInfo == null) { + SmartDialog.showToast('账号未登录'); + return; + } + if (hasLike.value && hasCoin.value && hasFav.value) { + // 已点赞、投币、收藏 + SmartDialog.showToast('已三连'); + return false; + } + var result = await VideoHttp.oneThree(bvid: bvid); + if (result['status']) { + hasLike.value = result["data"]["like"]; + hasCoin.value = result["data"]["coin"]; + hasFav.value = result["data"]["fav"]; + SmartDialog.showToast('三连成功'); + } else { + SmartDialog.showToast(result['msg']); + } + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index fff58dae..a2013282 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -135,6 +135,9 @@ class _BangumiInfoState extends State }; } + late final _coinKey = GlobalKey(); + late final _favKey = GlobalKey(); + @override void initState() { super.initState(); @@ -396,45 +399,69 @@ class _BangumiInfoState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Obx(() => ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), - onTap: - handleState(bangumiIntroController.actionLikeVideo), - selectStatus: bangumiIntroController.hasLike.value, - loadingStatus: false, - semanticsLabel: '点赞', - text: !widget.loadingStatus - ? Utils.numFormat( - widget.bangumiDetail!.stat!['likes']!) - : Utils.numFormat(bangumiItem!.stat!['likes']!), - )), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.b), - selectIcon: const Icon(FontAwesomeIcons.b), - onTap: - handleState(bangumiIntroController.actionCoinVideo), - selectStatus: bangumiIntroController.hasCoin.value, - loadingStatus: false, - semanticsLabel: '投币', - text: !widget.loadingStatus - ? Utils.numFormat( - widget.bangumiDetail!.stat!['coins']!) - : Utils.numFormat(bangumiItem!.stat!['coins']!)), + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: handleState(bangumiIntroController.actionLikeVideo), + onLongPress: bangumiIntroController.actionOneThree, + selectStatus: bangumiIntroController.hasLike.value, + loadingStatus: false, + semanticsLabel: '点赞', + text: !widget.loadingStatus + ? Utils.numFormat(widget.bangumiDetail!.stat!['likes']!) + : Utils.numFormat( + bangumiItem!.stat!['likes']!, + ), + needAnim: true, + hasOneThree: bangumiIntroController.hasLike.value && + bangumiIntroController.hasCoin.value && + bangumiIntroController.hasFav.value, + callBack: (start) { + if (start) { + _coinKey.currentState?.controller?.forward(); + _favKey.currentState?.controller?.forward(); + } else { + _coinKey.currentState?.controller?.reverse(); + _favKey.currentState?.controller?.reverse(); + } + }, + ), ), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.star), - selectIcon: const Icon(FontAwesomeIcons.solidStar), - onTap: () => showFavBottomSheet(), - selectStatus: bangumiIntroController.hasFav.value, - loadingStatus: false, - semanticsLabel: '收藏', - text: !widget.loadingStatus - ? Utils.numFormat( - widget.bangumiDetail!.stat!['favorite']!) - : Utils.numFormat(bangumiItem!.stat!['favorite']!)), + key: _coinKey, + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: handleState(bangumiIntroController.actionCoinVideo), + selectStatus: bangumiIntroController.hasCoin.value, + loadingStatus: false, + semanticsLabel: '投币', + text: !widget.loadingStatus + ? Utils.numFormat(widget.bangumiDetail!.stat!['coins']!) + : Utils.numFormat( + bangumiItem!.stat!['coins']!, + ), + needAnim: true, + ), + ), + Obx( + () => ActionItem( + key: _favKey, + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), + onTap: () => showFavBottomSheet(), + selectStatus: bangumiIntroController.hasFav.value, + loadingStatus: false, + semanticsLabel: '收藏', + text: !widget.loadingStatus + ? Utils.numFormat( + widget.bangumiDetail!.stat!['favorite']!) + : Utils.numFormat( + bangumiItem!.stat!['favorite']!, + ), + needAnim: true, + ), ), ActionItem( icon: const Icon(FontAwesomeIcons.comment), diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 8fa55948..20d8348e 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -152,6 +152,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { }; } + late final _coinKey = GlobalKey(); + late final _favKey = GlobalKey(); + @override void initState() { super.initState(); @@ -572,16 +575,30 @@ class _VideoInfoState extends State with TickerProviderStateMixin { children: [ Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), - onTap: handleState(videoIntroController.actionLikeVideo), - onLongPress: handleState(videoIntroController.actionOneThree), - selectStatus: videoIntroController.hasLike.value, - loadingStatus: loadingStatus, - semanticsLabel: '点赞', - text: !loadingStatus - ? Utils.numFormat(widget.videoDetail!.stat!.like!) - : '-'), + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: handleState(videoIntroController.actionLikeVideo), + onLongPress: handleState(videoIntroController.actionOneThree), + selectStatus: videoIntroController.hasLike.value, + loadingStatus: loadingStatus, + semanticsLabel: '点赞', + text: !loadingStatus + ? Utils.numFormat(widget.videoDetail!.stat!.like!) + : '-', + 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(); + } + }, + ), ), Obx( () => ActionItem( @@ -601,28 +618,34 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // text: '稍后再看'), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.b), - selectIcon: const Icon(FontAwesomeIcons.b), - onTap: handleState(videoIntroController.actionCoinVideo), - selectStatus: videoIntroController.hasCoin.value, - loadingStatus: loadingStatus, - semanticsLabel: '投币', - text: !loadingStatus - ? Utils.numFormat(widget.videoDetail!.stat!.coin!) - : '-'), + key: _coinKey, + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: handleState(videoIntroController.actionCoinVideo), + selectStatus: videoIntroController.hasCoin.value, + loadingStatus: loadingStatus, + semanticsLabel: '投币', + text: !loadingStatus + ? Utils.numFormat(widget.videoDetail!.stat!.coin!) + : '-', + needAnim: true, + ), ), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.star), - selectIcon: const Icon(FontAwesomeIcons.solidStar), - onTap: () => showFavBottomSheet(), - onLongPress: () => showFavBottomSheet(type: 'longPress'), - selectStatus: videoIntroController.hasFav.value, - loadingStatus: loadingStatus, - semanticsLabel: '收藏', - text: !loadingStatus - ? Utils.numFormat(widget.videoDetail!.stat!.favorite!) - : '-'), + key: _favKey, + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), + onTap: () => showFavBottomSheet(), + onLongPress: () => showFavBottomSheet(type: 'longPress'), + selectStatus: videoIntroController.hasFav.value, + loadingStatus: loadingStatus, + semanticsLabel: '收藏', + text: !loadingStatus + ? Utils.numFormat(widget.videoDetail!.stat!.favorite!) + : '-', + needAnim: true, + ), ), ActionItem( icon: const Icon(FontAwesomeIcons.comment), diff --git a/lib/pages/video/detail/introduction/widgets/action_item.dart b/lib/pages/video/detail/introduction/widgets/action_item.dart index 765028e9..bc08362d 100644 --- a/lib/pages/video/detail/introduction/widgets/action_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_item.dart @@ -1,7 +1,9 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; -class ActionItem extends StatelessWidget { +class ActionItem extends StatefulWidget { final Icon? icon; final Icon? selectIcon; final Function? onTap; @@ -10,6 +12,9 @@ class ActionItem extends StatelessWidget { final String? text; final bool selectStatus; final String semanticsLabel; + final bool needAnim; + final bool hasOneThree; + final Function? callBack; const ActionItem({ Key? key, @@ -20,62 +25,188 @@ class ActionItem extends StatelessWidget { this.loadingStatus, this.text, this.selectStatus = false, + this.needAnim = false, + this.hasOneThree = false, + this.callBack, required this.semanticsLabel, }) : super(key: key); + @override + State createState() => ActionItemState(); +} + +class ActionItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController? controller; + late Animation? _animation; + + bool get _isThumbUp => widget.semanticsLabel == '点赞'; + late int _lastTime; + bool _hideCircle = false; + + void _startLongPress() { + _lastTime = DateTime.now().millisecondsSinceEpoch; + if (!widget.hasOneThree) { + controller?.forward(); + widget.callBack!(true); + } + } + + void _cancelLongPress() { + int duration = DateTime.now().millisecondsSinceEpoch - _lastTime; + if (duration < 1500) { + controller?.reverse(); + widget.callBack!(false); + } + if (duration <= 500) { + feedBack(); + widget.onTap!(); + } + } + + @override + void initState() { + super.initState(); + if (widget.needAnim) { + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + reverseDuration: const Duration(milliseconds: 500), + ); + + _animation = Tween(begin: 0, end: -2 * pi).animate(controller!) + ..addListener(() { + setState(() { + _hideCircle = _animation?.value == -2 * pi; + if (_hideCircle) { + controller?.reset(); + if (_isThumbUp) { + widget.onLongPress!(); + } + } + }); + }); + } + } + + @override + void dispose() { + _animation?.removeListener(() {}); + controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Expanded( - child: Semantics( - label: (text ?? "") + (selectStatus ? "已" : "") + semanticsLabel, - child: InkWell( - borderRadius: BorderRadius.circular(6), - onTap: () => { - feedBack(), - onTap!(), - }, - onLongPress: () => { - if (onLongPress != null) {onLongPress!()} - }, - // borderRadius: StyleString.mdRadius, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: Semantics( + label: (widget.text ?? "") + + (widget.selectStatus ? "已" : "") + + widget.semanticsLabel, + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: _isThumbUp + ? null + : () { + feedBack(); + widget.onTap!(); + }, + onLongPress: _isThumbUp + ? null + : () { + if (widget.onLongPress != null) { + widget.onLongPress!(); + } + }, + onTapDown: (details) => _isThumbUp ? _startLongPress() : null, + onTapUp: (details) => _isThumbUp ? _cancelLongPress() : null, + onTapCancel: () => _isThumbUp ? _cancelLongPress() : null, + // borderRadius: StyleString.mdRadius, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // const SizedBox(height: 2), + Stack( + alignment: Alignment.center, children: [ - // const SizedBox(height: 2), + if (widget.needAnim && !_hideCircle) + CustomPaint( + size: const Size(28, 28), + painter: _ArcPainter( + color: Theme.of(context).colorScheme.primary, + sweepAngle: _animation!.value, + ), + ), Icon( - selectStatus ? selectIcon!.icon! : icon!.icon!, + widget.selectStatus + ? widget.selectIcon!.icon! + : widget.icon!.icon!, size: 18, - color: selectStatus + color: widget.selectStatus ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.outline, ), - const SizedBox(height: 3), - AnimatedOpacity( - opacity: loadingStatus! ? 0 : 1, - duration: const Duration(milliseconds: 200), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: Text( - text ?? '', - key: ValueKey(text ?? ''), - style: TextStyle( - color: selectStatus - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize), - semanticsLabel: "", - ), - ), - ), ], ), - ))); + const SizedBox(height: 3), + AnimatedOpacity( + opacity: widget.loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Text( + widget.text ?? '', + key: ValueKey(widget.text ?? ''), + style: TextStyle( + color: widget.selectStatus + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.labelSmall!.fontSize), + semanticsLabel: "", + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ArcPainter extends CustomPainter { + const _ArcPainter({ + required this.color, + required this.sweepAngle, + }); + final Color color; + final double sweepAngle; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; + + final rect = Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: size.width / 2, + ); + + const startAngle = -pi / 2; + // const sweepAngle = -2 * pi; + + canvas.drawArc(rect, startAngle, sweepAngle, false, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; } }