diff --git a/lib/app/modules/hi_help/views/hi_help_view.dart b/lib/app/modules/hi_help/views/hi_help_view.dart index f44c347..14c931a 100755 --- a/lib/app/modules/hi_help/views/hi_help_view.dart +++ b/lib/app/modules/hi_help/views/hi_help_view.dart @@ -13,76 +13,86 @@ import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart'; import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart'; import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart'; import '../../../routes/app_pages.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart'; -class HIHelpView extends GetView { +class HIHelpView extends GetView implements HasSwipeConfig { const HIHelpView({super.key}); + @override + SwipeConfig get swipeConfig => + SwipeConfig(enableLeft: true, onLeft: () => Get.back()); + @override Widget build(BuildContext context) { final ScrollController scrollController = ScrollController(); return HIBaseScaffold( - child: Column( - children: [ - Expanded( - child: Obx( - () => EasyRefresh( - // controller: controller.refreshController, - // onRefresh: controller.kr_onRefresh, - // onLoad: controller.kr_onLoadMore, - // header: const DeliveryHeader( - // triggerOffset: 50.0, - // springRebound: true, - // ), - // footer: const DeliveryFooter( - // triggerOffset: 50.0, - // springRebound: true, - // ), - onRefresh: null, // 设置为 null 来禁用下拉刷新 - onLoad: null, // 设置为 null 来禁用上拉加载 - child: Padding( - padding: EdgeInsets.only(right: 0.w), // 滚动条与内容间距 - child: HiFixedScrollbar( - controller: scrollController, - child: ListView.builder( + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Column( + children: [ + Expanded( + child: Obx( + () => EasyRefresh( + // controller: controller.refreshController, + // onRefresh: controller.kr_onRefresh, + // onLoad: controller.kr_onLoadMore, + // header: const DeliveryHeader( + // triggerOffset: 50.0, + // springRebound: true, + // ), + // footer: const DeliveryFooter( + // triggerOffset: 50.0, + // springRebound: true, + // ), + onRefresh: null, // 设置为 null 来禁用下拉刷新 + onLoad: null, // 设置为 null 来禁用上拉加载 + child: Padding( + padding: EdgeInsets.only(right: 0.w), // 滚动条与内容间距 + child: HiFixedScrollbar( controller: scrollController, - padding: EdgeInsets.symmetric(horizontal: 40.w), - itemCount: controller.kr_messages.length, - itemBuilder: (context, index) { - final message = controller.kr_messages[index]; - final collapsibleItemData = HICollapsibleItem( - title: message.title, - content: message.content, - ); - return Padding( - padding: EdgeInsets.only(bottom: 10.w), - child: HICollapsibleItemWidget(item: collapsibleItemData), - ); - }, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.only(left: 40.w - 20, right: 40.w), + itemCount: controller.kr_messages.length, + itemBuilder: (context, index) { + final message = controller.kr_messages[index]; + final collapsibleItemData = HICollapsibleItem( + title: message.title, + content: message.content, + ); + return Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: HICollapsibleItemWidget( + item: collapsibleItemData), + ); + }, + ), ), ), ), ), ), - ), - SizedBox(height: 60.w), - // 增加在线客服入口 - Padding( - padding: EdgeInsets.symmetric(horizontal: 40.w), - child: MenuListItem( - // 2. 创建一个 MenuItem 数据对象 - item: MenuItem( - iconName: 'icon-5', - title: '在线客服', - // 3. 使用 onTap 回调来处理跳转逻辑 - onTap: () { - Get.toNamed(Routes.KR_CRISP); - }, + SizedBox(height: 60.w), + // 增加在线客服入口 + Padding( + padding: EdgeInsets.symmetric(horizontal: 40.w), + child: MenuListItem( + // 2. 创建一个 MenuItem 数据对象 + item: MenuItem( + iconName: 'icon-5', + title: '在线客服', + // 3. 使用 onTap 回调来处理跳转逻辑 + onTap: () { + Get.toNamed(Routes.KR_CRISP); + }, + ), ), ), - ), - SizedBox(height: 30.w) - ], + SizedBox(height: 30.w) + ], + ), ), ); } diff --git a/lib/app/modules/hi_menu/views/hi_menu_view.dart b/lib/app/modules/hi_menu/views/hi_menu_view.dart index 33d410f..6f74d26 100755 --- a/lib/app/modules/hi_menu/views/hi_menu_view.dart +++ b/lib/app/modules/hi_menu/views/hi_menu_view.dart @@ -6,6 +6,8 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; import '../../../routes/app_pages.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart'; import '../controllers/hi_menu_controller.dart'; import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart'; import 'package:kaer_with_panels/app/localization/app_translations.dart'; @@ -16,9 +18,15 @@ import 'package:kaer_with_panels/app/common/app_run_data.dart'; import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart'; import 'package:kaer_with_panels/app/modules/hi_menu/widgets/user_info_card.dart'; -class HIMenuView extends GetView { +class HIMenuView extends GetView implements HasSwipeConfig { const HIMenuView({super.key}); + @override + SwipeConfig get swipeConfig => SwipeConfig( + enableRight: true, + onRight: () => Get.offNamed(Routes.KR_HOME), + ); + @override Widget build(BuildContext context) { return HIBaseScaffold( @@ -37,9 +45,13 @@ class HIMenuView extends GetView { mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容 children: [ Obx(() { - final account = KRAppRunData.getInstance().kr_account.value; - final isDeviceLogin = account != null && account.startsWith('9000'); - final accountText = (account ==null || isDeviceLogin) ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}'; + final account = + KRAppRunData.getInstance().kr_account.value; + final isDeviceLogin = + account != null && account.startsWith('9000'); + final accountText = (account == null || isDeviceLogin) + ? '待绑定' + : '${KRAppRunData.getInstance().kr_account.value.toString()}'; return UserInfoCard( controller: controller, userId: accountText, @@ -51,7 +63,8 @@ class HIMenuView extends GetView { // ListView.separated 现在也会继承 Padding 的约束 ListView.separated( shrinkWrap: true, // 让 ListView 高度自适应内容 - physics: const NeverScrollableScrollPhysics(), // 在 Stack 中,禁用其自身的滚动 + physics: + const NeverScrollableScrollPhysics(), // 在 Stack 中,禁用其自身的滚动 itemCount: _menuItems.length, // 渲染每一项 itemBuilder: (context, index) { @@ -68,7 +81,7 @@ class HIMenuView extends GetView { ], ), // 版本号信息 - Obx((){ + Obx(() { // 1. 从 Obx 的回调函数中 return 一个 Widget return Positioned( bottom: 40.0, @@ -91,7 +104,6 @@ class HIMenuView extends GetView { } } - const List _menuItems = [ MenuItem( iconName: 'icon-1', @@ -118,4 +130,4 @@ const List _menuItems = [ title: '在线客服', route: Routes.KR_CRISP, ), -]; \ No newline at end of file +]; diff --git a/lib/app/modules/kr_home/views/kr_home_view.dart b/lib/app/modules/kr_home/views/kr_home_view.dart index 6c1280b..0049b48 100755 --- a/lib/app/modules/kr_home/views/kr_home_view.dart +++ b/lib/app/modules/kr_home/views/kr_home_view.dart @@ -18,8 +18,22 @@ import 'kr_home_subscription_view.dart'; import './hi_animated_connect_button.dart'; import 'package:kaer_with_panels/app/services/global_overlay_service.dart'; -class KRHomeView extends StatefulWidget { +import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart'; +import 'package:kaer_with_panels/app/routes/app_pages.dart'; + +class KRHomeView extends StatefulWidget implements HasSwipeConfig { const KRHomeView({super.key}); + @override + SwipeConfig get swipeConfig => SwipeConfig( + enableLeft: true, + enableRight: true, + onLeft: () { + Get.toNamed(Routes.HI_MENU); + }, + onRight: () => + GlobalOverlayService.instance.triggerSubscriptionAnimation(), + ); @override State createState() => _KRHomeViewState(); diff --git a/lib/app/modules/kr_login/views/kr_login_view.dart b/lib/app/modules/kr_login/views/kr_login_view.dart index 2f9cf6c..efd0693 100755 --- a/lib/app/modules/kr_login/views/kr_login_view.dart +++ b/lib/app/modules/kr_login/views/kr_login_view.dart @@ -47,7 +47,7 @@ class KRLoginView extends GetView { SizedBox(height: 20.w), _buildUserInfoSection(), SizedBox(height: 12.w), - _buildContentByEntry(), + _buildBindEmailLayout(), SizedBox(height: 100.w), // 为底部帮助按钮留出空间 ], ), diff --git a/lib/app/modules/kr_message/views/kr_message_view.dart b/lib/app/modules/kr_message/views/kr_message_view.dart index 400d06b..8092c93 100755 --- a/lib/app/modules/kr_message/views/kr_message_view.dart +++ b/lib/app/modules/kr_message/views/kr_message_view.dart @@ -13,93 +13,104 @@ import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart'; import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart'; import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart'; import '../../../widgets/kr_simple_loading.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart'; -class KRMessageView extends GetView { +class KRMessageView extends GetView + implements HasSwipeConfig { const KRMessageView({super.key}); + @override + SwipeConfig get swipeConfig => + SwipeConfig(enableLeft: true, onLeft: () => Get.back()); + @override Widget build(BuildContext context) { final ScrollController scrollController = ScrollController(); return HIBaseScaffold( - child: Stack( - children: [ - // 主要内容区域 - Obx(() { - return Column( - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.only(bottom: 90.w), - child: EasyRefresh( - controller: controller.refreshController, - onRefresh: controller.kr_onRefresh, - onLoad: controller.kr_onLoadMore, - header: ClassicHeader( - dragText: '下拉刷新', - armedText: '释放刷新', - readyText: '正在刷新...', - processingText: '正在刷新...', - processedText: '刷新成功', - failedText: '刷新失败', - messageText: '最后更新于 %T', - textStyle: - TextStyle(color: Colors.white.withOpacity(0.7)), - messageStyle: TextStyle( - color: Colors.white.withOpacity(0.5), - fontSize: 12.sp), - iconTheme: - IconThemeData(color: Colors.white.withOpacity(0.7)), - ), - footer: ClassicFooter( - dragText: '上拉加载', - armedText: '释放加载', - readyText: '正在加载...', - processingText: '正在加载...', - processedText: '加载成功', - failedText: '加载失败', - noMoreText: '没有更多数据了', - messageText: '最后更新于 %T', - textStyle: - TextStyle(color: Colors.white.withOpacity(0.7)), - messageStyle: TextStyle( - color: Colors.white.withOpacity(0.5), - fontSize: 12.sp), - iconTheme: - IconThemeData(color: Colors.white.withOpacity(0.7)), - ), - child: Padding( - padding: EdgeInsets.only(right: 0.w), - child: HiFixedScrollbar( - controller: scrollController, - child: ListView.builder( + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Stack( + children: [ + // 主要内容区域 + Obx(() { + return Column( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: 90.w), + child: EasyRefresh( + controller: controller.refreshController, + onRefresh: controller.kr_onRefresh, + onLoad: controller.kr_onLoadMore, + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '正在刷新...', + processingText: '正在刷新...', + processedText: '刷新成功', + failedText: '刷新失败', + messageText: '最后更新于 %T', + textStyle: + TextStyle(color: Colors.white.withOpacity(0.7)), + messageStyle: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12.sp), + iconTheme: IconThemeData( + color: Colors.white.withOpacity(0.7)), + ), + footer: ClassicFooter( + dragText: '上拉加载', + armedText: '释放加载', + readyText: '正在加载...', + processingText: '正在加载...', + processedText: '加载成功', + failedText: '加载失败', + noMoreText: '没有更多数据了', + messageText: '最后更新于 %T', + textStyle: + TextStyle(color: Colors.white.withOpacity(0.7)), + messageStyle: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12.sp), + iconTheme: IconThemeData( + color: Colors.white.withOpacity(0.7)), + ), + child: Padding( + padding: EdgeInsets.only(right: 0.w), + child: HiFixedScrollbar( controller: scrollController, - padding: EdgeInsets.symmetric(horizontal: 40.w), - itemCount: controller.kr_messages.length, - itemBuilder: (context, index) { - final message = controller.kr_messages[index]; - final collapsibleItemData = HICollapsibleItem( - title: message.title, - content: [message.content], - ); - return Padding( - padding: EdgeInsets.only(bottom: 10.w), - child: HICollapsibleItemWidget( - item: collapsibleItemData), - ); - }, + child: ListView.builder( + controller: scrollController, + padding: + EdgeInsets.only(left: 40.w - 20, right: 40.w), + itemCount: controller.kr_messages.length, + itemBuilder: (context, index) { + final message = controller.kr_messages[index]; + final collapsibleItemData = HICollapsibleItem( + title: message.title, + content: [message.content], + ); + return Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: HICollapsibleItemWidget( + item: collapsibleItemData), + ); + }, + ), ), ), ), ), ), - ), - ], - ); - }), - // 底部帮助入口 - const HIHelpEntrance(), - ], + ], + ); + }), + // 底部帮助入口 + const HIHelpEntrance(), + ], + ), ), ); } diff --git a/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart b/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart index 10ba3ee..ac73aa1 100755 --- a/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart +++ b/lib/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart @@ -14,12 +14,19 @@ import '../../../widgets/kr_network_image.dart'; import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart'; import 'package:kaer_with_panels/app/widgets/kr_local_image.dart'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart'; import 'dart:convert'; /// 购买会员页面视图 -class KRPurchaseMembershipView extends GetView { +class KRPurchaseMembershipView extends GetView + implements HasSwipeConfig { const KRPurchaseMembershipView({super.key}); + @override + SwipeConfig get swipeConfig => + SwipeConfig(enableLeft: true, onLeft: () => Get.back()); + @override Widget build(BuildContext context) { return HIBaseScaffold( @@ -27,106 +34,129 @@ class KRPurchaseMembershipView extends GetView { title: '套餐选择', subtitle: '*所有套餐均不限流量不限速度', topContentAreaHeight: 110, - child: Obx(() { - return Stack( - children: [ - Padding( - padding: EdgeInsets.only(bottom: 80.0), - child: SingleChildScrollView( - child: Column( - children: [ - /*_kr_buildAccountSection(context),*/ - if (controller.kr_isLoading.value) - Container( - height: MediaQuery.of(context).size.height * 0.5, - child: Center( - child: KRSimpleLoading( - color: Colors.white, - size: 50.0, - ), - ), - ) - else if (controller.kr_plans.isEmpty) - Container( - height: MediaQuery.of(context).size.height * 0.5, - child: Center( - child: Text( - AppTranslations.kr_purchaseMembership.noData, - style: KrAppTextStyle( - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color, - ), - ), - ), - ) - else - Container( - margin: EdgeInsets.symmetric(horizontal: 40.0), - child: Column( - children: [ - // 套餐选择部分 + child: Stack( + children: [ + Positioned( + left: 0, + top: 0, + bottom: 0, + width: 20, + child: const _LeftEdgeSwipeBack(), + ), + Padding( + padding: const EdgeInsets.only(left: 20), + child: Stack( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 80.0), + child: Obx(() => SingleChildScrollView( + child: Column( + children: [ + /*_kr_buildAccountSection(context),*/ + if (controller.kr_isLoading.value) Container( - padding: EdgeInsets.all(0.0), + height: + MediaQuery.of(context).size.height * 0.5, + child: Center( + child: KRSimpleLoading( + color: Colors.white, + size: 50.0, + ), + ), + ) + else if (controller.kr_plans.isEmpty) + Container( + height: + MediaQuery.of(context).size.height * 0.5, + child: Center( + child: Text( + AppTranslations + .kr_purchaseMembership.noData, + style: KrAppTextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, + ), + ), + ), + ) + else + Container( + margin: EdgeInsets.only( + left: 40.0 - 20, right: 40.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - // 使用 List.generate 动态创建卡片列表 - children: List.generate( - controller.kr_getTotalOptionsCount( - controller.kr_plans[controller - .kr_selectedPlanIndex.value]), - (index) { - final plan = controller.kr_plans[ - controller - .kr_selectedPlanIndex.value]; - final discountIndex = - plan.kr_discount.isEmpty - ? null - : index; - // 使用 Padding 来为每个卡片添加底部的间距,模拟 mainAxisSpacing - return Padding( - padding: - EdgeInsets.only(bottom: 8.0), - child: SizedBox( - height: 130.0, - child: _kr_buildPlanOptionCard( - plan, - controller - .kr_selectedPlanIndex.value, - discountIndex, - context, - index, - ), + // 套餐选择部分 + Container( + padding: EdgeInsets.all(0.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + // 使用 List.generate 动态创建卡片列表 + children: List.generate( + controller.kr_getTotalOptionsCount( + controller.kr_plans[controller + .kr_selectedPlanIndex + .value]), + (index) { + final plan = controller + .kr_plans[ + controller + .kr_selectedPlanIndex + .value]; + final discountIndex = + plan.kr_discount.isEmpty + ? null + : index; + // 使用 Padding 来为每个卡片添加底部的间距,模拟 mainAxisSpacing + return Padding( + padding: EdgeInsets.only( + bottom: 8.0), + child: SizedBox( + height: 130.0, + child: + _kr_buildPlanOptionCard( + plan, + controller + .kr_selectedPlanIndex + .value, + discountIndex, + context, + index, + ), + ), + ); + }, ), - ); - }, + ), + ], ), ), ], ), ), - ], - ), + ], ), - ], + )), + ), + Positioned( + top: 160.0, // 距离顶部的距离 + right: 10.0, // 固定在右侧 20.w 的位置 + child: KrLocalImage( + imageName: 'purchase_slogan', + imageType: ImageType.svg, ), - )), - Positioned( - top: 160.0, // 距离顶部的距离 - right: 10.0, // 固定在右侧 20.w 的位置 - child: KrLocalImage( - imageName: 'purchase_slogan', - imageType: ImageType.svg, - ), + ), + const HIHelpEntrance(isLight: false) + ], ), - const HIHelpEntrance(isLight: false) - ], - ); - }), + ), + ], + ), ); } @@ -328,3 +358,47 @@ class KRPurchaseMembershipView extends GetView { ); } } + +class _LeftEdgeSwipeBack extends StatefulWidget { + const _LeftEdgeSwipeBack(); + @override + State<_LeftEdgeSwipeBack> createState() => _LeftEdgeSwipeBackState(); +} + +class _LeftEdgeSwipeBackState extends State<_LeftEdgeSwipeBack> { + Offset? _start; + Offset? _last; + DateTime? _startTime; + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: (details) { + _start = details.globalPosition; + _startTime = DateTime.now(); + KRLogUtil.kr_d( + 'Purchase left edge dragStart x=${details.globalPosition.dx}', + tag: 'PurchaseSwipe'); + }, + onHorizontalDragUpdate: (details) { + _last = details.globalPosition; + }, + onHorizontalDragEnd: (details) { + final durationMs = DateTime.now() + .difference(_startTime ?? DateTime.now()) + .inMilliseconds; + final distance = + _last != null && _start != null ? (_last!.dx - _start!.dx) : 0.0; + KRLogUtil.kr_d( + 'Purchase left edge dragEnd dur=$durationMs dist=$distance', + tag: 'PurchaseSwipe'); + if (distance > 20) { + Get.back(); + } + _start = null; + _last = null; + _startTime = null; + }, + ); + } +} diff --git a/lib/app/network/base_response.dart b/lib/app/network/base_response.dart index 6767ca5..1a479ff 100755 --- a/lib/app/network/base_response.dart +++ b/lib/app/network/base_response.dart @@ -27,9 +27,7 @@ class BaseResponse { final nonce = dataMap['time'] ?? ""; // 判断是否需要解密:根据站点配置的 enable_security 字段 - final shouldDecrypt = KRSiteConfigService().isDeviceSecurityEnabled(); - - if (shouldDecrypt && cipherText.isNotEmpty && nonce.isNotEmpty) { + if (cipherText.isNotEmpty && nonce.isNotEmpty) { try { if (kDebugMode) { print('═══════════════════════════════════════'); diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 15f6e24..7121620 100755 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -43,6 +43,7 @@ import '../modules/kr_splash/views/kr_splash_view.dart'; import '../modules/kr_device_management/bindings/kr_device_management_binding.dart'; import '../modules/kr_device_management/views/kr_device_management_view.dart'; import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart'; +import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart'; part 'app_routes.dart'; @@ -54,20 +55,23 @@ class AppPages { static final routes = [ GetPage( name: Routes.KR_SPLASH, - page: () => const KRSplashView(), + page: () => SwipeWrapper.detect(() => const KRSplashView()), binding: KRSplashBinding(), + popGesture: false, transition: Transition.fade, transitionDuration: const Duration(milliseconds: 500), ), GetPage( name: _Paths.KR_MAIN, - page: () => const KRMainView(), + page: () => SwipeWrapper.detect(() => const KRMainView()), binding: KRMainBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_HOME, - page: () => KRHomeView(), + page: () => SwipeWrapper.detect(() => KRHomeView()), binding: KRHomeBinding(), + popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 customTransition: SlideOutOnlyTransition( direction: SlideDirection.leftToRight, @@ -76,8 +80,9 @@ class AppPages { ), GetPage( name: _Paths.HI_MENU, - page: () => const HIMenuView(), + page: () => SwipeWrapper.detect(() => const HIMenuView()), binding: HIMenuBinding(), + popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 customTransition: ContextAwareSlideTransition( targetRoute: _Paths.HI_MENU, @@ -86,90 +91,107 @@ class AppPages { ), GetPage( name: _Paths.MR_LOGIN, - page: () => const KRLoginView(), + page: () => SwipeWrapper.detect(() => const KRLoginView()), binding: MrLoginBinding(), + popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 ), GetPage( name: _Paths.KR_SETTING, - page: () => const KRSettingView(), + page: () => SwipeWrapper.detect(() => const KRSettingView()), binding: KRSettingBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_USER_INFO, - page: () => const KRUserInfoView(), + page: () => SwipeWrapper.detect(() => const KRUserInfoView()), binding: KRUserInfoBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_INVITE, - page: () => const KRInviteView(), + page: () => SwipeWrapper.detect(() => const KRInviteView()), binding: KRInviteBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_STATISTICS, - page: () => const KRStatisticsView(), + page: () => SwipeWrapper.detect(() => const KRStatisticsView()), binding: KRStatisticsBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_LANGUAGE_SELECTOR, - page: () => const KRLanguageSelectorView(), + page: () => SwipeWrapper.detect(() => const KRLanguageSelectorView()), binding: KRLanguageSelectorBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_COUNTRY_SELECTOR, - page: () => const KRCountrySelectorView(), + page: () => SwipeWrapper.detect(() => const KRCountrySelectorView()), binding: KRCountrySelectorBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_PURCHASE_MEMBERSHIP, - page: () => const KRPurchaseMembershipView(), + page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()), binding: KRPurchaseMembershipBinding(), + popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 ), GetPage( name: _Paths.KR_MESSAGE, - page: () => const KRMessageView(), + page: () => SwipeWrapper.detect(() => const KRMessageView()), binding: KrMessageBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_DELETE_ACCOUNT, - page: () => const KRDeleteAccountView(), + page: () => SwipeWrapper.detect(() => const KRDeleteAccountView()), binding: KrDeleteAccountBinding(), + popGesture: false, ), GetPage( name: Routes.KR_WEBVIEW, - page: () => const KRWebView(), + page: () => SwipeWrapper.detect(() => const KRWebView()), binding: KRWebViewBinding(), + popGesture: false, ), GetPage( name: Routes.KR_ORDER_STATUS, - page: () => const KROrderStatusView(), + page: () => SwipeWrapper.detect(() => const KROrderStatusView()), binding: KROrderStatusBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_CRISP, - page: () => const KRCrispView(), + page: () => SwipeWrapper.detect(() => const KRCrispView()), binding: KRCrispBinding(), + popGesture: false, ), GetPage( name: _Paths.KR_DEVICE_MANAGEMENT, - page: () => const KRDeviceManagementView(), + page: () => SwipeWrapper.detect(() => const KRDeviceManagementView()), binding: KRDeviceManagementBinding(), + popGesture: false, ), GetPage( name: _Paths.HI_NODE_LIST, - page: () => const HINodePageView(), + page: () => SwipeWrapper.detect(() => const HINodePageView()), binding: HiNodeListBinding(), + popGesture: false, ), GetPage( name: _Paths.HI_HELP, - page: () => const HIHelpView(), + page: () => SwipeWrapper.detect(() => const HIHelpView()), binding: HIHelpBinding(), + popGesture: false, ), GetPage( name: _Paths.HI_USER_INFO, - page: () => const HIUserInfoView(), + page: () => SwipeWrapper.detect(() => const HIUserInfoView()), binding: HIUserInfoBinding(), + popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 ), ]; diff --git a/lib/app/widgets/hi_edge_swipe_detector.dart b/lib/app/widgets/hi_edge_swipe_detector.dart new file mode 100644 index 0000000..7a62ea1 --- /dev/null +++ b/lib/app/widgets/hi_edge_swipe_detector.dart @@ -0,0 +1,156 @@ +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'swipe/swipe_controller.dart'; +import 'swipe/swipe_config.dart'; +import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; + +class HIEdgeSwipeDetector extends StatefulWidget { + final Widget child; + final double reservedEdgeWidth; + final double detectionEdgeWidth; + final double minSwipeDistance; + final double minSwipeVelocity; + final SwipeConfig? config; + const HIEdgeSwipeDetector( + {super.key, + required this.child, + this.reservedEdgeWidth = 0, + this.detectionEdgeWidth = 20, + this.minSwipeDistance = 20, + this.minSwipeVelocity = 300, + this.config}); + @override + State createState() => _HIEdgeSwipeDetectorState(); +} + +class _HIEdgeSwipeDetectorState extends State { + Offset? _start; + Offset? _last; + DateTime? _startTime; + final SwipeController _controller = + Get.put(SwipeController(), permanent: true); + bool _fromLeft = false; + bool _fromRight = false; + @override + Widget build(BuildContext context) { + KRLogUtil.kr_d('HIEdgeSwipeDetector build'); + return Stack( + fit: StackFit.expand, + children: [ + widget.child, + Positioned( + left: 0, + top: 0, + bottom: 0, + width: widget.detectionEdgeWidth, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: (details) { + _fromLeft = true; + _fromRight = false; + _start = details.globalPosition; + _startTime = DateTime.now(); + KRLogUtil.kr_d('dragStart LEFT x=${details.globalPosition.dx}', + tag: 'HIEdgeSwipeDetector'); + }, + onHorizontalDragUpdate: (details) { + _last = details.globalPosition; + KRLogUtil.kr_d('dragUpdate LEFT dx=${details.delta.dx}', + tag: 'HIEdgeSwipeDetector'); + }, + onHorizontalDragEnd: (details) { + if (_start == null || _startTime == null) return; + final durationMs = + DateTime.now().difference(_startTime!).inMilliseconds; + final velocity = details.velocity.pixelsPerSecond.dx.abs(); + final distance = _last != null ? (_last!.dx - _start!.dx) : 0.0; + final okByVelocity = velocity >= widget.minSwipeVelocity; + final okByDistance = distance.abs() >= widget.minSwipeDistance; + KRLogUtil.kr_d( + 'dragEnd LEFT vel=${details.velocity.pixelsPerSecond.dx} dur=$durationMs dist=$distance okV=$okByVelocity okD=$okByDistance', + tag: 'HIEdgeSwipeDetector'); + if (!okByVelocity && !okByDistance) { + _reset(); + return; + } + if (distance > 0) { + final cfg = widget.config; + if (cfg == null || !cfg.enableLeft) { + _reset(); + return; + } + if (cfg.onLeft != null) { + cfg.onLeft!(); + } else { + Get.back(); + } + } + _reset(); + }, + ), + ), + Positioned( + right: 0, + top: 0, + bottom: 0, + width: widget.detectionEdgeWidth, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: (details) { + _fromLeft = false; + _fromRight = true; + _start = details.globalPosition; + _startTime = DateTime.now(); + KRLogUtil.kr_d('dragStart RIGHT x=${details.globalPosition.dx}', + tag: 'HIEdgeSwipeDetector'); + }, + onHorizontalDragUpdate: (details) { + _last = details.globalPosition; + KRLogUtil.kr_d('dragUpdate RIGHT dx=${details.delta.dx}', + tag: 'HIEdgeSwipeDetector'); + }, + onHorizontalDragEnd: (details) { + if (_start == null || _startTime == null) return; + final durationMs = + DateTime.now().difference(_startTime!).inMilliseconds; + final velocity = details.velocity.pixelsPerSecond.dx.abs(); + final distance = _last != null ? (_last!.dx - _start!.dx) : 0.0; + final okByVelocity = velocity >= widget.minSwipeVelocity; + final okByDistance = distance.abs() >= widget.minSwipeDistance; + KRLogUtil.kr_d( + 'dragEnd RIGHT vel=${details.velocity.pixelsPerSecond.dx} dur=$durationMs dist=$distance okV=$okByVelocity okD=$okByDistance', + tag: 'HIEdgeSwipeDetector'); + if (!okByVelocity && !okByDistance) { + _reset(); + return; + } + if (distance < 0) { + final cfg = widget.config; + if (cfg == null || !cfg.enableRight) { + _reset(); + return; + } + if (cfg.onRight != null) { + cfg.onRight!(); + } else { + _controller.openRightMenu(); + } + } + _reset(); + }, + ), + ), + ], + ); + } + + void _reset() { + _start = null; + _last = null; + _startTime = null; + _fromLeft = false; + _fromRight = false; + } +} diff --git a/lib/app/widgets/swipe/has_swipe_config.dart b/lib/app/widgets/swipe/has_swipe_config.dart new file mode 100644 index 0000000..9fb4d9b --- /dev/null +++ b/lib/app/widgets/swipe/has_swipe_config.dart @@ -0,0 +1,6 @@ +import 'swipe_config.dart'; + +abstract class HasSwipeConfig { + SwipeConfig get swipeConfig; +} + diff --git a/lib/app/widgets/swipe/swipe_config.dart b/lib/app/widgets/swipe/swipe_config.dart new file mode 100644 index 0000000..e95ed9c --- /dev/null +++ b/lib/app/widgets/swipe/swipe_config.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; + +class SwipeConfig { + final bool enableLeft; + final bool enableRight; + final VoidCallback? onLeft; + final VoidCallback? onRight; + const SwipeConfig({this.enableLeft = false, this.enableRight = false, this.onLeft, this.onRight}); +} + diff --git a/lib/app/widgets/swipe/swipe_controller.dart b/lib/app/widgets/swipe/swipe_controller.dart new file mode 100644 index 0000000..a9db9a5 --- /dev/null +++ b/lib/app/widgets/swipe/swipe_controller.dart @@ -0,0 +1,61 @@ +import 'package:get/get.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get_navigation/src/extension_navigation.dart'; +import 'package:kaer_with_panels/app/routes/app_pages.dart'; +import 'swipe_config.dart'; +import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; + +class SwipeController extends GetxController { + final Rxn currentConfig = Rxn(); + final RxInt currentPageIndex = 0.obs; + final RxBool leftMenuOpen = false.obs; + final RxBool rightMenuOpen = false.obs; + void setConfig(SwipeConfig? config) { + currentConfig.value = config; + } + + void setPageIndex(int index) { + currentPageIndex.value = index; + } + + void closeMenus() { + leftMenuOpen.value = false; + rightMenuOpen.value = false; + } + + void openLeftMenu() { + KRLogUtil.kr_d('openLeftMenu', tag: 'SwipeController'); + leftMenuOpen.value = true; + Get.toNamed(Routes.HI_MENU); + } + + void openRightMenu() { + KRLogUtil.kr_d('openRightMenu', tag: 'SwipeController'); + rightMenuOpen.value = true; + Get.toNamed(Routes.KR_SETTING); + } + + void triggerLeft() { + final cfg = currentConfig.value; + if (cfg == null || !cfg.enableLeft) return; + KRLogUtil.kr_d('call11 onLeft$cfg', tag: 'SwipeController'); + + if (cfg.onLeft != null) { + KRLogUtil.kr_d('call onLeft', tag: 'SwipeController'); + cfg.onLeft!.call(); + return; + } + Get.back(); + } + + void triggerRight() { + final cfg = currentConfig.value; + if (cfg == null || !cfg.enableRight) return; + if (cfg.onRight != null) { + KRLogUtil.kr_d('call onRight', tag: 'SwipeController'); + cfg.onRight!.call(); + return; + } + openRightMenu(); + } +} diff --git a/lib/app/widgets/swipe/swipe_wrapper.dart b/lib/app/widgets/swipe/swipe_wrapper.dart new file mode 100644 index 0000000..ae8ac5e --- /dev/null +++ b/lib/app/widgets/swipe/swipe_wrapper.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../hi_edge_swipe_detector.dart'; +import 'swipe_config.dart'; +import 'has_swipe_config.dart'; +import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; + +class SwipeWrapper extends StatelessWidget { + final Widget child; + final SwipeConfig config; + const SwipeWrapper({super.key, required this.child, required this.config}); + + static Widget detect(Widget Function() builder) { + final w = builder(); + SwipeConfig effective = const SwipeConfig(); + if (w is HasSwipeConfig) { + effective = (w as HasSwipeConfig).swipeConfig; + } else if (GetPlatform.isIOS) { + effective = SwipeConfig(enableLeft: true, onLeft: () => Get.back()); + } + final enabled = effective.enableLeft || + effective.enableRight || + effective.onLeft != null || + effective.onRight != null; + KRLogUtil.kr_d( + 'SwipeWrapper.detect enabled=$enabled left=${effective.enableLeft} right=${effective.enableRight}', + tag: 'SwipeWrapper'); + if (!enabled) return w; + return HIEdgeSwipeDetector(child: w, config: effective); + } + + @override + Widget build(BuildContext context) { + final effective = config; + final enabled = effective.enableLeft || + effective.enableRight || + effective.onLeft != null || + effective.onRight != null; + KRLogUtil.kr_d( + 'SwipeWrapper.build enabled=$enabled left=${effective.enableLeft} right=${effective.enableRight}', + tag: 'SwipeWrapper'); + if (!enabled) return child; + return HIEdgeSwipeDetector(child: child, config: effective); + } +}