diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d94a247..016ad70 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -37,6 +37,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 53ae014..aed9fe8 100755 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -58,6 +58,11 @@ 需要相册权限以支持图片上传功能 SERVICE_IDENTIFIER $(SERVICE_IDENTIFIER) + LSApplicationQueriesSchemes + + twitter + x-scheme-handler + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/lib/app/modules/hi_anti_lost/bindings/hi_anti_lost_binding.dart b/lib/app/modules/hi_anti_lost/bindings/hi_anti_lost_binding.dart new file mode 100644 index 0000000..b286329 --- /dev/null +++ b/lib/app/modules/hi_anti_lost/bindings/hi_anti_lost_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/hi_anti_lost_controller.dart'; + +class HIAntiLostBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => HIAntiLostController(), + ); + } +} diff --git a/lib/app/modules/hi_anti_lost/controllers/hi_anti_lost_controller.dart b/lib/app/modules/hi_anti_lost/controllers/hi_anti_lost_controller.dart new file mode 100644 index 0000000..9ba4b63 --- /dev/null +++ b/lib/app/modules/hi_anti_lost/controllers/hi_anti_lost_controller.dart @@ -0,0 +1,85 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:gal/gal.dart'; +import 'package:kaer_with_panels/app/utils/kr_common_util.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class HIAntiLostController extends GetxController { + final GlobalKey repaintKey = GlobalKey(); + + Future saveImage() async { + KRCommonUtil.kr_showLoading(message: "正在保存..."); + try { + // Check permission + final hasAccess = await Gal.hasAccess(); + if (!hasAccess) { + await Gal.requestAccess(); + } + + // Capture image + RenderRepaintBoundary? boundary = + repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + + if (boundary == null) { + throw Exception("Cannot find boundary"); + } + + // Check if the boundary needs layout (sometimes Offstage needs a frame) + if (boundary.debugNeedsPaint) { + await Future.delayed(const Duration(milliseconds: 20)); + boundary = repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + } + + ui.Image image = await boundary!.toImage(pixelRatio: 3.0); + ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData != null) { + final Uint8List pngBytes = byteData.buffer.asUint8List(); + await Gal.putImageBytes(pngBytes); + KRCommonUtil.kr_showToast("保存成功"); + } else { + KRCommonUtil.kr_showToast("生成图片失败"); + } + } catch (e) { + debugPrint("Save image error: $e"); + if (e is GalException) { + KRCommonUtil.kr_showToast("保存失败: No Permission"); + } else { + KRCommonUtil.kr_showToast("保存失败"); + } + } finally { + KRCommonUtil.kr_hideLoading(); + } + } + + void openTwitter() async { + // Try Deep Link first + final Uri appUrl = Uri.parse('twitter://user?screen_name=hifasttech'); + final Uri webUrl = Uri.parse('https://x.com/hifasttech'); + + try { + if (await canLaunchUrl(appUrl)) { + await launchUrl(appUrl); + } else { + // Fallback to web + if (!await launchUrl(webUrl, mode: LaunchMode.externalApplication)) { + KRCommonUtil.kr_showToast("无法打开链接"); + } + } + } catch (e) { + debugPrint("Open Twitter error: $e"); + // Fallback to web if anything goes wrong + try { + if (!await launchUrl(webUrl, mode: LaunchMode.externalApplication)) { + KRCommonUtil.kr_showToast("无法打开链接"); + } + } catch (e) { + KRCommonUtil.kr_showToast("无法打开链接"); + } + } + } +} diff --git a/lib/app/modules/hi_anti_lost/views/hi_anti_lost_view.dart b/lib/app/modules/hi_anti_lost/views/hi_anti_lost_view.dart new file mode 100644 index 0000000..ad8c498 --- /dev/null +++ b/lib/app/modules/hi_anti_lost/views/hi_anti_lost_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:kaer_with_panels/app/widgets/kr_local_image.dart'; +import 'package:kaer_with_panels/app/modules/hi_anti_lost/controllers/hi_anti_lost_controller.dart'; +import 'package:kaer_with_panels/app/modules/hi_anti_lost/widgets/hi_anti_lost_share_card.dart'; + +class HIAntiLostView extends GetView { + const HIAntiLostView({super.key}); + + @override + Widget build(BuildContext context) { + return HIBaseScaffold( + showBack: true, + title: null, + topContentAreaHeight: 0, + backgroundColor: Colors.black, // Dark background + child: Stack( + fit: StackFit.expand, + children: [ + // 1. Visible Content + Column( + children: [ + SizedBox(height: 60.h), + // Card Container + Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 40.w), + padding: EdgeInsets.symmetric(vertical: 16.w, horizontal: 0.w), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(25.r), + border: Border.all( + color: Theme.of(context).primaryColor, width: 4), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '保存二维码,防止失联', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 25.h), + Text( + '建议保存Hi快VPN防丢二维码到相册\n永久保障您的互联网自由', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 14.sp, + height: 1.5, + ), + ), + SizedBox(height: 10.h), + // QR Code + LayoutBuilder(builder: (context, constraints) { + // Use a reasonable size for the QR code + return Container( + width: 124.w, + height: 124.w, + child: QrImageView( + data: + 'https://github.com/hi-vpn/hi-client', // Replace with real URL + version: QrVersions.auto, + backgroundColor: Colors.transparent, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).primaryColor, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).primaryColor, + ), + // center image + embeddedImage: + const AssetImage('assets/images/kr-logo.png'), + embeddedImageStyle: QrEmbeddedImageStyle( + size: Size(36.w, 36.w), + ), + ), + ); + }), + ], + ), + ), + + SizedBox(height: 30.h), + + // Save Button + GestureDetector( + onTap: controller.saveImage, + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 40.w), + height: 44.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(22.h), + ), + child: Text( + '保存到本地', + style: TextStyle( + color: Colors.black, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + SizedBox(height: 60.h), + ], + ), + + // 2. Bottom Button (Follow us on X) + Positioned( + bottom: 40.h, + left: 0, + right: 0, + child: Center( + child: GestureDetector( + onTap: controller.openTwitter, + child: KrLocalImage( + imageName: 'followX', + width: 140, + height: 30, + imageType: ImageType + .png, // Assuming it is png as user said followX.png + ), + ), + ), + ), + + // 3. Invisible Share Card for Generation + Positioned( + left: 10000, + child: RepaintBoundary( + key: controller.repaintKey, + child: const HIAntiLostShareCard(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/hi_anti_lost/widgets/hi_anti_lost_share_card.dart b/lib/app/modules/hi_anti_lost/widgets/hi_anti_lost_share_card.dart new file mode 100644 index 0000000..e8bfa17 --- /dev/null +++ b/lib/app/modules/hi_anti_lost/widgets/hi_anti_lost_share_card.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart'; + +class HIAntiLostShareCard extends StatelessWidget { + const HIAntiLostShareCard({super.key}); + + @override + Widget build(BuildContext context) { + // Fixed width for the generated image, independent of screen size + final double cardWidth = 375.0; + final double cardHeight = 600.0; // Approx height + + return Container( + width: cardWidth, + // minimal height or let it expand + padding: EdgeInsets.all(20), + color: Colors.black, // Dark background + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 40), + // Title + Text( + '保存二维码,防止失联', + style: TextStyle( + color: const Color(0xFFCCFF00), + fontSize: 24, + fontWeight: FontWeight.bold, + fontFamily: 'AlibabaPuHuiTi-Medium', + ), + ), + SizedBox(height: 20), + // Description + Text( + '建议保存Hi快VPN防丢二维码到相册\n永久保障您的互联网自由', + textAlign: TextAlign.center, + style: TextStyle( + color: const Color(0xFFCCFF00), + fontSize: 14, + height: 1.5, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + ), + SizedBox(height: 40), + // QR Code Card + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black, + border: Border.all(color: const Color(0xFFCCFF00), width: 3), + borderRadius: BorderRadius.circular(20), + ), + child: QrImageView( + data: 'https://github.com/hi-vpn/hi-client', // Replace with actual URL + version: QrVersions.auto, + size: 200, + backgroundColor: Colors.transparent, + eyeStyle: const QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Color(0xFFCCFF00), + ), + dataModuleStyle: const QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Color(0xFFCCFF00), + ), + embeddedImage: const AssetImage('assets/images/kr-logo.png'), + embeddedImageStyle: const QrEmbeddedImageStyle( + size: Size(50, 50), + ), + ), + ), + SizedBox(height: 40), + // Footer branding + Text( + 'HiVPN', + style: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 12, + letterSpacing: 2, + ), + ), + SizedBox(height: 20), + ], + ), + ); + } +} 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 341695a..cb4b01c 100755 --- a/lib/app/modules/hi_menu/views/hi_menu_view.dart +++ b/lib/app/modules/hi_menu/views/hi_menu_view.dart @@ -51,6 +51,11 @@ class HIMenuView extends GetView implements HasSwipeConfig { title: '邀请好友', route: Routes.KR_INVITE, ), + // const MenuItem( + // iconName: 'icon-6', + // title: '软件防丢', + // route: Routes.HI_ANTI_LOST, + // ), MenuItem( iconName: 'icon-5', title: '在线客服', diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 9235877..d57a324 100755 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -7,6 +7,8 @@ import 'package:kaer_with_panels/app/modules/hi_node_list/bindings/hi_node_list_ import 'package:kaer_with_panels/app/modules/hi_node_list/views/hi_page_node_view.dart'; import 'package:kaer_with_panels/app/modules/hi_user_info/bindings/hi_user_info_binding.dart'; import 'package:kaer_with_panels/app/modules/hi_user_info/views/hi_user_info_view.dart'; +import 'package:kaer_with_panels/app/modules/hi_anti_lost/bindings/hi_anti_lost_binding.dart'; +import 'package:kaer_with_panels/app/modules/hi_anti_lost/views/hi_anti_lost_view.dart'; import '../modules/kr_crisp_chat/bindings/kr_crisp_binding.dart'; import '../modules/kr_crisp_chat/views/kr_crisp_view.dart'; @@ -130,5 +132,11 @@ class AppPages { popGesture: false, arguments: {'showSubscriptionButton': true}, // 显示购买按钮 ), + GetPage( + name: _Paths.HI_ANTI_LOST, + page: () => SwipeWrapper.detect(() => const HIAntiLostView()), + binding: HIAntiLostBinding(), + popGesture: false, + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 51610d2..60534f3 100755 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -24,6 +24,7 @@ abstract class Routes { static const HI_NODE_LIST = _Paths.HI_NODE_LIST; static const HI_HELP = _Paths.HI_HELP; static const HI_USER_INFO = _Paths.HI_USER_INFO; + static const HI_ANTI_LOST = _Paths.HI_ANTI_LOST; } abstract class _Paths { @@ -48,4 +49,5 @@ abstract class _Paths { static const HI_NODE_LIST = '/hi_node_list'; static const HI_HELP = '/hi_help'; static const HI_USER_INFO = '/hi-user-info'; + static const HI_ANTI_LOST = '/hi-anti-lost'; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a408754..8bc0808 100755 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import connectivity_plus import device_info_plus import flutter_inappwebview_macos import flutter_udid +import gal import in_app_purchase_storekit import package_info_plus import path_provider_foundation @@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) + GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bdf974f..1ac3a60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -680,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + gal: + dependency: "direct main" + description: + name: gal + sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee" + url: "https://pub.dev" + source: hosted + version: "2.3.2" get: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 23b36fa..c00d81a 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,6 +112,7 @@ dependencies: in_app_purchase: ^3.2.3 in_app_purchase_storekit: ^0.4.2 intl: ^0.20.2 + gal: ^2.3.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e8fc12e..9e6f5c2 100755 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterUdidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); + GalPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GalPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 2363a7a..fb054fa 100755 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus flutter_inappwebview_windows flutter_udid + gal permission_handler_windows protocol_handler_windows screen_retriever_windows