feat: 增加侧滑事件

This commit is contained in:
speakeloudest 2025-12-02 06:09:25 -08:00
parent 8e27ddeded
commit e82480b937
13 changed files with 669 additions and 250 deletions

View File

@ -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/widgets/hi_fixed_scrollbar.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/hi_menu_list_item.dart';
import '../../../routes/app_pages.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<HIHelpController> { class HIHelpView extends GetView<HIHelpController> implements HasSwipeConfig {
const HIHelpView({super.key}); const HIHelpView({super.key});
@override
SwipeConfig get swipeConfig =>
SwipeConfig(enableLeft: true, onLeft: () => Get.back());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
return HIBaseScaffold( return HIBaseScaffold(
child: Column( child: Padding(
children: [ padding: const EdgeInsets.only(left: 20),
Expanded( child: Column(
child: Obx( children: [
() => EasyRefresh( Expanded(
// controller: controller.refreshController, child: Obx(
// onRefresh: controller.kr_onRefresh, () => EasyRefresh(
// onLoad: controller.kr_onLoadMore, // controller: controller.refreshController,
// header: const DeliveryHeader( // onRefresh: controller.kr_onRefresh,
// triggerOffset: 50.0, // onLoad: controller.kr_onLoadMore,
// springRebound: true, // header: const DeliveryHeader(
// ), // triggerOffset: 50.0,
// footer: const DeliveryFooter( // springRebound: true,
// triggerOffset: 50.0, // ),
// springRebound: true, // footer: const DeliveryFooter(
// ), // triggerOffset: 50.0,
onRefresh: null, // null // springRebound: true,
onLoad: null, // null // ),
child: Padding( onRefresh: null, // null
padding: EdgeInsets.only(right: 0.w), // onLoad: null, // null
child: HiFixedScrollbar( child: Padding(
controller: scrollController, padding: EdgeInsets.only(right: 0.w), //
child: ListView.builder( child: HiFixedScrollbar(
controller: scrollController, controller: scrollController,
padding: EdgeInsets.symmetric(horizontal: 40.w), child: ListView.builder(
itemCount: controller.kr_messages.length, controller: scrollController,
itemBuilder: (context, index) { padding: EdgeInsets.only(left: 40.w - 20, right: 40.w),
final message = controller.kr_messages[index]; itemCount: controller.kr_messages.length,
final collapsibleItemData = HICollapsibleItem( itemBuilder: (context, index) {
title: message.title, final message = controller.kr_messages[index];
content: message.content, final collapsibleItemData = HICollapsibleItem(
); title: message.title,
return Padding( content: message.content,
padding: EdgeInsets.only(bottom: 10.w), );
child: HICollapsibleItemWidget(item: collapsibleItemData), return Padding(
); padding: EdgeInsets.only(bottom: 10.w),
}, child: HICollapsibleItemWidget(
item: collapsibleItemData),
);
},
),
), ),
), ),
), ),
), ),
), ),
), SizedBox(height: 60.w),
SizedBox(height: 60.w), // 线
// 线 Padding(
Padding( padding: EdgeInsets.symmetric(horizontal: 40.w),
padding: EdgeInsets.symmetric(horizontal: 40.w), child: MenuListItem(
child: MenuListItem( // 2. MenuItem
// 2. MenuItem item: MenuItem(
item: MenuItem( iconName: 'icon-5',
iconName: 'icon-5', title: '在线客服',
title: '在线客服', // 3. 使 onTap
// 3. 使 onTap onTap: () {
onTap: () { Get.toNamed(Routes.KR_CRISP);
Get.toNamed(Routes.KR_CRISP); },
}, ),
), ),
), ),
), SizedBox(height: 30.w)
SizedBox(height: 30.w) ],
], ),
), ),
); );
} }

View File

