diff --git a/README.md b/README.md index a017cc79a..1c8acd96a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 投币动画 - [x] 取消/追番,更新追番状态 - [x] 取消/订阅合集 - [x] SponsorBlock diff --git a/assets/images/paycoins/ic_22_gun_sister.png b/assets/images/paycoins/ic_22_gun_sister.png new file mode 100644 index 000000000..621c773f4 Binary files /dev/null and b/assets/images/paycoins/ic_22_gun_sister.png differ diff --git a/assets/images/paycoins/ic_22_mario.png b/assets/images/paycoins/ic_22_mario.png new file mode 100644 index 000000000..cdf0dea52 Binary files /dev/null and b/assets/images/paycoins/ic_22_mario.png differ diff --git a/assets/images/paycoins/ic_22_not_enough_pay.png b/assets/images/paycoins/ic_22_not_enough_pay.png new file mode 100644 index 000000000..fcc4363be Binary files /dev/null and b/assets/images/paycoins/ic_22_not_enough_pay.png differ diff --git a/assets/images/paycoins/ic_coins_one.png b/assets/images/paycoins/ic_coins_one.png new file mode 100644 index 000000000..317afca98 Binary files /dev/null and b/assets/images/paycoins/ic_coins_one.png differ diff --git a/assets/images/paycoins/ic_coins_two.png b/assets/images/paycoins/ic_coins_two.png new file mode 100644 index 000000000..66028bead Binary files /dev/null and b/assets/images/paycoins/ic_coins_two.png differ diff --git a/assets/images/paycoins/ic_left.png b/assets/images/paycoins/ic_left.png new file mode 100644 index 000000000..61cec96a1 Binary files /dev/null and b/assets/images/paycoins/ic_left.png differ diff --git a/assets/images/paycoins/ic_left_disable.png b/assets/images/paycoins/ic_left_disable.png new file mode 100644 index 000000000..075045393 Binary files /dev/null and b/assets/images/paycoins/ic_left_disable.png differ diff --git a/assets/images/paycoins/ic_pay_coins_box.png b/assets/images/paycoins/ic_pay_coins_box.png new file mode 100644 index 000000000..aea8aa31b Binary files /dev/null and b/assets/images/paycoins/ic_pay_coins_box.png differ diff --git a/assets/images/paycoins/ic_pay_coins_close.png b/assets/images/paycoins/ic_pay_coins_close.png new file mode 100644 index 000000000..14f60a907 Binary files /dev/null and b/assets/images/paycoins/ic_pay_coins_close.png differ diff --git a/assets/images/paycoins/ic_right.png b/assets/images/paycoins/ic_right.png new file mode 100644 index 000000000..f534c745c Binary files /dev/null and b/assets/images/paycoins/ic_right.png differ diff --git a/assets/images/paycoins/ic_right_disable.png b/assets/images/paycoins/ic_right_disable.png new file mode 100644 index 000000000..1490bbc98 Binary files /dev/null and b/assets/images/paycoins/ic_right_disable.png differ diff --git a/assets/images/paycoins/ic_thunder_1.png b/assets/images/paycoins/ic_thunder_1.png new file mode 100644 index 000000000..80aacc839 Binary files /dev/null and b/assets/images/paycoins/ic_thunder_1.png differ diff --git a/assets/images/paycoins/ic_thunder_2.png b/assets/images/paycoins/ic_thunder_2.png new file mode 100644 index 000000000..4995220d4 Binary files /dev/null and b/assets/images/paycoins/ic_thunder_2.png differ diff --git a/assets/images/paycoins/ic_thunder_3.png b/assets/images/paycoins/ic_thunder_3.png new file mode 100644 index 000000000..f5d8df427 Binary files /dev/null and b/assets/images/paycoins/ic_thunder_3.png differ diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart index 650f8e845..5e19347a2 100644 --- a/lib/common/widgets/self_sized_horizontal_list.dart +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -7,6 +7,7 @@ class SelfSizedHorizontalList extends StatefulWidget { final int itemCount; final double gapSize; final EdgeInsetsGeometry? padding; + const SelfSizedHorizontalList({ super.key, required this.childBuilder, diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 69cacb600..c0d3deeb5 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -4,10 +4,12 @@ import 'package:PiliPalaX/http/init.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/http/user.dart'; import 'package:PiliPalaX/pages/common/common_controller.dart'; +import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:hive/hive.dart'; import 'package:PiliPalaX/http/constants.dart'; import 'package:PiliPalaX/http/search.dart'; @@ -175,66 +177,103 @@ class BangumiIntroController extends CommonController { } } + void coinVideo(int coin) async { + var res = await VideoHttp.coinVideo(bvid: bvid, multiply: _tempThemeValue); + if (res['status']) { + SmartDialog.showToast('投币成功'); + hasCoin.value = true; + dynamic bangumiDetail = (loadingState.value as Success).response; + bangumiDetail.stat!['coins'] = + bangumiDetail.stat!['coins'] + _tempThemeValue; + loadingState.value = LoadingState.success(bangumiDetail); + } else { + SmartDialog.showToast(res['msg']); + } + } + // 投币 Future actionCoinVideo() async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } - showDialog( - context: Get.context!, - builder: (context) { - return AlertDialog( - title: const Text('选择投币个数'), - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), - content: StatefulBuilder(builder: (context, StateSetter setState) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - value: 1, - title: const Text('1枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - RadioListTile( - value: 2, - title: const Text('2枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - ], - ); - }), - actions: [ - TextButton(onPressed: () => Get.back(), child: const Text('取消')), - TextButton( - onPressed: () async { - var res = await VideoHttp.coinVideo( - bvid: bvid, multiply: _tempThemeValue); - if (res['status']) { - SmartDialog.showToast('投币成功'); - hasCoin.value = true; - dynamic bangumiDetail = - (loadingState.value as Success).response; - bangumiDetail.stat!['coins'] = - bangumiDetail.stat!['coins'] + _tempThemeValue; - loadingState.value = LoadingState.success(bangumiDetail); - } else { - SmartDialog.showToast(res['msg']); - } - Get.back(); - }, - child: const Text('确定')) - ], + Navigator.of(Get.context!).push( + GetDialogRoute( + pageBuilder: (buildContext, animation, secondaryAnimation) { + return PayCoinsPage( + callback: coinVideo, ); - }); + }, + transitionDuration: const Duration(milliseconds: 225), + transitionBuilder: (context, animation, secondaryAnimation, child) { + const begin = 0.0; + const end = 1.0; + const curve = Curves.linear; + + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return FadeTransition( + opacity: animation.drive(tween), + child: child, + ); + }, + ), + ); + // showDialog( + // context: Get.context!, + // builder: (context) { + // return AlertDialog( + // title: const Text('选择投币个数'), + // contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + // content: StatefulBuilder(builder: (context, StateSetter setState) { + // return Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // RadioListTile( + // value: 1, + // title: const Text('1枚'), + // groupValue: _tempThemeValue, + // onChanged: (value) { + // _tempThemeValue = value!; + // Get.appUpdate(); + // }, + // ), + // RadioListTile( + // value: 2, + // title: const Text('2枚'), + // groupValue: _tempThemeValue, + // onChanged: (value) { + // _tempThemeValue = value!; + // Get.appUpdate(); + // }, + // ), + // ], + // ); + // }), + // actions: [ + // TextButton(onPressed: () => Get.back(), child: const Text('取消')), + // TextButton( + // onPressed: () async { + // var res = await VideoHttp.coinVideo( + // bvid: bvid, multiply: _tempThemeValue); + // if (res['status']) { + // SmartDialog.showToast('投币成功'); + // hasCoin.value = true; + // dynamic bangumiDetail = + // (loadingState.value as Success).response; + // bangumiDetail.stat!['coins'] = + // bangumiDetail.stat!['coins'] + _tempThemeValue; + // loadingState.value = LoadingState.success(bangumiDetail); + // } else { + // SmartDialog.showToast(res['msg']); + // } + // Get.back(); + // }, + // child: const Text('确定')) + // ], + // ); + // }); } // (取消)收藏 bangumi diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 1f8159dea..c8f2b7e48 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'dart:math'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:hive/hive.dart'; import 'package:PiliPalaX/http/constants.dart'; import 'package:PiliPalaX/http/user.dart'; @@ -280,51 +282,76 @@ class VideoIntroController extends GetxController { } } + void coinVideo(int coin) async { + var res = await VideoHttp.coinVideo(bvid: bvid, multiply: coin); + if (res['status']) { + print(res); + SmartDialog.showToast('投币成功'); + hasCoin.value = true; + videoDetail.value.stat!.coin = videoDetail.value.stat!.coin! + coin; + } else { + SmartDialog.showToast(res['msg']); + } + } + // 投币 Future actionCoinVideo() async { if (userInfo == null) { SmartDialog.showToast('账号未登录'); return; } - void coinVideo(int coin) async { - var res = await VideoHttp.coinVideo(bvid: bvid, multiply: coin); - if (res['status']) { - print(res); - SmartDialog.showToast('投币成功'); - hasCoin.value = true; - videoDetail.value.stat!.coin = videoDetail.value.stat!.coin! + coin; - } else { - SmartDialog.showToast(res['msg']); - } - } - showDialog( - context: Get.context!, - builder: (context) { - return AlertDialog( - title: const Text('选择投币个数'), - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('取消', - style: TextStyle( - color: Theme.of(context).colorScheme.outline))), - TextButton( - onPressed: () async { - coinVideo(1); - Get.back(); - }, - child: const Text('投 1 枚')), - TextButton( - onPressed: () async { - coinVideo(1); - Get.back(); - }, - child: const Text('投 2 枚')) - ], + Navigator.of(Get.context!).push( + GetDialogRoute( + pageBuilder: (buildContext, animation, secondaryAnimation) { + return PayCoinsPage( + callback: coinVideo, ); - }); + }, + transitionDuration: const Duration(milliseconds: 225), + transitionBuilder: (context, animation, secondaryAnimation, child) { + const begin = 0.0; + const end = 1.0; + const curve = Curves.linear; + + var tween = Tween(begin: begin, end: end) + .chain(CurveTween(curve: curve)); + + return FadeTransition( + opacity: animation.drive(tween), + child: child, + ); + }, + ), + ); + // showDialog( + // context: Get.context!, + // builder: (context) { + // return AlertDialog( + // title: const Text('选择投币个数'), + // contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + // actions: [ + // TextButton( + // onPressed: () => Get.back(), + // child: Text('取消', + // style: TextStyle( + // color: Theme.of(context).colorScheme.outline))), + // TextButton( + // onPressed: () async { + // coinVideo(1); + // Get.back(); + // }, + // child: const Text('投 1 枚')), + // TextButton( + // onPressed: () async { + // coinVideo(2); + // Get.back(); + // }, + // child: const Text('投 2 枚')) + // ], + // ); + // }, + // ); } // (取消)收藏 @@ -694,3 +721,281 @@ class VideoIntroController extends GetxController { return res; } } + +class PayCoinsPage extends StatefulWidget { + const PayCoinsPage({super.key, required this.callback}); + + final Function callback; + + @override + State createState() => _PayCoinsPageState(); +} + +class _PayCoinsPageState extends State + with TickerProviderStateMixin { + bool _isPaying = false; + late final _controller = PageController(viewportFraction: 0.30); + + int get _index => _controller.hasClients ? _controller.page?.round() ?? 0 : 0; + + late AnimationController _slide22Controller; + late AnimationController _scale22Controller; + late AnimationController _coinSlideController; + late AnimationController _coinFadeController; + late AnimationController _boxAnimController; + + @override + void initState() { + super.initState(); + _slide22Controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _scale22Controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _coinSlideController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _coinFadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + _boxAnimController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _scale(); + } + + @override + void dispose() { + _slide22Controller.dispose(); + _scale22Controller.dispose(); + _coinSlideController.dispose(); + _coinFadeController.dispose(); + _boxAnimController.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _scale() { + _scale22Controller.forward().whenComplete(() { + _scale22Controller.reverse(); + }); + } + + void _onScroll(int index) { + _controller.animateToPage( + index, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + _scale(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, constraints) { + return _buildBody(constraints.maxHeight > constraints.maxWidth); + }); + } + + Widget _buildBody(isV) => Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + Visibility( + visible: !_isPaying, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: GestureDetector( + onTap: _index == 0 + ? null + : () { + _onScroll(0); + }, + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Image.asset( + width: 16, + height: 28, + _index == 0 + ? 'assets/images/paycoins/ic_left_disable.png' + : 'assets/images/paycoins/ic_left.png', + ), + ), + ), + ), + Expanded( + child: SizedBox( + height: 100, + child: PageView.builder( + itemCount: 2, + controller: _controller, + onPageChanged: (index) => setState(() { + _scale(); + }), + itemBuilder: (context, index) { + return ListenableBuilder( + listenable: _controller, + builder: (context, child) { + double factor = index == 0 ? 1 : 0; + if (_controller.position.hasContentDimensions) { + factor = 1 - (_controller.page! - index).abs(); + } + return Visibility( + visible: !_isPaying || _index == index, + child: Center( + child: SizedBox( + height: 70 + (factor * 30), + width: 70 + (factor * 30), + child: Stack( + alignment: Alignment.center, + children: [ + SlideTransition( + position: _boxAnimController.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -0.2), + ), + ), + child: Image.asset( + 'assets/images/paycoins/ic_pay_coins_box.png', + ), + ), + SlideTransition( + position: _coinSlideController.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -2), + ), + ), + child: FadeTransition( + opacity: Tween(begin: 1, end: 0) + .animate(_coinFadeController), + child: Image.asset( + height: 35 + (factor * 15), + width: 35 + (factor * 15), + index == 0 + ? 'assets/images/paycoins/ic_coins_one.png' + : 'assets/images/paycoins/ic_coins_two.png', + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ), + Visibility( + visible: !_isPaying, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: GestureDetector( + onTap: _index == 1 + ? null + : () { + _onScroll(1); + }, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Image.asset( + width: 16, + height: 28, + _index == 1 + ? 'assets/images/paycoins/ic_right_disable.png' + : 'assets/images/paycoins/ic_right.png', + ), + ), + ), + ), + ], + ), + const SizedBox(height: 25), + GestureDetector( + behavior: HitTestBehavior.opaque, + onPanUpdate: _handlePanUpdate, + child: SizedBox( + width: double.infinity, + height: 140, + child: Center( + child: GestureDetector( + onPanUpdate: (e) => _handlePanUpdate(e, true), + child: ScaleTransition( + scale: _scale22Controller.drive( + Tween(begin: 1, end: 1.2), + ), + child: SlideTransition( + position: _slide22Controller.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -0.2), + ), + ), + child: SizedBox( + width: 100, + height: 140, + child: Image.asset( + _index == 0 + ? 'assets/images/paycoins/ic_22_gun_sister.png' + : 'assets/images/paycoins/ic_22_mario.png', + ), + ), + ), + ), + ), + ), + ), + ), + SizedBox( + height: (isV ? 50 : 0) + MediaQuery.of(context).padding.bottom), + ], + ); + + void _handlePanUpdate(DragUpdateDetails e, [bool needV = false]) { + if (needV && e.delta.dy.abs() > max(2, e.delta.dx.abs())) { + if (e.delta.dy < 0) { + setState(() { + _isPaying = true; + }); + _slide22Controller.forward().whenComplete(() { + _slide22Controller.reverse().whenComplete(() { + _boxAnimController.forward().whenComplete(() { + _boxAnimController.reverse(); + }); + _coinSlideController.forward().whenComplete(() { + _coinFadeController.forward().whenComplete(() { + Get.back(); + widget.callback(_index + 1); + }); + }); + }); + }); + } + } else if (e.delta.dx.abs() > max(2, e.delta.dy.abs())) { + if (e.delta.dx > 0) { + if (_index == 1) { + _onScroll(0); + setState(() {}); + } + } else { + if (_index == 0) { + _onScroll(1); + setState(() {}); + } + } + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1a020673f..8a8d236ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -252,6 +252,7 @@ flutter: - assets/images/logo/ - assets/images/live/ - assets/images/video/ + - assets/images/paycoins/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware