Explorar el Código

fix: 优化加载国旗组件,支持svg和png

lilu hace 3 meses
padre
commit
bd823cd806
Se han modificado 33 ficheros con 618 adiciones y 162 borrados
  1. 37 23
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  2. BIN
      assets/images/streaming/appletv.png
  3. BIN
      assets/images/streaming/hbo.png
  4. BIN
      assets/images/streaming/hulu.png
  5. BIN
      assets/images/streaming/max.png
  6. BIN
      assets/images/streaming/netflix.png
  7. BIN
      assets/images/streaming/primevideo.png
  8. 2 2
      lib/app/base/base_controller.dart
  9. 12 0
      lib/app/constants/assets.dart
  10. 1 1
      lib/app/controllers/api_controller.dart
  11. 2 5
      lib/app/controllers/core_controller.dart
  12. 1 1
      lib/app/dialog/loading/loading_dialog.dart
  13. 1 1
      lib/app/dialog/loading/simple_loading_dialog.dart
  14. 38 8
      lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart
  15. 81 11
      lib/app/modules/forgotpwd/views/forgotpwd_view.dart
  16. 24 74
      lib/app/modules/home/views/home_view.dart
  17. 3 2
      lib/app/modules/login/views/login_view.dart
  18. 12 18
      lib/app/modules/node/widgets/node_list.dart
  19. 37 14
      lib/app/modules/setting/views/setting_view.dart
  20. 3 2
      lib/app/modules/signup/views/signup_view.dart
  21. 252 0
      lib/app/widgets/country_icon.dart
  22. 10 0
      lib/config/translations/ar_AR/ar_ar_translation.dart
  23. 10 0
      lib/config/translations/de_DE/de_de_translation.dart
  24. 10 0
      lib/config/translations/en_US/en_us_translation.dart
  25. 10 0
      lib/config/translations/es_ES/es_es_translation.dart
  26. 10 0
      lib/config/translations/fa_IR/fa_ir_translation.dart
  27. 10 0
      lib/config/translations/fr_FR/fr_fr_translation.dart
  28. 10 0
      lib/config/translations/ja_JP/ja_jp_translation.dart
  29. 10 0
      lib/config/translations/ko_KR/ko_kr_translation.dart
  30. 10 0
      lib/config/translations/my_MM/my_mm_translation.dart
  31. 10 0
      lib/config/translations/ru_RU/ru_ru_translation.dart
  32. 11 0
      lib/config/translations/strings_enum.dart
  33. 1 0
      pubspec.yaml

+ 37 - 23
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -15,6 +15,7 @@ import android.os.Looper
 import android.os.Message
 import android.os.Messenger
 import android.os.ParcelFileDescriptor
+import android.os.SystemClock
 import androidx.core.app.NotificationCompat
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_CONNECTED
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_ERROR
@@ -33,37 +34,41 @@ class XRayService : VpnService() {
     private val timerHandler = Handler(Looper.getMainLooper())
     private var isTimerRunning = false
     private var isTimerPaused = false
-    private var currentTimerTime = 0L
-    private var timerStartTime = 0L
     private var timerMode = 0 // 0: 普通计时, 1: 倒计时
     
+    // 使用 SystemClock.elapsedRealtime() 确保时间准确性(包括休眠时间)
+    private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
+    private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
+    private var timerPausedElapsed = 0L // 暂停时已经过的时间
+    
     // 计时器Runnable
     private val timerRunnable = object : Runnable {
         override fun run() {
             if (isTimerRunning && !isTimerPaused) {
-                val newTime = if (timerMode == 0) {
-                    // 普通计时:递增
-                    currentTimerTime + 1000L
+                // 计算从开始到现在经过的真实时间
+                val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
+                
+                val currentTime = if (timerMode == 0) {
+                    // 普通计时:初始时间 + 经过时间
+                    timerInitialTime + elapsedTime
                 } else {
-                    // 倒计时:递减
-                    currentTimerTime - 1000L
+                    // 倒计时:初始时间 - 经过时间
+                    timerInitialTime - elapsedTime
                 }
                 
-                currentTimerTime = newTime
-                
                 // 检查倒计时是否结束
-                if (timerMode == 1 && newTime <= 0) {
-                    VLog.i(TAG, "倒计时结束 - 关闭VPN")
+                if (timerMode == 1 && currentTime <= 0) {
+                    VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
                     dealStopMsg()
                     notifyStop()
                     return
                 }
                 
                 // 更新通知
-                updateNotification()
+                updateNotification(currentTime)
                 
                 // 发送计时更新
-                sendTimerUpdate()
+                sendTimerUpdate(currentTime)
                 
                 // 继续下一秒
                 timerHandler.postDelayed(this, 1000L)
@@ -311,8 +316,9 @@ class XRayService : VpnService() {
             return
         VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
         timerMode = mode
-        currentTimerTime = initialTime
-        timerStartTime = System.currentTimeMillis()
+        timerInitialTime = initialTime
+        timerBaseRealtime = SystemClock.elapsedRealtime()
+        timerPausedElapsed = 0L
 
         isTimerRunning = true
         isTimerPaused = false
@@ -331,8 +337,12 @@ class XRayService : VpnService() {
     }
     
     private fun pauseTimer() {
-        if (isTimerRunning) {
-            VLog.i(TAG, "暂停计时")
+        if (isTimerRunning && !isTimerPaused) {
+            // 记录暂停时已经过的时间
+            val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
+            timerPausedElapsed = elapsedTime
+            
+            VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
             isTimerPaused = true
             timerHandler.removeCallbacks(timerRunnable)
         }
@@ -340,27 +350,31 @@ class XRayService : VpnService() {
     
     private fun resumeTimer() {
         if (isTimerRunning && isTimerPaused) {
-            VLog.i(TAG, "恢复计时")
+            VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
+            
+            // 恢复时重新设置基准时间,但要减去之前已经过的时间
+            timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
+            
             isTimerPaused = false
             timerHandler.post(timerRunnable)
         }
     }
     
-    private fun sendTimerUpdate() {
+    private fun sendTimerUpdate(currentTime: Long) {
         try {
             replyMessenger?.let { messenger ->
                 try {
                     val msg = Message.obtain().apply {
                         what = XRAY_MSG_REPLY_TIMER_UPDATE
                         data = Bundle().apply {
-                            putLong("currentTime", currentTimerTime)
+                            putLong("currentTime", currentTime)
                             putInt("mode", timerMode)
                             putBoolean("isRunning", isTimerRunning)
                             putBoolean("isPaused", isTimerPaused)
                         }
                     }
                     messenger.send(msg)
-                    VLog.i(TAG, "计时更新消息已发送: time=$currentTimerTime, mode=$timerMode")
+                    VLog.i(TAG, "计时更新消息已发送: time=$currentTime, mode=$timerMode")
                 } catch (e: DeadObjectException) {
                     VLog.w(TAG, "Messenger已失效,清理引用", e)
                     replyMessenger = null
@@ -375,8 +389,8 @@ class XRayService : VpnService() {
         }
     }
     
-    private fun updateNotification() {
-        val timeText = formatTime(currentTimerTime)
+    private fun updateNotification(currentTime: Long) {
+        val timeText = formatTime(currentTime)
         val modeText = if (timerMode == 0) "计时中" else "倒计时"
         val statusText = if (isTimerPaused) "已暂停" else "运行中"
         

BIN
assets/images/streaming/appletv.png


BIN
assets/images/streaming/hbo.png


BIN
assets/images/streaming/hulu.png


BIN
assets/images/streaming/max.png


BIN
assets/images/streaming/netflix.png


BIN
assets/images/streaming/primevideo.png


+ 2 - 2
lib/app/base/base_controller.dart

@@ -60,7 +60,7 @@ abstract class BaseController extends GetxController {
       }
     } else {
       isNetworkError.value = false;
-      setError(Strings.unknownError.tr);
+      setError(error.toString());
     }
   }
 
@@ -101,7 +101,7 @@ abstract class BaseController extends GetxController {
     } else {
       IXSnackBar.showIXErrorSnackBar(
         title: Strings.error.tr,
-        message: Strings.unknownError.tr,
+        message: error.toString(),
       );
     }
   }

+ 12 - 0
lib/app/constants/assets.dart

@@ -8,6 +8,18 @@ class Assets {
     }
   }
 
+  // 流媒体图标
+  static String getStreamingIcon(String code) {
+    if (code.isEmpty) {
+      return 'assets/flags/xx.svg';
+    } else {
+      return 'assets/images/streaming/${code.toLowerCase()}.png';
+    }
+  }
+
+  // 默认国旗
+  static const String defaultFlag = 'assets/flags/xx.svg';
+
   static const String splashLogo = 'assets/images/splash_logo.png';
   static const String splashCenterBg = 'assets/images/splash_center_bg.png';
 

+ 1 - 1
lib/app/controllers/api_controller.dart

@@ -347,7 +347,7 @@ class ApiController extends GetxService {
     } else {
       IXSnackBar.showIXErrorSnackBar(
         title: Strings.error.tr,
-        message: Strings.unknownError.tr,
+        message: error.toString(),
       );
     }
   }

+ 2 - 5
lib/app/controllers/core_controller.dart

@@ -353,7 +353,7 @@ class CoreController extends GetxService {
     } else {
       IXSnackBar.showIXErrorSnackBar(
         title: Strings.error.tr,
-        message: Strings.unknownError.tr,
+        message: error.toString(),
       );
     }
   }
@@ -384,10 +384,7 @@ class CoreController extends GetxService {
           );
       }
     } else {
-      ErrorDialog.show(
-        title: Strings.error.tr,
-        message: Strings.unknownError.tr,
-      );
+      ErrorDialog.show(title: Strings.error.tr, message: error.toString());
     }
   }
 }

+ 1 - 1
lib/app/dialog/loading/loading_dialog.dart

@@ -134,7 +134,7 @@ class _LoadingDialogState extends State<LoadingDialog>
     } else {
       IXSnackBar.showIXErrorSnackBar(
         title: Strings.error.tr,
-        message: Strings.unknownError.tr,
+        message: error.toString(),
       );
     }
   }

+ 1 - 1
lib/app/dialog/loading/simple_loading_dialog.dart

@@ -115,7 +115,7 @@ class _SimpleLoadingDialogState extends State<SimpleLoadingDialog>
     } else {
       IXSnackBar.showIXErrorSnackBar(
         title: Strings.error.tr,
-        message: Strings.unknownError.tr,
+        message: error.toString(),
       );
     }
   }

+ 38 - 8
lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart

@@ -1,23 +1,53 @@
+import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
 class ForgotpwdController extends GetxController {
-  //TODO: Implement ForgotpwdController
+  final usernameController = TextEditingController();
+  final passwordController = TextEditingController();
+  final usernameFocusNode = FocusNode();
+  final passwordFocusNode = FocusNode();
+  final _isLogin = false.obs;
+  bool get isLogin => _isLogin.value;
+  set isLogin(bool value) {
+    _isLogin.value = value;
+  }
 
-  final count = 0.obs;
   @override
   void onInit() {
     super.onInit();
   }
 
-  @override
-  void onReady() {
-    super.onReady();
-  }
-
   @override
   void onClose() {
+    usernameController.dispose();
+    passwordController.dispose();
+    usernameFocusNode.dispose();
+    passwordFocusNode.dispose();
     super.onClose();
   }
 
-  void increment() => count.value++;
+  void checkLogin() {
+    final username = usernameController.text;
+    final password = passwordController.text;
+    if (validatorInputValue(username) &&
+        validatorInputValue(password) &&
+        username == password) {
+      isLogin = true;
+    } else {
+      isLogin = false;
+    }
+  }
+
+  //6-20 characters (letters or numbers)
+  bool validatorInputValue(String value) {
+    // 只允许字母和数字并且是6-20位
+    return value.length >= 6 &&
+        value.length <= 20 &&
+        RegExp(r'^[a-zA-Z0-9]+$').hasMatch(value);
+  }
+
+  bool validatorConfirmInputValue(String value) {
+    // 只允许字母和数字并且是6-20位
+    return value == usernameController.text;
+  }
 }

+ 81 - 11
lib/app/modules/forgotpwd/views/forgotpwd_view.dart

@@ -1,22 +1,92 @@
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 
 import 'package:get/get.dart';
+import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../base/base_view.dart';
+import '../../../constants/iconfont/iconfont.dart';
+import '../../../widgets/ix_app_bar.dart';
+import '../../../widgets/ix_text_field.dart';
+import '../../../widgets/submit_btn.dart';
 import '../controllers/forgotpwd_controller.dart';
 
-class ForgotpwdView extends GetView<ForgotpwdController> {
+class ForgotpwdView extends BaseView<ForgotpwdController> {
   const ForgotpwdView({super.key});
   @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        title: const Text('ForgotpwdView'),
-        centerTitle: true,
-      ),
-      body: const Center(
-        child: Text(
-          'ForgotpwdView is working',
-          style: TextStyle(fontSize: 20),
+  PreferredSizeWidget? get appBar => IXAppBar(title: '');
+
+  @override
+  Widget buildContent(BuildContext context) {
+    return SafeArea(
+      child: Padding(
+        padding: EdgeInsets.symmetric(horizontal: 14.w),
+        child: SingleChildScrollView(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              10.verticalSpaceFromWidth,
+
+              // 标题
+              Text(
+                Strings.changePassword.tr,
+                style: TextStyle(
+                  fontSize: 28.sp,
+                  height: 1.2,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                ),
+              ),
+
+              24.verticalSpaceFromWidth,
+
+              // 描述文字
+              Text(
+                Strings.changePasswordDescription.tr,
+                style: TextStyle(
+                  fontSize: 16.sp,
+                  color: Get.reactiveTheme.hintColor,
+                  height: 1.4,
+                ),
+              ),
+
+              20.verticalSpaceFromWidth,
+              IXTextField(
+                hintText: Strings.enterNewPassword.tr,
+                prefixIcon: IconFont.icon11,
+                controller: controller.usernameController,
+                focusNode: controller.usernameFocusNode,
+                isPassword: true,
+                validator: controller.validatorInputValue,
+                tipText: Strings.usernamePasswordRule.tr,
+                errorText: Strings.usernamePasswordRule.tr,
+                onChanged: (value) {
+                  controller.checkLogin();
+                },
+              ),
+
+              16.verticalSpaceFromWidth,
+              IXTextField(
+                hintText: Strings.enterConfirmPassword.tr,
+                prefixIcon: IconFont.icon11,
+                controller: controller.passwordController,
+                focusNode: controller.passwordFocusNode,
+                validator: controller.validatorConfirmInputValue,
+                isPassword: true,
+                tipText: Strings.usernamePasswordRule.tr,
+                errorText: Strings.confirmPasswordMustBeTheSame.tr,
+                onChanged: (value) {
+                  controller.checkLogin();
+                },
+              ),
+              156.verticalSpaceFromWidth,
+              SubmitButton(
+                text: Strings.yes.tr,
+                enabled: controller.isLogin,
+                onPressed: () {},
+              ),
+            ],
+          ),
         ),
       ),
     );

+ 24 - 74
lib/app/modules/home/views/home_view.dart

@@ -1,3 +1,5 @@
+import 'dart:io';
+
 import 'package:carousel_slider/carousel_slider.dart';
 import 'package:flutter/material.dart' hide ConnectionState;
 import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -9,10 +11,10 @@ import '../../../base/base_view.dart';
 import '../../../constants/assets.dart';
 import '../../../routes/app_pages.dart';
 import '../../../widgets/click_opacity.dart';
+import '../../../widgets/country_icon.dart';
 import '../../../widgets/ix_image.dart';
 import '../widgets/connection_button.dart';
 import '../controllers/home_controller.dart';
-import 'package:flutter_svg/flutter_svg.dart';
 
 import '../widgets/menu_list.dart';
 
@@ -127,7 +129,10 @@ class HomeView extends BaseView<HomeController> {
             Expanded(
               child: Column(
                 mainAxisAlignment: MainAxisAlignment.end,
-                children: [MenuList(), 10.verticalSpaceFromWidth],
+                children: [
+                  MenuList(),
+                  if (Platform.isAndroid) 10.verticalSpaceFromWidth,
+                ],
               ),
             ),
           ],
@@ -184,31 +189,13 @@ class HomeView extends BaseView<HomeController> {
           child: Row(
             children: [
               // 国旗图标
-              ClipRRect(
-                borderRadius: BorderRadius.circular(4.r), // 设置圆角
-                child: SvgPicture.asset(
-                  Assets.getCountryFlagImage(
-                    controller.selectedLocation?.country ?? '',
-                  ),
-                  width: 32.w,
-                  height: 24.w,
-                  fit: BoxFit.cover,
-                  // placeholderBuilder: (context) => Container(
-                  //   width: 32.w,
-                  //   height: 24.w,
-                  //   decoration: BoxDecoration(
-                  //     borderRadius: BorderRadius.circular(4.r),
-                  //     color: Colors.grey[200],
-                  //   ),
-                  //   alignment: Alignment.center,
-                  //   child: Icon(
-                  //     Icons.flag,
-                  //     size: 16.w,
-                  //     color: Colors.grey[400],
-                  //   ),
-                  // ),
-                ),
+              CountryIcon(
+                countryCode: controller.selectedLocation?.country ?? '',
+                width: 32.w,
+                height: 24.w,
+                borderRadius: 4.r,
               ),
+
               10.horizontalSpace,
 
               // 位置名称
@@ -294,35 +281,14 @@ class HomeView extends BaseView<HomeController> {
                                     borderRadius: BorderRadius.circular(5.r),
                                     border: Border.all(
                                       color: Get.reactiveTheme.canvasColor,
-                                      width: 0.4,
+                                      width: 0.4.w,
                                     ),
                                   ),
-                                  child: ClipRRect(
-                                    borderRadius: BorderRadius.circular(4.r),
-                                    child: SvgPicture.asset(
-                                      Assets.getCountryFlagImage(
-                                        location.country ?? '',
-                                      ),
-                                      width: 24.w,
-                                      height: 16.w,
-                                      fit: BoxFit.cover,
-                                      //   placeholderBuilder: (context) =>
-                                      //       Container(
-                                      //         width: 24.w,
-                                      //         height: 16.w,
-                                      //         decoration: BoxDecoration(
-                                      //           borderRadius:
-                                      //               BorderRadius.circular(4.r),
-                                      //           color: Colors.grey[200],
-                                      //         ),
-                                      //         alignment: Alignment.center,
-                                      //         child: Icon(
-                                      //           Icons.flag,
-                                      //           size: 10.w,
-                                      //           color: Colors.grey[400],
-                                      //         ),
-                                      //       ),
-                                    ),
+                                  child: CountryIcon(
+                                    countryCode: location.country ?? '',
+                                    width: 24.w,
+                                    height: 16.w,
+                                    borderRadius: 4.r,
                                   ),
                                 );
                               }),
@@ -383,27 +349,11 @@ class HomeView extends BaseView<HomeController> {
                                   crossAxisAlignment: CrossAxisAlignment.center,
                                   children: [
                                     // 国旗图标
-                                    ClipRRect(
-                                      borderRadius: BorderRadius.circular(
-                                        4.r,
-                                      ), // 设置圆角
-                                      child: SvgPicture.asset(
-                                        Assets.getCountryFlagImage(
-                                          location.country ?? '',
-                                        ),
-                                        width: 28.w,
-                                        height: 21.w,
-                                        fit: BoxFit.cover,
-                                        // placeholderBuilder: (context) =>
-                                        //     Container(
-                                        //       color: Colors.grey[200],
-                                        //       child: Icon(
-                                        //         Icons.flag,
-                                        //         size: 12.w,
-                                        //         color: Colors.grey[400],
-                                        //       ),
-                                        //     ),
-                                      ),
+                                    CountryIcon(
+                                      countryCode: location.country ?? '',
+                                      width: 28.w,
+                                      height: 21.w,
+                                      borderRadius: 4.r,
                                     ),
                                     SizedBox(width: 10.w),
 

+ 3 - 2
lib/app/modules/login/views/login_view.dart

@@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 
 import 'package:nomo/app/base/base_view.dart';
+import 'package:nomo/app/constants/iconfont/iconfont.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../../../../config/translations/strings_enum.dart';
@@ -55,7 +56,7 @@ class LoginView extends BaseView<LoginController> {
               20.verticalSpaceFromWidth,
               IXTextField(
                 hintText: Strings.username.tr,
-                prefixIcon: Icons.person_outline,
+                prefixIcon: IconFont.icon10,
                 controller: controller.usernameController,
                 focusNode: controller.usernameFocusNode,
                 validator: controller.validatorInputValue,
@@ -69,7 +70,7 @@ class LoginView extends BaseView<LoginController> {
               16.verticalSpaceFromWidth,
               IXTextField(
                 hintText: Strings.password.tr,
-                prefixIcon: Icons.lock_outline,
+                prefixIcon: IconFont.icon11,
                 controller: controller.passwordController,
                 focusNode: controller.passwordFocusNode,
                 validator: controller.validatorInputValue,

+ 12 - 18
lib/app/modules/node/widgets/node_list.dart

@@ -1,14 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_sticky_header/flutter_sticky_header.dart';
-import 'package:flutter_svg/flutter_svg.dart';
+
 import 'package:get/get.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
-import '../../../constants/assets.dart';
 import '../../../constants/iconfont/iconfont.dart';
 import '../../../data/models/launch/groups.dart';
+import '../../../widgets/country_icon.dart';
 import '../../home/controllers/home_controller.dart';
 import '../controllers/node_controller.dart';
 
@@ -167,14 +167,11 @@ class _CountrySection extends StatelessWidget {
               child: Row(
                 children: [
                   // 国旗图标
-                  ClipRRect(
-                    borderRadius: BorderRadius.circular(4.r),
-                    child: SvgPicture.asset(
-                      Assets.getCountryFlagImage(countryIcon),
-                      width: 32.w,
-                      height: 24.w,
-                      fit: BoxFit.cover,
-                    ),
+                  CountryIcon(
+                    countryCode: countryIcon,
+                    width: 32.w,
+                    height: 24.w,
+                    borderRadius: 4.r,
                   ),
                   10.horizontalSpace,
                   Text(
@@ -245,14 +242,11 @@ class _CountrySection extends StatelessWidget {
                             ),
                             child: Row(
                               children: [
-                                ClipRRect(
-                                  borderRadius: BorderRadius.circular(4.r),
-                                  child: SvgPicture.asset(
-                                    Assets.getCountryFlagImage(locationIcon),
-                                    width: 32.w,
-                                    height: 24.w,
-                                    fit: BoxFit.cover,
-                                  ),
+                                CountryIcon(
+                                  countryCode: locationIcon,
+                                  width: 32.w,
+                                  height: 24.w,
+                                  borderRadius: 4.r,
                                 ),
                                 10.horizontalSpace,
                                 Expanded(

+ 37 - 14
lib/app/modules/setting/views/setting_view.dart

@@ -1,3 +1,5 @@
+import 'dart:io';
+
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -247,21 +249,23 @@ class SettingView extends BaseView<SettingController> {
               },
             ),
             _buildDivider(),
-            _buildSettingItem(
-              icon: IconFont.icon32,
-              iconColor: Get.reactiveTheme.primaryColor,
-              title: Strings.splitTunneling.tr,
-              trailing: Icon(
-                IconFont.icon02,
-                size: 20.w,
-                color: Get.reactiveTheme.hintColor,
+            if (Platform.isAndroid) ...[
+              _buildSettingItem(
+                icon: IconFont.icon32,
+                iconColor: Get.reactiveTheme.primaryColor,
+                title: Strings.splitTunneling.tr,
+                trailing: Icon(
+                  IconFont.icon02,
+                  size: 20.w,
+                  color: Get.reactiveTheme.hintColor,
+                ),
+                onTap: () {
+                  // TODO: 跳转到分流隧道页面
+                  Get.toNamed(Routes.SPLITTUNNELING);
+                },
               ),
-              onTap: () {
-                // TODO: 跳转到分流隧道页面
-                Get.toNamed(Routes.SPLITTUNNELING);
-              },
-            ),
-            _buildDivider(),
+              _buildDivider(),
+            ],
             _buildSettingItem(
               icon: IconFont.icon33,
               iconColor: Get.reactiveTheme.primaryColor,
@@ -452,6 +456,25 @@ class SettingView extends BaseView<SettingController> {
         ),
         child: Column(
           children: [
+            _buildSettingItem(
+              icon: IconFont.icon11,
+              iconColor:
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+                  DarkThemeColors.settingSecurityLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
+              title: Strings.changePassword.tr,
+              onTap: () {
+                // TODO: 跳转到忘记密码页面
+                Get.toNamed(Routes.FORGOTPWD);
+              },
+            ),
+            _buildDivider(),
             _buildSettingItem(
               icon: IconFont.icon40,
               iconColor:

+ 3 - 2
lib/app/modules/signup/views/signup_view.dart

@@ -7,6 +7,7 @@ import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../base/base_view.dart';
+import '../../../constants/iconfont/iconfont.dart';
 import '../../../routes/app_pages.dart';
 import '../../../widgets/ix_app_bar.dart';
 import '../../../widgets/ix_text_field.dart';
@@ -55,7 +56,7 @@ class SignupView extends BaseView<SignupController> {
               20.verticalSpaceFromWidth,
               IXTextField(
                 hintText: Strings.username.tr,
-                prefixIcon: Icons.person_outline,
+                prefixIcon: IconFont.icon10,
                 controller: controller.usernameController,
                 focusNode: controller.usernameFocusNode,
                 validator: controller.validatorInputValue,
@@ -69,7 +70,7 @@ class SignupView extends BaseView<SignupController> {
               16.verticalSpaceFromWidth,
               IXTextField(
                 hintText: Strings.password.tr,
-                prefixIcon: Icons.lock_outline,
+                prefixIcon: IconFont.icon11,
                 controller: controller.passwordController,
                 focusNode: controller.passwordFocusNode,
                 validator: controller.validatorInputValue,

+ 252 - 0
lib/app/widgets/country_icon.dart

@@ -0,0 +1,252 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'ix_image.dart';
+
+/// 国旗/图标组件
+///
+/// 智能查找逻辑:
+/// 1. 优先从 assets/flags/{countryCode}.svg 查找
+/// 2. 找不到则从 assets/images/streaming/{countryCode}.png 查找
+/// 3. 都找不到则显示默认图标 assets/flags/xx.svg
+class CountryIcon extends StatefulWidget {
+  /// 国家/地区代码(如: us, cn, netflix 等)
+  final String countryCode;
+
+  /// 宽度
+  final double width;
+
+  /// 高度
+  final double height;
+
+  /// 圆角
+  final double? borderRadius;
+
+  /// 适配方式
+  final BoxFit? fit;
+
+  const CountryIcon({
+    super.key,
+    required this.countryCode,
+    required this.width,
+    required this.height,
+    this.borderRadius,
+    this.fit,
+  });
+
+  @override
+  State<CountryIcon> createState() => _CountryIconState();
+}
+
+class _CountryIconState extends State<CountryIcon> {
+  /// 图片路径
+  String? _imagePath;
+
+  /// 图片类型:svg 或 png
+  String? _imageType;
+
+  /// 是否正在加载
+  bool _isLoading = true;
+
+  @override
+  void initState() {
+    super.initState();
+    _findImage();
+  }
+
+  @override
+  void didUpdateWidget(CountryIcon oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.countryCode != widget.countryCode) {
+      _findImage();
+    }
+  }
+
+  /// 智能查找图片
+  Future<void> _findImage() async {
+    if (!mounted) return;
+
+    setState(() {
+      _isLoading = true;
+    });
+
+    final code = widget.countryCode.toLowerCase().trim();
+
+    // 如果为空,直接使用默认图标
+    if (code.isEmpty) {
+      setState(() {
+        _imagePath = 'assets/flags/xx.svg';
+        _imageType = 'svg';
+        _isLoading = false;
+      });
+      return;
+    }
+
+    // 1. 先检查 flags 目录的 svg
+    final flagPath = 'assets/flags/$code.svg';
+    if (await _assetExists(flagPath)) {
+      setState(() {
+        _imagePath = flagPath;
+        _imageType = 'svg';
+        _isLoading = false;
+      });
+      return;
+    }
+
+    // 2. 检查 streaming 目录的 png
+    final streamingPath = 'assets/images/streaming/$code.png';
+    if (await _assetExists(streamingPath)) {
+      setState(() {
+        _imagePath = streamingPath;
+        _imageType = 'png';
+        _isLoading = false;
+      });
+      return;
+    }
+
+    // 3. 使用默认图标
+    setState(() {
+      _imagePath = 'assets/flags/xx.svg';
+      _imageType = 'svg';
+      _isLoading = false;
+    });
+  }
+
+  /// 检查资源文件是否存在
+  Future<bool> _assetExists(String path) async {
+    try {
+      await rootBundle.load(path);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // 加载中显示占位符
+    if (_isLoading || _imagePath == null || _imageType == null) {
+      return _buildPlaceholder();
+    }
+
+    // 根据图片类型返回不同的组件
+    if (_imageType == 'svg') {
+      return ClipRRect(
+        borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
+        child: SvgPicture.asset(
+          _imagePath!,
+          width: widget.width,
+          height: widget.height,
+          fit: widget.fit ?? BoxFit.cover,
+          placeholderBuilder: (context) => _buildPlaceholder(),
+        ),
+      );
+    } else {
+      // png 类型使用 IXImage
+      return IXImage(
+        source: _imagePath!,
+        sourceType: ImageSourceType.asset,
+        width: widget.width,
+        height: widget.height,
+        borderRadius: widget.borderRadius,
+        fit: widget.fit,
+      );
+    }
+  }
+
+  /// 构建占位符
+  Widget _buildPlaceholder() {
+    return Container(
+      width: widget.width,
+      height: widget.height,
+      decoration: BoxDecoration(
+        color: Colors.grey.withValues(alpha: 0.2),
+        borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
+      ),
+    );
+  }
+}
+
+/// 国旗/图标组件的简化版本(同步加载,不检查文件是否存在)
+///
+/// 性能更好,但可能在资源不存在时显示错误
+class CountryIconFast extends StatelessWidget {
+  /// 国家/地区代码
+  final String countryCode;
+
+  /// 宽度
+  final double width;
+
+  /// 高度
+  final double height;
+
+  /// 圆角
+  final double? borderRadius;
+
+  /// 适配方式
+  final BoxFit? fit;
+
+  /// 是否优先使用 streaming 目录
+  final bool preferStreaming;
+
+  const CountryIconFast({
+    Key? key,
+    required this.countryCode,
+    required this.width,
+    required this.height,
+    this.borderRadius,
+    this.fit,
+    this.preferStreaming = false,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final code = countryCode.toLowerCase().trim();
+
+    if (code.isEmpty) {
+      return _buildSvg('assets/flags/xx.svg');
+    }
+
+    // 如果优先使用 streaming
+    if (preferStreaming) {
+      return _buildPng('assets/images/streaming/$code.png');
+    }
+
+    // 默认优先使用 flags
+    return _buildSvg('assets/flags/$code.svg');
+  }
+
+  Widget _buildSvg(String path) {
+    return ClipRRect(
+      borderRadius: BorderRadius.circular(borderRadius ?? 0),
+      child: SvgPicture.asset(
+        path,
+        width: width,
+        height: height,
+        fit: fit ?? BoxFit.cover,
+        placeholderBuilder: (context) => _buildFallback(),
+      ),
+    );
+  }
+
+  Widget _buildPng(String path) {
+    return IXImage(
+      source: path,
+      sourceType: ImageSourceType.asset,
+      width: width,
+      height: height,
+      borderRadius: borderRadius,
+      fit: fit,
+    );
+  }
+
+  Widget _buildFallback() {
+    // 降级到默认图标
+    return SvgPicture.asset(
+      'assets/flags/xx.svg',
+      width: width,
+      height: height,
+      fit: fit ?? BoxFit.cover,
+    );
+  }
+}

+ 10 - 0
lib/config/translations/ar_AR/ar_ar_translation.dart

@@ -360,4 +360,14 @@ final Map<String, String> arAR = {
   Strings.excellentPremiumFeatures: 'ميزات مميزة ممتازة',
   Strings.worthRecommending: 'يستحق التوصية',
   Strings.loveTheDesign: 'أحب التصميم',
+
+  // Change Password
+  Strings.changePassword: 'تغيير كلمة المرور',
+  Strings.changePasswordDescription:
+      'يمكنك تغيير كلمة المرور الخاصة بك في أي وقت لضمان الأمان. لا يوجد حد لتغييرات كلمة المرور',
+  Strings.enterNewPassword: 'أدخل كلمة المرور الجديدة',
+  Strings.enterConfirmPassword: 'أدخل تأكيد كلمة المرور',
+  Strings.confirmPasswordMustBeTheSame:
+      'كلمتا المرور المُدخلتان غير متطابقتين',
+  Strings.yes: 'نعم',
 };

+ 10 - 0
lib/config/translations/de_DE/de_de_translation.dart

@@ -365,4 +365,14 @@ const Map<String, String> deDE = {
   Strings.excellentPremiumFeatures: 'Ausgezeichnete Premium-Funktionen',
   Strings.worthRecommending: 'Empfehlenswert',
   Strings.loveTheDesign: 'Ich liebe das Design',
+
+  // Change Password
+  Strings.changePassword: 'Passwort ändern',
+  Strings.changePasswordDescription:
+      'Sie können Ihr Passwort jederzeit ändern, um die Sicherheit zu gewährleisten. Keine Begrenzung für Passwortänderungen',
+  Strings.enterNewPassword: 'Neues Passwort eingeben',
+  Strings.enterConfirmPassword: 'Passwort bestätigen',
+  Strings.confirmPasswordMustBeTheSame:
+      'Die zweimal eingegebenen Passwörter stimmen nicht überein',
+  Strings.yes: 'Ja',
 };

+ 10 - 0
lib/config/translations/en_US/en_us_translation.dart

@@ -366,4 +366,14 @@ Map<String, String> enUs = {
   Strings.excellentPremiumFeatures: 'Excellent premium features',
   Strings.worthRecommending: 'Worth recommending',
   Strings.loveTheDesign: 'I love the design',
+
+  // Change Password
+  Strings.changePassword: 'Change Password',
+  Strings.changePasswordDescription:
+      'You can change your password anytime to ensure security. No limit on password changes',
+  Strings.enterNewPassword: 'Enter new password',
+  Strings.enterConfirmPassword: 'Enter confirm password',
+  Strings.confirmPasswordMustBeTheSame:
+      'The passwords entered twice are inconsistent',
+  Strings.yes: 'Yes',
 };

+ 10 - 0
lib/config/translations/es_ES/es_es_translation.dart

@@ -371,4 +371,14 @@ const Map<String, String> esEs = {
   Strings.excellentPremiumFeatures: 'Excelentes funciones premium',
   Strings.worthRecommending: 'Vale la pena recomendarlo',
   Strings.loveTheDesign: 'Me encanta el diseño',
+
+  // Change Password
+  Strings.changePassword: 'Cambiar contraseña',
+  Strings.changePasswordDescription:
+      'Puedes cambiar tu contraseña en cualquier momento para garantizar la seguridad. Sin límite de cambios de contraseña',
+  Strings.enterNewPassword: 'Ingresa nueva contraseña',
+  Strings.enterConfirmPassword: 'Confirma la contraseña',
+  Strings.confirmPasswordMustBeTheSame:
+      'Las contraseñas ingresadas dos veces no coinciden',
+  Strings.yes: 'Sí',
 };

+ 10 - 0
lib/config/translations/fa_IR/fa_ir_translation.dart

@@ -367,4 +367,14 @@ const Map<String, String> faIR = {
   Strings.excellentPremiumFeatures: 'ویژگی‌های پریمیوم عالی',
   Strings.worthRecommending: 'ارزش توصیه دارد',
   Strings.loveTheDesign: 'من طراحی را دوست دارم',
+
+  // Change Password
+  Strings.changePassword: 'تغییر رمز عبور',
+  Strings.changePasswordDescription:
+      'شما می‌توانید رمز عبور خود را در هر زمان برای اطمینان از امنیت تغییر دهید. محدودیتی برای تغییر رمز عبور وجود ندارد',
+  Strings.enterNewPassword: 'رمز عبور جدید را وارد کنید',
+  Strings.enterConfirmPassword: 'تأیید رمز عبور را وارد کنید',
+  Strings.confirmPasswordMustBeTheSame:
+      'رمزهای عبور وارد شده دو بار یکسان نیستند',
+  Strings.yes: 'بله',
 };

+ 10 - 0
lib/config/translations/fr_FR/fr_fr_translation.dart

@@ -372,4 +372,14 @@ const Map<String, String> frFR = {
   Strings.excellentPremiumFeatures: 'Excellentes fonctionnalités premium',
   Strings.worthRecommending: 'Vaut la peine d\'être recommandé',
   Strings.loveTheDesign: 'J\'adore le design',
+
+  // Change Password
+  Strings.changePassword: 'Changer le mot de passe',
+  Strings.changePasswordDescription:
+      'Vous pouvez changer votre mot de passe à tout moment pour assurer la sécurité. Aucune limite de changements de mot de passe',
+  Strings.enterNewPassword: 'Entrez le nouveau mot de passe',
+  Strings.enterConfirmPassword: 'Confirmez le mot de passe',
+  Strings.confirmPasswordMustBeTheSame:
+      'Les mots de passe saisis deux fois ne correspondent pas',
+  Strings.yes: 'Oui',
 };

+ 10 - 0
lib/config/translations/ja_JP/ja_jp_translation.dart

@@ -345,4 +345,14 @@ const Map<String, String> jaJP = {
   Strings.excellentPremiumFeatures: '優れたプレミアム機能',
   Strings.worthRecommending: 'おすすめする価値があります',
   Strings.loveTheDesign: 'デザインが大好きです',
+
+  // Change Password
+  Strings.changePassword: 'パスワード変更',
+  Strings.changePasswordDescription:
+      'セキュリティを確保するために、いつでもパスワードを変更できます。パスワード変更に制限はありません',
+  Strings.enterNewPassword: '新しいパスワードを入力',
+  Strings.enterConfirmPassword: 'パスワードを確認',
+  Strings.confirmPasswordMustBeTheSame:
+      '2回入力されたパスワードが一致しません',
+  Strings.yes: 'はい',
 };

+ 10 - 0
lib/config/translations/ko_KR/ko_kr_translation.dart

@@ -338,4 +338,14 @@ const Map<String, String> koKR = {
   Strings.excellentPremiumFeatures: '뛰어난 프리미엄 기능',
   Strings.worthRecommending: '추천할 가치가 있음',
   Strings.loveTheDesign: '디자인이 마음에 듦',
+
+  // Change Password
+  Strings.changePassword: '비밀번호 변경',
+  Strings.changePasswordDescription:
+      '보안을 위해 언제든지 비밀번호를 변경할 수 있습니다. 비밀번호 변경 제한 없음',
+  Strings.enterNewPassword: '새 비밀번호 입력',
+  Strings.enterConfirmPassword: '비밀번호 확인',
+  Strings.confirmPasswordMustBeTheSame:
+      '두 번 입력한 비밀번호가 일치하지 않습니다',
+  Strings.yes: '예',
 };

+ 10 - 0
lib/config/translations/my_MM/my_mm_translation.dart

@@ -375,4 +375,14 @@ const Map<String, String> myMM = {
   Strings.excellentPremiumFeatures: 'ကောင်းမွန်သော ပရီမီယံ features များ',
   Strings.worthRecommending: 'အကြံပြုထိုက်သည်',
   Strings.loveTheDesign: 'ဒီဇိုင်းကို နှစ်သက်သည်',
+
+  // Change Password
+  Strings.changePassword: 'စကားဝှက် ပြောင်းလဲရန်',
+  Strings.changePasswordDescription:
+      'လုံခြုံရေးကို သေချာစေရန် သင့်စကားဝှက်ကို အချိန်မရွေး ပြောင်းလဲနိုင်သည်။ စကားဝှက်ပြောင်းလဲမှုတွင် ကန့်သတ်ချက်မရှိပါ',
+  Strings.enterNewPassword: 'စကားဝှက်အသစ် ထည့်ပါ',
+  Strings.enterConfirmPassword: 'စကားဝှက်ကို အတည်ပြုပါ',
+  Strings.confirmPasswordMustBeTheSame:
+      'နှစ်ကြိမ် ထည့်သွင်းသော စကားဝှက်များ မတူညီပါ',
+  Strings.yes: 'ဟုတ်ကဲ့',
 };

+ 10 - 0
lib/config/translations/ru_RU/ru_ru_translation.dart

@@ -371,4 +371,14 @@ const Map<String, String> ruRU = {
   Strings.excellentPremiumFeatures: 'Превосходные премиум-функции',
   Strings.worthRecommending: 'Стоит рекомендовать',
   Strings.loveTheDesign: 'Мне нравится дизайн',
+
+  // Change Password
+  Strings.changePassword: 'Изменить пароль',
+  Strings.changePasswordDescription:
+      'Вы можете изменить свой пароль в любое время для обеспечения безопасности. Без ограничений на изменение пароля',
+  Strings.enterNewPassword: 'Введите новый пароль',
+  Strings.enterConfirmPassword: 'Подтвердите пароль',
+  Strings.confirmPasswordMustBeTheSame:
+      'Введённые дважды пароли не совпадают',
+  Strings.yes: 'Да',
 };

+ 11 - 0
lib/config/translations/strings_enum.dart

@@ -386,4 +386,15 @@ class Strings {
   static const String excellentPremiumFeatures = 'Excellent premium features';
   static const String worthRecommending = 'Worth recommending';
   static const String loveTheDesign = 'I love the design';
+
+  // Forgot Password
+  static const String changePassword = 'Change Password';
+  static const String changePasswordDescription =
+      'You can change your password anytime to ensuresecurity. No limit on password changes';
+  static const String enterNewPassword = 'Enter new password';
+  static const String enterConfirmPassword = 'Enter confirm password';
+  // 两次输入的密码不一致
+  static const String confirmPasswordMustBeTheSame =
+      'The passwords entered twice are inconsistent';
+  static const String yes = 'Yes';
 }

+ 1 - 0
pubspec.yaml

@@ -114,6 +114,7 @@ flutter:
   uses-material-design: true
   assets:
     - assets/images/
+    - assets/images/streaming/
     - assets/vectors/
     - assets/vectors/boost/
     - assets/vectors/status/