@ -6,6 +6,8 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import '../../../routes/app_pages.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 '../controllers/hi_menu_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart'; import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/localization/app_translations.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/hi_menu_list_item.dart';
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/user_info_card.dart'; import 'package:kaer_with_panels/app/modules/hi_menu/widgets/user_info_card.dart';
class HIMenuView extends GetView<HIMenuController> { class HIMenuView extends GetView<HIMenuController> implements HasSwipeConfig {
const HIMenuView({super.key}); const HIMenuView({super.key});
@override
SwipeConfig get swipeConfig => SwipeConfig(
enableRight: true,
onRight: () => Get.offNamed(Routes.KR_HOME),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return HIBaseScaffold( return HIBaseScaffold(
@ -37,9 +45,13 @@ class HIMenuView extends GetView<HIMenuController> {
mainAxisSize: MainAxisSize.min, // Column mainAxisSize: MainAxisSize.min, // Column
children: [ children: [
Obx(() { Obx(() {
final account = KRAppRunData.getInstance().kr_account.value; final account =
final isDeviceLogin = account != null && account.startsWith('9000'); KRAppRunData.getInstance().kr_account.value;
final accountText = (account ==null || isDeviceLogin) ? '待绑定' : '${KRAppRunData.getInstance().kr_account.value.toString()}'; final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = (account == null || isDeviceLogin)
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
return UserInfoCard( return UserInfoCard(
controller: controller, controller: controller,
userId: accountText, userId: accountText,
@ -51,7 +63,8 @@ class HIMenuView extends GetView<HIMenuController> {
// ListView.separated Padding // ListView.separated Padding
ListView.separated( ListView.separated(
shrinkWrap: true, // ListView shrinkWrap: true, // ListView
physics: const NeverScrollableScrollPhysics(), // Stack physics:
const NeverScrollableScrollPhysics(), // Stack
itemCount: _menuItems.length, itemCount: _menuItems.length,
// //
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -68,7 +81,7 @@ class HIMenuView extends GetView<HIMenuController> {
], ],
), ),
// //
Obx((){ Obx(() {
// 1. Obx return Widget // 1. Obx return Widget
return Positioned( return Positioned(
bottom: 40.0, bottom: 40.0,
@ -91,7 +104,6 @@ class HIMenuView extends GetView<HIMenuController> {
} }
} }
const List<MenuItem> _menuItems = [ const List<MenuItem> _menuItems = [
MenuItem( MenuItem(
iconName: 'icon-1', iconName: 'icon-1',
@ -118,4 +130,4 @@ const List<MenuItem> _menuItems = [
title: '在线客服', title: '在线客服',
route: Routes.KR_CRISP, route: Routes.KR_CRISP,
), ),
]; ];

View File

@ -18,8 +18,22 @@ import 'kr_home_subscription_view.dart';
import './hi_animated_connect_button.dart'; import './hi_animated_connect_button.dart';
import 'package:kaer_with_panels/app/services/global_overlay_service.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}); const KRHomeView({super.key});
@override
SwipeConfig get swipeConfig => SwipeConfig(
enableLeft: true,
enableRight: true,
onLeft: () {
Get.toNamed(Routes.HI_MENU);
},
onRight: () =>
GlobalOverlayService.instance.triggerSubscriptionAnimation(),
);
@override @override
State<KRHomeView> createState() => _KRHomeViewState(); State<KRHomeView> createState() => _KRHomeViewState();

View File

@ -47,7 +47,7 @@ class KRLoginView extends GetView<KRLoginController> {
SizedBox(height: 20.w), SizedBox(height: 20.w),
_buildUserInfoSection(), _buildUserInfoSection(),
SizedBox(height: 12.w), SizedBox(height: 12.w),
_buildContentByEntry(), _buildBindEmailLayout(),
SizedBox(height: 100.w), // SizedBox(height: 100.w), //
], ],
), ),

View File

@ -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_collapsible_list.dart';
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart'; import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
import '../../../widgets/kr_simple_loading.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<KRMessageController> { class KRMessageView extends GetView<KRMessageController>
implements HasSwipeConfig {
const KRMessageView({super.key}); const KRMessageView({super.key});
@override
SwipeConfig get swipeConfig =>
SwipeConfig(enableLeft: true, onLeft: () => Get.back());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
return HIBaseScaffold( return HIBaseScaffold(
child: Stack( child: Padding(
children: [ padding: const EdgeInsets.only(left: 20),
// child: Stack(
Obx(() { children: [
return Column( //
children: [ Obx(() {
Expanded( return Column(
child: Padding( children: [
padding: EdgeInsets.only(bottom: 90.w), Expanded(
child: EasyRefresh( child: Padding(
controller: controller.refreshController, padding: EdgeInsets.only(bottom: 90.w),
onRefresh: controller.kr_onRefresh, child: EasyRefresh(
onLoad: controller.kr_onLoadMore, controller: controller.refreshController,
header: ClassicHeader( onRefresh: controller.kr_onRefresh,
dragText: '下拉刷新', onLoad: controller.kr_onLoadMore,
armedText: '释放刷新', header: ClassicHeader(
readyText: '正在刷新...', dragText: '下拉刷新',
processingText: '正在刷新...', armedText: '释放刷新',
processedText: '刷新成功', readyText: '正在刷新...',
failedText: '刷新失败', processingText: '正在刷新...',
messageText: '最后更新于 %T', processedText: '刷新成功',
textStyle: failedText: '刷新失败',
TextStyle(color: Colors.white.withOpacity(0.7)), messageText: '最后更新于 %T',
messageStyle: TextStyle( textStyle:
color: Colors.white.withOpacity(0.5), TextStyle(color: Colors.white.withOpacity(0.7)),
fontSize: 12.sp), messageStyle: TextStyle(
iconTheme: color: Colors.white.withOpacity(0.5),
IconThemeData(color: Colors.white.withOpacity(0.7)), fontSize: 12.sp),
), iconTheme: IconThemeData(
footer: ClassicFooter( color: Colors.white.withOpacity(0.7)),
dragText: '上拉加载', ),
armedText: '释放加载', footer: ClassicFooter(
readyText: '正在加载...', dragText: '上拉加载',
processingText: '正在加载...', armedText: '释放加载',
processedText: '加载成功', readyText: '正在加载...',
failedText: '加载失败', processingText: '正在加载...',
noMoreText: '没有更多数据了', processedText: '加载成功',
messageText: '最后更新于 %T', failedText: '加载失败',
textStyle: noMoreText: '没有更多数据了',
TextStyle(color: Colors.white.withOpacity(0.7)), messageText: '最后更新于 %T',
messageStyle: TextStyle( textStyle:
color: Colors.white.withOpacity(0.5), TextStyle(color: Colors.white.withOpacity(0.7)),
fontSize: 12.sp), messageStyle: TextStyle(
iconTheme: color: Colors.white.withOpacity(0.5),
IconThemeData(color: Colors.white.withOpacity(0.7)), fontSize: 12.sp),
), iconTheme: IconThemeData(
child: Padding( color: Colors.white.withOpacity(0.7)),
padding: EdgeInsets.only(right: 0.w), ),
child: HiFixedScrollbar( child: Padding(
controller: scrollController, padding: EdgeInsets.only(right: 0.w),
child: ListView.builder( child: HiFixedScrollbar(
controller: scrollController, controller: scrollController,
padding: EdgeInsets.symmetric(horizontal: 40.w), child: ListView.builder(
itemCount: controller.kr_messages.length, controller: scrollController,
itemBuilder: (context, index) { padding:
final message = controller.kr_messages[index]; EdgeInsets.only(left: 40.w - 20, right: 40.w),
final collapsibleItemData = HICollapsibleItem( itemCount: controller.kr_messages.length,
title: message.title, itemBuilder: (context, index) {
content: [message.content], final message = controller.kr_messages[index];
); final collapsibleItemData = HICollapsibleItem(
return Padding( title: message.title,
padding: EdgeInsets.only(bottom: 10.w), content: [message.content],
child: HICollapsibleItemWidget( );
item: collapsibleItemData), return Padding(
); padding: EdgeInsets.only(bottom: 10.w),
}, child: HICollapsibleItemWidget(
item: collapsibleItemData),
);
},
),
), ),
), ),
), ),
), ),
), ),
), ],
], );
); }),
}), //
// const HIHelpEntrance(),
const HIHelpEntrance(), ],
], ),
), ),
); );
} }

View File

@ -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/dialogs/kr_dialog.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.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/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'; import 'dart:convert';
/// ///
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> { class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
implements HasSwipeConfig {
const KRPurchaseMembershipView({super.key}); const KRPurchaseMembershipView({super.key});
@override
SwipeConfig get swipeConfig =>
SwipeConfig(enableLeft: true, onLeft: () => Get.back());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return HIBaseScaffold( return HIBaseScaffold(
@ -27,106 +34,129 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
title: '套餐选择', title: '套餐选择',
subtitle: '*所有套餐均不限流量不限速度', subtitle: '*所有套餐均不限流量不限速度',
topContentAreaHeight: 110, topContentAreaHeight: 110,
child: Obx(() { child: Stack(
return Stack( children: [
children: [ Positioned(
Padding( left: 0,
padding: EdgeInsets.only(bottom: 80.0), top: 0,
child: SingleChildScrollView( bottom: 0,
child: Column( width: 20,
children: [ child: const _LeftEdgeSwipeBack(),
/*_kr_buildAccountSection(context),*/ ),
if (controller.kr_isLoading.value) Padding(
Container( padding: const EdgeInsets.only(left: 20),
height: MediaQuery.of(context).size.height * 0.5, child: Stack(
child: Center( children: [
child: KRSimpleLoading( Padding(
color: Colors.white, padding: EdgeInsets.only(bottom: 80.0),
size: 50.0, child: Obx(() => SingleChildScrollView(
), child: Column(
), children: [
) /*_kr_buildAccountSection(context),*/
else if (controller.kr_plans.isEmpty) if (controller.kr_isLoading.value)
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: [
//
Container( 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Column( //
// 使 List.generate Container(
children: List.generate( padding: EdgeInsets.all(0.0),
controller.kr_getTotalOptionsCount( child: Column(
controller.kr_plans[controller crossAxisAlignment:
.kr_selectedPlanIndex.value]), CrossAxisAlignment.start,
(index) { children: [
final plan = controller.kr_plans[ Column(
controller // 使 List.generate
.kr_selectedPlanIndex.value]; children: List.generate(
final discountIndex = controller.kr_getTotalOptionsCount(
plan.kr_discount.isEmpty controller.kr_plans[controller
? null .kr_selectedPlanIndex
: index; .value]),
// 使 Padding mainAxisSpacing (index) {
return Padding( final plan = controller
padding: .kr_plans[
EdgeInsets.only(bottom: 8.0), controller
child: SizedBox( .kr_selectedPlanIndex
height: 130.0, .value];
child: _kr_buildPlanOptionCard( final discountIndex =
plan, plan.kr_discount.isEmpty
controller ? null
.kr_selectedPlanIndex.value, : index;
discountIndex, // 使 Padding mainAxisSpacing
context, return Padding(
index, 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( const HIHelpEntrance(isLight: false)
top: 160.0, // ],
right: 10.0, // 20.w
child: KrLocalImage(
imageName: 'purchase_slogan',
imageType: ImageType.svg,
),
), ),
const HIHelpEntrance(isLight: false) ),
], ],
); ),
}),
); );
} }
@ -328,3 +358,47 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
); );
} }
} }
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;
},
);
}
}

View File

@ -27,9 +27,7 @@ class BaseResponse<T> {
final nonce = dataMap['time'] ?? ""; final nonce = dataMap['time'] ?? "";
// enable_security // enable_security
final shouldDecrypt = KRSiteConfigService().isDeviceSecurityEnabled(); if (cipherText.isNotEmpty && nonce.isNotEmpty) {
if (shouldDecrypt && cipherText.isNotEmpty && nonce.isNotEmpty) {
try { try {
if (kDebugMode) { if (kDebugMode) {
print('═══════════════════════════════════════'); print('═══════════════════════════════════════');

View File

@ -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/bindings/kr_device_management_binding.dart';
import '../modules/kr_device_management/views/kr_device_management_view.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/routes/transitions/slide_transparent_transition.dart';
import 'package:kaer_with_panels/app/widgets/swipe/swipe_wrapper.dart';
part 'app_routes.dart'; part 'app_routes.dart';
@ -54,20 +55,23 @@ class AppPages {
static final routes = [ static final routes = [
GetPage( GetPage(
name: Routes.KR_SPLASH, name: Routes.KR_SPLASH,
page: () => const KRSplashView(), page: () => SwipeWrapper.detect(() => const KRSplashView()),
binding: KRSplashBinding(), binding: KRSplashBinding(),
popGesture: false,
transition: Transition.fade, transition: Transition.fade,
transitionDuration: const Duration(milliseconds: 500), transitionDuration: const Duration(milliseconds: 500),
), ),
GetPage( GetPage(
name: _Paths.KR_MAIN, name: _Paths.KR_MAIN,
page: () => const KRMainView(), page: () => SwipeWrapper.detect(() => const KRMainView()),
binding: KRMainBinding(), binding: KRMainBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_HOME, name: _Paths.KR_HOME,
page: () => KRHomeView(), page: () => SwipeWrapper.detect(() => KRHomeView()),
binding: KRHomeBinding(), binding: KRHomeBinding(),
popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
customTransition: SlideOutOnlyTransition( customTransition: SlideOutOnlyTransition(
direction: SlideDirection.leftToRight, direction: SlideDirection.leftToRight,
@ -76,8 +80,9 @@ class AppPages {
), ),
GetPage( GetPage(
name: _Paths.HI_MENU, name: _Paths.HI_MENU,
page: () => const HIMenuView(), page: () => SwipeWrapper.detect(() => const HIMenuView()),
binding: HIMenuBinding(), binding: HIMenuBinding(),
popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
customTransition: ContextAwareSlideTransition( customTransition: ContextAwareSlideTransition(
targetRoute: _Paths.HI_MENU, targetRoute: _Paths.HI_MENU,
@ -86,90 +91,107 @@ class AppPages {
), ),
GetPage( GetPage(
name: _Paths.MR_LOGIN, name: _Paths.MR_LOGIN,
page: () => const KRLoginView(), page: () => SwipeWrapper.detect(() => const KRLoginView()),
binding: MrLoginBinding(), binding: MrLoginBinding(),
popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
), ),
GetPage( GetPage(
name: _Paths.KR_SETTING, name: _Paths.KR_SETTING,
page: () => const KRSettingView(), page: () => SwipeWrapper.detect(() => const KRSettingView()),
binding: KRSettingBinding(), binding: KRSettingBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_USER_INFO, name: _Paths.KR_USER_INFO,
page: () => const KRUserInfoView(), page: () => SwipeWrapper.detect(() => const KRUserInfoView()),
binding: KRUserInfoBinding(), binding: KRUserInfoBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_INVITE, name: _Paths.KR_INVITE,
page: () => const KRInviteView(), page: () => SwipeWrapper.detect(() => const KRInviteView()),
binding: KRInviteBinding(), binding: KRInviteBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_STATISTICS, name: _Paths.KR_STATISTICS,
page: () => const KRStatisticsView(), page: () => SwipeWrapper.detect(() => const KRStatisticsView()),
binding: KRStatisticsBinding(), binding: KRStatisticsBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_LANGUAGE_SELECTOR, name: _Paths.KR_LANGUAGE_SELECTOR,
page: () => const KRLanguageSelectorView(), page: () => SwipeWrapper.detect(() => const KRLanguageSelectorView()),
binding: KRLanguageSelectorBinding(), binding: KRLanguageSelectorBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_COUNTRY_SELECTOR, name: _Paths.KR_COUNTRY_SELECTOR,
page: () => const KRCountrySelectorView(), page: () => SwipeWrapper.detect(() => const KRCountrySelectorView()),
binding: KRCountrySelectorBinding(), binding: KRCountrySelectorBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_PURCHASE_MEMBERSHIP, name: _Paths.KR_PURCHASE_MEMBERSHIP,
page: () => const KRPurchaseMembershipView(), page: () => SwipeWrapper.detect(() => const KRPurchaseMembershipView()),
binding: KRPurchaseMembershipBinding(), binding: KRPurchaseMembershipBinding(),
popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
), ),
GetPage( GetPage(
name: _Paths.KR_MESSAGE, name: _Paths.KR_MESSAGE,
page: () => const KRMessageView(), page: () => SwipeWrapper.detect(() => const KRMessageView()),
binding: KrMessageBinding(), binding: KrMessageBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_DELETE_ACCOUNT, name: _Paths.KR_DELETE_ACCOUNT,
page: () => const KRDeleteAccountView(), page: () => SwipeWrapper.detect(() => const KRDeleteAccountView()),
binding: KrDeleteAccountBinding(), binding: KrDeleteAccountBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: Routes.KR_WEBVIEW, name: Routes.KR_WEBVIEW,
page: () => const KRWebView(), page: () => SwipeWrapper.detect(() => const KRWebView()),
binding: KRWebViewBinding(), binding: KRWebViewBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: Routes.KR_ORDER_STATUS, name: Routes.KR_ORDER_STATUS,
page: () => const KROrderStatusView(), page: () => SwipeWrapper.detect(() => const KROrderStatusView()),
binding: KROrderStatusBinding(), binding: KROrderStatusBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_CRISP, name: _Paths.KR_CRISP,
page: () => const KRCrispView(), page: () => SwipeWrapper.detect(() => const KRCrispView()),
binding: KRCrispBinding(), binding: KRCrispBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.KR_DEVICE_MANAGEMENT, name: _Paths.KR_DEVICE_MANAGEMENT,
page: () => const KRDeviceManagementView(), page: () => SwipeWrapper.detect(() => const KRDeviceManagementView()),
binding: KRDeviceManagementBinding(), binding: KRDeviceManagementBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.HI_NODE_LIST, name: _Paths.HI_NODE_LIST,
page: () => const HINodePageView(), page: () => SwipeWrapper.detect(() => const HINodePageView()),
binding: HiNodeListBinding(), binding: HiNodeListBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.HI_HELP, name: _Paths.HI_HELP,
page: () => const HIHelpView(), page: () => SwipeWrapper.detect(() => const HIHelpView()),
binding: HIHelpBinding(), binding: HIHelpBinding(),
popGesture: false,
), ),
GetPage( GetPage(
name: _Paths.HI_USER_INFO, name: _Paths.HI_USER_INFO,
page: () => const HIUserInfoView(), page: () => SwipeWrapper.detect(() => const HIUserInfoView()),
binding: HIUserInfoBinding(), binding: HIUserInfoBinding(),
popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
), ),
]; ];

View File

@ -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<HIEdgeSwipeDetector> createState() => _HIEdgeSwipeDetectorState();
}
class _HIEdgeSwipeDetectorState extends State<HIEdgeSwipeDetector> {
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;
}
}

View File

@ -0,0 +1,6 @@
import 'swipe_config.dart';
abstract class HasSwipeConfig {
SwipeConfig get swipeConfig;
}

View File

@ -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});
}

View File

@ -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<SwipeConfig> currentConfig = Rxn<SwipeConfig>();
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();
}
}

View File

@ -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);
}
}