feat: 增加防丢失页面

This commit is contained in:
speakeloudest 2026-01-12 06:49:03 -08:00
parent 0474f3c9d9
commit 26f6c10557
16 changed files with 390 additions and 0 deletions

View File

@ -37,6 +37,17 @@
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" /> <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<queries>
<!-- Twitter/X -->
<package android:name="com.twitter.android" />
<package android:name="com.x.android" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="twitter" />
</intent>
</queries>
<!-- 如果 targetSdkVersion >= 33 --> <!-- 如果 targetSdkVersion >= 33 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application

BIN
assets/images/followX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

7
assets/images/icon-6.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6 4.53333H11.7867V1.81333H9.06665V0H11.7867C12.2676 0 12.7288 0.191047 13.0689 0.531113C13.4089 0.871179 13.6 1.33241 13.6 1.81333V4.53333Z" fill="black"/>
<path d="M1.81333 4.53333H0V1.81333C0 1.33241 0.191047 0.871179 0.531113 0.531113C0.871179 0.191047 1.33241 0 1.81333 0H4.53333V1.81333H1.81333V4.53333Z" fill="black"/>
<path d="M4.53333 13.6H1.81333C1.33241 13.6 0.871179 13.4089 0.531113 13.0689C0.191047 12.7288 0 12.2676 0 11.7867V9.06665H1.81333V11.7867H4.53333V13.6Z" fill="black"/>
<path d="M11.7867 13.6H9.06665V11.7867H11.7867V9.06665H13.6V11.7867C13.6 12.2676 13.4089 12.7288 13.0689 13.0689C12.7288 13.4089 12.2676 13.6 11.7867 13.6Z" fill="black"/>
<path d="M6.79996 3.62665C6.07857 3.62665 5.38672 3.91322 4.87663 4.42332C4.36653 4.93342 4.07996 5.62526 4.07996 6.34665C4.07996 8.15998 6.07009 9.81238 6.79996 10.88C7.52982 9.81238 9.51996 8.15998 9.51996 6.34665C9.51996 5.62526 9.23339 4.93342 8.72329 4.42332C8.21319 3.91322 7.52134 3.62665 6.79996 3.62665ZM6.79996 7.25331C6.62063 7.25331 6.44534 7.20014 6.29624 7.10051C6.14714 7.00089 6.03093 6.85929 5.96231 6.69361C5.89368 6.52794 5.87573 6.34564 5.91071 6.16977C5.94569 5.99389 6.03205 5.83234 6.15885 5.70554C6.28565 5.57874 6.4472 5.49239 6.62307 5.4574C6.79895 5.42242 6.98125 5.44037 7.14692 5.509C7.31259 5.57762 7.4542 5.69383 7.55382 5.84293C7.65345 5.99203 7.70662 6.16733 7.70662 6.34665C7.70662 6.58711 7.6111 6.81772 7.44107 6.98776C7.27103 7.15779 7.04042 7.25331 6.79996 7.25331Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -58,6 +58,11 @@
<string>需要相册权限以支持图片上传功能</string> <string>需要相册权限以支持图片上传功能</string>
<key>SERVICE_IDENTIFIER</key> <key>SERVICE_IDENTIFIER</key>
<string>$(SERVICE_IDENTIFIER)</string> <string>$(SERVICE_IDENTIFIER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>twitter</string>
<string>x-scheme-handler</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

View File

@ -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>(
() => HIAntiLostController(),
);
}
}

View File

@ -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<void> 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("无法打开链接");
}
}
}
}

View File

@ -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<HIAntiLostController> {
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(),
),
),
],
),
);
}
}

View File

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

View File

@ -51,6 +51,11 @@ class HIMenuView extends GetView<HIMenuController> implements HasSwipeConfig {
title: '邀请好友', title: '邀请好友',
route: Routes.KR_INVITE, route: Routes.KR_INVITE,
), ),
// const MenuItem(
// iconName: 'icon-6',
// title: '软件防丢',
// route: Routes.HI_ANTI_LOST,
// ),
MenuItem( MenuItem(
iconName: 'icon-5', iconName: 'icon-5',
title: '在线客服', title: '在线客服',

View File

@ -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_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/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_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/bindings/kr_crisp_binding.dart';
import '../modules/kr_crisp_chat/views/kr_crisp_view.dart'; import '../modules/kr_crisp_chat/views/kr_crisp_view.dart';
@ -130,5 +132,11 @@ class AppPages {
popGesture: false, popGesture: false,
arguments: {'showSubscriptionButton': true}, // arguments: {'showSubscriptionButton': true}, //
), ),
GetPage(
name: _Paths.HI_ANTI_LOST,
page: () => SwipeWrapper.detect(() => const HIAntiLostView()),
binding: HIAntiLostBinding(),
popGesture: false,
),
]; ];
} }

View File

@ -24,6 +24,7 @@ abstract class Routes {
static const HI_NODE_LIST = _Paths.HI_NODE_LIST; static const HI_NODE_LIST = _Paths.HI_NODE_LIST;
static const HI_HELP = _Paths.HI_HELP; static const HI_HELP = _Paths.HI_HELP;
static const HI_USER_INFO = _Paths.HI_USER_INFO; static const HI_USER_INFO = _Paths.HI_USER_INFO;
static const HI_ANTI_LOST = _Paths.HI_ANTI_LOST;
} }
abstract class _Paths { abstract class _Paths {
@ -48,4 +49,5 @@ abstract class _Paths {
static const HI_NODE_LIST = '/hi_node_list'; static const HI_NODE_LIST = '/hi_node_list';
static const HI_HELP = '/hi_help'; static const HI_HELP = '/hi_help';
static const HI_USER_INFO = '/hi-user-info'; static const HI_USER_INFO = '/hi-user-info';
static const HI_ANTI_LOST = '/hi-anti-lost';
} }

View File

@ -9,6 +9,7 @@ import connectivity_plus
import device_info_plus import device_info_plus
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_udid import flutter_udid
import gal
import in_app_purchase_storekit import in_app_purchase_storekit
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@ -680,6 +680,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
gal:
dependency: "direct main"
description:
name: gal
sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -112,6 +112,7 @@ dependencies:
in_app_purchase: ^3.2.3 in_app_purchase: ^3.2.3
in_app_purchase_storekit: ^0.4.2 in_app_purchase_storekit: ^0.4.2
intl: ^0.20.2 intl: ^0.20.2
gal: ^2.3.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@ -9,6 +9,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_udid/flutter_udid_plugin_c_api.h> #include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <gal/gal_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h> #include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterUdidPluginCApiRegisterWithRegistrar( FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar( ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
flutter_inappwebview_windows flutter_inappwebview_windows
flutter_udid flutter_udid
gal
permission_handler_windows permission_handler_windows
protocol_handler_windows protocol_handler_windows
screen_retriever_windows screen_retriever_windows