Просмотр исходного кода

fix:   内核库更新,充值页面更新,各种页面样式细节调整

lilu 3 месяцев назад
Родитель
Сommit
db016867ce
86 измененных файлов с 4676 добавлено и 1928 удалено
  1. BIN
      android/app/libs/libxray.aar
  2. 18 33
      android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt
  3. 0 14
      android/app/src/main/kotlin/app/xixi/nomo/ProxyNode.kt
  4. 0 20
      android/app/src/main/kotlin/app/xixi/nomo/ProxyStartOptions.kt
  5. 0 11
      android/app/src/main/kotlin/app/xixi/nomo/StatusDetectOptions.kt
  6. 2 1
      android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt
  7. 1 2
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  8. 2 8
      android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt
  9. BIN
      assets/fonts/iconfont.ttf
  10. BIN
      assets/images/banner_test.png
  11. BIN
      assets/images/media_bg.jpg
  12. BIN
      assets/images/pre_code_email.png
  13. BIN
      assets/images/pre_code_email_tip_blue.png
  14. BIN
      assets/images/pre_code_email_tip_white.png
  15. BIN
      assets/images/pre_code_save_local.png
  16. BIN
      assets/images/subscription_bg.mp4
  17. BIN
      assets/images/subscription_diamond.png
  18. BIN
      assets/images/subscription_green_shield.png
  19. BIN
      assets/images/subscription_plan_change_1.png
  20. BIN
      assets/images/subscription_plan_change_2.png
  21. BIN
      assets/images/subscription_plan_change_3.png
  22. BIN
      assets/images/subscription_wallet.png
  23. 7 3
      ios/Runner/Info.plist
  24. 27 0
      lib/app/constants/assets.dart
  25. 287 0
      lib/app/constants/iconfont/iconfont.css
  26. 77 0
      lib/app/constants/iconfont/iconfont.dart
  27. 24 61
      lib/app/controllers/api_controller.dart
  28. 85 6
      lib/app/controllers/core_controller.dart
  29. 2 0
      lib/app/data/models/launch/app_config.dart
  30. 45 3
      lib/app/data/models/launch/app_config.freezed.dart
  31. 4 0
      lib/app/data/models/launch/app_config.g.dart
  32. 2 4
      lib/app/data/models/launch/launch.dart
  33. 40 77
      lib/app/data/models/launch/launch.freezed.dart
  34. 4 6
      lib/app/data/models/launch/launch.g.dart
  35. 17 0
      lib/app/data/models/launch/nodes_config.dart
  36. 267 0
      lib/app/data/models/launch/nodes_config.freezed.dart
  37. 23 0
      lib/app/data/models/launch/nodes_config.g.dart
  38. 17 0
      lib/app/data/models/launch/smart_geo.dart
  39. 253 0
      lib/app/data/models/launch/smart_geo.freezed.dart
  40. 23 0
      lib/app/data/models/launch/smart_geo.g.dart
  41. 25 1
      lib/app/dialog/all_dialog.dart
  42. 26 26
      lib/app/dialog/custom_dialog.dart
  43. 165 0
      lib/app/dialog/update_dailog.dart
  44. 11 1
      lib/app/modules/account/controllers/account_controller.dart
  45. 91 164
      lib/app/modules/account/views/account_view.dart
  46. 63 5
      lib/app/modules/deviceauth/controllers/deviceauth_controller.dart
  47. 167 542
      lib/app/modules/deviceauth/views/deviceauth_view.dart
  48. 196 0
      lib/app/modules/deviceauth/widgets/device_card.dart
  49. 14 6
      lib/app/modules/home/controllers/home_controller.dart
  50. 53 14
      lib/app/modules/home/views/home_view.dart
  51. 11 7
      lib/app/modules/home/widgets/connection_button.dart
  52. 16 15
      lib/app/modules/home/widgets/menu_list.dart
  53. 1 1
      lib/app/modules/language/views/language_view.dart
  54. 10 0
      lib/app/modules/medialocation/bindings/medialocation_binding.dart
  55. 90 0
      lib/app/modules/medialocation/controllers/medialocation_controller.dart
  56. 384 0
      lib/app/modules/medialocation/views/medialocation_view.dart
  57. 11 0
      lib/app/modules/node/controllers/node_controller.dart
  58. 4 1
      lib/app/modules/node/views/node_view.dart
  59. 36 14
      lib/app/modules/node/widgets/node_list.dart
  60. 64 8
      lib/app/modules/precode/controllers/precode_controller.dart
  61. 8 11
      lib/app/modules/precode/sendemail/controllers/precode_sendemail_controller.dart
  62. 73 236
      lib/app/modules/precode/sendemail/views/precode_sendemail_view.dart
  63. 68 140
      lib/app/modules/precode/views/precode_view.dart
  64. 271 0
      lib/app/modules/precode/widgets/precode_save_dialog.dart
  65. 4 3
      lib/app/modules/routingmode/views/routingmode_view.dart
  66. 188 106
      lib/app/modules/setting/views/setting_view.dart
  67. 5 0
      lib/app/modules/splash/controllers/splash_controller.dart
  68. 6 18
      lib/app/modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart
  69. 26 44
      lib/app/modules/splittunneling/views/splittunneling_view.dart
  70. 10 0
      lib/app/modules/subscription/bindings/subscription_binding.dart
  71. 96 0
      lib/app/modules/subscription/controllers/subscription_controller.dart
  72. 497 0
      lib/app/modules/subscription/views/subscription_view.dart
  73. 19 2
      lib/app/routes/app_pages.dart
  74. 4 0
      lib/app/routes/app_routes.dart
  75. 48 0
      lib/app/widgets/infintte_rotate.dart
  76. 184 0
      lib/app/widgets/info_card.dart
  77. 4 2
      lib/app/widgets/ix_app_bar.dart
  78. 9 7
      lib/app/widgets/ix_text_field.dart
  79. 3 3
      lib/app/widgets/submit_btn.dart
  80. 11 0
      lib/config/theme/dark_theme_colors.dart
  81. 0 123
      lib/utils/file_cache_manager.dart
  82. 0 179
      lib/utils/file_stream_util.dart
  83. 369 0
      lib/utils/geo_downloader.dart
  84. 4 0
      macos/Flutter/GeneratedPluginRegistrant.swift
  85. 96 0
      pubspec.lock
  86. 8 0
      pubspec.yaml

BIN
android/app/libs/libxray.aar


+ 18 - 33
android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt

@@ -17,23 +17,24 @@ import android.net.VpnService
 import android.telephony.TelephonyManager
 import android.telephony.TelephonyManager
 import android.util.Base64
 import android.util.Base64
 import android.util.Log
 import android.util.Log
+import androidx.core.graphics.createBitmap
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_PERMISSION_DENIED
 import app.xixi.nomo.XRayApi.Companion.VPN_STATE_PERMISSION_DENIED
 import app.xixi.nomo.XRayApi.OnVpnServiceEvent
 import app.xixi.nomo.XRayApi.OnVpnServiceEvent
 import com.google.gson.Gson
 import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonParser
+import com.google.gson.JsonSyntaxException
 import io.flutter.plugin.common.BinaryMessenger
 import io.flutter.plugin.common.BinaryMessenger
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import java.io.ByteArrayOutputStream
 import java.io.ByteArrayOutputStream
+import java.io.File
 import java.io.InputStream
 import java.io.InputStream
-import androidx.core.graphics.createBitmap
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.JsonParser
-import com.google.gson.JsonSyntaxException
 import java.util.Locale
 import java.util.Locale
+import java.util.Map
 
 
 // 消息数据类
 // 消息数据类
 data class VpnStatusMessage(
 data class VpnStatusMessage(
@@ -371,34 +372,18 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         VLog.i(TAG, "sessionId: ${config.sessionId}")
         VLog.i(TAG, "sessionId: ${config.sessionId}")
         VLog.i(TAG, "socksPort: ${config.socksPort}")
         VLog.i(TAG, "socksPort: ${config.socksPort}")
         VLog.i(TAG, "tunnelConfig: ${config.tunnelConfig}")
         VLog.i(TAG, "tunnelConfig: ${config.tunnelConfig}")
-        
-        val detectOptions = StatusDetectOptions()
-        detectOptions.statusDetectInterval = 60
-        val detectUrls = ArrayList<String>()
-        detectUrls.add("https://www.baidu.com")
-        detectOptions.statusDetectUrls = detectUrls
-        val jsonArray = safeStringToJsonArray(configJson)
-        val proxyNodes= ArrayList<ProxyNode>()
-        jsonArray?.forEach {
-            if(it.isJsonObject) {
-                val obj: JsonObject = it.asJsonObject
-                val nodeId = obj.get("nodeId").asString
-                val coreConfig = obj.get("coreConfig").asString
-                VLog.i(TAG, "coreConfig: $coreConfig")
-                val proxyNode = ProxyNode()
-                proxyNode.nodeId = nodeId
-//                VLog.i(TAG, "读取配置文件: connect.json")
-//                proxyNode.coreConfig = readAssets("connect.json")
-                proxyNode.coreConfig = coreConfig
-                proxyNodes.add(proxyNode)
-            }
-        }
+        VLog.i(TAG, "configJson: $configJson")
 
 
-        val startOptions = ProxyStartOptions()
-        startOptions.socksPort = config.socksPort
-        startOptions.nodes = proxyNodes
-        startOptions.statusDetectOptions = detectOptions
-        config.startOptions = startOptions
+        var geoPath = ""
+        if (File(activity.filesDir, "geo/geoip.dat").exists() && File(activity.filesDir, "geo/geosite.dat").exists()) {
+            geoPath = activity.filesDir.absolutePath + "/geo"
+        }
+        VLog.i(TAG, "geoDir: $geoPath")
+        val startOptions = Map.of<String?, Any?>(
+            "geoPath", geoPath,
+            "nodesConfig", configJson
+        )
+        config.startOptions = Gson().toJson(startOptions)
         
         
         VLog.i(TAG, "配置准备完成,调用 xrayApi.startXray()")
         VLog.i(TAG, "配置准备完成,调用 xrayApi.startXray()")
         val result = xrayApi?.startXray(config)
         val result = xrayApi?.startXray(config)

+ 0 - 14
android/app/src/main/kotlin/app/xixi/nomo/ProxyNode.kt

@@ -1,14 +0,0 @@
-package app.xixi.nomo
-
-import com.google.gson.annotations.SerializedName
-
-data class ProxyNode(
-    @SerializedName("nodeId")
-    var nodeId: String? = null,
-
-    @SerializedName("coreConfig")
-    var coreConfig: String? = null,
-
-    @SerializedName("coreRuntimeEnvVars")
-    var coreRuntimeEnvVars: Map<String, String>? = null
-)

+ 0 - 20
android/app/src/main/kotlin/app/xixi/nomo/ProxyStartOptions.kt

@@ -1,20 +0,0 @@
-package app.xixi.nomo
-
-import com.google.gson.annotations.SerializedName
-
-data class ProxyStartOptions(
-    @SerializedName("socksPort")
-    var socksPort: Int = 0,
-
-    @SerializedName("statusDetectOptions")
-    var statusDetectOptions: StatusDetectOptions? = null,
-
-    @SerializedName("nodes")
-    var nodes: List<ProxyNode>? = null,
-
-    @SerializedName("maxRetryCount")
-    var maxRetryCount: Int = 0,
-
-    @SerializedName("geoPath")
-    var geoPath: String? = null
-)

+ 0 - 11
android/app/src/main/kotlin/app/xixi/nomo/StatusDetectOptions.kt

@@ -1,11 +0,0 @@
-package app.xixi.nomo
-
-import com.google.gson.annotations.SerializedName
-
-data class StatusDetectOptions(
-    @SerializedName("statusDetectInterval")
-    var statusDetectInterval: Int = 0, // seconds
-
-    @SerializedName("statusDetectUrls")
-    var statusDetectUrls: List<String>? = null
-)

+ 2 - 1
android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt

@@ -15,6 +15,7 @@ import android.os.Message
 import android.os.Messenger
 import android.os.Messenger
 import java.util.concurrent.locks.ReentrantLock
 import java.util.concurrent.locks.ReentrantLock
 
 
+
 class XRayApi {
 class XRayApi {
     private val lock = ReentrantLock()
     private val lock = ReentrantLock()
     private val mReplyMsgHandler = Messenger(ReplyMsgHandler(Looper.getMainLooper()))
     private val mReplyMsgHandler = Messenger(ReplyMsgHandler(Looper.getMainLooper()))
@@ -251,7 +252,7 @@ class XRayApi {
             putInt("socksPort", config.socksPort)
             putInt("socksPort", config.socksPort)
             putString("sessionId", config.sessionId)
             putString("sessionId", config.sessionId)
             putString("tunnelConfig", config.tunnelConfig)
             putString("tunnelConfig", config.tunnelConfig)
-            putString("startOptions", config.getProxyStartOptions())
+            putString("startOptions", config.startOptions)
             putStringArrayList("allowVpnApps", config.allowVpnApps)
             putStringArrayList("allowVpnApps", config.allowVpnApps)
             putStringArrayList("disallowVpnApps", config.disallowVpnApps)
             putStringArrayList("disallowVpnApps", config.disallowVpnApps)
         }
         }

+ 1 - 2
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -22,7 +22,6 @@ import go.Seq
 import ixvpn_mobile.Ixvpn_mobile
 import ixvpn_mobile.Ixvpn_mobile
 import ixvpn_mobile.ProxyConnectorHandler
 import ixvpn_mobile.ProxyConnectorHandler
 
 
-
 class XRayService : VpnService() {
 class XRayService : VpnService() {
 
 
     private val mMessenger = Messenger(IncomingHandler())
     private val mMessenger = Messenger(IncomingHandler())
@@ -153,7 +152,7 @@ class XRayService : VpnService() {
         // 只在有replyTo时才记录Messenger对象
         // 只在有replyTo时才记录Messenger对象
         if (msg.replyTo != null) {
         if (msg.replyTo != null) {
             replyMessenger = msg.replyTo
             replyMessenger = msg.replyTo
-            VLog.i(TAG, "记录replyTo Messenger: $replyMessenger")
+            VLog.i(TAG, "记录replyTo Messenger: ${msg.what}")
         } else {
         } else {
             VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
             VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
         }
         }

+ 2 - 8
android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt

@@ -6,17 +6,11 @@ data class XrayConfig(
     var sessionId: String = "",
     var sessionId: String = "",
     var socksPort: Int = 0,
     var socksPort: Int = 0,
     var tunnelConfig: String = "",
     var tunnelConfig: String = "",
-    var startOptions: ProxyStartOptions? = null,
+    var startOptions: String = "",
     var allowVpnApps: ArrayList<String> = ArrayList(),
     var allowVpnApps: ArrayList<String> = ArrayList(),
     var disallowVpnApps: ArrayList<String> = ArrayList()
     var disallowVpnApps: ArrayList<String> = ArrayList()
 ) {
 ) {
     fun getProxyStartOptions(): String {
     fun getProxyStartOptions(): String {
-        return try {
-            val gson = Gson()
-            gson.toJson(startOptions)
-        } catch (e: Exception) {
-            e.printStackTrace()
-            ""
-        }
+        return startOptions
     }
     }
 }
 }

BIN
assets/fonts/iconfont.ttf


BIN
assets/images/banner_test.png


BIN
assets/images/media_bg.jpg


BIN
assets/images/pre_code_email.png


BIN
assets/images/pre_code_email_tip_blue.png


BIN
assets/images/pre_code_email_tip_white.png


BIN
assets/images/pre_code_save_local.png


BIN
assets/images/subscription_bg.mp4


BIN
assets/images/subscription_diamond.png


BIN
assets/images/subscription_green_shield.png


BIN
assets/images/subscription_plan_change_1.png


BIN
assets/images/subscription_plan_change_2.png


BIN
assets/images/subscription_plan_change_3.png


BIN
assets/images/subscription_wallet.png


+ 7 - 3
ios/Runner/Info.plist

@@ -45,7 +45,11 @@
 		<true/>
 		<true/>
 		<key>UIApplicationSupportsIndirectInputEvents</key>
 		<key>UIApplicationSupportsIndirectInputEvents</key>
 		<true/>
 		<true/>
-		<key>UIStatusBarHidden</key>
-		<false/>
-	</dict>
+	<key>UIStatusBarHidden</key>
+	<false/>
+	<key>NSPhotoLibraryAddUsageDescription</key>
+	<string>We need access to save your Pre Code image to your photo library.</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>We need access to save your Pre Code image to your photo library.</string>
+</dict>
 </plist>
 </plist>

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

@@ -58,4 +58,31 @@ class Assets {
   static const String smirkingFace = 'assets/vectors/boost/smirking_face.svg';
   static const String smirkingFace = 'assets/vectors/boost/smirking_face.svg';
   static const String smilingFaceWithHearts =
   static const String smilingFaceWithHearts =
       'assets/vectors/boost/smiling_face_with_hearts.svg';
       'assets/vectors/boost/smiling_face_with_hearts.svg';
+
+  // 订阅
+  static const String subscriptionDiamond =
+      'assets/images/subscription_diamond.png';
+  static const String subscriptionWallet =
+      'assets/images/subscription_wallet.png';
+  static const String subscriptionGreenShield =
+      'assets/images/subscription_green_shield.png';
+
+  static const String subscriptionPlanChange1 =
+      'assets/images/subscription_plan_change_1.png';
+  static const String subscriptionPlanChange2 =
+      'assets/images/subscription_plan_change_2.png';
+  static const String subscriptionPlanChange3 =
+      'assets/images/subscription_plan_change_3.png';
+
+  static const String preCodeEmail = 'assets/images/pre_code_email.png';
+  static const String preCodeSaveLocal =
+      'assets/images/pre_code_save_local.png';
+  static const String preCodeEmailTipBlue =
+      'assets/images/pre_code_email_tip_blue.png';
+  static const String preCodeEmailTipWhite =
+      'assets/images/pre_code_email_tip_white.png';
+
+  static const String bannerTest = 'assets/images/banner_test.png';
+  static const String subscriptionBg = 'assets/images/subscription_bg.mp4';
+  static const String mediaBg = 'assets/images/media_bg.jpg';
 }
 }

+ 287 - 0
lib/app/constants/iconfont/iconfont.css

@@ -0,0 +1,287 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 5058747 */
+  src: url('iconfont.woff2?t=1762428692505') format('woff2'),
+       url('iconfont.woff?t=1762428692505') format('woff'),
+       url('iconfont.ttf?t=1762428692505') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-icon68:before {
+  content: "\e78e";
+}
+
+.icon-icon22:before {
+  content: "\e780";
+}
+
+.icon-icon17:before {
+  content: "\e781";
+}
+
+.icon-icon19:before {
+  content: "\e782";
+}
+
+.icon-icon18:before {
+  content: "\e783";
+}
+
+.icon-icon15:before {
+  content: "\e784";
+}
+
+.icon-icon14:before {
+  content: "\e785";
+}
+
+.icon-icon10:before {
+  content: "\e786";
+}
+
+.icon-icon07:before {
+  content: "\e787";
+}
+
+.icon-icon09:before {
+  content: "\e788";
+}
+
+.icon-icon01:before {
+  content: "\e789";
+}
+
+.icon-icon06:before {
+  content: "\e78a";
+}
+
+.icon-icon02:before {
+  content: "\e78b";
+}
+
+.icon-icon04:before {
+  content: "\e78c";
+}
+
+.icon-icon03:before {
+  content: "\e78d";
+}
+
+.icon-icon23:before {
+  content: "\e76c";
+}
+
+.icon-icon50:before {
+  content: "\e76d";
+}
+
+.icon-icon54:before {
+  content: "\e76e";
+}
+
+.icon-icon37:before {
+  content: "\e76f";
+}
+
+.icon-icon11:before {
+  content: "\e770";
+}
+
+.icon-icon40:before {
+  content: "\e771";
+}
+
+.icon-icon29:before {
+  content: "\e772";
+}
+
+.icon-icon46:before {
+  content: "\e773";
+}
+
+.icon-icon28:before {
+  content: "\e774";
+}
+
+.icon-icon38:before {
+  content: "\e775";
+}
+
+.icon-icon42:before {
+  content: "\e776";
+}
+
+.icon-icon13:before {
+  content: "\e777";
+}
+
+.icon-icon34:before {
+  content: "\e778";
+}
+
+.icon-icon31:before {
+  content: "\e779";
+}
+
+.icon-icon30:before {
+  content: "\e77a";
+}
+
+.icon-icon21:before {
+  content: "\e77b";
+}
+
+.icon-icon26:before {
+  content: "\e77c";
+}
+
+.icon-icon05:before {
+  content: "\e74b";
+}
+
+.icon-icon35:before {
+  content: "\e77d";
+}
+
+.icon-icon27:before {
+  content: "\e77e";
+}
+
+.icon-icon16:before {
+  content: "\e77f";
+}
+
+.icon-icon25:before {
+  content: "\e75d";
+}
+
+.icon-icon45:before {
+  content: "\e75e";
+}
+
+.icon-icon32:before {
+  content: "\e75f";
+}
+
+.icon-icon56:before {
+  content: "\e760";
+}
+
+.icon-icon24:before {
+  content: "\e74d";
+}
+
+.icon-icon41:before {
+  content: "\e761";
+}
+
+.icon-icon55:before {
+  content: "\e762";
+}
+
+.icon-icon62:before {
+  content: "\e763";
+}
+
+.icon-icon52:before {
+  content: "\e764";
+}
+
+.icon-icon65:before {
+  content: "\e765";
+}
+
+.icon-icon12:before {
+  content: "\e74c";
+}
+
+.icon-icon08:before {
+  content: "\e766";
+}
+
+.icon-icon33:before {
+  content: "\e767";
+}
+
+.icon-icon20:before {
+  content: "\e768";
+}
+
+.icon-icon39:before {
+  content: "\e769";
+}
+
+.icon-icon59:before {
+  content: "\e76a";
+}
+
+.icon-icon58:before {
+  content: "\e76b";
+}
+
+.icon-icon36:before {
+  content: "\e754";
+}
+
+.icon-icon53:before {
+  content: "\e755";
+}
+
+.icon-icon47:before {
+  content: "\e74e";
+}
+
+.icon-icon57:before {
+  content: "\e756";
+}
+
+.icon-icon43:before {
+  content: "\e757";
+}
+
+.icon-icon49:before {
+  content: "\e758";
+}
+
+.icon-icon66:before {
+  content: "\e759";
+}
+
+.icon-icon48:before {
+  content: "\e75a";
+}
+
+.icon-icon67:before {
+  content: "\e75b";
+}
+
+.icon-icon44:before {
+  content: "\e75c";
+}
+
+.icon-icon63:before {
+  content: "\e750";
+}
+
+.icon-icon64:before {
+  content: "\e74f";
+}
+
+.icon-icon60:before {
+  content: "\e751";
+}
+
+.icon-icon51:before {
+  content: "\e752";
+}
+
+.icon-icon61:before {
+  content: "\e753";
+}
+

+ 77 - 0
lib/app/constants/iconfont/iconfont.dart

@@ -0,0 +1,77 @@
+// ignore_for_file: constant_identifier_names
+
+import 'package:flutter/widgets.dart';
+
+class IconFont {
+  static const String _family = 'iconfont';
+  IconFont._();
+
+  static const IconData icon22 = IconData(0xe780, fontFamily: _family);
+  static const IconData icon17 = IconData(0xe781, fontFamily: _family);
+  static const IconData icon19 = IconData(0xe782, fontFamily: _family);
+  static const IconData icon18 = IconData(0xe783, fontFamily: _family);
+  static const IconData icon15 = IconData(0xe784, fontFamily: _family);
+  static const IconData icon14 = IconData(0xe785, fontFamily: _family);
+  static const IconData icon10 = IconData(0xe786, fontFamily: _family);
+  static const IconData icon07 = IconData(0xe787, fontFamily: _family);
+  static const IconData icon09 = IconData(0xe788, fontFamily: _family);
+  static const IconData icon01 = IconData(0xe789, fontFamily: _family);
+  static const IconData icon06 = IconData(0xe78a, fontFamily: _family);
+  static const IconData icon02 = IconData(0xe78b, fontFamily: _family);
+  static const IconData icon04 = IconData(0xe78c, fontFamily: _family);
+  static const IconData icon03 = IconData(0xe78d, fontFamily: _family);
+  static const IconData icon23 = IconData(0xe76c, fontFamily: _family);
+  static const IconData icon50 = IconData(0xe76d, fontFamily: _family);
+  static const IconData icon54 = IconData(0xe76e, fontFamily: _family);
+  static const IconData icon37 = IconData(0xe76f, fontFamily: _family);
+  static const IconData icon11 = IconData(0xe770, fontFamily: _family);
+  static const IconData icon40 = IconData(0xe771, fontFamily: _family);
+  static const IconData icon29 = IconData(0xe772, fontFamily: _family);
+  static const IconData icon46 = IconData(0xe773, fontFamily: _family);
+  static const IconData icon28 = IconData(0xe774, fontFamily: _family);
+  static const IconData icon38 = IconData(0xe775, fontFamily: _family);
+  static const IconData icon42 = IconData(0xe776, fontFamily: _family);
+  static const IconData icon13 = IconData(0xe777, fontFamily: _family);
+  static const IconData icon34 = IconData(0xe778, fontFamily: _family);
+  static const IconData icon31 = IconData(0xe779, fontFamily: _family);
+  static const IconData icon30 = IconData(0xe77a, fontFamily: _family);
+  static const IconData icon21 = IconData(0xe77b, fontFamily: _family);
+  static const IconData icon26 = IconData(0xe77c, fontFamily: _family);
+  static const IconData icon05 = IconData(0xe74b, fontFamily: _family);
+  static const IconData icon35 = IconData(0xe77d, fontFamily: _family);
+  static const IconData icon27 = IconData(0xe77e, fontFamily: _family);
+  static const IconData icon16 = IconData(0xe77f, fontFamily: _family);
+  static const IconData icon25 = IconData(0xe75d, fontFamily: _family);
+  static const IconData icon45 = IconData(0xe75e, fontFamily: _family);
+  static const IconData icon32 = IconData(0xe75f, fontFamily: _family);
+  static const IconData icon56 = IconData(0xe760, fontFamily: _family);
+  static const IconData icon24 = IconData(0xe74d, fontFamily: _family);
+  static const IconData icon41 = IconData(0xe761, fontFamily: _family);
+  static const IconData icon55 = IconData(0xe762, fontFamily: _family);
+  static const IconData icon62 = IconData(0xe763, fontFamily: _family);
+  static const IconData icon52 = IconData(0xe764, fontFamily: _family);
+  static const IconData icon65 = IconData(0xe765, fontFamily: _family);
+  static const IconData icon12 = IconData(0xe74c, fontFamily: _family);
+  static const IconData icon08 = IconData(0xe766, fontFamily: _family);
+  static const IconData icon33 = IconData(0xe767, fontFamily: _family);
+  static const IconData icon20 = IconData(0xe768, fontFamily: _family);
+  static const IconData icon39 = IconData(0xe769, fontFamily: _family);
+  static const IconData icon59 = IconData(0xe76a, fontFamily: _family);
+  static const IconData icon58 = IconData(0xe76b, fontFamily: _family);
+  static const IconData icon36 = IconData(0xe754, fontFamily: _family);
+  static const IconData icon53 = IconData(0xe755, fontFamily: _family);
+  static const IconData icon47 = IconData(0xe74e, fontFamily: _family);
+  static const IconData icon57 = IconData(0xe756, fontFamily: _family);
+  static const IconData icon43 = IconData(0xe757, fontFamily: _family);
+  static const IconData icon49 = IconData(0xe758, fontFamily: _family);
+  static const IconData icon66 = IconData(0xe759, fontFamily: _family);
+  static const IconData icon48 = IconData(0xe75a, fontFamily: _family);
+  static const IconData icon67 = IconData(0xe75b, fontFamily: _family);
+  static const IconData icon44 = IconData(0xe75c, fontFamily: _family);
+  static const IconData icon63 = IconData(0xe750, fontFamily: _family);
+  static const IconData icon64 = IconData(0xe74f, fontFamily: _family);
+  static const IconData icon60 = IconData(0xe751, fontFamily: _family);
+  static const IconData icon51 = IconData(0xe752, fontFamily: _family);
+  static const IconData icon61 = IconData(0xe753, fontFamily: _family);
+  static const IconData icon68 = IconData(0xe78e, fontFamily: _family);
+}

+ 24 - 61
lib/app/controllers/api_controller.dart

@@ -3,6 +3,7 @@ import 'dart:io';
 
 
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:dio/dio.dart';
 import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/api/router/api_router.dart';
 import 'package:nomo/app/api/router/api_router.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
 import 'package:nomo/app/data/sp/ix_sp.dart';
@@ -13,9 +14,10 @@ import '../../config/translations/localization_service.dart';
 import '../../config/translations/strings_enum.dart';
 import '../../config/translations/strings_enum.dart';
 import '../../pigeons/core_api.g.dart';
 import '../../pigeons/core_api.g.dart';
 import '../../utils/device_manager.dart';
 import '../../utils/device_manager.dart';
-import '../../utils/file_cache_manager.dart';
+import '../../utils/geo_downloader.dart';
 import '../../utils/log/logger.dart';
 import '../../utils/log/logger.dart';
 import '../../utils/network_helper.dart';
 import '../../utils/network_helper.dart';
+import '../../utils/system_helper.dart';
 import '../api/core/api_core.dart';
 import '../api/core/api_core.dart';
 import '../components/country_restricted_overlay.dart';
 import '../components/country_restricted_overlay.dart';
 import '../components/ix_snackbar.dart';
 import '../components/ix_snackbar.dart';
@@ -29,6 +31,7 @@ import '../data/models/failure.dart';
 import '../data/models/fingerprint.dart';
 import '../data/models/fingerprint.dart';
 import '../data/models/launch/groups.dart';
 import '../data/models/launch/groups.dart';
 import '../data/models/launch/launch.dart';
 import '../data/models/launch/launch.dart';
+import '../dialog/update_dailog.dart';
 
 
 class ApiController extends GetxService {
 class ApiController extends GetxService {
   final TAG = 'ApiController';
   final TAG = 'ApiController';
@@ -162,9 +165,6 @@ class ApiController extends GetxService {
             launch.appConfig!.websiteUrl!.isNotEmpty) {
             launch.appConfig!.websiteUrl!.isNotEmpty) {
           Configs.websiteUrl = launch.appConfig!.websiteUrl!;
           Configs.websiteUrl = launch.appConfig!.websiteUrl!;
         }
         }
-
-        // 下载ips和domains文件
-        readIpDomain();
       }
       }
     } catch (e) {
     } catch (e) {
       log(TAG, 'initLaunch error: $e');
       log(TAG, 'initLaunch error: $e');
@@ -269,18 +269,8 @@ class ApiController extends GetxService {
       final isVpnRunning = await CoreApi().isConnected() ?? false;
       final isVpnRunning = await CoreApi().isConnected() ?? false;
       if (!isVpnRunning) {
       if (!isVpnRunning) {
         await checkUpdate();
         await checkUpdate();
-        switch (MemberLevel.fromLevel(data.userConfig?.memberLevel)) {
-          case MemberLevel.guest:
-            //游客禁用,必须登录
-            if (data.appConfig?.visitorDisabled ?? true) {
-            } else {
-              //允许游客访问,如果不存在Main,则跳转Main
-            }
-            break;
-          default:
-            log('login user, default not handle');
-            break;
-        }
+        // 下载smartgeo文件
+        GeoDownloader().downloadSmartGeo(smartGeo: data.appConfig!.smartGeo!);
       }
       }
     } catch (e, s) {
     } catch (e, s) {
       if (IXSP.getLastIsUserDisabled()) {
       if (IXSP.getLastIsUserDisabled()) {
@@ -361,50 +351,6 @@ class ApiController extends GetxService {
     }
     }
   }
   }
 
 
-  Future<void> readIpDomain() async {
-    try {
-      final fileCacheManager = FileCacheManager();
-      final launch = IXSP.getLaunch();
-      if (launch != null && launch.appConfig?.skipGeo != null) {
-        // 读取文件并计算MD5
-        if (launch.appConfig?.skipGeo?.ipsUrl != null &&
-            launch.appConfig!.skipGeo!.ipsUrl!.isNotEmpty) {
-          final remoteUrl =
-              '${Configs.assetUrl}/${launch.appConfig?.skipGeo?.ipsUrl}';
-          await fileCacheManager.getFileContent(
-            remoteUrl: remoteUrl,
-            fileName: 'ips.txt',
-            expectedMd5: launch.appConfig?.skipGeo?.ipsMd5 ?? '',
-            onProgress: (progress) {
-              log(
-                TAG,
-                'Downloading IPs: ${(progress * 100).toStringAsFixed(2)}%',
-              );
-            },
-          );
-        }
-        if (launch.appConfig?.skipGeo?.domainsUrl != null &&
-            launch.appConfig!.skipGeo!.domainsUrl!.isNotEmpty) {
-          final remoteUrl =
-              '${Configs.assetUrl}/${launch.appConfig?.skipGeo?.domainsUrl}';
-          await fileCacheManager.getFileContent(
-            remoteUrl: remoteUrl,
-            fileName: 'domains.txt',
-            expectedMd5: launch.appConfig?.skipGeo?.domainsMd5 ?? '',
-            onProgress: (progress) {
-              log(
-                TAG,
-                'Downloading Domains: ${(progress * 100).toStringAsFixed(2)}%',
-              );
-            },
-          );
-        }
-      }
-    } catch (e) {
-      log(TAG, 'readIpsDomain error: $e');
-    }
-  }
-
   // 更新检查 - 智能时间控制版本
   // 更新检查 - 智能时间控制版本
   Future<bool> checkUpdate({bool isClickCheck = false}) async {
   Future<bool> checkUpdate({bool isClickCheck = false}) async {
     try {
     try {
@@ -422,7 +368,24 @@ class ApiController extends GetxService {
       }
       }
 
 
       if (hasUpdate) {
       if (hasUpdate) {
-        if (hasForceUpdate) {}
+        Get.dialog(
+          WillPopScope(
+            onWillPop: () async => false,
+            child: UpdateDialog(
+              upgrade: upgrade,
+              onUpdate: () =>
+                  SystemHelper.openGooglePlayUrl(upgrade?.appStoreUrl ?? ''),
+              onLater: hasForceUpdate
+                  ? null
+                  : () {
+                      Navigator.of(Get.context!).pop();
+                    },
+            ),
+          ),
+          barrierDismissible: false,
+        ).then((_) {
+          log('UpdateDialog closed');
+        });
       }
       }
       return hasUpdate;
       return hasUpdate;
     } catch (e) {
     } catch (e) {

+ 85 - 6
lib/app/controllers/core_controller.dart

@@ -5,10 +5,14 @@ import 'package:dio/dio.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:uuid/uuid.dart';
 import 'package:uuid/uuid.dart';
 
 
+import '../../config/translations/strings_enum.dart';
 import '../../pigeons/core_api.g.dart';
 import '../../pigeons/core_api.g.dart';
 import '../../utils/haptic_feedback_manager.dart';
 import '../../utils/haptic_feedback_manager.dart';
 import '../../utils/log/logger.dart';
 import '../../utils/log/logger.dart';
+import '../components/ix_snackbar.dart';
 import '../constants/enums.dart';
 import '../constants/enums.dart';
+import '../data/models/api_exception.dart';
+import '../data/models/failure.dart';
 import '../data/models/vpn_message.dart';
 import '../data/models/vpn_message.dart';
 import '../data/sp/ix_sp.dart';
 import '../data/sp/ix_sp.dart';
 import '../dialog/error_dialog.dart';
 import '../dialog/error_dialog.dart';
@@ -109,12 +113,12 @@ class CoreController extends GetxService {
 
 
       if (state == ConnectionState.connecting) {
       if (state == ConnectionState.connecting) {
         final sessionId = Uuid().v4();
         final sessionId = Uuid().v4();
-        final socksPort = launch.socksPort!;
-        final tunnelConfig = launch.tunnelConfig!;
-        final configJson = jsonEncode(launch.nodes);
+        final socksPort = launch.nodesConfig!.socketPort!;
+        final tunnelConfig = launch.nodesConfig!.tunnelConfig!;
+        final configJson = jsonEncode(launch.nodesConfig!);
         CoreApi().connect(sessionId, socksPort, tunnelConfig, configJson);
         CoreApi().connect(sessionId, socksPort, tunnelConfig, configJson);
       }
       }
-    } on DioException catch (e) {
+    } on DioException catch (e, s) {
       // 只有当前 token 没有被替换时才清空
       // 只有当前 token 没有被替换时才清空
       if (_cancelToken == currentToken) {
       if (_cancelToken == currentToken) {
         _cancelToken = null;
         _cancelToken = null;
@@ -129,8 +133,9 @@ class CoreController extends GetxService {
       if (state == ConnectionState.connecting) {
       if (state == ConnectionState.connecting) {
         state = ConnectionState.disconnected;
         state = ConnectionState.disconnected;
       }
       }
+      handleErrorDialog(e, s);
       log(TAG, 'getDispatchInfo error: $e');
       log(TAG, 'getDispatchInfo error: $e');
-    } catch (e) {
+    } catch (e, s) {
       // 只有当前 token 没有被替换时才清空
       // 只有当前 token 没有被替换时才清空
       if (_cancelToken == currentToken) {
       if (_cancelToken == currentToken) {
         _cancelToken = null;
         _cancelToken = null;
@@ -139,6 +144,7 @@ class CoreController extends GetxService {
       if (state == ConnectionState.connecting) {
       if (state == ConnectionState.connecting) {
         state = ConnectionState.disconnected;
         state = ConnectionState.disconnected;
       }
       }
+      handleErrorDialog(e, s);
       log(TAG, 'getDispatchInfo error: $e');
       log(TAG, 'getDispatchInfo error: $e');
     }
     }
   }
   }
@@ -176,7 +182,10 @@ class CoreController extends GetxService {
 
 
   void _handleVpnStatus(VpnStatusMessage message) {
   void _handleVpnStatus(VpnStatusMessage message) {
     final vpnError = VpnStatus.fromValue(message.status);
     final vpnError = VpnStatus.fromValue(message.status);
-    log(TAG, 'VPN状态变化: ${vpnError.label}, message=${message.message}');
+    log(
+      TAG,
+      'VPN状态变化: ${vpnError.label}, status=${message.status}, message=${message.message}',
+    );
     // 根据状态码处理不同的VPN状态
     // 根据状态码处理不同的VPN状态
     switch (vpnError) {
     switch (vpnError) {
       case VpnStatus.idle:
       case VpnStatus.idle:
@@ -255,6 +264,7 @@ class CoreController extends GetxService {
     state = ConnectionState.disconnected;
     state = ConnectionState.disconnected;
     timer = "00:00:00";
     timer = "00:00:00";
     HapticFeedbackManager.connectionDisconnected();
     HapticFeedbackManager.connectionDisconnected();
+    ErrorDialog.show(message: 'VPN连接错误');
   }
   }
 
 
   void _onVpnServiceDisconnected() {
   void _onVpnServiceDisconnected() {
@@ -311,4 +321,73 @@ class CoreController extends GetxService {
       return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
       return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
     }
     }
   }
   }
+
+  void handleSnackBarError(dynamic error, StackTrace stackTrace) {
+    if (error is ApiException) {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: error.message,
+      );
+    } else if (error is Failure) {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: error.message ?? Strings.unknownError.tr,
+      );
+    } else if (error is DioException) {
+      switch (error.type) {
+        case DioExceptionType.connectionError:
+        case DioExceptionType.connectionTimeout:
+        case DioExceptionType.receiveTimeout:
+        case DioExceptionType.sendTimeout:
+          IXSnackBar.showIXErrorSnackBar(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectNetwork.tr,
+          );
+          break;
+        default:
+          IXSnackBar.showIXErrorSnackBar(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectServer.tr,
+          );
+      }
+    } else {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: Strings.unknownError.tr,
+      );
+    }
+  }
+
+  void handleErrorDialog(dynamic error, StackTrace stackTrace) {
+    if (error is ApiException) {
+      ErrorDialog.show(title: Strings.error.tr, message: error.message);
+    } else if (error is Failure) {
+      ErrorDialog.show(
+        title: Strings.error.tr,
+        message: error.message ?? Strings.unknownError.tr,
+      );
+    } else if (error is DioException) {
+      switch (error.type) {
+        case DioExceptionType.connectionError:
+        case DioExceptionType.connectionTimeout:
+        case DioExceptionType.receiveTimeout:
+        case DioExceptionType.sendTimeout:
+          ErrorDialog.show(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectNetwork.tr,
+          );
+          break;
+        default:
+          ErrorDialog.show(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectServer.tr,
+          );
+      }
+    } else {
+      ErrorDialog.show(
+        title: Strings.error.tr,
+        message: Strings.unknownError.tr,
+      );
+    }
+  }
 }
 }

+ 2 - 0
lib/app/data/models/launch/app_config.dart

@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
 
 
 import 'privacy_terms.dart';
 import 'privacy_terms.dart';
 import 'skip_geo.dart';
 import 'skip_geo.dart';
+import 'smart_geo.dart';
 part 'app_config.freezed.dart';
 part 'app_config.freezed.dart';
 part 'app_config.g.dart';
 part 'app_config.g.dart';
 
 
@@ -48,6 +49,7 @@ class AppConfig with _$AppConfig {
     int? adTimeoutDuration,
     int? adTimeoutDuration,
     int? reportActiveInterval,
     int? reportActiveInterval,
     int? serverTime,
     int? serverTime,
+    SmartGeo? smartGeo,
   }) = _AppConfig;
   }) = _AppConfig;
 
 
   factory AppConfig.fromJson(Map<String, Object?> json) =>
   factory AppConfig.fromJson(Map<String, Object?> json) =>

+ 45 - 3
lib/app/data/models/launch/app_config.freezed.dart

@@ -62,6 +62,7 @@ mixin _$AppConfig {
   int? get adTimeoutDuration => throw _privateConstructorUsedError;
   int? get adTimeoutDuration => throw _privateConstructorUsedError;
   int? get reportActiveInterval => throw _privateConstructorUsedError;
   int? get reportActiveInterval => throw _privateConstructorUsedError;
   int? get serverTime => throw _privateConstructorUsedError;
   int? get serverTime => throw _privateConstructorUsedError;
+  SmartGeo? get smartGeo => throw _privateConstructorUsedError;
 
 
   /// Serializes this AppConfig to a JSON map.
   /// Serializes this AppConfig to a JSON map.
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -118,7 +119,10 @@ abstract class $AppConfigCopyWith<$Res> {
     int? adTimeoutDuration,
     int? adTimeoutDuration,
     int? reportActiveInterval,
     int? reportActiveInterval,
     int? serverTime,
     int? serverTime,
+    SmartGeo? smartGeo,
   });
   });
+
+  $SmartGeoCopyWith<$Res>? get smartGeo;
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -175,6 +179,7 @@ class _$AppConfigCopyWithImpl<$Res, $Val extends AppConfig>
     Object? adTimeoutDuration = freezed,
     Object? adTimeoutDuration = freezed,
     Object? reportActiveInterval = freezed,
     Object? reportActiveInterval = freezed,
     Object? serverTime = freezed,
     Object? serverTime = freezed,
+    Object? smartGeo = freezed,
   }) {
   }) {
     return _then(
     return _then(
       _value.copyWith(
       _value.copyWith(
@@ -334,10 +339,28 @@ class _$AppConfigCopyWithImpl<$Res, $Val extends AppConfig>
                 ? _value.serverTime
                 ? _value.serverTime
                 : serverTime // ignore: cast_nullable_to_non_nullable
                 : serverTime // ignore: cast_nullable_to_non_nullable
                       as int?,
                       as int?,
+            smartGeo: freezed == smartGeo
+                ? _value.smartGeo
+                : smartGeo // ignore: cast_nullable_to_non_nullable
+                      as SmartGeo?,
           )
           )
           as $Val,
           as $Val,
     );
     );
   }
   }
+
+  /// Create a copy of AppConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $SmartGeoCopyWith<$Res>? get smartGeo {
+    if (_value.smartGeo == null) {
+      return null;
+    }
+
+    return $SmartGeoCopyWith<$Res>(_value.smartGeo!, (value) {
+      return _then(_value.copyWith(smartGeo: value) as $Val);
+    });
+  }
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -389,7 +412,11 @@ abstract class _$$AppConfigImplCopyWith<$Res>
     int? adTimeoutDuration,
     int? adTimeoutDuration,
     int? reportActiveInterval,
     int? reportActiveInterval,
     int? serverTime,
     int? serverTime,
+    SmartGeo? smartGeo,
   });
   });
+
+  @override
+  $SmartGeoCopyWith<$Res>? get smartGeo;
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -445,6 +472,7 @@ class __$$AppConfigImplCopyWithImpl<$Res>
     Object? adTimeoutDuration = freezed,
     Object? adTimeoutDuration = freezed,
     Object? reportActiveInterval = freezed,
     Object? reportActiveInterval = freezed,
     Object? serverTime = freezed,
     Object? serverTime = freezed,
+    Object? smartGeo = freezed,
   }) {
   }) {
     return _then(
     return _then(
       _$AppConfigImpl(
       _$AppConfigImpl(
@@ -604,6 +632,10 @@ class __$$AppConfigImplCopyWithImpl<$Res>
             ? _value.serverTime
             ? _value.serverTime
             : serverTime // ignore: cast_nullable_to_non_nullable
             : serverTime // ignore: cast_nullable_to_non_nullable
                   as int?,
                   as int?,
+        smartGeo: freezed == smartGeo
+            ? _value.smartGeo
+            : smartGeo // ignore: cast_nullable_to_non_nullable
+                  as SmartGeo?,
       ),
       ),
     );
     );
   }
   }
@@ -652,6 +684,7 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
     this.adTimeoutDuration,
     this.adTimeoutDuration,
     this.reportActiveInterval,
     this.reportActiveInterval,
     this.serverTime,
     this.serverTime,
+    this.smartGeo,
   }) : _apiIps = apiIps,
   }) : _apiIps = apiIps,
        _apiUrls = apiUrls,
        _apiUrls = apiUrls,
        _routerApiUrls = routerApiUrls,
        _routerApiUrls = routerApiUrls,
@@ -913,10 +946,12 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
   final int? reportActiveInterval;
   final int? reportActiveInterval;
   @override
   @override
   final int? serverTime;
   final int? serverTime;
+  @override
+  final SmartGeo? smartGeo;
 
 
   @override
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'AppConfig(apiIps: $apiIps, apiUrls: $apiUrls, routerApiUrls: $routerApiUrls, appStatUrls: $appStatUrls, assetUrls: $assetUrls, logFileUploadUrls: $logFileUploadUrls, realityApiUrls: $realityApiUrls, realityAppStatUrls: $realityAppStatUrls, realityLogFileUploadUrls: $realityLogFileUploadUrls, autoPing: $autoPing, autoPingInterval: $autoPingInterval, backupApiUrls: $backupApiUrls, cacheDataEffectiveDays: $cacheDataEffectiveDays, contacts: $contacts, follows: $follows, providers: $providers, signalThresholds: $signalThresholds, packetLossThresholds: $packetLossThresholds, skipGeo: $skipGeo, speedTestTargetList: $speedTestTargetList, websiteUrl: $websiteUrl, visitorDisabled: $visitorDisabled, privacyAgreement: $privacyAgreement, email: $email, blackPkgs: $blackPkgs, whitePkgs: $whitePkgs, accelerationSampleCount: $accelerationSampleCount, minAccelerationSampleCount: $minAccelerationSampleCount, connectionWarningThreshold: $connectionWarningThreshold, connectiveTestUrl: $connectiveTestUrl, disabledLogModules: $disabledLogModules, realTimeDbPath: $realTimeDbPath, boostTimeInterval: $boostTimeInterval, pingDisplayMode: $pingDisplayMode, peekTimeInterval: $peekTimeInterval, enableAd: $enableAd, adTimeoutDuration: $adTimeoutDuration, reportActiveInterval: $reportActiveInterval, serverTime: $serverTime)';
+    return 'AppConfig(apiIps: $apiIps, apiUrls: $apiUrls, routerApiUrls: $routerApiUrls, appStatUrls: $appStatUrls, assetUrls: $assetUrls, logFileUploadUrls: $logFileUploadUrls, realityApiUrls: $realityApiUrls, realityAppStatUrls: $realityAppStatUrls, realityLogFileUploadUrls: $realityLogFileUploadUrls, autoPing: $autoPing, autoPingInterval: $autoPingInterval, backupApiUrls: $backupApiUrls, cacheDataEffectiveDays: $cacheDataEffectiveDays, contacts: $contacts, follows: $follows, providers: $providers, signalThresholds: $signalThresholds, packetLossThresholds: $packetLossThresholds, skipGeo: $skipGeo, speedTestTargetList: $speedTestTargetList, websiteUrl: $websiteUrl, visitorDisabled: $visitorDisabled, privacyAgreement: $privacyAgreement, email: $email, blackPkgs: $blackPkgs, whitePkgs: $whitePkgs, accelerationSampleCount: $accelerationSampleCount, minAccelerationSampleCount: $minAccelerationSampleCount, connectionWarningThreshold: $connectionWarningThreshold, connectiveTestUrl: $connectiveTestUrl, disabledLogModules: $disabledLogModules, realTimeDbPath: $realTimeDbPath, boostTimeInterval: $boostTimeInterval, pingDisplayMode: $pingDisplayMode, peekTimeInterval: $peekTimeInterval, enableAd: $enableAd, adTimeoutDuration: $adTimeoutDuration, reportActiveInterval: $reportActiveInterval, serverTime: $serverTime, smartGeo: $smartGeo)';
   }
   }
 
 
   @override
   @override
@@ -981,7 +1016,8 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
       ..add(DiagnosticsProperty('enableAd', enableAd))
       ..add(DiagnosticsProperty('enableAd', enableAd))
       ..add(DiagnosticsProperty('adTimeoutDuration', adTimeoutDuration))
       ..add(DiagnosticsProperty('adTimeoutDuration', adTimeoutDuration))
       ..add(DiagnosticsProperty('reportActiveInterval', reportActiveInterval))
       ..add(DiagnosticsProperty('reportActiveInterval', reportActiveInterval))
-      ..add(DiagnosticsProperty('serverTime', serverTime));
+      ..add(DiagnosticsProperty('serverTime', serverTime))
+      ..add(DiagnosticsProperty('smartGeo', smartGeo));
   }
   }
 
 
   @override
   @override
@@ -1101,7 +1137,9 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
             (identical(other.reportActiveInterval, reportActiveInterval) ||
             (identical(other.reportActiveInterval, reportActiveInterval) ||
                 other.reportActiveInterval == reportActiveInterval) &&
                 other.reportActiveInterval == reportActiveInterval) &&
             (identical(other.serverTime, serverTime) ||
             (identical(other.serverTime, serverTime) ||
-                other.serverTime == serverTime));
+                other.serverTime == serverTime) &&
+            (identical(other.smartGeo, smartGeo) ||
+                other.smartGeo == smartGeo));
   }
   }
 
 
   @JsonKey(includeFromJson: false, includeToJson: false)
   @JsonKey(includeFromJson: false, includeToJson: false)
@@ -1147,6 +1185,7 @@ class _$AppConfigImpl with DiagnosticableTreeMixin implements _AppConfig {
     adTimeoutDuration,
     adTimeoutDuration,
     reportActiveInterval,
     reportActiveInterval,
     serverTime,
     serverTime,
+    smartGeo,
   ]);
   ]);
 
 
   /// Create a copy of AppConfig
   /// Create a copy of AppConfig
@@ -1204,6 +1243,7 @@ abstract class _AppConfig implements AppConfig {
     final int? adTimeoutDuration,
     final int? adTimeoutDuration,
     final int? reportActiveInterval,
     final int? reportActiveInterval,
     final int? serverTime,
     final int? serverTime,
+    final SmartGeo? smartGeo,
   }) = _$AppConfigImpl;
   }) = _$AppConfigImpl;
 
 
   factory _AppConfig.fromJson(Map<String, dynamic> json) =
   factory _AppConfig.fromJson(Map<String, dynamic> json) =
@@ -1287,6 +1327,8 @@ abstract class _AppConfig implements AppConfig {
   int? get reportActiveInterval;
   int? get reportActiveInterval;
   @override
   @override
   int? get serverTime;
   int? get serverTime;
+  @override
+  SmartGeo? get smartGeo;
 
 
   /// Create a copy of AppConfig
   /// Create a copy of AppConfig
   /// with the given fields replaced by the non-null parameter values.
   /// with the given fields replaced by the non-null parameter values.

+ 4 - 0
lib/app/data/models/launch/app_config.g.dart

@@ -90,6 +90,9 @@ _$AppConfigImpl _$$AppConfigImplFromJson(
   adTimeoutDuration: (json['adTimeoutDuration'] as num?)?.toInt(),
   adTimeoutDuration: (json['adTimeoutDuration'] as num?)?.toInt(),
   reportActiveInterval: (json['reportActiveInterval'] as num?)?.toInt(),
   reportActiveInterval: (json['reportActiveInterval'] as num?)?.toInt(),
   serverTime: (json['serverTime'] as num?)?.toInt(),
   serverTime: (json['serverTime'] as num?)?.toInt(),
+  smartGeo: json['smartGeo'] == null
+      ? null
+      : SmartGeo.fromJson(json['smartGeo'] as Map<String, dynamic>),
 );
 );
 
 
 Map<String, dynamic> _$$AppConfigImplToJson(_$AppConfigImpl instance) =>
 Map<String, dynamic> _$$AppConfigImplToJson(_$AppConfigImpl instance) =>
@@ -133,6 +136,7 @@ Map<String, dynamic> _$$AppConfigImplToJson(_$AppConfigImpl instance) =>
       'adTimeoutDuration': instance.adTimeoutDuration,
       'adTimeoutDuration': instance.adTimeoutDuration,
       'reportActiveInterval': instance.reportActiveInterval,
       'reportActiveInterval': instance.reportActiveInterval,
       'serverTime': instance.serverTime,
       'serverTime': instance.serverTime,
+      'smartGeo': instance.smartGeo,
     };
     };
 
 
 _$ContactsImpl _$$ContactsImplFromJson(Map<String, dynamic> json) =>
 _$ContactsImpl _$$ContactsImplFromJson(Map<String, dynamic> json) =>

+ 2 - 4
lib/app/data/models/launch/launch.dart

@@ -7,7 +7,7 @@ import 'groups.dart';
 import 'user.dart';
 import 'user.dart';
 import 'app_config.dart';
 import 'app_config.dart';
 import 'upgrade.dart';
 import 'upgrade.dart';
-
+import 'nodes_config.dart';
 part 'launch.freezed.dart';
 part 'launch.freezed.dart';
 part 'launch.g.dart';
 part 'launch.g.dart';
 
 
@@ -21,9 +21,7 @@ class Launch with _$Launch {
     Groups? groups,
     Groups? groups,
     List<Ranks>? ranks,
     List<Ranks>? ranks,
     dynamic exData,
     dynamic exData,
-    List<dynamic>? nodes,
-    String? tunnelConfig,
-    int? socksPort,
+    NodesConfig? nodesConfig,
   }) = _Launch;
   }) = _Launch;
 
 
   factory Launch.fromJson(Map<String, Object?> json) => _$LaunchFromJson(json);
   factory Launch.fromJson(Map<String, Object?> json) => _$LaunchFromJson(json);

+ 40 - 77
lib/app/data/models/launch/launch.freezed.dart

@@ -28,9 +28,7 @@ mixin _$Launch {
   Groups? get groups => throw _privateConstructorUsedError;
   Groups? get groups => throw _privateConstructorUsedError;
   List<Ranks>? get ranks => throw _privateConstructorUsedError;
   List<Ranks>? get ranks => throw _privateConstructorUsedError;
   dynamic get exData => throw _privateConstructorUsedError;
   dynamic get exData => throw _privateConstructorUsedError;
-  List<dynamic>? get nodes => throw _privateConstructorUsedError;
-  String? get tunnelConfig => throw _privateConstructorUsedError;
-  int? get socksPort => throw _privateConstructorUsedError;
+  NodesConfig? get nodesConfig => throw _privateConstructorUsedError;
 
 
   /// Serializes this Launch to a JSON map.
   /// Serializes this Launch to a JSON map.
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -54,9 +52,7 @@ abstract class $LaunchCopyWith<$Res> {
     Groups? groups,
     Groups? groups,
     List<Ranks>? ranks,
     List<Ranks>? ranks,
     dynamic exData,
     dynamic exData,
-    List<dynamic>? nodes,
-    String? tunnelConfig,
-    int? socksPort,
+    NodesConfig? nodesConfig,
   });
   });
 
 
   $UserCopyWith<$Res>? get userConfig;
   $UserCopyWith<$Res>? get userConfig;
@@ -64,6 +60,7 @@ abstract class $LaunchCopyWith<$Res> {
   $UpgradeCopyWith<$Res>? get upgradeConfig;
   $UpgradeCopyWith<$Res>? get upgradeConfig;
   $AdConfigCopyWith<$Res>? get adConfig;
   $AdConfigCopyWith<$Res>? get adConfig;
   $GroupsCopyWith<$Res>? get groups;
   $GroupsCopyWith<$Res>? get groups;
+  $NodesConfigCopyWith<$Res>? get nodesConfig;
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -88,9 +85,7 @@ class _$LaunchCopyWithImpl<$Res, $Val extends Launch>
     Object? groups = freezed,
     Object? groups = freezed,
     Object? ranks = freezed,
     Object? ranks = freezed,
     Object? exData = freezed,
     Object? exData = freezed,
-    Object? nodes = freezed,
-    Object? tunnelConfig = freezed,
-    Object? socksPort = freezed,
+    Object? nodesConfig = freezed,
   }) {
   }) {
     return _then(
     return _then(
       _value.copyWith(
       _value.copyWith(
@@ -122,18 +117,10 @@ class _$LaunchCopyWithImpl<$Res, $Val extends Launch>
                 ? _value.exData
                 ? _value.exData
                 : exData // ignore: cast_nullable_to_non_nullable
                 : exData // ignore: cast_nullable_to_non_nullable
                       as dynamic,
                       as dynamic,
-            nodes: freezed == nodes
-                ? _value.nodes
-                : nodes // ignore: cast_nullable_to_non_nullable
-                      as List<dynamic>?,
-            tunnelConfig: freezed == tunnelConfig
-                ? _value.tunnelConfig
-                : tunnelConfig // ignore: cast_nullable_to_non_nullable
-                      as String?,
-            socksPort: freezed == socksPort
-                ? _value.socksPort
-                : socksPort // ignore: cast_nullable_to_non_nullable
-                      as int?,
+            nodesConfig: freezed == nodesConfig
+                ? _value.nodesConfig
+                : nodesConfig // ignore: cast_nullable_to_non_nullable
+                      as NodesConfig?,
           )
           )
           as $Val,
           as $Val,
     );
     );
@@ -208,6 +195,20 @@ class _$LaunchCopyWithImpl<$Res, $Val extends Launch>
       return _then(_value.copyWith(groups: value) as $Val);
       return _then(_value.copyWith(groups: value) as $Val);
     });
     });
   }
   }
+
+  /// Create a copy of Launch
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $NodesConfigCopyWith<$Res>? get nodesConfig {
+    if (_value.nodesConfig == null) {
+      return null;
+    }
+
+    return $NodesConfigCopyWith<$Res>(_value.nodesConfig!, (value) {
+      return _then(_value.copyWith(nodesConfig: value) as $Val);
+    });
+  }
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -226,9 +227,7 @@ abstract class _$$LaunchImplCopyWith<$Res> implements $LaunchCopyWith<$Res> {
     Groups? groups,
     Groups? groups,
     List<Ranks>? ranks,
     List<Ranks>? ranks,
     dynamic exData,
     dynamic exData,
-    List<dynamic>? nodes,
-    String? tunnelConfig,
-    int? socksPort,
+    NodesConfig? nodesConfig,
   });
   });
 
 
   @override
   @override
@@ -241,6 +240,8 @@ abstract class _$$LaunchImplCopyWith<$Res> implements $LaunchCopyWith<$Res> {
   $AdConfigCopyWith<$Res>? get adConfig;
   $AdConfigCopyWith<$Res>? get adConfig;
   @override
   @override
   $GroupsCopyWith<$Res>? get groups;
   $GroupsCopyWith<$Res>? get groups;
+  @override
+  $NodesConfigCopyWith<$Res>? get nodesConfig;
 }
 }
 
 
 /// @nodoc
 /// @nodoc
@@ -264,9 +265,7 @@ class __$$LaunchImplCopyWithImpl<$Res>
     Object? groups = freezed,
     Object? groups = freezed,
     Object? ranks = freezed,
     Object? ranks = freezed,
     Object? exData = freezed,
     Object? exData = freezed,
-    Object? nodes = freezed,
-    Object? tunnelConfig = freezed,
-    Object? socksPort = freezed,
+    Object? nodesConfig = freezed,
   }) {
   }) {
     return _then(
     return _then(
       _$LaunchImpl(
       _$LaunchImpl(
@@ -298,18 +297,10 @@ class __$$LaunchImplCopyWithImpl<$Res>
             ? _value.exData
             ? _value.exData
             : exData // ignore: cast_nullable_to_non_nullable
             : exData // ignore: cast_nullable_to_non_nullable
                   as dynamic,
                   as dynamic,
-        nodes: freezed == nodes
-            ? _value._nodes
-            : nodes // ignore: cast_nullable_to_non_nullable
-                  as List<dynamic>?,
-        tunnelConfig: freezed == tunnelConfig
-            ? _value.tunnelConfig
-            : tunnelConfig // ignore: cast_nullable_to_non_nullable
-                  as String?,
-        socksPort: freezed == socksPort
-            ? _value.socksPort
-            : socksPort // ignore: cast_nullable_to_non_nullable
-                  as int?,
+        nodesConfig: freezed == nodesConfig
+            ? _value.nodesConfig
+            : nodesConfig // ignore: cast_nullable_to_non_nullable
+                  as NodesConfig?,
       ),
       ),
     );
     );
   }
   }
@@ -326,11 +317,8 @@ class _$LaunchImpl with DiagnosticableTreeMixin implements _Launch {
     this.groups,
     this.groups,
     final List<Ranks>? ranks,
     final List<Ranks>? ranks,
     this.exData,
     this.exData,
-    final List<dynamic>? nodes,
-    this.tunnelConfig,
-    this.socksPort,
-  }) : _ranks = ranks,
-       _nodes = nodes;
+    this.nodesConfig,
+  }) : _ranks = ranks;
 
 
   factory _$LaunchImpl.fromJson(Map<String, dynamic> json) =>
   factory _$LaunchImpl.fromJson(Map<String, dynamic> json) =>
       _$$LaunchImplFromJson(json);
       _$$LaunchImplFromJson(json);
@@ -357,24 +345,12 @@ class _$LaunchImpl with DiagnosticableTreeMixin implements _Launch {
 
 
   @override
   @override
   final dynamic exData;
   final dynamic exData;
-  final List<dynamic>? _nodes;
-  @override
-  List<dynamic>? get nodes {
-    final value = _nodes;
-    if (value == null) return null;
-    if (_nodes is EqualUnmodifiableListView) return _nodes;
-    // ignore: implicit_dynamic_type
-    return EqualUnmodifiableListView(value);
-  }
-
-  @override
-  final String? tunnelConfig;
   @override
   @override
-  final int? socksPort;
+  final NodesConfig? nodesConfig;
 
 
   @override
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'Launch(userConfig: $userConfig, appConfig: $appConfig, upgradeConfig: $upgradeConfig, adConfig: $adConfig, groups: $groups, ranks: $ranks, exData: $exData, nodes: $nodes, tunnelConfig: $tunnelConfig, socksPort: $socksPort)';
+    return 'Launch(userConfig: $userConfig, appConfig: $appConfig, upgradeConfig: $upgradeConfig, adConfig: $adConfig, groups: $groups, ranks: $ranks, exData: $exData, nodesConfig: $nodesConfig)';
   }
   }
 
 
   @override
   @override
@@ -389,9 +365,7 @@ class _$LaunchImpl with DiagnosticableTreeMixin implements _Launch {
       ..add(DiagnosticsProperty('groups', groups))
       ..add(DiagnosticsProperty('groups', groups))
       ..add(DiagnosticsProperty('ranks', ranks))
       ..add(DiagnosticsProperty('ranks', ranks))
       ..add(DiagnosticsProperty('exData', exData))
       ..add(DiagnosticsProperty('exData', exData))
-      ..add(DiagnosticsProperty('nodes', nodes))
-      ..add(DiagnosticsProperty('tunnelConfig', tunnelConfig))
-      ..add(DiagnosticsProperty('socksPort', socksPort));
+      ..add(DiagnosticsProperty('nodesConfig', nodesConfig));
   }
   }
 
 
   @override
   @override
@@ -410,11 +384,8 @@ class _$LaunchImpl with DiagnosticableTreeMixin implements _Launch {
             (identical(other.groups, groups) || other.groups == groups) &&
             (identical(other.groups, groups) || other.groups == groups) &&
             const DeepCollectionEquality().equals(other._ranks, _ranks) &&
             const DeepCollectionEquality().equals(other._ranks, _ranks) &&
             const DeepCollectionEquality().equals(other.exData, exData) &&
             const DeepCollectionEquality().equals(other.exData, exData) &&
-            const DeepCollectionEquality().equals(other._nodes, _nodes) &&
-            (identical(other.tunnelConfig, tunnelConfig) ||
-                other.tunnelConfig == tunnelConfig) &&
-            (identical(other.socksPort, socksPort) ||
-                other.socksPort == socksPort));
+            (identical(other.nodesConfig, nodesConfig) ||
+                other.nodesConfig == nodesConfig));
   }
   }
 
 
   @JsonKey(includeFromJson: false, includeToJson: false)
   @JsonKey(includeFromJson: false, includeToJson: false)
@@ -428,9 +399,7 @@ class _$LaunchImpl with DiagnosticableTreeMixin implements _Launch {
     groups,
     groups,
     const DeepCollectionEquality().hash(_ranks),
     const DeepCollectionEquality().hash(_ranks),
     const DeepCollectionEquality().hash(exData),
     const DeepCollectionEquality().hash(exData),
-    const DeepCollectionEquality().hash(_nodes),
-    tunnelConfig,
-    socksPort,
+    nodesConfig,
   );
   );
 
 
   /// Create a copy of Launch
   /// Create a copy of Launch
@@ -456,9 +425,7 @@ abstract class _Launch implements Launch {
     final Groups? groups,
     final Groups? groups,
     final List<Ranks>? ranks,
     final List<Ranks>? ranks,
     final dynamic exData,
     final dynamic exData,
-    final List<dynamic>? nodes,
-    final String? tunnelConfig,
-    final int? socksPort,
+    final NodesConfig? nodesConfig,
   }) = _$LaunchImpl;
   }) = _$LaunchImpl;
 
 
   factory _Launch.fromJson(Map<String, dynamic> json) = _$LaunchImpl.fromJson;
   factory _Launch.fromJson(Map<String, dynamic> json) = _$LaunchImpl.fromJson;
@@ -478,11 +445,7 @@ abstract class _Launch implements Launch {
   @override
   @override
   dynamic get exData;
   dynamic get exData;
   @override
   @override
-  List<dynamic>? get nodes;
-  @override
-  String? get tunnelConfig;
-  @override
-  int? get socksPort;
+  NodesConfig? get nodesConfig;
 
 
   /// Create a copy of Launch
   /// Create a copy of Launch
   /// with the given fields replaced by the non-null parameter values.
   /// with the given fields replaced by the non-null parameter values.

+ 4 - 6
lib/app/data/models/launch/launch.g.dart

@@ -26,9 +26,9 @@ _$LaunchImpl _$$LaunchImplFromJson(Map<String, dynamic> json) => _$LaunchImpl(
       ?.map((e) => Ranks.fromJson(e as Map<String, dynamic>))
       ?.map((e) => Ranks.fromJson(e as Map<String, dynamic>))
       .toList(),
       .toList(),
   exData: json['exData'],
   exData: json['exData'],
-  nodes: json['nodes'] as List<dynamic>?,
-  tunnelConfig: json['tunnelConfig'] as String?,
-  socksPort: (json['socksPort'] as num?)?.toInt(),
+  nodesConfig: json['nodesConfig'] == null
+      ? null
+      : NodesConfig.fromJson(json['nodesConfig'] as Map<String, dynamic>),
 );
 );
 
 
 Map<String, dynamic> _$$LaunchImplToJson(_$LaunchImpl instance) =>
 Map<String, dynamic> _$$LaunchImplToJson(_$LaunchImpl instance) =>
@@ -40,7 +40,5 @@ Map<String, dynamic> _$$LaunchImplToJson(_$LaunchImpl instance) =>
       'groups': instance.groups,
       'groups': instance.groups,
       'ranks': instance.ranks,
       'ranks': instance.ranks,
       'exData': instance.exData,
       'exData': instance.exData,
-      'nodes': instance.nodes,
-      'tunnelConfig': instance.tunnelConfig,
-      'socksPort': instance.socksPort,
+      'nodesConfig': instance.nodesConfig,
     };
     };

+ 17 - 0
lib/app/data/models/launch/nodes_config.dart

@@ -0,0 +1,17 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter/foundation.dart';
+part 'nodes_config.freezed.dart';
+part 'nodes_config.g.dart';
+
+@freezed
+abstract class NodesConfig with _$NodesConfig {
+  const factory NodesConfig({
+    List<dynamic>? nodes,
+    String? tunnelConfig,
+    int? socketPort,
+    int? maxTryCount,
+  }) = _NodesConfig;
+
+  factory NodesConfig.fromJson(Map<String, Object?> json) =>
+      _$NodesConfigFromJson(json);
+}

+ 267 - 0
lib/app/data/models/launch/nodes_config.freezed.dart

@@ -0,0 +1,267 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'nodes_config.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+  'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+NodesConfig _$NodesConfigFromJson(Map<String, dynamic> json) {
+  return _NodesConfig.fromJson(json);
+}
+
+/// @nodoc
+mixin _$NodesConfig {
+  List<dynamic>? get nodes => throw _privateConstructorUsedError;
+  String? get tunnelConfig => throw _privateConstructorUsedError;
+  int? get socketPort => throw _privateConstructorUsedError;
+  int? get maxTryCount => throw _privateConstructorUsedError;
+
+  /// Serializes this NodesConfig to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of NodesConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $NodesConfigCopyWith<NodesConfig> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $NodesConfigCopyWith<$Res> {
+  factory $NodesConfigCopyWith(
+    NodesConfig value,
+    $Res Function(NodesConfig) then,
+  ) = _$NodesConfigCopyWithImpl<$Res, NodesConfig>;
+  @useResult
+  $Res call({
+    List<dynamic>? nodes,
+    String? tunnelConfig,
+    int? socketPort,
+    int? maxTryCount,
+  });
+}
+
+/// @nodoc
+class _$NodesConfigCopyWithImpl<$Res, $Val extends NodesConfig>
+    implements $NodesConfigCopyWith<$Res> {
+  _$NodesConfigCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of NodesConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? nodes = freezed,
+    Object? tunnelConfig = freezed,
+    Object? socketPort = freezed,
+    Object? maxTryCount = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            nodes: freezed == nodes
+                ? _value.nodes
+                : nodes // ignore: cast_nullable_to_non_nullable
+                      as List<dynamic>?,
+            tunnelConfig: freezed == tunnelConfig
+                ? _value.tunnelConfig
+                : tunnelConfig // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            socketPort: freezed == socketPort
+                ? _value.socketPort
+                : socketPort // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            maxTryCount: freezed == maxTryCount
+                ? _value.maxTryCount
+                : maxTryCount // ignore: cast_nullable_to_non_nullable
+                      as int?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$NodesConfigImplCopyWith<$Res>
+    implements $NodesConfigCopyWith<$Res> {
+  factory _$$NodesConfigImplCopyWith(
+    _$NodesConfigImpl value,
+    $Res Function(_$NodesConfigImpl) then,
+  ) = __$$NodesConfigImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    List<dynamic>? nodes,
+    String? tunnelConfig,
+    int? socketPort,
+    int? maxTryCount,
+  });
+}
+
+/// @nodoc
+class __$$NodesConfigImplCopyWithImpl<$Res>
+    extends _$NodesConfigCopyWithImpl<$Res, _$NodesConfigImpl>
+    implements _$$NodesConfigImplCopyWith<$Res> {
+  __$$NodesConfigImplCopyWithImpl(
+    _$NodesConfigImpl _value,
+    $Res Function(_$NodesConfigImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of NodesConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? nodes = freezed,
+    Object? tunnelConfig = freezed,
+    Object? socketPort = freezed,
+    Object? maxTryCount = freezed,
+  }) {
+    return _then(
+      _$NodesConfigImpl(
+        nodes: freezed == nodes
+            ? _value._nodes
+            : nodes // ignore: cast_nullable_to_non_nullable
+                  as List<dynamic>?,
+        tunnelConfig: freezed == tunnelConfig
+            ? _value.tunnelConfig
+            : tunnelConfig // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        socketPort: freezed == socketPort
+            ? _value.socketPort
+            : socketPort // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        maxTryCount: freezed == maxTryCount
+            ? _value.maxTryCount
+            : maxTryCount // ignore: cast_nullable_to_non_nullable
+                  as int?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$NodesConfigImpl with DiagnosticableTreeMixin implements _NodesConfig {
+  const _$NodesConfigImpl({
+    final List<dynamic>? nodes,
+    this.tunnelConfig,
+    this.socketPort,
+    this.maxTryCount,
+  }) : _nodes = nodes;
+
+  factory _$NodesConfigImpl.fromJson(Map<String, dynamic> json) =>
+      _$$NodesConfigImplFromJson(json);
+
+  final List<dynamic>? _nodes;
+  @override
+  List<dynamic>? get nodes {
+    final value = _nodes;
+    if (value == null) return null;
+    if (_nodes is EqualUnmodifiableListView) return _nodes;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
+  @override
+  final String? tunnelConfig;
+  @override
+  final int? socketPort;
+  @override
+  final int? maxTryCount;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'NodesConfig(nodes: $nodes, tunnelConfig: $tunnelConfig, socketPort: $socketPort, maxTryCount: $maxTryCount)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'NodesConfig'))
+      ..add(DiagnosticsProperty('nodes', nodes))
+      ..add(DiagnosticsProperty('tunnelConfig', tunnelConfig))
+      ..add(DiagnosticsProperty('socketPort', socketPort))
+      ..add(DiagnosticsProperty('maxTryCount', maxTryCount));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$NodesConfigImpl &&
+            const DeepCollectionEquality().equals(other._nodes, _nodes) &&
+            (identical(other.tunnelConfig, tunnelConfig) ||
+                other.tunnelConfig == tunnelConfig) &&
+            (identical(other.socketPort, socketPort) ||
+                other.socketPort == socketPort) &&
+            (identical(other.maxTryCount, maxTryCount) ||
+                other.maxTryCount == maxTryCount));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    const DeepCollectionEquality().hash(_nodes),
+    tunnelConfig,
+    socketPort,
+    maxTryCount,
+  );
+
+  /// Create a copy of NodesConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$NodesConfigImplCopyWith<_$NodesConfigImpl> get copyWith =>
+      __$$NodesConfigImplCopyWithImpl<_$NodesConfigImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$NodesConfigImplToJson(this);
+  }
+}
+
+abstract class _NodesConfig implements NodesConfig {
+  const factory _NodesConfig({
+    final List<dynamic>? nodes,
+    final String? tunnelConfig,
+    final int? socketPort,
+    final int? maxTryCount,
+  }) = _$NodesConfigImpl;
+
+  factory _NodesConfig.fromJson(Map<String, dynamic> json) =
+      _$NodesConfigImpl.fromJson;
+
+  @override
+  List<dynamic>? get nodes;
+  @override
+  String? get tunnelConfig;
+  @override
+  int? get socketPort;
+  @override
+  int? get maxTryCount;
+
+  /// Create a copy of NodesConfig
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$NodesConfigImplCopyWith<_$NodesConfigImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 23 - 0
lib/app/data/models/launch/nodes_config.g.dart

@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'nodes_config.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$NodesConfigImpl _$$NodesConfigImplFromJson(Map<String, dynamic> json) =>
+    _$NodesConfigImpl(
+      nodes: json['nodes'] as List<dynamic>?,
+      tunnelConfig: json['tunnelConfig'] as String?,
+      socketPort: (json['socketPort'] as num?)?.toInt(),
+      maxTryCount: (json['maxTryCount'] as num?)?.toInt(),
+    );
+
+Map<String, dynamic> _$$NodesConfigImplToJson(_$NodesConfigImpl instance) =>
+    <String, dynamic>{
+      'nodes': instance.nodes,
+      'tunnelConfig': instance.tunnelConfig,
+      'socketPort': instance.socketPort,
+      'maxTryCount': instance.maxTryCount,
+    };

+ 17 - 0
lib/app/data/models/launch/smart_geo.dart

@@ -0,0 +1,17 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter/foundation.dart';
+part 'smart_geo.freezed.dart';
+part 'smart_geo.g.dart';
+
+@freezed
+abstract class SmartGeo with _$SmartGeo {
+  const factory SmartGeo({
+    String? geoSiteUrl,
+    String? geoSiteMd5,
+    String? geoIpUrl,
+    String? geoIpMd5,
+  }) = _SmartGeo;
+
+  factory SmartGeo.fromJson(Map<String, Object?> json) =>
+      _$SmartGeoFromJson(json);
+}

+ 253 - 0
lib/app/data/models/launch/smart_geo.freezed.dart

@@ -0,0 +1,253 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'smart_geo.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+  'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
+);
+
+SmartGeo _$SmartGeoFromJson(Map<String, dynamic> json) {
+  return _SmartGeo.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SmartGeo {
+  String? get geoSiteUrl => throw _privateConstructorUsedError;
+  String? get geoSiteMd5 => throw _privateConstructorUsedError;
+  String? get geoIpUrl => throw _privateConstructorUsedError;
+  String? get geoIpMd5 => throw _privateConstructorUsedError;
+
+  /// Serializes this SmartGeo to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SmartGeo
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SmartGeoCopyWith<SmartGeo> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SmartGeoCopyWith<$Res> {
+  factory $SmartGeoCopyWith(SmartGeo value, $Res Function(SmartGeo) then) =
+      _$SmartGeoCopyWithImpl<$Res, SmartGeo>;
+  @useResult
+  $Res call({
+    String? geoSiteUrl,
+    String? geoSiteMd5,
+    String? geoIpUrl,
+    String? geoIpMd5,
+  });
+}
+
+/// @nodoc
+class _$SmartGeoCopyWithImpl<$Res, $Val extends SmartGeo>
+    implements $SmartGeoCopyWith<$Res> {
+  _$SmartGeoCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SmartGeo
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? geoSiteUrl = freezed,
+    Object? geoSiteMd5 = freezed,
+    Object? geoIpUrl = freezed,
+    Object? geoIpMd5 = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            geoSiteUrl: freezed == geoSiteUrl
+                ? _value.geoSiteUrl
+                : geoSiteUrl // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            geoSiteMd5: freezed == geoSiteMd5
+                ? _value.geoSiteMd5
+                : geoSiteMd5 // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            geoIpUrl: freezed == geoIpUrl
+                ? _value.geoIpUrl
+                : geoIpUrl // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            geoIpMd5: freezed == geoIpMd5
+                ? _value.geoIpMd5
+                : geoIpMd5 // ignore: cast_nullable_to_non_nullable
+                      as String?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$SmartGeoImplCopyWith<$Res>
+    implements $SmartGeoCopyWith<$Res> {
+  factory _$$SmartGeoImplCopyWith(
+    _$SmartGeoImpl value,
+    $Res Function(_$SmartGeoImpl) then,
+  ) = __$$SmartGeoImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    String? geoSiteUrl,
+    String? geoSiteMd5,
+    String? geoIpUrl,
+    String? geoIpMd5,
+  });
+}
+
+/// @nodoc
+class __$$SmartGeoImplCopyWithImpl<$Res>
+    extends _$SmartGeoCopyWithImpl<$Res, _$SmartGeoImpl>
+    implements _$$SmartGeoImplCopyWith<$Res> {
+  __$$SmartGeoImplCopyWithImpl(
+    _$SmartGeoImpl _value,
+    $Res Function(_$SmartGeoImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of SmartGeo
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? geoSiteUrl = freezed,
+    Object? geoSiteMd5 = freezed,
+    Object? geoIpUrl = freezed,
+    Object? geoIpMd5 = freezed,
+  }) {
+    return _then(
+      _$SmartGeoImpl(
+        geoSiteUrl: freezed == geoSiteUrl
+            ? _value.geoSiteUrl
+            : geoSiteUrl // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        geoSiteMd5: freezed == geoSiteMd5
+            ? _value.geoSiteMd5
+            : geoSiteMd5 // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        geoIpUrl: freezed == geoIpUrl
+            ? _value.geoIpUrl
+            : geoIpUrl // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        geoIpMd5: freezed == geoIpMd5
+            ? _value.geoIpMd5
+            : geoIpMd5 // ignore: cast_nullable_to_non_nullable
+                  as String?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SmartGeoImpl with DiagnosticableTreeMixin implements _SmartGeo {
+  const _$SmartGeoImpl({
+    this.geoSiteUrl,
+    this.geoSiteMd5,
+    this.geoIpUrl,
+    this.geoIpMd5,
+  });
+
+  factory _$SmartGeoImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SmartGeoImplFromJson(json);
+
+  @override
+  final String? geoSiteUrl;
+  @override
+  final String? geoSiteMd5;
+  @override
+  final String? geoIpUrl;
+  @override
+  final String? geoIpMd5;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'SmartGeo(geoSiteUrl: $geoSiteUrl, geoSiteMd5: $geoSiteMd5, geoIpUrl: $geoIpUrl, geoIpMd5: $geoIpMd5)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'SmartGeo'))
+      ..add(DiagnosticsProperty('geoSiteUrl', geoSiteUrl))
+      ..add(DiagnosticsProperty('geoSiteMd5', geoSiteMd5))
+      ..add(DiagnosticsProperty('geoIpUrl', geoIpUrl))
+      ..add(DiagnosticsProperty('geoIpMd5', geoIpMd5));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SmartGeoImpl &&
+            (identical(other.geoSiteUrl, geoSiteUrl) ||
+                other.geoSiteUrl == geoSiteUrl) &&
+            (identical(other.geoSiteMd5, geoSiteMd5) ||
+                other.geoSiteMd5 == geoSiteMd5) &&
+            (identical(other.geoIpUrl, geoIpUrl) ||
+                other.geoIpUrl == geoIpUrl) &&
+            (identical(other.geoIpMd5, geoIpMd5) ||
+                other.geoIpMd5 == geoIpMd5));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode =>
+      Object.hash(runtimeType, geoSiteUrl, geoSiteMd5, geoIpUrl, geoIpMd5);
+
+  /// Create a copy of SmartGeo
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SmartGeoImplCopyWith<_$SmartGeoImpl> get copyWith =>
+      __$$SmartGeoImplCopyWithImpl<_$SmartGeoImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SmartGeoImplToJson(this);
+  }
+}
+
+abstract class _SmartGeo implements SmartGeo {
+  const factory _SmartGeo({
+    final String? geoSiteUrl,
+    final String? geoSiteMd5,
+    final String? geoIpUrl,
+    final String? geoIpMd5,
+  }) = _$SmartGeoImpl;
+
+  factory _SmartGeo.fromJson(Map<String, dynamic> json) =
+      _$SmartGeoImpl.fromJson;
+
+  @override
+  String? get geoSiteUrl;
+  @override
+  String? get geoSiteMd5;
+  @override
+  String? get geoIpUrl;
+  @override
+  String? get geoIpMd5;
+
+  /// Create a copy of SmartGeo
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SmartGeoImplCopyWith<_$SmartGeoImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 23 - 0
lib/app/data/models/launch/smart_geo.g.dart

@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'smart_geo.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$SmartGeoImpl _$$SmartGeoImplFromJson(Map<String, dynamic> json) =>
+    _$SmartGeoImpl(
+      geoSiteUrl: json['geoSiteUrl'] as String?,
+      geoSiteMd5: json['geoSiteMd5'] as String?,
+      geoIpUrl: json['geoIpUrl'] as String?,
+      geoIpMd5: json['geoIpMd5'] as String?,
+    );
+
+Map<String, dynamic> _$$SmartGeoImplToJson(_$SmartGeoImpl instance) =>
+    <String, dynamic>{
+      'geoSiteUrl': instance.geoSiteUrl,
+      'geoSiteMd5': instance.geoSiteMd5,
+      'geoIpUrl': instance.geoIpUrl,
+      'geoIpMd5': instance.geoIpMd5,
+    };

+ 25 - 1
lib/app/dialog/all_dialog.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+import '../constants/iconfont/iconfont.dart';
 import 'custom_dialog.dart';
 import 'custom_dialog.dart';
 
 
 /// 弹窗使用示例
 /// 弹窗使用示例
@@ -17,6 +18,7 @@ class AllDialog {
       onPressed: () {
       onPressed: () {
         // 处理激活成功后的逻辑
         // 处理激活成功后的逻辑
         print('Premium activated successfully');
         print('Premium activated successfully');
+        Navigator.of(Get.context!).pop();
       },
       },
     );
     );
   }
   }
@@ -29,10 +31,11 @@ class AllDialog {
           'Your Pre Code has been sent to your email.\nPlease check your inbox (and spam folder).',
           'Your Pre Code has been sent to your email.\nPlease check your inbox (and spam folder).',
       buttonText: 'OK',
       buttonText: 'OK',
       icon: Icons.mark_email_read,
       icon: Icons.mark_email_read,
-      iconColor: const Color(0xFF00A8E8),
+      iconColor: Colors.white,
       onPressed: () {
       onPressed: () {
         // 处理邮件发送成功后的逻辑
         // 处理邮件发送成功后的逻辑
         print('Email sent successfully');
         print('Email sent successfully');
+        Navigator.of(Get.context!).pop();
       },
       },
     );
     );
   }
   }
@@ -52,10 +55,12 @@ class AllDialog {
       onPressed: () {
       onPressed: () {
         // 处理重试逻辑
         // 处理重试逻辑
         print('Retry network connection');
         print('Retry network connection');
+        Navigator.of(Get.context!).pop();
       },
       },
       onCancel: () {
       onCancel: () {
         // 处理取消逻辑
         // 处理取消逻辑
         print('Cancel network retry');
         print('Cancel network retry');
+        Navigator.of(Get.context!).pop();
       },
       },
     );
     );
   }
   }
@@ -75,10 +80,12 @@ class AllDialog {
         // 处理退出登录逻辑
         // 处理退出登录逻辑
         print('User confirmed logout');
         print('User confirmed logout');
         // 这里可以调用退出登录的API
         // 这里可以调用退出登录的API
+        Navigator.of(Get.context!).pop();
       },
       },
       onCancel: () {
       onCancel: () {
         // 处理取消退出逻辑
         // 处理取消退出逻辑
         print('User cancelled logout');
         print('User cancelled logout');
+        Navigator.of(Get.context!).pop();
       },
       },
     );
     );
   }
   }
@@ -100,6 +107,23 @@ class AllDialog {
     );
     );
   }
   }
 
 
+  /// 显示UID信息弹窗
+  static void showUidInfo() {
+    CustomDialog.showInfo(
+      icon: IconFont.icon14,
+      iconColor: Get.theme.textTheme.bodyLarge!.color,
+      title: 'What is UID?',
+      message:
+          'Device ID (UID) This is your device\'s unique identifier. Providing this ID helps our support team verify your device and resolve your issues more quickly.',
+      buttonText: 'OK',
+      onPressed: () {
+        // 处理邮件发送成功后的逻辑
+        print('UID info dialog closed');
+        Navigator.of(Get.context!).pop();
+      },
+    );
+  }
+
   /// 显示自定义成功弹窗
   /// 显示自定义成功弹窗
   static void showCustomSuccess({
   static void showCustomSuccess({
     required String title,
     required String title,

+ 26 - 26
lib/app/dialog/custom_dialog.dart

@@ -177,12 +177,7 @@ class _CustomDialogWidget extends StatelessWidget {
     }
     }
 
 
     // 使用动画构建弹窗
     // 使用动画构建弹窗
-    return AnimatedBuilder(
-      animation: animation!,
-      builder: (context, child) {
-        return _buildAnimatedDialog();
-      },
-    );
+    return _buildAnimatedDialog();
   }
   }
 
 
   /// 构建普通弹窗
   /// 构建普通弹窗
@@ -202,31 +197,36 @@ class _CustomDialogWidget extends StatelessWidget {
       child: AnimatedBuilder(
       child: AnimatedBuilder(
         animation: animation!,
         animation: animation!,
         builder: (context, child) {
         builder: (context, child) {
+          // 使用不同的曲线来避免关闭时的抖动
+          final scaleValue = Tween<double>(begin: 0.8, end: 1.0)
+              .animate(
+                CurvedAnimation(
+                  parent: animation!,
+                  curve: Curves.easeOutCubic,
+                  reverseCurve: Curves.easeInCubic,
+                ),
+              )
+              .value;
+
+          final offsetValue = Tween<double>(begin: 30, end: 0)
+              .animate(
+                CurvedAnimation(
+                  parent: animation!,
+                  curve: Curves.easeOutCubic,
+                  reverseCurve: Curves.easeInCubic,
+                ),
+              )
+              .value;
+
           return Transform.scale(
           return Transform.scale(
-            scale: Tween<double>(begin: 0.3, end: 1.0)
-                .animate(
-                  CurvedAnimation(parent: animation!, curve: Curves.elasticOut),
-                )
-                .value,
+            scale: scaleValue,
             child: Transform.translate(
             child: Transform.translate(
-              offset: Offset(
-                0,
-                Tween<double>(begin: 100, end: 0)
-                    .animate(
-                      CurvedAnimation(
-                        parent: animation!,
-                        curve: Curves.easeOutCubic,
-                      ),
-                    )
-                    .value,
-              ),
-              child: Opacity(
-                opacity: animation!.value,
-                child: _buildDialogContent(),
-              ),
+              offset: Offset(0, offsetValue),
+              child: Opacity(opacity: animation!.value, child: child),
             ),
             ),
           );
           );
         },
         },
+        child: _buildDialogContent(),
       ),
       ),
     );
     );
   }
   }

+ 165 - 0
lib/app/dialog/update_dailog.dart

@@ -0,0 +1,165 @@
+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 '../data/models/launch/upgrade.dart';
+import '../widgets/submit_btn.dart';
+
+class UpdateDialog extends StatelessWidget {
+  final Upgrade? upgrade;
+  final VoidCallback? onUpdate;
+  final VoidCallback? onLater;
+
+  const UpdateDialog({
+    super.key,
+    required this.upgrade,
+    this.onUpdate,
+    this.onLater,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Dialog(
+      backgroundColor: Colors.transparent,
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.r)),
+      child: Container(
+        constraints: BoxConstraints(
+          maxWidth: MediaQuery.of(context).size.width * 0.85,
+          maxHeight: 360.w,
+        ),
+        child: Stack(
+          alignment: Alignment.topCenter,
+          children: [
+            Container(
+              width: double.infinity,
+              decoration: BoxDecoration(
+                color: Get.reactiveTheme.highlightColor,
+                border: Border.all(
+                  color: Get.reactiveTheme.dividerColor,
+                  width: 1.w,
+                ),
+                borderRadius: BorderRadius.all(
+                  Radius.circular(24.r),
+                ), // 四个角 16dp
+              ),
+              child: Column(
+                children: [
+                  24.verticalSpace,
+                  Icon(
+                    Icons.rocket,
+                    size: 96.w,
+                    color: Get.reactiveTheme.primaryColor,
+                  ),
+                  16.verticalSpace,
+                  // Title
+                  Text(
+                    Strings.newVersionAvailable.tr,
+                    style: TextStyle(
+                      fontSize: 16.sp,
+                      fontWeight: FontWeight.w600,
+                      height: 1.4,
+                      color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                    ),
+                  ),
+                  4.verticalSpace,
+                  // Version
+                  Text(
+                    "${upgrade?.versionName}",
+                    style: TextStyle(
+                      fontSize: 12.sp,
+                      height: 1.4,
+                      color: Get.reactiveTheme.hintColor,
+                    ),
+                  ),
+                  12.verticalSpace,
+                  SingleChildScrollView(
+                    child: Padding(
+                      padding: EdgeInsets.symmetric(horizontal: 24.w),
+                      child: ConstrainedBox(
+                        constraints: BoxConstraints(
+                          maxHeight: 80.w, // 设置最大高度
+                        ),
+                        child: SingleChildScrollView(
+                          child: Column(
+                            crossAxisAlignment: CrossAxisAlignment.start,
+                            children: [
+                              ...upgrade!.info!
+                                  .split('\n')
+                                  .map(
+                                    (text) => Column(
+                                      children: [
+                                        _buildBulletPoint(text),
+                                        4.verticalSpace,
+                                      ],
+                                    ),
+                                  ),
+                              16.verticalSpace,
+                            ],
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+            Positioned(
+              bottom: 24.w,
+              left: 0,
+              right: 0,
+              child: Padding(
+                padding: EdgeInsets.symmetric(horizontal: 16.w),
+                child: Row(
+                  children: [
+                    if (upgrade?.forced == false)
+                      Expanded(
+                        child: SubmitButton(
+                          onPressed: onLater ?? () {},
+                          text: Strings.later.tr,
+                        ),
+                      ),
+                    if (upgrade?.forced == false) 8.horizontalSpace,
+                    Expanded(
+                      child: SubmitButton(
+                        text: Strings.updateNow.tr,
+                        onPressed: onUpdate,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildBulletPoint(String text) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          '• ',
+          style: TextStyle(
+            fontSize: 12.sp,
+            height: 1.4,
+            color: Get.reactiveTheme.hintColor,
+          ),
+        ),
+        Expanded(
+          child: Text(
+            text,
+            style: TextStyle(
+              fontSize: 12.sp,
+              height: 1.4,
+              color: Get.reactiveTheme.hintColor,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 11 - 1
lib/app/modules/account/controllers/account_controller.dart

@@ -1,11 +1,13 @@
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 
 
+import '../../../../utils/device_manager.dart';
+
 class AccountController extends GetxController {
 class AccountController extends GetxController {
   // 是否是会员(true: Premium, false: Free)
   // 是否是会员(true: Premium, false: Free)
   final isPremium = true.obs;
   final isPremium = true.obs;
 
 
   // UID
   // UID
-  final uid = '123-456-789-101';
+  String uid = '';
 
 
   // Free 用户剩余时间
   // Free 用户剩余时间
   final freeTime = '01:60:59 / Days';
   final freeTime = '01:60:59 / Days';
@@ -21,4 +23,12 @@ class AccountController extends GetxController {
   void togglePremium() {
   void togglePremium() {
     isPremium.value = !isPremium.value;
     isPremium.value = !isPremium.value;
   }
   }
+
+  @override
+  void onInit() {
+    super.onInit();
+    uid = DeviceManager.getCacheDeviceId().length > 12
+        ? '${DeviceManager.getCacheDeviceId().substring(0, 6)}***${DeviceManager.getCacheDeviceId().substring(DeviceManager.getCacheDeviceId().length - 6)}'
+        : DeviceManager.getCacheDeviceId();
+  }
 }
 }

+ 91 - 164
lib/app/modules/account/views/account_view.dart

@@ -5,9 +5,12 @@ import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
+import 'package:nomo/app/widgets/submit_btn.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
+import '../../../../config/theme/dark_theme_colors.dart';
 import '../../../constants/assets.dart';
 import '../../../constants/assets.dart';
+import '../../../constants/iconfont/iconfont.dart';
 import '../../../widgets/ix_image.dart';
 import '../../../widgets/ix_image.dart';
 import '../controllers/account_controller.dart';
 import '../controllers/account_controller.dart';
 
 
@@ -22,7 +25,7 @@ class AccountView extends BaseView<AccountController> {
     return Obx(() {
     return Obx(() {
       final isPremium = controller.isPremium.value;
       final isPremium = controller.isPremium.value;
       return SingleChildScrollView(
       return SingleChildScrollView(
-        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
         child: Column(
         child: Column(
           children: [
           children: [
             // Account 信息卡片
             // Account 信息卡片
@@ -30,11 +33,6 @@ class AccountView extends BaseView<AccountController> {
 
 
             20.verticalSpaceFromWidth,
             20.verticalSpaceFromWidth,
 
 
-            // Premium 功能列表
-            _buildPremiumFeatures(isPremium),
-
-            30.verticalSpaceFromWidth,
-
             // 底部按钮
             // 底部按钮
             _buildBottomButtons(isPremium),
             _buildBottomButtons(isPremium),
 
 
@@ -50,7 +48,7 @@ class AccountView extends BaseView<AccountController> {
     return Container(
     return Container(
       decoration: BoxDecoration(
       decoration: BoxDecoration(
         color: Get.reactiveTheme.highlightColor,
         color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
+        borderRadius: BorderRadius.circular(12.r),
       ),
       ),
       child: Column(
       child: Column(
         children: [
         children: [
@@ -64,6 +62,9 @@ class AccountView extends BaseView<AccountController> {
 
 
           // Time/Term 条目
           // Time/Term 条目
           if (isPremium) _buildValidTermItem() else _buildFreeTimeItem(),
           if (isPremium) _buildValidTermItem() else _buildFreeTimeItem(),
+          _buildDivider(),
+          // Premium 功能列表
+          _buildPremiumFeatures(isPremium),
         ],
         ],
       ),
       ),
     );
     );
@@ -72,27 +73,27 @@ class AccountView extends BaseView<AccountController> {
   /// Account 条目
   /// Account 条目
   Widget _buildAccountItem(bool isPremium) {
   Widget _buildAccountItem(bool isPremium) {
     return Container(
     return Container(
-      height: 56.h,
+      height: 56.w,
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       child: Row(
       child: Row(
         children: [
         children: [
           // 图标
           // 图标
           Container(
           Container(
-            width: 32.w,
-            height: 32.w,
+            width: 30.w,
+            height: 30.w,
             decoration: BoxDecoration(
             decoration: BoxDecoration(
-              color: const Color(0xFF00A8E8),
+              color: Get.reactiveTheme.shadowColor,
               borderRadius: BorderRadius.circular(8.r),
               borderRadius: BorderRadius.circular(8.r),
             ),
             ),
-            child: Icon(Icons.account_circle, size: 20.w, color: Colors.white),
+            child: Icon(IconFont.icon29, size: 20.w, color: Colors.white),
           ),
           ),
-          SizedBox(width: 12.w),
+          10.horizontalSpace,
           // 标题
           // 标题
           Expanded(
           Expanded(
             child: Text(
             child: Text(
               'Account',
               'Account',
               style: TextStyle(
               style: TextStyle(
-                fontSize: 16.sp,
+                fontSize: 14.sp,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 fontWeight: FontWeight.w400,
                 fontWeight: FontWeight.w400,
               ),
               ),
@@ -118,38 +119,38 @@ class AccountView extends BaseView<AccountController> {
         Get.snackbar('已复制', 'UID 已复制到剪贴板');
         Get.snackbar('已复制', 'UID 已复制到剪贴板');
       },
       },
       child: Container(
       child: Container(
-        height: 56.h,
+        height: 56.w,
         padding: EdgeInsets.symmetric(horizontal: 16.w),
         padding: EdgeInsets.symmetric(horizontal: 16.w),
         child: Row(
         child: Row(
           children: [
           children: [
             // 图标
             // 图标
             Container(
             Container(
-              width: 32.w,
-              height: 32.w,
+              width: 30.w,
+              height: 30.w,
               decoration: BoxDecoration(
               decoration: BoxDecoration(
-                color: const Color(0xFF00A8E8),
+                color: Get.reactiveTheme.shadowColor,
                 borderRadius: BorderRadius.circular(8.r),
                 borderRadius: BorderRadius.circular(8.r),
               ),
               ),
-              child: Icon(
-                Icons.badge_outlined,
-                size: 20.w,
-                color: Colors.white,
-              ),
+              child: Icon(IconFont.icon14, size: 20.w, color: Colors.white),
             ),
             ),
-            SizedBox(width: 12.w),
+            10.horizontalSpace,
             // UID
             // UID
             Expanded(
             Expanded(
               child: Text(
               child: Text(
                 controller.uid,
                 controller.uid,
                 style: TextStyle(
                 style: TextStyle(
-                  fontSize: 16.sp,
+                  fontSize: 14.sp,
                   color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                   color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                   fontWeight: FontWeight.w400,
                   fontWeight: FontWeight.w400,
                 ),
                 ),
               ),
               ),
             ),
             ),
             // 复制图标
             // 复制图标
-            Icon(Icons.copy, size: 20.w, color: Get.reactiveTheme.hintColor),
+            Icon(
+              IconFont.icon57,
+              size: 20.w,
+              color: Get.reactiveTheme.hintColor,
+            ),
           ],
           ],
         ),
         ),
       ),
       ),
@@ -159,27 +160,27 @@ class AccountView extends BaseView<AccountController> {
   /// Free Time 条目
   /// Free Time 条目
   Widget _buildFreeTimeItem() {
   Widget _buildFreeTimeItem() {
     return Container(
     return Container(
-      height: 56.h,
+      height: 56.w,
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       child: Row(
       child: Row(
         children: [
         children: [
           // 图标
           // 图标
           Container(
           Container(
-            width: 32.w,
-            height: 32.w,
+            width: 30.w,
+            height: 30.w,
             decoration: BoxDecoration(
             decoration: BoxDecoration(
-              color: const Color(0xFF00A8E8),
+              color: Get.reactiveTheme.shadowColor,
               borderRadius: BorderRadius.circular(8.r),
               borderRadius: BorderRadius.circular(8.r),
             ),
             ),
-            child: Icon(Icons.access_time, size: 20.w, color: Colors.white),
+            child: Icon(IconFont.icon30, size: 20.w, color: Colors.white),
           ),
           ),
-          SizedBox(width: 12.w),
+          10.horizontalSpace,
           // 标题
           // 标题
           Expanded(
           Expanded(
             child: Text(
             child: Text(
               'Free Time',
               'Free Time',
               style: TextStyle(
               style: TextStyle(
-                fontSize: 16.sp,
+                fontSize: 14.sp,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 fontWeight: FontWeight.w400,
                 fontWeight: FontWeight.w400,
               ),
               ),
@@ -189,9 +190,9 @@ class AccountView extends BaseView<AccountController> {
           Text(
           Text(
             controller.freeTime,
             controller.freeTime,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 14.sp,
-              color: const Color(0xFFFF9500),
-              fontWeight: FontWeight.w500,
+              fontSize: 13.sp,
+              color: DarkThemeColors.validTermColor,
+              fontWeight: FontWeight.w400,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -202,27 +203,27 @@ class AccountView extends BaseView<AccountController> {
   /// Valid Term 条目
   /// Valid Term 条目
   Widget _buildValidTermItem() {
   Widget _buildValidTermItem() {
     return Container(
     return Container(
-      height: 56.h,
+      height: 56.w,
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       child: Row(
       child: Row(
         children: [
         children: [
           // 图标
           // 图标
           Container(
           Container(
-            width: 32.w,
-            height: 32.w,
+            width: 30.w,
+            height: 30.w,
             decoration: BoxDecoration(
             decoration: BoxDecoration(
-              color: const Color(0xFF00A8E8),
+              color: Get.reactiveTheme.shadowColor,
               borderRadius: BorderRadius.circular(8.r),
               borderRadius: BorderRadius.circular(8.r),
             ),
             ),
-            child: Icon(Icons.event, size: 20.w, color: Colors.white),
+            child: Icon(IconFont.icon30, size: 20.w, color: Colors.white),
           ),
           ),
-          SizedBox(width: 12.w),
+          10.horizontalSpace,
           // 标题
           // 标题
           Expanded(
           Expanded(
             child: Text(
             child: Text(
               'Valid Term',
               'Valid Term',
               style: TextStyle(
               style: TextStyle(
-                fontSize: 16.sp,
+                fontSize: 14.sp,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 fontWeight: FontWeight.w400,
                 fontWeight: FontWeight.w400,
               ),
               ),
@@ -232,9 +233,9 @@ class AccountView extends BaseView<AccountController> {
           Text(
           Text(
             controller.validTerm,
             controller.validTerm,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 14.sp,
-              color: const Color(0xFF00A8E8),
-              fontWeight: FontWeight.w500,
+              fontSize: 13.sp,
+              color: Get.reactiveTheme.primaryColor,
+              fontWeight: FontWeight.w400,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -242,87 +243,46 @@ class AccountView extends BaseView<AccountController> {
     );
     );
   }
   }
 
 
-  /// Premium 功能列表
   Widget _buildPremiumFeatures(bool isPremium) {
   Widget _buildPremiumFeatures(bool isPremium) {
-    final features = [
-      {'icon': Icons.lock_open, 'title': 'Unlock all free locations'},
-      {'icon': Icons.flash_on, 'title': 'Unlock smart mode'},
-      {'icon': Icons.timeline, 'title': 'Unlock Multi-hop mode'},
-      {'icon': Icons.phone_iphone, 'title': 'Premium can share X devices'},
-      {'icon': Icons.dns, 'title': 'Own your own private server'},
-      {'icon': Icons.block, 'title': 'Close ads'},
-    ];
-
-    return Container(
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
-      ),
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 14.w),
       child: Column(
       child: Column(
         children: [
         children: [
-          for (int i = 0; i < features.length; i++) ...[
-            _buildFeatureItem(
-              icon: features[i]['icon'] as IconData,
-              title: features[i]['title'] as String,
-              isActive: isPremium,
-            ),
-            if (i < features.length - 1) _buildDivider(),
-          ],
+          _buildFeatureItem(IconFont.icon60, 'Unlock all free locations'),
+          _buildFeatureItem(IconFont.icon61, 'Unlock smart mode'),
+          _buildFeatureItem(IconFont.icon62, 'Unlock Multi-hop mode'),
+          _buildFeatureItem(IconFont.icon63, 'Premium can share X devices'),
+          _buildFeatureItem(IconFont.icon64, 'Own your own private server'),
+          _buildFeatureItem(IconFont.icon65, 'Close ads'),
         ],
         ],
       ),
       ),
     );
     );
   }
   }
 
 
-  /// 功能条目
-  Widget _buildFeatureItem({
-    required IconData icon,
-    required String title,
-    required bool isActive,
-  }) {
-    return Container(
-      height: 56.h,
-      padding: EdgeInsets.symmetric(horizontal: 16.w),
+  Widget _buildFeatureItem(IconData icon, String title) {
+    return SizedBox(
+      height: 44.w,
       child: Row(
       child: Row(
         children: [
         children: [
-          // 图标
-          Icon(
-            icon,
-            size: 24.w,
-            color: isActive
-                ? const Color(0xFFFF9500)
-                : Get.reactiveTheme.hintColor.withOpacity(0.5),
-          ),
-          SizedBox(width: 12.w),
-          // 标题
+          Icon(icon, color: DarkThemeColors.subscriptionColor, size: 24.w),
+          12.horizontalSpace,
           Expanded(
           Expanded(
             child: Text(
             child: Text(
               title,
               title,
               style: TextStyle(
               style: TextStyle(
-                fontSize: 16.sp,
-                color: isActive
-                    ? Get.reactiveTheme.hintColor
-                    : Get.reactiveTheme.hintColor.withOpacity(0.5),
-                fontWeight: FontWeight.w400,
+                fontSize: 13.sp,
+                color: Get.reactiveTheme.hintColor,
               ),
               ),
             ),
             ),
           ),
           ),
-          // 状态图标
           Container(
           Container(
-            width: 24.w,
-            height: 24.w,
+            width: 20.w,
+            height: 20.w,
             decoration: BoxDecoration(
             decoration: BoxDecoration(
-              color: isActive
-                  ? const Color(0xFF00D9A3)
-                  : Get.reactiveTheme.hintColor.withOpacity(0.3),
               shape: BoxShape.circle,
               shape: BoxShape.circle,
+              color: DarkThemeColors.subscriptionSelectColor,
             ),
             ),
-            child: Icon(
-              isActive ? Icons.check : Icons.close,
-              size: 16.w,
-              color: isActive
-                  ? Colors.white
-                  : Get.reactiveTheme.hintColor.withOpacity(0.5),
-            ),
+            child: Icon(Icons.check, color: Colors.white, size: 12.w),
           ),
           ),
         ],
         ],
       ),
       ),
@@ -334,31 +294,32 @@ class AccountView extends BaseView<AccountController> {
     if (isPremium) {
     if (isPremium) {
       return Column(
       return Column(
         children: [
         children: [
-          // Change Subscription 按钮
-          _buildPrimaryButton(
+          SubmitButton(
             text: 'Change Subscription',
             text: 'Change Subscription',
-            onTap: () {
+            bgColor: Get.reactiveTheme.highlightColor,
+            textColor: DarkThemeColors.subscriptionColor,
+            onPressed: () {
               // TODO: 修改订阅
               // TODO: 修改订阅
             },
             },
           ),
           ),
-          SizedBox(height: 16.h),
+          20.verticalSpaceFromWidth,
           // Device Authorization 按钮
           // Device Authorization 按钮
           _buildSecondaryButton(
           _buildSecondaryButton(
             text:
             text:
                 'Device Authorization (${controller.deviceCount}/${controller.maxDeviceCount})',
                 'Device Authorization (${controller.deviceCount}/${controller.maxDeviceCount})',
-            icon: Icons.link,
+            icon: IconFont.icon11,
             onTap: () {
             onTap: () {
               // TODO: 设备授权
               // TODO: 设备授权
             },
             },
           ),
           ),
-          SizedBox(height: 12.h),
+          10.verticalSpaceFromWidth,
           // 提示文字
           // 提示文字
           Text(
           Text(
             'You can authorize other devices as Premium users\n(${controller.deviceCount}/${controller.maxDeviceCount})',
             'You can authorize other devices as Premium users\n(${controller.deviceCount}/${controller.maxDeviceCount})',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
               fontSize: 12.sp,
               fontSize: 12.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.6),
+              color: Get.reactiveTheme.hintColor,
               height: 1.5,
               height: 1.5,
             ),
             ),
           ),
           ),
@@ -368,29 +329,31 @@ class AccountView extends BaseView<AccountController> {
       return Column(
       return Column(
         children: [
         children: [
           // Upgrade to Premium 按钮
           // Upgrade to Premium 按钮
-          _buildPrimaryButton(
+          SubmitButton(
             text: 'Upgrade to Premium',
             text: 'Upgrade to Premium',
-            onTap: () {
-              // TODO: 升级到会员
+            bgColor: Get.reactiveTheme.highlightColor,
+            textColor: DarkThemeColors.subscriptionColor,
+            onPressed: () {
+              // TODO: 修改订阅
             },
             },
           ),
           ),
-          SizedBox(height: 16.h),
+          20.verticalSpaceFromWidth,
           // Activate Pre Code 按钮
           // Activate Pre Code 按钮
           _buildSecondaryButton(
           _buildSecondaryButton(
             text: 'Activate Pre Code',
             text: 'Activate Pre Code',
-            icon: Icons.workspace_premium,
+            icon: IconFont.icon23,
             onTap: () {
             onTap: () {
               // TODO: 激活兑换码
               // TODO: 激活兑换码
             },
             },
           ),
           ),
-          SizedBox(height: 12.h),
+          10.verticalSpaceFromWidth,
           // 提示文字
           // 提示文字
           Text(
           Text(
-            'Have a Premium code? Activate it here.',
+            'If you have a Pre code, please enter it to claim your Pre benefits.',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
               fontSize: 12.sp,
               fontSize: 12.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.6),
+              color: Get.reactiveTheme.hintColor,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -398,36 +361,6 @@ class AccountView extends BaseView<AccountController> {
     }
     }
   }
   }
 
 
-  /// 主按钮(橙色)
-  Widget _buildPrimaryButton({
-    required String text,
-    required VoidCallback onTap,
-  }) {
-    return ClickOpacity(
-      onTap: onTap,
-      child: Container(
-        height: 52.h,
-        decoration: BoxDecoration(
-          gradient: const LinearGradient(
-            colors: [Color(0xFFFFB800), Color(0xFFFF9500)],
-            begin: Alignment.centerLeft,
-            end: Alignment.centerRight,
-          ),
-          borderRadius: BorderRadius.circular(26.r),
-        ),
-        alignment: Alignment.center,
-        child: Text(
-          text,
-          style: TextStyle(
-            fontSize: 16.sp,
-            color: Colors.white,
-            fontWeight: FontWeight.w600,
-          ),
-        ),
-      ),
-    );
-  }
-
   /// 次要按钮(黑色边框)
   /// 次要按钮(黑色边框)
   Widget _buildSecondaryButton({
   Widget _buildSecondaryButton({
     required String text,
     required String text,
@@ -437,10 +370,10 @@ class AccountView extends BaseView<AccountController> {
     return ClickOpacity(
     return ClickOpacity(
       onTap: onTap,
       onTap: onTap,
       child: Container(
       child: Container(
-        height: 52.h,
+        height: 52.w,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
-          border: Border.all(color: const Color(0xFFFF9500), width: 1.5),
-          borderRadius: BorderRadius.circular(26.r),
+          border: Border.all(color: Get.reactiveTheme.dividerColor, width: 1.w),
+          borderRadius: BorderRadius.circular(12.r),
         ),
         ),
         child: Row(
         child: Row(
           mainAxisAlignment: MainAxisAlignment.center,
           mainAxisAlignment: MainAxisAlignment.center,
@@ -449,12 +382,12 @@ class AccountView extends BaseView<AccountController> {
               text,
               text,
               style: TextStyle(
               style: TextStyle(
                 fontSize: 16.sp,
                 fontSize: 16.sp,
-                color: const Color(0xFFFF9500),
-                fontWeight: FontWeight.w600,
+                color: DarkThemeColors.subscriptionColor,
+                fontWeight: FontWeight.w400,
               ),
               ),
             ),
             ),
             SizedBox(width: 8.w),
             SizedBox(width: 8.w),
-            Icon(icon, size: 20.w, color: const Color(0xFFFF9500)),
+            Icon(icon, size: 20.w, color: DarkThemeColors.subscriptionColor),
           ],
           ],
         ),
         ),
       ),
       ),
@@ -463,12 +396,6 @@ class AccountView extends BaseView<AccountController> {
 
 
   /// 构建分割线
   /// 构建分割线
   Widget _buildDivider() {
   Widget _buildDivider() {
-    return Padding(
-      padding: EdgeInsets.only(left: 60.w),
-      child: Divider(
-        height: 1,
-        color: Get.reactiveTheme.dividerColor.withOpacity(0.3),
-      ),
-    );
+    return Divider(height: 1.w, color: Get.reactiveTheme.dividerColor);
   }
   }
 }
 }

+ 63 - 5
lib/app/modules/deviceauth/controllers/deviceauth_controller.dart

@@ -1,5 +1,7 @@
 import 'dart:async';
 import 'dart:async';
+import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
+import 'package:nomo/app/components/ix_snackbar.dart';
 
 
 class DeviceauthController extends GetxController {
 class DeviceauthController extends GetxController {
   // 用户类型:true为VIP用户,false为免费用户
   // 用户类型:true为VIP用户,false为免费用户
@@ -77,41 +79,97 @@ class DeviceauthController extends GetxController {
   /// 提交授权码
   /// 提交授权码
   void submitAuthCode() {
   void submitAuthCode() {
     if (inputCode.value.length == 6) {
     if (inputCode.value.length == 6) {
+      // 检查设备数量限制
+      if (currentDevices.length >= maxDevices) {
+        IXSnackBar.showIXErrorSnackBar(
+          title: 'Device Limit Reached',
+          message: 'You can only authorize up to $maxDevices devices',
+        );
+        inputCode.value = '';
+        isCodeComplete.value = false;
+        return;
+      }
+
       isAuthorizing.value = true;
       isAuthorizing.value = true;
       authorizationStatus.value = AuthorizationStatus.configuring;
       authorizationStatus.value = AuthorizationStatus.configuring;
 
 
-      // 模拟授权过程
+      // 模拟授权过程(实际应该调用API)
       Timer(const Duration(seconds: 2), () {
       Timer(const Duration(seconds: 2), () {
         isAuthorizing.value = false;
         isAuthorizing.value = false;
         authorizationStatus.value = AuthorizationStatus.success;
         authorizationStatus.value = AuthorizationStatus.success;
 
 
         // 添加新设备
         // 添加新设备
+        final newId = (currentDevices.length + 1).toString();
         currentDevices.add(
         currentDevices.add(
           DeviceInfo(
           DeviceInfo(
-            id: '2',
+            id: newId,
             name: 'Android devices',
             name: 'Android devices',
             type: DeviceTypeEnum.android,
             type: DeviceTypeEnum.android,
-            uid: 'UID 123-456-789-101',
+            uid: 'UID 123-456-789-10$newId',
+            date: _getCurrentDateTime(),
             isCurrent: false,
             isCurrent: false,
           ),
           ),
         );
         );
 
 
+        // 显示成功提示
+        IXSnackBar.showIXSnackBar(
+          title: 'Device Authorized',
+          message: 'New device has been successfully authorized',
+        );
+
         // 清空输入码
         // 清空输入码
         inputCode.value = '';
         inputCode.value = '';
         isCodeComplete.value = false;
         isCodeComplete.value = false;
+
+        // 重置状态
+        Timer(const Duration(milliseconds: 500), () {
+          authorizationStatus.value = AuthorizationStatus.waiting;
+        });
       });
       });
     }
     }
   }
   }
 
 
+  /// 获取当前日期时间
+  String _getCurrentDateTime() {
+    final now = DateTime.now();
+    return '${now.year}/${now.month.toString().padLeft(2, '0')}/${now.day.toString().padLeft(2, '0')} '
+        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
+  }
+
   /// 移除设备
   /// 移除设备
   void removeDevice(String deviceId) {
   void removeDevice(String deviceId) {
-    currentDevices.removeWhere((device) => device.id == deviceId);
+    final device = currentDevices.firstWhere((d) => d.id == deviceId);
+
+    Get.dialog(
+      AlertDialog(
+        title: const Text('Relieve Device'),
+        content: Text(
+          'Are you sure you want to relieve ${device.name}?\n\n'
+          'This device will lose Premium access.',
+        ),
+        actions: [
+          TextButton(onPressed: () => Get.back(), child: const Text('Cancel')),
+          TextButton(
+            onPressed: () {
+              currentDevices.removeWhere((d) => d.id == deviceId);
+              Get.back();
+              Get.snackbar(
+                'Device Relieved',
+                '${device.name} has been removed from authorized devices',
+                snackPosition: SnackPosition.bottom,
+              );
+            },
+            child: const Text('Relieve', style: TextStyle(color: Colors.red)),
+          ),
+        ],
+      ),
+    );
   }
   }
 
 
   /// 复制授权码
   /// 复制授权码
   void copyAuthCode() {
   void copyAuthCode() {
     // TODO: 实现复制到剪贴板
     // TODO: 实现复制到剪贴板
-    Get.snackbar('已复制', '授权码已复制到剪贴板');
+    IXSnackBar.showIXSnackBar(title: '已复制', message: '授权码已复制到剪贴板');
   }
   }
 
 
   /// 获取倒计时显示文本
   /// 获取倒计时显示文本

+ 167 - 542
lib/app/modules/deviceauth/views/deviceauth_view.dart

@@ -8,7 +8,11 @@ import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:pinput/pinput.dart';
 import 'package:pinput/pinput.dart';
 
 
+import '../../../../config/theme/dark_theme_colors.dart';
+import '../../../constants/iconfont/iconfont.dart';
+import '../../../widgets/info_card.dart';
 import '../controllers/deviceauth_controller.dart';
 import '../controllers/deviceauth_controller.dart';
+import '../widgets/device_card.dart';
 
 
 class DeviceauthView extends BaseView<DeviceauthController> {
 class DeviceauthView extends BaseView<DeviceauthController> {
   const DeviceauthView({super.key});
   const DeviceauthView({super.key});
@@ -21,7 +25,7 @@ class DeviceauthView extends BaseView<DeviceauthController> {
     return Obx(() {
     return Obx(() {
       final isPremium = controller.isPremium.value;
       final isPremium = controller.isPremium.value;
       return SingleChildScrollView(
       return SingleChildScrollView(
-        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.w),
         child: Column(
         child: Column(
           children: [
           children: [
             if (isPremium) ...[
             if (isPremium) ...[
@@ -43,9 +47,6 @@ class DeviceauthView extends BaseView<DeviceauthController> {
       children: [
       children: [
         // 授权码显示区域
         // 授权码显示区域
         _buildAuthCodeDisplay(),
         _buildAuthCodeDisplay(),
-
-        30.verticalSpaceFromWidth,
-
         // 说明卡片
         // 说明卡片
         _buildInstructionCards(),
         _buildInstructionCards(),
       ],
       ],
@@ -61,11 +62,14 @@ class DeviceauthView extends BaseView<DeviceauthController> {
 
 
         20.verticalSpaceFromWidth,
         20.verticalSpaceFromWidth,
 
 
+        Padding(
+          padding: EdgeInsets.symmetric(horizontal: 8.w),
+          child: Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
+        ),
+
         // 设备管理区域
         // 设备管理区域
         _buildDeviceManagementSection(),
         _buildDeviceManagementSection(),
 
 
-        20.verticalSpaceFromWidth,
-
         // 授权步骤说明
         // 授权步骤说明
         _buildAuthorizationSteps(),
         _buildAuthorizationSteps(),
       ],
       ],
@@ -75,11 +79,7 @@ class DeviceauthView extends BaseView<DeviceauthController> {
   /// 授权码显示区域
   /// 授权码显示区域
   Widget _buildAuthCodeDisplay() {
   Widget _buildAuthCodeDisplay() {
     return Container(
     return Container(
-      padding: EdgeInsets.all(24.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
-      ),
+      padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
       child: Column(
       child: Column(
         children: [
         children: [
           // 6位授权码
           // 6位授权码
@@ -87,42 +87,44 @@ class DeviceauthView extends BaseView<DeviceauthController> {
             () => Text(
             () => Text(
               controller.authCode.value,
               controller.authCode.value,
               style: TextStyle(
               style: TextStyle(
-                fontSize: 48.sp,
-                fontWeight: FontWeight.bold,
-                color: Colors.white,
-                letterSpacing: 4.w,
+                fontSize: 34.sp,
+                fontWeight: FontWeight.w500,
+                height: 1.2,
+                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                letterSpacing: 10.w,
               ),
               ),
             ),
             ),
           ),
           ),
 
 
-          20.verticalSpaceFromWidth,
+          14.verticalSpaceFromWidth,
 
 
           // 倒计时和复制按钮
           // 倒计时和复制按钮
           Row(
           Row(
             mainAxisAlignment: MainAxisAlignment.center,
             mainAxisAlignment: MainAxisAlignment.center,
             children: [
             children: [
               Icon(
               Icon(
-                Icons.access_time,
-                size: 16.w,
+                IconFont.icon48,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
-              SizedBox(width: 8.w),
+              4.horizontalSpace,
               Obx(
               Obx(
                 () => Text(
                 () => Text(
                   controller.countdownText,
                   controller.countdownText,
                   style: TextStyle(
                   style: TextStyle(
-                    fontSize: 16.sp,
+                    fontSize: 12.sp,
+                    height: 1.7,
                     color: Get.reactiveTheme.hintColor,
                     color: Get.reactiveTheme.hintColor,
                   ),
                   ),
                 ),
                 ),
               ),
               ),
-              SizedBox(width: 16.w),
+              20.horizontalSpace,
               Container(
               Container(
-                width: 1,
-                height: 20.h,
-                color: Get.reactiveTheme.hintColor.withOpacity(0.3),
+                width: 1.w,
+                height: 20.w,
+                color: Get.reactiveTheme.dividerColor,
               ),
               ),
-              SizedBox(width: 16.w),
+              20.horizontalSpace,
               ClickOpacity(
               ClickOpacity(
                 onTap: () {
                 onTap: () {
                   Clipboard.setData(
                   Clipboard.setData(
@@ -136,14 +138,15 @@ class DeviceauthView extends BaseView<DeviceauthController> {
                     Text(
                     Text(
                       'Copy',
                       'Copy',
                       style: TextStyle(
                       style: TextStyle(
-                        fontSize: 16.sp,
+                        fontSize: 12.sp,
+                        height: 1.7,
                         color: Get.reactiveTheme.hintColor,
                         color: Get.reactiveTheme.hintColor,
                       ),
                       ),
                     ),
                     ),
-                    SizedBox(width: 4.w),
+                    4.horizontalSpace,
                     Icon(
                     Icon(
-                      Icons.copy,
-                      size: 16.w,
+                      IconFont.icon57,
+                      size: 20.w,
                       color: Get.reactiveTheme.hintColor,
                       color: Get.reactiveTheme.hintColor,
                     ),
                     ),
                   ],
                   ],
@@ -158,9 +161,9 @@ class DeviceauthView extends BaseView<DeviceauthController> {
           Text(
           Text(
             'Please keep this page open.',
             'Please keep this page open.',
             style: TextStyle(
             style: TextStyle(
-              fontSize: 16.sp,
-              color: const Color(0xFFFFB800),
-              fontWeight: FontWeight.w500,
+              fontSize: 13.sp,
+              color: DarkThemeColors.validTermColor,
+              height: 1.4,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -170,113 +173,29 @@ class DeviceauthView extends BaseView<DeviceauthController> {
 
 
   /// 说明卡片
   /// 说明卡片
   Widget _buildInstructionCards() {
   Widget _buildInstructionCards() {
-    return Container(
-      padding: EdgeInsets.all(20.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-        borderRadius: BorderRadius.circular(16.r),
-        border: Border.all(
-          color: const Color(0xFF00A8E8).withOpacity(0.3),
-          width: 1,
+    return InfoCard(
+      title: '',
+      items: [
+        InfoItem(
+          icon: IconFont.icon11,
+          title: 'Authorization Code',
+          description:
+              'This 6-digit code allows a VIP user to link your device. It refreshes every 15 minutes.',
+          iconColor: DarkThemeColors.shadowColor,
         ),
         ),
-      ),
-      child: Column(
-        children: [
-          _buildInstructionItem(
-            icon: Icons.vpn_key,
-            title: 'Authorization Code',
-            description:
-                'This 6-digit code allows a VIP user to link your device. It refreshes every 15 minutes.',
-          ),
-
-          20.verticalSpaceFromWidth,
-
-          _buildInstructionItem(
-            icon: Icons.workspace_premium,
-            title: 'Share with Pre User',
-            description:
-                'Tell the VIP user this code so they can enter it on their device to authorize you.',
-          ),
-
-          20.verticalSpaceFromWidth,
-
-          _buildInstructionItem(
-            icon: Icons.access_time,
-            title: 'Waiting for Authorization',
-            description:
-                'Please keep this page open.\nOnce approved, your account will automatically upgrade and reconnect.',
-            highlightText: 'Please keep this page open.',
-          ),
-        ],
-      ),
-    );
-  }
-
-  /// 说明条目
-  Widget _buildInstructionItem({
-    required IconData icon,
-    required String title,
-    required String description,
-    String? highlightText,
-  }) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        Container(
-          width: 40.w,
-          height: 40.w,
-          decoration: BoxDecoration(
-            color: const Color(0xFF00A8E8).withOpacity(0.2),
-            borderRadius: BorderRadius.circular(8.r),
-          ),
-          child: Icon(icon, size: 20.w, color: const Color(0xFF00A8E8)),
+        InfoItem(
+          icon: IconFont.icon23,
+          title: 'Share with Pre User',
+          description:
+              'Tell the VIP user this code so they can enter it on their device to authorize you.',
+          iconColor: DarkThemeColors.shadowColor,
         ),
         ),
-        SizedBox(width: 16.w),
-        Expanded(
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Text(
-                title,
-                style: TextStyle(
-                  fontSize: 16.sp,
-                  fontWeight: FontWeight.bold,
-                  color: Colors.white,
-                ),
-              ),
-              SizedBox(height: 8.h),
-              highlightText != null
-                  ? RichText(
-                      text: TextSpan(
-                        children: [
-                          TextSpan(
-                            text: highlightText,
-                            style: TextStyle(
-                              fontSize: 14.sp,
-                              color: const Color(0xFF00A8E8),
-                            ),
-                          ),
-                          TextSpan(
-                            text:
-                                '\n${description.substring(highlightText.length + 1)}',
-                            style: TextStyle(
-                              fontSize: 14.sp,
-                              color: Get.reactiveTheme.hintColor,
-                            ),
-                          ),
-                        ],
-                      ),
-                    )
-                  : Text(
-                      description,
-                      style: TextStyle(
-                        fontSize: 14.sp,
-                        color: Get.reactiveTheme.hintColor,
-                        height: 1.4,
-                      ),
-                    ),
-            ],
-          ),
+        InfoItem(
+          icon: IconFont.icon49,
+          title: 'Waiting for Authorization',
+          description:
+              'Please keep this page open.\nOnce approved, your account will automatically upgrade and reconnect.',
+          iconColor: DarkThemeColors.shadowColor,
         ),
         ),
       ],
       ],
     );
     );
@@ -284,430 +203,136 @@ class DeviceauthView extends BaseView<DeviceauthController> {
 
 
   /// 顶部6个输入框
   /// 顶部6个输入框
   Widget _buildTopCodeInput() {
   Widget _buildTopCodeInput() {
-    return Container(
-      padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
-      ),
-      child: Obx(
-        () => Pinput(
-          length: 6,
-          defaultPinTheme: PinTheme(
-            width: 40.w,
-            height: 50.h,
-            textStyle: TextStyle(
-              fontSize: 24.sp,
-              fontWeight: FontWeight.bold,
-              color: Colors.white,
-            ),
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-              borderRadius: BorderRadius.circular(8.r),
-              border: Border.all(color: Colors.transparent, width: 2),
-            ),
-          ),
-          focusedPinTheme: PinTheme(
-            width: 40.w,
-            height: 50.h,
-            textStyle: TextStyle(
-              fontSize: 24.sp,
-              fontWeight: FontWeight.bold,
-              color: Colors.white,
-            ),
-            decoration: BoxDecoration(
-              color: const Color(0xFF00A8E8).withOpacity(0.2),
-              borderRadius: BorderRadius.circular(8.r),
-              border: Border.all(color: const Color(0xFF00A8E8), width: 2),
-            ),
+    return Obx(
+      () => Pinput(
+        length: 6,
+        defaultPinTheme: PinTheme(
+          width: 50.w,
+          height: 50.w,
+          textStyle: TextStyle(
+            fontSize: 27.sp,
+            fontWeight: FontWeight.w600,
+            color: Get.reactiveTheme.primaryColor,
           ),
           ),
-          submittedPinTheme: PinTheme(
-            width: 40.w,
-            height: 50.h,
-            textStyle: TextStyle(
-              fontSize: 24.sp,
-              fontWeight: FontWeight.bold,
-              color: Colors.white,
-            ),
-            decoration: BoxDecoration(
-              color: const Color(0xFF00A8E8).withOpacity(0.2),
-              borderRadius: BorderRadius.circular(8.r),
-              border: Border.all(color: Colors.transparent, width: 2),
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.highlightColor,
+            borderRadius: BorderRadius.circular(5.r),
+            border: Border.all(
+              color: Get.reactiveTheme.dividerColor,
+              width: 1.5.w,
             ),
             ),
           ),
           ),
-          onChanged: (value) {
-            controller.setInputCode(value);
-          },
-          onCompleted: (pin) {
-            controller.submitAuthCode();
-          },
-          inputFormatters: [FilteringTextInputFormatter.digitsOnly],
-          keyboardType: TextInputType.number,
-          showCursor: true,
-          cursor: Container(
-            width: 2,
-            height: 20.h,
-            color: const Color(0xFF00A8E8),
-          ),
         ),
         ),
-      ),
-    );
-  }
-
-  /// 设备管理区域
-  Widget _buildDeviceManagementSection() {
-    return Container(
-      padding: EdgeInsets.all(20.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
-      ),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          // 标题
-          Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            children: [
-              Text(
-                'Manage Devices',
-                style: TextStyle(
-                  fontSize: 18.sp,
-                  fontWeight: FontWeight.bold,
-                  color: Colors.white,
-                ),
-              ),
-              Obx(
-                () => Text(
-                  controller.deviceCountText,
-                  style: TextStyle(
-                    fontSize: 16.sp,
-                    color: Get.reactiveTheme.hintColor,
-                  ),
-                ),
-              ),
-            ],
-          ),
-
-          20.verticalSpaceFromWidth,
-
-          // 设备列表
-          Obx(
-            () => Column(
-              children: [
-                ...controller.currentDevices.map((device) {
-                  return _buildDeviceCard(device);
-                }).toList(),
-                // 等待激活的设备
-                if (controller.inputCode.value.isEmpty)
-                  _buildAwaitingDeviceCard(),
-              ],
-            ),
+        focusedPinTheme: PinTheme(
+          width: 50.w,
+          height: 50.w,
+          textStyle: TextStyle(
+            fontSize: 27.sp,
+            fontWeight: FontWeight.w600,
+            color: Get.reactiveTheme.primaryColor,
           ),
           ),
-
-          20.verticalSpaceFromWidth,
-
-          // 授权限制说明
-          Text(
-            'Authorize up to 4 devices as Premium (${controller.deviceCountText})',
-            style: TextStyle(
-              fontSize: 14.sp,
-              color: Get.reactiveTheme.hintColor,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  /// 等待激活的设备卡片
-  Widget _buildAwaitingDeviceCard() {
-    return Container(
-      margin: EdgeInsets.only(bottom: 12.h),
-      padding: EdgeInsets.all(16.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-        borderRadius: BorderRadius.circular(12.r),
-      ),
-      child: Row(
-        children: [
-          // 设备图标
-          Container(
-            width: 40.w,
-            height: 40.w,
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.hintColor.withOpacity(0.2),
-              borderRadius: BorderRadius.circular(8.r),
-            ),
-            child: Icon(
-              Icons.phone_iphone,
-              size: 20.w,
-              color: Get.reactiveTheme.hintColor,
-            ),
-          ),
-
-          SizedBox(width: 12.w),
-
-          // 设备信息
-          Expanded(
-            child: Text(
-              'Awaiting Activation',
-              style: TextStyle(
-                fontSize: 16.sp,
-                fontWeight: FontWeight.w500,
-                color: Colors.white,
-              ),
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.cardColor,
+            borderRadius: BorderRadius.circular(5.r),
+            border: Border.all(
+              color: Get.reactiveTheme.primaryColor,
+              width: 1.5.w,
             ),
             ),
           ),
           ),
-        ],
-      ),
-    );
-  }
-
-  /// 设备卡片
-  Widget _buildDeviceCard(DeviceInfo device) {
-    return Container(
-      margin: EdgeInsets.only(bottom: 12.h),
-      padding: EdgeInsets.all(16.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-        borderRadius: BorderRadius.circular(12.r),
-      ),
-      child: Row(
-        children: [
-          // 设备图标
-          Container(
-            width: 40.w,
-            height: 40.w,
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.hintColor.withOpacity(0.2),
-              borderRadius: BorderRadius.circular(8.r),
-            ),
-            child: Icon(
-              _getDeviceIcon(device.type),
-              size: 20.w,
-              color: Get.reactiveTheme.hintColor,
-            ),
+        ),
+        submittedPinTheme: PinTheme(
+          width: 50.w,
+          height: 50.w,
+          textStyle: TextStyle(
+            fontSize: 27.sp,
+            fontWeight: FontWeight.w600,
+            color: Get.reactiveTheme.primaryColor,
           ),
           ),
-
-          SizedBox(width: 12.w),
-
-          // 设备信息
-          Expanded(
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Text(
-                  device.name,
-                  style: TextStyle(
-                    fontSize: 16.sp,
-                    fontWeight: FontWeight.w500,
-                    color: Colors.white,
-                  ),
-                ),
-                if (device.uid != null) ...[
-                  SizedBox(height: 4.h),
-                  Text(
-                    device.uid!,
-                    style: TextStyle(
-                      fontSize: 14.sp,
-                      color: Get.reactiveTheme.hintColor,
-                    ),
-                  ),
-                ],
-                if (device.date != null) ...[
-                  SizedBox(height: 4.h),
-                  Text(
-                    device.date!,
-                    style: TextStyle(
-                      fontSize: 12.sp,
-                      color: Get.reactiveTheme.hintColor,
-                    ),
-                  ),
-                ],
-              ],
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.cardColor,
+            borderRadius: BorderRadius.circular(5.r),
+            border: Border.all(
+              color: Get.reactiveTheme.dividerColor,
+              width: 1.5.w,
             ),
             ),
           ),
           ),
-
-          // 操作按钮
-          if (device.isCurrent)
-            ClickOpacity(
-              onTap: () => controller.removeDevice(device.id),
-              child: Container(
-                padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
-                decoration: BoxDecoration(
-                  color: Get.reactiveTheme.hintColor.withOpacity(0.2),
-                  borderRadius: BorderRadius.circular(6.r),
-                ),
-                child: Row(
-                  mainAxisSize: MainAxisSize.min,
-                  children: [
-                    Icon(
-                      Icons.delete,
-                      size: 16.w,
-                      color: Get.reactiveTheme.hintColor,
-                    ),
-                    SizedBox(width: 4.w),
-                    Text(
-                      'Relieve',
-                      style: TextStyle(
-                        fontSize: 12.sp,
-                        color: Get.reactiveTheme.hintColor,
-                      ),
-                    ),
-                  ],
-                ),
-              ),
-            )
-          else
-            ClickOpacity(
-              onTap: () => controller.removeDevice(device.id),
-              child: Container(
-                padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
-                decoration: BoxDecoration(
-                  color: const Color(0xFF00A8E8),
-                  borderRadius: BorderRadius.circular(6.r),
-                ),
-                child: Text(
-                  'Relieve',
-                  style: TextStyle(
-                    fontSize: 12.sp,
-                    color: Colors.white,
-                    fontWeight: FontWeight.w500,
-                  ),
-                ),
-              ),
-            ),
-        ],
+        ),
+        onChanged: (value) {
+          controller.setInputCode(value);
+        },
+        onCompleted: (pin) {
+          controller.submitAuthCode();
+        },
+        inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+        keyboardType: TextInputType.number,
+        showCursor: true,
+        cursor: Container(
+          width: 1.w,
+          height: 30.w,
+          color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+        ),
       ),
       ),
     );
     );
   }
   }
 
 
-  /// 获取设备图标
-  IconData _getDeviceIcon(DeviceTypeEnum type) {
-    switch (type) {
-      case DeviceTypeEnum.ios:
-        return Icons.phone_iphone;
-      case DeviceTypeEnum.android:
-        return Icons.android;
-      case DeviceTypeEnum.windows:
-        return Icons.laptop_windows;
-      case DeviceTypeEnum.mac:
-        return Icons.laptop_mac;
-    }
+  /// 设备管理区域
+  Widget _buildDeviceManagementSection() {
+    return Obx(() {
+      final authStatus = controller.authorizationStatus.value;
+      final isConfiguring = authStatus == AuthorizationStatus.configuring;
+
+      return Container(
+        margin: EdgeInsets.only(top: 20.w),
+        decoration: BoxDecoration(
+          color: Get.reactiveTheme.highlightColor,
+          borderRadius: BorderRadius.circular(12.r),
+        ),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 设备列表
+            ...controller.currentDevices.map((device) {
+              return DeviceCard(
+                device: device,
+                onRelieve: () => controller.removeDevice(device.id),
+                showRelieveButton: true,
+              );
+            }),
+
+            // 加载中状态或等待激活的设备
+            if (isConfiguring || controller.inputCode.value.isEmpty)
+              const AwaitingActivationCard(),
+          ],
+        ),
+      );
+    });
   }
   }
 
 
   /// 授权步骤说明
   /// 授权步骤说明
   Widget _buildAuthorizationSteps() {
   Widget _buildAuthorizationSteps() {
-    return Container(
-      padding: EdgeInsets.all(20.w),
-      decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
-      ),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          Text(
-            'Authorization Steps',
-            style: TextStyle(
-              fontSize: 18.sp,
-              fontWeight: FontWeight.bold,
-              color: Colors.white,
-            ),
-          ),
-
-          20.verticalSpaceFromWidth,
-
-          _buildStepItem(
-            stepNumber: 1,
-            title: 'Enter Code',
-            description:
-                'Input the 6-digit code shown on the other device (free user). This code refreshes every 15 minutes.',
-            icon: Icons.input,
-            isCompleted: controller.inputCode.value.isNotEmpty,
-          ),
-
-          16.verticalSpaceFromWidth,
-
-          _buildStepItem(
-            stepNumber: 2,
-            title: 'Verify Device',
-            description:
-                'We\'ll check if the entered code matches an active device waiting for authorization.',
-            icon: Icons.verified_user,
-            isCompleted: controller.isCodeComplete.value,
-          ),
-
-          16.verticalSpaceFromWidth,
-
-          _buildStepItem(
-            stepNumber: 3,
-            title: 'Authorization Successful',
-            description:
-                'Once confirmed, the device will automatically upgrade and link to your account.',
-            icon: Icons.check_circle,
-            isCompleted:
-                controller.authorizationStatus.value ==
-                AuthorizationStatus.success,
-          ),
-        ],
-      ),
-    );
-  }
-
-  /// 步骤条目
-  Widget _buildStepItem({
-    required int stepNumber,
-    required String title,
-    required String description,
-    required IconData icon,
-    required bool isCompleted,
-  }) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: [
-        Container(
-          width: 32.w,
-          height: 32.w,
-          decoration: BoxDecoration(
-            color: isCompleted
-                ? const Color(0xFF00D9A3)
-                : Get.reactiveTheme.hintColor.withOpacity(0.2),
-            borderRadius: BorderRadius.circular(8.r),
-          ),
-          child: Center(
-            child: isCompleted
-                ? Icon(Icons.check, size: 16.w, color: Colors.white)
-                : Icon(icon, size: 16.w, color: Get.reactiveTheme.hintColor),
-          ),
+    return InfoCard(
+      title: '',
+      items: [
+        InfoItem(
+          icon: IconFont.icon46,
+          title: 'Enter Code',
+          description:
+              'Input the 6-digit code shown on the other device (free user).This code refreshes every 15 minutes.',
+          iconColor: DarkThemeColors.shadowColor,
         ),
         ),
-
-        SizedBox(width: 12.w),
-
-        Expanded(
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              Text(
-                title,
-                style: TextStyle(
-                  fontSize: 16.sp,
-                  fontWeight: FontWeight.w500,
-                  color: Colors.white,
-                ),
-              ),
-              SizedBox(height: 4.h),
-              Text(
-                description,
-                style: TextStyle(
-                  fontSize: 14.sp,
-                  color: Get.reactiveTheme.hintColor,
-                  height: 1.4,
-                ),
-              ),
-            ],
-          ),
+        InfoItem(
+          icon: IconFont.icon16,
+          title: 'Verify Device',
+          description:
+              'We’ll check if the entered code matches an active device waiting for authorization.',
+          iconColor: DarkThemeColors.shadowColor,
+        ),
+        InfoItem(
+          icon: IconFont.icon43,
+          title: 'Authorization Successful',
+          description:
+              'Once confirmed, the device will automatically upgrade and link to your account.',
+          iconColor: DarkThemeColors.shadowColor,
         ),
         ),
       ],
       ],
     );
     );

+ 196 - 0
lib/app/modules/deviceauth/widgets/device_card.dart

@@ -0,0 +1,196 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.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/iconfont/iconfont.dart';
+import '../../../widgets/infintte_rotate.dart';
+import '../controllers/deviceauth_controller.dart';
+
+/// 设备卡片组件
+class DeviceCard extends StatelessWidget {
+  final DeviceInfo device;
+  final VoidCallback? onRelieve;
+  final bool showRelieveButton;
+
+  const DeviceCard({
+    super.key,
+    required this.device,
+    this.onRelieve,
+    this.showRelieveButton = true,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: EdgeInsets.all(14.w),
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: BorderSide(
+            color: Get.reactiveTheme.dividerColor, // 颜色
+            width: 1.w, // 宽度
+          ),
+        ),
+      ),
+      child: Row(
+        children: [
+          // 设备图标
+          Container(
+            width: 30.w,
+            height: 30.w,
+            decoration: BoxDecoration(
+              color: _getDeviceIconBgColor(device.type),
+              borderRadius: BorderRadius.circular(8.r),
+            ),
+            child: Icon(
+              _getDeviceIcon(device.type),
+              size: 20.w,
+              color: Colors.white,
+            ),
+          ),
+
+          10.horizontalSpace,
+
+          // 设备信息
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  device.name,
+                  style: TextStyle(
+                    fontSize: 14.sp,
+                    fontWeight: FontWeight.w500,
+                    color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                    height: 1.4,
+                  ),
+                ),
+                if (device.uid != null)
+                  Text(
+                    device.uid!,
+                    style: TextStyle(
+                      fontSize: 13.sp,
+                      color: Get.reactiveTheme.hintColor,
+                      height: 1.4,
+                    ),
+                  ),
+                if (device.date != null)
+                  Text(
+                    device.date!,
+                    style: TextStyle(
+                      fontSize: 13.sp,
+                      color: Get.reactiveTheme.hintColor,
+                      height: 1.4,
+                    ),
+                  ),
+              ],
+            ),
+          ),
+
+          // Relieve按钮
+          if (showRelieveButton)
+            ClickOpacity(
+              onTap: onRelieve,
+              child: Container(
+                padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
+                decoration: BoxDecoration(
+                  color: Get.reactiveTheme.primaryColor,
+                  borderRadius: BorderRadius.circular(12.r),
+                ),
+                child: Row(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    Icon(IconFont.icon11, size: 20.w, color: Colors.white),
+                    SizedBox(width: 4.w),
+                    Text(
+                      'Relieve',
+                      style: TextStyle(
+                        fontSize: 12.sp,
+                        height: 1.7,
+                        color: Colors.white,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+
+  /// 获取设备图标
+  IconData _getDeviceIcon(DeviceTypeEnum type) {
+    switch (type) {
+      case DeviceTypeEnum.ios:
+        return IconFont.icon45;
+      case DeviceTypeEnum.android:
+        return IconFont.icon47;
+      case DeviceTypeEnum.windows:
+        return Icons.laptop_windows;
+      case DeviceTypeEnum.mac:
+        return Icons.laptop_mac;
+    }
+  }
+
+  /// 获取设备图标背景色
+  Color _getDeviceIconBgColor(DeviceTypeEnum type) {
+    switch (type) {
+      case DeviceTypeEnum.ios:
+        return Get.reactiveTheme.shadowColor; // iOS蓝色
+      case DeviceTypeEnum.android:
+        return Get.reactiveTheme.shadowColor; // Android绿色
+      case DeviceTypeEnum.windows:
+        return const Color(0xFF0078D4); // Windows蓝色
+      case DeviceTypeEnum.mac:
+        return const Color(0xFF5E5CE6); // Mac紫色
+    }
+  }
+}
+
+/// 等待激活设备卡片
+class AwaitingActivationCard extends StatelessWidget {
+  const AwaitingActivationCard({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: EdgeInsets.all(14.w),
+      child: Row(
+        children: [
+          // 设备图标 - 暗灰色
+          Container(
+            width: 30.w,
+            height: 30.w,
+            decoration: BoxDecoration(
+              color: Get.reactiveTheme.cardColor,
+              borderRadius: BorderRadius.circular(8.r),
+            ),
+            child: Icon(
+              IconFont.icon31,
+              size: 20.w,
+              color: Get.reactiveTheme.hintColor,
+            ),
+          ),
+
+          10.horizontalSpace,
+
+          // 等待激活文字
+          Expanded(
+            child: Text(
+              'Awaiting Activation',
+              style: TextStyle(
+                fontSize: 13.sp,
+                color: Get.reactiveTheme.hintColor,
+                height: 1.4,
+              ),
+            ),
+          ),
+
+          const InfiniteRotateIcon(),
+        ],
+      ),
+    );
+  }
+}

+ 14 - 6
lib/app/modules/home/controllers/home_controller.dart

@@ -45,7 +45,8 @@ class HomeController extends BaseController {
 
 
   // 最近使用的位置列表
   // 最近使用的位置列表
   final _recentLocations = <Locations>[].obs;
   final _recentLocations = <Locations>[].obs;
-  List<Locations> get recentLocations => _recentLocations;
+  List<Locations> get recentLocations =>
+      _recentLocations.where((loc) => loc.id != selectedLocation?.id).toList();
 
 
   // 是否是会员
   // 是否是会员
   final _isPremium = false.obs;
   final _isPremium = false.obs;
@@ -122,8 +123,15 @@ class HomeController extends BaseController {
     _saveLocationsToStorage();
     _saveLocationsToStorage();
   }
   }
 
 
-  void handleConnect() {
-    coreController.selectLocationConnect();
+  void handleConnect({bool delay = false}) {
+    if (delay) {
+      // 延迟300ms
+      Future.delayed(const Duration(milliseconds: 300), () {
+        coreController.selectLocationConnect();
+      });
+    } else {
+      coreController.selectLocationConnect();
+    }
   }
   }
 
 
   /// 更新最近使用的位置列表
   /// 更新最近使用的位置列表
@@ -134,9 +142,9 @@ class HomeController extends BaseController {
     // 添加到列表开头
     // 添加到列表开头
     _recentLocations.insert(0, location);
     _recentLocations.insert(0, location);
 
 
-    // 保持最多3个最近使用的位置
-    if (_recentLocations.length > 3) {
-      _recentLocations.removeRange(3, _recentLocations.length);
+    // 保持最多4个最近使用的位置(过滤掉当前选中后能显示3个)
+    if (_recentLocations.length > 4) {
+      _recentLocations.removeRange(4, _recentLocations.length);
     }
     }
   }
   }
 
 

+ 53 - 14
lib/app/modules/home/views/home_view.dart

@@ -1,6 +1,8 @@
+import 'package:carousel_slider/carousel_slider.dart';
 import 'package:flutter/material.dart' hide ConnectionState;
 import 'package:flutter/material.dart' hide ConnectionState;
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
+import 'package:nomo/app/constants/iconfont/iconfont.dart';
 import '../../../../config/theme/theme_extensions/theme_extension.dart';
 import '../../../../config/theme/theme_extensions/theme_extension.dart';
 import '../../../base/base_view.dart';
 import '../../../base/base_view.dart';
 import '../../../constants/assets.dart';
 import '../../../constants/assets.dart';
@@ -32,17 +34,31 @@ class HomeView extends BaseView<HomeController> {
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
               children: [
                 Obx(
                 Obx(
-                  () => IXImage(
-                    source: controller.isPremium ? Assets.premium : Assets.free,
-                    width: controller.isPremium ? 92.w : 64.w,
-                    height: 28.w,
-                    sourceType: ImageSourceType.asset,
+                  () => ClickOpacity(
+                    onTap: () => Get.toNamed(Routes.SUBSCRIPTION),
+                    child: IXImage(
+                      source: controller.isPremium
+                          ? Assets.premium
+                          : Assets.free,
+                      width: controller.isPremium ? 92.w : 64.w,
+                      height: 28.w,
+                      sourceType: ImageSourceType.asset,
+                    ),
                   ),
                   ),
                 ),
                 ),
                 ClickOpacity(
                 ClickOpacity(
                   child: Padding(
                   child: Padding(
-                    padding: EdgeInsets.all(10.w),
-                    child: Icon(Icons.settings, size: 20.w),
+                    padding: EdgeInsets.only(
+                      left: 10.w,
+                      right: 0.w,
+                      top: 10.w,
+                      bottom: 10.w,
+                    ),
+                    child: Icon(
+                      IconFont.icon09,
+                      size: 26.w,
+                      color: Get.reactiveTheme.hintColor,
+                    ),
                   ),
                   ),
                   onTap: () {
                   onTap: () {
                     Get.toNamed(Routes.SETTING);
                     Get.toNamed(Routes.SETTING);
@@ -54,12 +70,32 @@ class HomeView extends BaseView<HomeController> {
                 ),
                 ),
               ],
               ],
             ),
             ),
-            80.verticalSpaceFromWidth,
+
+            // 80.verticalSpaceFromWidth,
+            Padding(
+              padding: EdgeInsets.symmetric(vertical: 20.w),
+              child: CarouselSlider(
+                options: CarouselOptions(height: 80.w, viewportFraction: 1.0),
+                items: [1, 2, 3, 4, 5].map((i) {
+                  return Builder(
+                    builder: (BuildContext context) {
+                      return IXImage(
+                        source: Assets.bannerTest,
+                        width: double.infinity,
+                        height: 80.w,
+                        sourceType: ImageSourceType.asset,
+                        borderRadius: 14.r,
+                      );
+                    },
+                  );
+                }).toList(),
+              ),
+            ),
             Text(
             Text(
               "Active time",
               "Active time",
               style: TextStyle(
               style: TextStyle(
                 fontSize: 18.sp,
                 fontSize: 18.sp,
-                height: 1.5,
+                height: 1.3,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
             ),
             ),
@@ -69,7 +105,7 @@ class HomeView extends BaseView<HomeController> {
                 controller.coreController.timer,
                 controller.coreController.timer,
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: 28.sp,
                   fontSize: 28.sp,
-                  height: 1.5,
+                  height: 1.2,
                   color: Get.reactiveTheme.primaryColor,
                   color: Get.reactiveTheme.primaryColor,
                 ),
                 ),
               ),
               ),
@@ -188,8 +224,8 @@ class HomeView extends BaseView<HomeController> {
               ),
               ),
               // 箭头图标
               // 箭头图标
               Icon(
               Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 color: Get.reactiveTheme.textTheme.bodyLarge!.color,
               ),
               ),
             ],
             ],
@@ -222,7 +258,7 @@ class HomeView extends BaseView<HomeController> {
                 child: Row(
                 child: Row(
                   children: [
                   children: [
                     Icon(
                     Icon(
-                      Icons.access_time,
+                      IconFont.icon68,
                       size: 16.w,
                       size: 16.w,
                       color: Get.reactiveTheme.hintColor,
                       color: Get.reactiveTheme.hintColor,
                     ),
                     ),
@@ -301,7 +337,7 @@ class HomeView extends BaseView<HomeController> {
                             : 0.0,
                             : 0.0,
                         duration: const Duration(milliseconds: 300),
                         duration: const Duration(milliseconds: 300),
                         child: Icon(
                         child: Icon(
-                          Icons.keyboard_arrow_right,
+                          IconFont.icon02,
                           size: 20.w,
                           size: 20.w,
                           color: Get.reactiveTheme.hintColor,
                           color: Get.reactiveTheme.hintColor,
                         ),
                         ),
@@ -328,7 +364,10 @@ class HomeView extends BaseView<HomeController> {
                       children: controller.recentLocations.map((location) {
                       children: controller.recentLocations.map((location) {
                         return ClickOpacity(
                         return ClickOpacity(
                           onTap: () {
                           onTap: () {
+                            controller.isRecentLocationsExpanded =
+                                !controller.isRecentLocationsExpanded;
                             controller.selectLocation(location);
                             controller.selectLocation(location);
+                            controller.handleConnect();
                           },
                           },
                           child: Column(
                           child: Column(
                             children: [
                             children: [

+ 11 - 7
lib/app/modules/home/widgets/connection_button.dart

@@ -41,11 +41,6 @@ class _ConnectionButtonState extends State<ConnectionButton>
   }
   }
 
 
   void _onTap() {
   void _onTap() {
-    // 切换位置
-    setState(() {
-      _isAtTop = !_isAtTop;
-    });
-
     if (widget.onTap != null) {
     if (widget.onTap != null) {
       widget.onTap!();
       widget.onTap!();
     }
     }
@@ -121,7 +116,9 @@ class _ConnectionButtonState extends State<ConnectionButton>
         text = "Disconnected";
         text = "Disconnected";
         textColor = Get.reactiveTheme.hintColor;
         textColor = Get.reactiveTheme.hintColor;
         if (_isAtTop) {
         if (_isAtTop) {
-          _isAtTop = false;
+          setState(() {
+            _isAtTop = false;
+          });
         }
         }
         break;
         break;
       case ConnectionState.connecting:
       case ConnectionState.connecting:
@@ -133,6 +130,11 @@ class _ConnectionButtonState extends State<ConnectionButton>
         statusImgPath = Assets.connecting;
         statusImgPath = Assets.connecting;
         text = "Connecting";
         text = "Connecting";
         textColor = Get.reactiveTheme.hintColor;
         textColor = Get.reactiveTheme.hintColor;
+        // 切换位置
+        setState(() {
+          _isAtTop = !_isAtTop;
+        });
+
         break;
         break;
       case ConnectionState.connected:
       case ConnectionState.connected:
         gradientColor = [
         gradientColor = [
@@ -144,7 +146,9 @@ class _ConnectionButtonState extends State<ConnectionButton>
         text = "Connected";
         text = "Connected";
         textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
         textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
         if (!_isAtTop) {
         if (!_isAtTop) {
-          _isAtTop = true;
+          setState(() {
+            _isAtTop = true;
+          });
         }
         }
         break;
         break;
       case ConnectionState.error:
       case ConnectionState.error:

+ 16 - 15
lib/app/modules/home/widgets/menu_list.dart

@@ -3,9 +3,12 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
+import '../../../constants/iconfont/iconfont.dart';
+import '../../../routes/app_pages.dart';
+
 /// 菜单项数据模型
 /// 菜单项数据模型
 class MenuItem {
 class MenuItem {
-  final String icon;
+  final IconData icon;
   final String title;
   final String title;
   final Color iconColor;
   final Color iconColor;
   final VoidCallback? onTap;
   final VoidCallback? onTap;
@@ -26,16 +29,17 @@ class MenuList extends StatelessWidget {
   List<MenuItem> _getMenuItems() {
   List<MenuItem> _getMenuItems() {
     return [
     return [
       MenuItem(
       MenuItem(
-        icon: Icons.movie.codePoint.toString(),
+        icon: IconFont.icon19,
         title: 'Movies&TV',
         title: 'Movies&TV',
         iconColor: const Color(0xFFFF3B30),
         iconColor: const Color(0xFFFF3B30),
         onTap: () {
         onTap: () {
           // 处理点击事件
           // 处理点击事件
           print('Movies&TV tapped');
           print('Movies&TV tapped');
+          Get.toNamed(Routes.MEDIALOCATION);
         },
         },
       ),
       ),
       MenuItem(
       MenuItem(
-        icon: Icons.people.codePoint.toString(),
+        icon: IconFont.icon26,
         title: 'Social',
         title: 'Social',
         iconColor: const Color(0xFF007AFF),
         iconColor: const Color(0xFF007AFF),
         onTap: () {
         onTap: () {
@@ -43,7 +47,7 @@ class MenuList extends StatelessWidget {
         },
         },
       ),
       ),
       MenuItem(
       MenuItem(
-        icon: Icons.headset_mic.codePoint.toString(),
+        icon: IconFont.icon28,
         title: 'Support',
         title: 'Support',
         iconColor: const Color(0xFF34C759),
         iconColor: const Color(0xFF34C759),
         onTap: () {
         onTap: () {
@@ -51,7 +55,7 @@ class MenuList extends StatelessWidget {
         },
         },
       ),
       ),
       MenuItem(
       MenuItem(
-        icon: Icons.sports_basketball.codePoint.toString(),
+        icon: IconFont.icon41,
         title: 'Sport',
         title: 'Sport',
         iconColor: const Color(0xFFFF9500),
         iconColor: const Color(0xFFFF9500),
         onTap: () {
         onTap: () {
@@ -59,7 +63,7 @@ class MenuList extends StatelessWidget {
         },
         },
       ),
       ),
       MenuItem(
       MenuItem(
-        icon: Icons.music_note.codePoint.toString(),
+        icon: IconFont.icon52,
         title: 'Movies&TV',
         title: 'Movies&TV',
         iconColor: const Color(0xFF00C7BE),
         iconColor: const Color(0xFF00C7BE),
         onTap: () {
         onTap: () {
@@ -67,7 +71,7 @@ class MenuList extends StatelessWidget {
         },
         },
       ),
       ),
       MenuItem(
       MenuItem(
-        icon: Icons.videogame_asset.codePoint.toString(),
+        icon: IconFont.icon53,
         title: 'Movies&TV',
         title: 'Movies&TV',
         iconColor: const Color(0xFFAF52DE),
         iconColor: const Color(0xFFAF52DE),
         onTap: () {
         onTap: () {
@@ -84,7 +88,7 @@ class MenuList extends StatelessWidget {
     return GridView.builder(
     return GridView.builder(
       shrinkWrap: true,
       shrinkWrap: true,
       physics: const NeverScrollableScrollPhysics(),
       physics: const NeverScrollableScrollPhysics(),
-      padding: EdgeInsets.symmetric(vertical: 16.w),
+      padding: EdgeInsets.symmetric(vertical: 15.w),
       gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
       gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
         crossAxisCount: 3, // 每排3个
         crossAxisCount: 3, // 每排3个
         crossAxisSpacing: 8.w, // 水平间距
         crossAxisSpacing: 8.w, // 水平间距
@@ -111,11 +115,8 @@ class MenuList extends StatelessWidget {
           mainAxisAlignment: MainAxisAlignment.center,
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
           children: [
             // 图标
             // 图标
-            Icon(
-              IconData(int.parse(item.icon), fontFamily: 'MaterialIcons'),
-              size: 20.w,
-              color: item.iconColor,
-            ),
+            Icon(item.icon, size: 20.w, color: item.iconColor),
+            4.verticalSpaceFromWidth,
             // 标题
             // 标题
             Text(
             Text(
               item.title,
               item.title,
@@ -123,8 +124,8 @@ class MenuList extends StatelessWidget {
               maxLines: 1,
               maxLines: 1,
               overflow: TextOverflow.ellipsis,
               overflow: TextOverflow.ellipsis,
               style: TextStyle(
               style: TextStyle(
-                fontSize: 12.sp,
-                height: 1.7,
+                fontSize: 13.sp,
+                height: 1.4,
                 color: Get.theme.hintColor,
                 color: Get.theme.hintColor,
                 fontWeight: FontWeight.w500,
                 fontWeight: FontWeight.w500,
               ),
               ),

+ 1 - 1
lib/app/modules/language/views/language_view.dart

@@ -105,7 +105,7 @@ class LanguageView extends BaseView<LanguageController> {
                   color: isSelected
                   color: isSelected
                       ? Get.reactiveTheme.shadowColor
                       ? Get.reactiveTheme.shadowColor
                       : Colors.grey[400]!,
                       : Colors.grey[400]!,
-                  width: 2.w,
+                  width: 1.5.w,
                 ),
                 ),
               ),
               ),
               child: isSelected
               child: isSelected

+ 10 - 0
lib/app/modules/medialocation/bindings/medialocation_binding.dart

@@ -0,0 +1,10 @@
+import 'package:get/get.dart';
+
+import '../controllers/medialocation_controller.dart';
+
+class MedialocationBinding extends Binding {
+  @override
+  List<Bind> dependencies() {
+    return [Bind.lazyPut(() => MedialocationController())];
+  }
+}

+ 90 - 0
lib/app/modules/medialocation/controllers/medialocation_controller.dart

@@ -0,0 +1,90 @@
+import 'package:get/get.dart';
+
+/// 流媒体服务数据模型
+class StreamingService {
+  final String name;
+  final String description;
+  final String logoUrl;
+
+  StreamingService({
+    required this.name,
+    required this.description,
+    required this.logoUrl,
+  });
+}
+
+class MedialocationController extends GetxController {
+  // VPN连接状态
+  final isConnected = false.obs;
+  final isConnecting = false.obs;
+
+  // 流媒体服务列表
+  final List<StreamingService> streamingServices = [
+    StreamingService(
+      name: 'Netflix',
+      description: 'Nifty Streaming',
+      logoUrl: 'assets/images/netflix_logo.png',
+    ),
+    StreamingService(
+      name: 'YouTube',
+      description: 'YouTube Streaming',
+      logoUrl: 'assets/images/youtube_logo.png',
+    ),
+    StreamingService(
+      name: 'hulu',
+      description: 'hulu Streaming',
+      logoUrl: 'assets/images/hulu_logo.png',
+    ),
+    StreamingService(
+      name: 'Amazon',
+      description: 'Amazon Streaming',
+      logoUrl: 'assets/images/amazon_logo.png',
+    ),
+  ];
+
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  /// 打开流媒体服务
+  void openStreamingService(StreamingService service) {
+    Get.snackbar(
+      'Opening',
+      '${service.name} will open soon',
+    );
+  }
+
+  /// 连接VPN
+  void connect() {
+    if (isConnecting.value) return;
+    
+    isConnecting.value = true;
+    
+    // 模拟连接过程
+    Future.delayed(const Duration(seconds: 2), () {
+      isConnecting.value = false;
+      isConnected.value = true;
+      Get.snackbar(
+        'Success',
+        'Connected successfully',
+      );
+    });
+  }
+
+  /// 断开VPN
+  void disconnect() {
+    isConnected.value = false;
+    isConnecting.value = false;
+  }
+}

+ 384 - 0
lib/app/modules/medialocation/views/medialocation_view.dart

@@ -0,0 +1,384 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:nomo/config/theme/dark_theme_colors.dart';
+
+import '../../../constants/assets.dart';
+import '../../../constants/iconfont/iconfont.dart';
+import '../../../widgets/click_opacity.dart';
+import '../../../widgets/ix_image.dart';
+import '../../../widgets/submit_btn.dart';
+import '../controllers/medialocation_controller.dart';
+
+class MedialocationView extends GetView<MedialocationController> {
+  const MedialocationView({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: DarkThemeColors.scaffoldBackgroundColor,
+      body: Stack(
+        children: [
+          // 视频背景层(只显示顶部214高度)
+          Positioned(
+            top: 0,
+            left: 0,
+            right: 0,
+            height: 214.w,
+            child: ClipRect(
+              child: IXImage(
+                source: Assets.mediaBg,
+                width: 375.w,
+                height: 214.w,
+                sourceType: ImageSourceType.asset,
+              ),
+            ),
+          ),
+          // 渐变遮罩层
+          Positioned(
+            top: 0,
+            left: 0,
+            right: 0,
+            height: 214.w,
+            child: Container(
+              decoration: BoxDecoration(
+                gradient: LinearGradient(
+                  begin: Alignment.topCenter,
+                  end: Alignment.bottomCenter,
+                  colors: [Colors.black.withValues(alpha: 0.6), Colors.black],
+                  stops: const [0.0, 1.0],
+                ),
+              ),
+            ),
+          ),
+          // 内容层
+          SafeArea(
+            child: Column(
+              children: [
+                // 顶部标题区域
+                _buildAppBar(),
+                _buildHeader(),
+
+                // 可滚动内容区域
+                Expanded(
+                  child: SingleChildScrollView(
+                    padding: EdgeInsets.symmetric(horizontal: 16.w),
+                    child: Column(
+                      children: [
+                        20.verticalSpace,
+
+                        // 流媒体服务卡片
+                        _buildStreamingServicesCard(),
+
+                        10.verticalSpace,
+
+                        // 功能说明列表
+                        _buildFeaturesList(),
+
+                        32.verticalSpace,
+                      ],
+                    ),
+                  ),
+                ),
+
+                // 底部连接按钮
+                _buildConnectButton(),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 顶部标题栏
+  Widget _buildAppBar() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          SizedBox(width: 32.w),
+          GestureDetector(
+            onTap: () => Get.back(),
+            child: Container(
+              width: 24.w,
+              height: 24.w,
+              decoration: BoxDecoration(
+                color: Colors.white.withValues(alpha: 0.1),
+                shape: BoxShape.circle,
+              ),
+              child: Icon(Icons.close_rounded, color: Colors.white, size: 16.w),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建顶部标题区域
+  Widget _buildHeader() {
+    return Container(
+      padding: EdgeInsets.symmetric(vertical: 20.w),
+      child: Column(
+        children: [
+          // Movies&TV 图标和标题
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Icon(
+                IconFont.icon19,
+                color: DarkThemeColors.errorColor,
+                size: 32.w,
+              ),
+              4.horizontalSpace,
+              Text(
+                'Movies&TV',
+                style: TextStyle(
+                  fontSize: 22.sp,
+                  fontWeight: FontWeight.w500,
+                  height: 1.3,
+                  color: Colors.white,
+                ),
+              ),
+            ],
+          ),
+
+          8.verticalSpace,
+
+          // 连接状态
+          Obx(() {
+            final text = controller.isConnected.value
+                ? 'Connected'
+                : controller.isConnecting.value
+                ? 'Connecting'
+                : 'Disconnected';
+            final textColor = controller.isConnected.value
+                ? DarkThemeColors.text1
+                : controller.isConnecting.value
+                ? DarkThemeColors.text1
+                : DarkThemeColors.text1;
+            final statusImgPath = controller.isConnected.value
+                ? Assets.connected
+                : controller.isConnecting.value
+                ? Assets.connecting
+                : Assets.disconnected;
+
+            return Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                IXImage(
+                  source: statusImgPath,
+                  sourceType: ImageSourceType.asset,
+                  width: 14.w,
+                  height: 14.w,
+                ),
+                4.horizontalSpace,
+                Text(
+                  text,
+                  style: TextStyle(
+                    fontSize: 14.sp,
+                    fontWeight: FontWeight.w500,
+                    color: textColor,
+                  ),
+                ),
+              ],
+            );
+          }),
+        ],
+      ),
+    );
+  }
+
+  /// 构建流媒体服务卡片
+  Widget _buildStreamingServicesCard() {
+    return Container(
+      decoration: BoxDecoration(
+        color: DarkThemeColors.bg2,
+        borderRadius: BorderRadius.circular(12.r),
+      ),
+      child: ListView.separated(
+        shrinkWrap: true,
+        physics: const NeverScrollableScrollPhysics(),
+        padding: EdgeInsets.zero,
+        itemCount: controller.streamingServices.length,
+        separatorBuilder: (context, index) => Divider(
+          height: 1.w,
+          thickness: 1.w,
+          color: DarkThemeColors.strokes1,
+        ),
+        itemBuilder: (context, index) {
+          final service = controller.streamingServices[index];
+          return _buildStreamingServiceItem(service);
+        },
+      ),
+    );
+  }
+
+  /// 构建单个流媒体服务项
+  Widget _buildStreamingServiceItem(StreamingService service) {
+    return ClickOpacity(
+      onTap: () => controller.openStreamingService(service),
+      child: Container(
+        padding: EdgeInsets.all(16.w),
+        child: Row(
+          children: [
+            // Logo
+            Container(
+              width: 40.w,
+              height: 40.w,
+              decoration: BoxDecoration(
+                color: _getServiceColor(service.name),
+                borderRadius: BorderRadius.circular(12.r),
+              ),
+              child: Center(
+                child: Text(
+                  service.name[0].toUpperCase(),
+                  style: TextStyle(
+                    fontSize: 24.sp,
+                    fontWeight: FontWeight.bold,
+                    color: DarkThemeColors.text1,
+                  ),
+                ),
+              ),
+            ),
+
+            10.horizontalSpace,
+
+            // 名称和描述
+            Expanded(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    service.name,
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      height: 1.4,
+                      fontWeight: FontWeight.w500,
+                      color: DarkThemeColors.text1,
+                    ),
+                  ),
+                  Text(
+                    service.description,
+                    style: TextStyle(
+                      fontSize: 13.sp,
+                      height: 1.4,
+                      color: DarkThemeColors.text2,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+
+            16.horizontalSpace,
+
+            // Open 按钮
+            Container(
+              padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
+              decoration: BoxDecoration(
+                color: DarkThemeColors.primaryColor,
+                borderRadius: BorderRadius.circular(20.r),
+              ),
+              child: Text(
+                'Open',
+                style: TextStyle(fontSize: 13.sp, color: DarkThemeColors.text1),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 构建功能说明列表
+  Widget _buildFeaturesList() {
+    return Column(
+      children: [
+        _buildFeatureItem(
+          icon: Icons.favorite_outline,
+          iconColor: Colors.red,
+          text: 'Access your favorite content',
+        ),
+        _buildFeatureItem(
+          icon: Icons.public,
+          iconColor: Colors.blue,
+          text: 'Connection from anywhere',
+        ),
+        _buildFeatureItem(
+          icon: Icons.speed,
+          iconColor: Colors.purple,
+          text: 'Ultra fast servers',
+        ),
+      ],
+    );
+  }
+
+  /// 构建单个功能说明项
+  Widget _buildFeatureItem({
+    required IconData icon,
+    required Color iconColor,
+    required String text,
+  }) {
+    return Container(
+      decoration: BoxDecoration(
+        color: DarkThemeColors.bg3,
+        borderRadius: BorderRadius.circular(12.r),
+      ),
+      height: 40.w,
+      margin: EdgeInsets.only(bottom: 10.w),
+      padding: EdgeInsets.symmetric(horizontal: 14.w),
+      child: Row(
+        children: [
+          Icon(icon, color: iconColor, size: 20.w),
+          10.horizontalSpace,
+          Expanded(
+            child: Text(
+              text,
+              style: TextStyle(fontSize: 13.sp, color: DarkThemeColors.text2),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建底部连接按钮
+  Widget _buildConnectButton() {
+    return Padding(
+      padding: EdgeInsets.all(16.w),
+      child: Obx(() {
+        return SubmitButton(
+          text: controller.isConnected.value
+              ? 'Disconnect'
+              : controller.isConnecting.value
+              ? 'Connecting...'
+              : 'Connect',
+          onPressed: controller.isConnected.value
+              ? controller.disconnect
+              : controller.connect,
+          isLoading: controller.isConnecting.value,
+          bgColor: controller.isConnected.value
+              ? Colors.red
+              : DarkThemeColors.primaryColor,
+        );
+      }),
+    );
+  }
+
+  /// 获取服务颜色
+  Color _getServiceColor(String serviceName) {
+    switch (serviceName.toLowerCase()) {
+      case 'netflix':
+        return const Color(0xFFE50914);
+      case 'youtube':
+        return const Color(0xFFFF0000);
+      case 'hulu':
+        return const Color(0xFF1CE783);
+      case 'amazon':
+        return const Color(0xFF00A8E1);
+      default:
+        return DarkThemeColors.primaryColor;
+    }
+  }
+}

+ 11 - 0
lib/app/modules/node/controllers/node_controller.dart

@@ -1,4 +1,6 @@
+import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
+import '../../../constants/iconfont/iconfont.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../data/sp/ix_sp.dart';
 import '../../../data/sp/ix_sp.dart';
 
 
@@ -65,6 +67,15 @@ class NodeController extends GetxController {
     return null;
     return null;
   }
   }
 
 
+  IconData getTabIcon(int tabIndex) {
+    if (tabIndex == 0) {
+      return IconFont.icon24;
+    } else if (tabIndex == 1) {
+      return IconFont.icon25;
+    }
+    return IconFont.icon24;
+  }
+
   /// 获取国家的展开状态
   /// 获取国家的展开状态
   bool getExpandedState(int tabIndex, String countryCode) {
   bool getExpandedState(int tabIndex, String countryCode) {
     final key = '${tabIndex}_$countryCode';
     final key = '${tabIndex}_$countryCode';

+ 4 - 1
lib/app/modules/node/views/node_view.dart

@@ -67,7 +67,10 @@ class NodeView extends BaseView<NodeController> {
       tabs: controller.tabTextList.map((e) {
       tabs: controller.tabTextList.map((e) {
         return Row(
         return Row(
           children: [
           children: [
-            Icon(Icons.terminal, size: 14.w),
+            Icon(
+              controller.getTabIcon(controller.tabTextList.indexOf(e)),
+              size: 14.w,
+            ),
             4.horizontalSpace,
             4.horizontalSpace,
             Text(e),
             Text(e),
           ],
           ],

+ 36 - 14
lib/app/modules/node/widgets/node_list.dart

@@ -7,6 +7,7 @@ import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
 import '../../../constants/assets.dart';
 import '../../../constants/assets.dart';
+import '../../../constants/iconfont/iconfont.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../home/controllers/home_controller.dart';
 import '../../home/controllers/home_controller.dart';
 import '../controllers/node_controller.dart';
 import '../controllers/node_controller.dart';
@@ -135,10 +136,23 @@ class _CountrySection extends StatelessWidget {
     final countryName = locationList.name ?? '';
     final countryName = locationList.name ?? '';
     final locations = locationList.locations ?? [];
     final locations = locationList.locations ?? [];
 
 
+    // 获取 HomeController 并判断当前国家是否有选中的节点
+    final homeController = Get.find<HomeController>();
+    final hasSelectedLocation = locations.any(
+      (loc) => loc.id == homeController.selectedLocation?.id,
+    );
+
+    // 根据展开状态和是否选中设置背景色
+    final backgroundColor = hasSelectedLocation
+        ? (expanded
+              ? Get.reactiveTheme.cardColor
+              : Get.reactiveTheme.highlightColor)
+        : Get.reactiveTheme.highlightColor;
+
     return Container(
     return Container(
       margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
       margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
       decoration: BoxDecoration(
       decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
+        color: backgroundColor,
         borderRadius: BorderRadius.circular(12),
         borderRadius: BorderRadius.circular(12),
       ),
       ),
       child: Column(
       child: Column(
@@ -166,7 +180,9 @@ class _CountrySection extends StatelessWidget {
                       fontSize: 16.sp,
                       fontSize: 16.sp,
                       height: 1.5,
                       height: 1.5,
                       fontWeight: FontWeight.w500,
                       fontWeight: FontWeight.w500,
-                      color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                      color: hasSelectedLocation
+                          ? Get.reactiveTheme.primaryColor
+                          : Get.reactiveTheme.textTheme.bodyLarge!.color,
                     ),
                     ),
                   ),
                   ),
                   const Spacer(),
                   const Spacer(),
@@ -175,7 +191,7 @@ class _CountrySection extends StatelessWidget {
                     turns: expanded ? 0.25 : 0.0,
                     turns: expanded ? 0.25 : 0.0,
                     duration: const Duration(milliseconds: 300),
                     duration: const Duration(milliseconds: 300),
                     child: Icon(
                     child: Icon(
-                      Icons.keyboard_arrow_right,
+                      IconFont.icon02,
                       size: 20.w,
                       size: 20.w,
                       color: Get.reactiveTheme.hintColor,
                       color: Get.reactiveTheme.hintColor,
                     ),
                     ),
@@ -197,14 +213,16 @@ class _CountrySection extends StatelessWidget {
                   children: locations.map((location) {
                   children: locations.map((location) {
                     final locationName = location.name ?? '';
                     final locationName = location.name ?? '';
                     final locationIcon = location.icon ?? countryIcon;
                     final locationIcon = location.icon ?? countryIcon;
-                    final latency = location.latency;
+                    // 判断当前节点是否被选中
+                    final isSelected =
+                        location.id == homeController.selectedLocation?.id;
 
 
                     return ClickOpacity(
                     return ClickOpacity(
                       onTap: () {
                       onTap: () {
-                        // 获取 HomeController 并保存选中的节点
+                        // 获取 HomeController
                         final homeController = Get.find<HomeController>();
                         final homeController = Get.find<HomeController>();
                         homeController.selectLocation(location);
                         homeController.selectLocation(location);
-                        homeController.handleConnect();
+                        homeController.handleConnect(delay: true);
                         // 返回上一页
                         // 返回上一页
                         Get.back();
                         Get.back();
                       },
                       },
@@ -240,18 +258,22 @@ class _CountrySection extends StatelessWidget {
                                     style: TextStyle(
                                     style: TextStyle(
                                       fontSize: 14.sp,
                                       fontSize: 14.sp,
                                       fontWeight: FontWeight.w500,
                                       fontWeight: FontWeight.w500,
-                                      color: Get.reactiveTheme.hintColor,
+                                      color: isSelected
+                                          ? Get.reactiveTheme.primaryColor
+                                          : Get
+                                                .reactiveTheme
+                                                .textTheme
+                                                .bodyLarge!
+                                                .color,
                                     ),
                                     ),
                                   ),
                                   ),
                                 ),
                                 ),
                                 // 显示延迟
                                 // 显示延迟
-                                if (latency != null && latency > 0)
-                                  Text(
-                                    '${latency}ms',
-                                    style: TextStyle(
-                                      fontSize: 12.sp,
-                                      color: Get.reactiveTheme.hintColor,
-                                    ),
+                                if (isSelected)
+                                  Icon(
+                                    IconFont.icon27,
+                                    size: 16.w,
+                                    color: Get.reactiveTheme.primaryColor,
                                   ),
                                   ),
                               ],
                               ],
                             ),
                             ),

+ 64 - 8
lib/app/modules/precode/controllers/precode_controller.dart

@@ -1,7 +1,13 @@
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
+import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
+import 'package:screenshot/screenshot.dart';
 
 
+import '../../../../config/translations/strings_enum.dart';
+import '../../../../utils/device_manager.dart';
+import '../../../components/ix_snackbar.dart';
 import '../../../routes/app_pages.dart';
 import '../../../routes/app_pages.dart';
+import '../widgets/precode_save_dialog.dart';
 
 
 class PrecodeController extends GetxController {
 class PrecodeController extends GetxController {
   // Pre Code 数据
   // Pre Code 数据
@@ -10,6 +16,15 @@ class PrecodeController extends GetxController {
   // 是否显示完整代码
   // 是否显示完整代码
   final isPreviewMode = false.obs;
   final isPreviewMode = false.obs;
 
 
+  // 截图控制器
+  final screenshotController = ScreenshotController();
+
+  // UID
+  String get uid => DeviceManager.getCacheDeviceId();
+
+  // 是否是Premium用户
+  final isPremium = true.obs;
+
   // 切换预览模式
   // 切换预览模式
   void togglePreview() {
   void togglePreview() {
     isPreviewMode.value = !isPreviewMode.value;
     isPreviewMode.value = !isPreviewMode.value;
@@ -32,11 +47,9 @@ class PrecodeController extends GetxController {
   // 复制代码
   // 复制代码
   void copyCode() {
   void copyCode() {
     Clipboard.setData(ClipboardData(text: preCode.value));
     Clipboard.setData(ClipboardData(text: preCode.value));
-    Get.snackbar(
-      '已复制',
-      'Pre Code 已复制到剪贴板',
-      snackPosition: SnackPosition.bottom,
-      duration: const Duration(seconds: 2),
+    IXSnackBar.showIXSnackBar(
+      title: Strings.copied.tr,
+      message: Strings.copied.tr,
     );
     );
   }
   }
 
 
@@ -45,9 +58,52 @@ class PrecodeController extends GetxController {
     Get.toNamed(Routes.PRECODE_SENDEMAIL);
     Get.toNamed(Routes.PRECODE_SENDEMAIL);
   }
   }
 
 
-  // 保存本地副本
+  // 保存本地副本 - 显示弹窗
   void saveLocalCopy() {
   void saveLocalCopy() {
-    // TODO: 实现保存本地副本功能
-    Get.snackbar('提示', '保存本地副本功能开发中...', snackPosition: SnackPosition.bottom);
+    Get.bottomSheet(
+      PrecodeSaveDialog(controller: this),
+      isScrollControlled: true,
+    );
+  }
+
+  // 保存截图到相册
+  Future<void> saveToGallery() async {
+    try {
+      // 截图
+      final imageBytes = await screenshotController.capture();
+
+      if (imageBytes == null) {
+        IXSnackBar.showIXSnackBar(
+          title: 'Error',
+          message: 'Failed to capture screenshot',
+        );
+        return;
+      }
+
+      // 保存到相册
+      final result = await ImageGallerySaverPlus.saveImage(
+        imageBytes,
+        quality: 100,
+        name: 'nomo_precode_${DateTime.now().millisecondsSinceEpoch}',
+      );
+
+      if (result['isSuccess'] == true) {
+        // 显示成功提示
+        IXSnackBar.showIXSnackBar(
+          title: "",
+          message: "The image has been saved to your local album",
+        );
+      } else {
+        IXSnackBar.showIXErrorSnackBar(
+          title: 'Error',
+          message: 'Failed to save image to gallery',
+        );
+      }
+    } catch (e) {
+      IXSnackBar.showIXErrorSnackBar(
+        title: 'Error',
+        message: 'Failed to save: $e',
+      );
+    }
   }
   }
 }
 }

+ 8 - 11
lib/app/modules/precode/sendemail/controllers/precode_sendemail_controller.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
+import 'package:nomo/app/dialog/all_dialog.dart';
 
 
 class PrecodeSendemailController extends GetxController {
 class PrecodeSendemailController extends GetxController {
   // 邮箱输入控制器
   // 邮箱输入控制器
@@ -56,18 +57,14 @@ class PrecodeSendemailController extends GetxController {
     try {
     try {
       // TODO: 实现实际的发送邮件功能
       // TODO: 实现实际的发送邮件功能
       await Future.delayed(const Duration(seconds: 2));
       await Future.delayed(const Duration(seconds: 2));
-
-      Get.snackbar(
-        '发送成功',
-        'Pre Code 已发送到 ${email.value}',
-        snackPosition: SnackPosition.bottom,
-      );
-
-      // 发送成功后返回
-      await Future.delayed(const Duration(milliseconds: 500));
-      Get.back();
+      AllDialog.showEmailSent();
     } catch (e) {
     } catch (e) {
-      Get.snackbar('发送失败', '请检查邮箱地址后重试', snackPosition: SnackPosition.bottom);
+      AllDialog.showCustomError(
+        title: 'Error',
+        message: 'Failed to send email',
+        buttonText: 'Retry',
+        cancelText: 'Cancel',
+      );
     } finally {
     } finally {
       isSending.value = false;
       isSending.value = false;
     }
     }

+ 73 - 236
lib/app/modules/precode/sendemail/views/precode_sendemail_view.dart

@@ -2,130 +2,69 @@ import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/base/base_view.dart';
-import 'package:nomo/app/widgets/click_opacity.dart';
+import 'package:nomo/app/widgets/submit_btn.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
+import '../../../../../config/theme/dark_theme_colors.dart';
+import '../../../../constants/assets.dart';
+import '../../../../constants/iconfont/iconfont.dart';
+import '../../../../widgets/info_card.dart';
+import '../../../../widgets/ix_app_bar.dart';
+import '../../../../widgets/ix_image.dart';
+import '../../../../widgets/ix_text_field.dart';
 import '../controllers/precode_sendemail_controller.dart';
 import '../controllers/precode_sendemail_controller.dart';
 
 
 class PrecodeSendemailView extends BaseView<PrecodeSendemailController> {
 class PrecodeSendemailView extends BaseView<PrecodeSendemailController> {
   const PrecodeSendemailView({super.key});
   const PrecodeSendemailView({super.key});
 
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Send Pre Code to Email');
+
   @override
   @override
   Widget buildContent(BuildContext context) {
   Widget buildContent(BuildContext context) {
     return Padding(
     return Padding(
       padding: EdgeInsets.symmetric(horizontal: 14.w),
       padding: EdgeInsets.symmetric(horizontal: 14.w),
-      child: Column(
-        children: [
-          // 自定义标题栏
-          _buildAppBar(),
-
-          40.verticalSpaceFromWidth,
-
-          // 邮箱输入框
-          _buildEmailInput(),
-
-          20.verticalSpaceFromWidth,
+      child: SingleChildScrollView(
+        child: Column(
+          children: [
+            10.verticalSpaceFromWidth,
 
 
-          // 发送按钮
-          _buildSendButton(),
+            // 邮箱输入框
+            _buildEmailInput(),
 
 
-          12.verticalSpaceFromWidth,
+            20.verticalSpaceFromWidth,
 
 
-          // 说明文字
-          Text(
-            'Your code will be backed up to this email.',
-            textAlign: TextAlign.center,
-            style: TextStyle(
-              fontSize: 14.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.6),
-            ),
-          ),
+            // 发送按钮
+            _buildSendButton(),
 
 
-          40.verticalSpaceFromWidth,
+            12.verticalSpaceFromWidth,
 
 
-          // 信息卡片列表
-          _buildInfoCardList(),
-        ],
-      ),
-    );
-  }
-
-  /// 自定义标题栏
-  Widget _buildAppBar() {
-    return Container(
-      height: 64.h,
-      alignment: Alignment.center,
-      child: Stack(
-        children: [
-          // 返回按钮
-          Positioned(
-            left: 0,
-            top: 0,
-            bottom: 0,
-            child: ClickOpacity(
-              onTap: () => Get.back(),
-              child: Container(
-                width: 48.w,
-                height: 48.w,
-                alignment: Alignment.center,
-                child: Icon(
-                  Icons.arrow_back,
-                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  size: 24.w,
-                ),
-              ),
-            ),
-          ),
-          // 标题
-          Center(
-            child: Text(
-              'Send Pre Code to Email',
+            // 说明文字
+            Text(
+              'Your code will be backed up to this email.',
+              textAlign: TextAlign.center,
               style: TextStyle(
               style: TextStyle(
-                fontSize: 20.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w600,
+                fontSize: 14.sp,
+                color: Get.reactiveTheme.hintColor.withOpacity(0.6),
               ),
               ),
             ),
             ),
-          ),
-        ],
+            // 信息卡片列表
+            _buildInfoCardList(),
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }
 
 
   /// 邮箱输入框
   /// 邮箱输入框
   Widget _buildEmailInput() {
   Widget _buildEmailInput() {
-    return Obx(
-      () => Container(
-        height: 56.h,
-        decoration: BoxDecoration(
-          color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
-          border: Border.all(
-            color: controller.isFocused.value
-                ? const Color(0xFF0A84FF)
-                : Colors.transparent,
-            width: 2,
-          ),
-        ),
-        child: TextField(
-          controller: controller.emailController,
-          focusNode: controller.emailFocusNode,
-          keyboardType: TextInputType.emailAddress,
-          style: TextStyle(
-            fontSize: 16.sp,
-            color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-          ),
-          decoration: InputDecoration(
-            hintText: 'Enter your email',
-            hintStyle: TextStyle(
-              fontSize: 16.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.5),
-            ),
-            border: InputBorder.none,
-            contentPadding: EdgeInsets.symmetric(horizontal: 20.w),
-          ),
-        ),
-      ),
+    return IXTextField(
+      hintText: 'Enter your email',
+      controller: controller.emailController,
+      focusNode: controller.emailFocusNode,
+      keyboardType: TextInputType.emailAddress,
+      textInputAction: TextInputAction.done,
+      onChanged: (value) => controller.email.value = value,
     );
     );
   }
   }
 
 
@@ -134,150 +73,48 @@ class PrecodeSendemailView extends BaseView<PrecodeSendemailController> {
     return Obx(() {
     return Obx(() {
       final canSend = controller.canSend;
       final canSend = controller.canSend;
       final isSending = controller.isSending.value;
       final isSending = controller.isSending.value;
-
-      return ClickOpacity(
-        onTap: canSend ? controller.sendEmail : null,
-        child: Container(
-          height: 56.h,
-          decoration: BoxDecoration(
-            color: canSend
-                ? const Color(0xFF0A84FF)
-                : Get.reactiveTheme.highlightColor,
-            borderRadius: BorderRadius.circular(16.r),
-          ),
-          child: Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: [
-              if (isSending)
-                SizedBox(
-                  width: 20.w,
-                  height: 20.w,
-                  child: const CircularProgressIndicator(
-                    strokeWidth: 2,
-                    valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
-                  ),
-                )
-              else
-                Icon(
-                  Icons.email_outlined,
-                  color: canSend
-                      ? Colors.white
-                      : Get.reactiveTheme.hintColor.withOpacity(0.5),
-                  size: 24.w,
-                ),
-              12.horizontalSpace,
-              Text(
-                isSending ? 'Sending...' : 'Send your Email',
-                style: TextStyle(
-                  fontSize: 16.sp,
-                  color: canSend
-                      ? Colors.white
-                      : Get.reactiveTheme.hintColor.withOpacity(0.5),
-                  fontWeight: FontWeight.w500,
-                ),
-              ),
-            ],
-          ),
+      return SubmitButton(
+        prefixIcon: IXImage(
+          source: Assets.preCodeEmailTipWhite,
+          width: 20.w,
+          height: 20.w,
+          sourceType: ImageSourceType.asset,
         ),
         ),
+        enabled: canSend,
+        isLoading: isSending,
+        text: 'Send your Email',
+        onPressed: controller.sendEmail,
       );
       );
     });
     });
   }
   }
 
 
   /// 信息卡片列表(带连接线)
   /// 信息卡片列表(带连接线)
   Widget _buildInfoCardList() {
   Widget _buildInfoCardList() {
-    final cards = [
-      {
-        'icon': Icons.workspace_premium_outlined,
-        'title': 'Your Pre Credential',
-        'description':
-            'This is your VIP credential. Please store it securely and do not share it with anyone.',
-      },
-      {
-        'icon': Icons.mark_email_read_outlined,
-        'title': 'Secure Email Backup',
-        'description':
-            'We will send an email containing this credential to your specified email address for safekeeping.',
-      },
-      {
-        'icon': Icons.check_circle_outline,
-        'title': 'Send and Save',
-        'description':
-            'After the email is sent, we recommend you also save this credential to a secure location on your device.',
-      },
-    ];
-
-    return Column(
-      children: List.generate(cards.length, (index) {
-        final card = cards[index];
-        final isLast = index == cards.length - 1;
-
-        return Row(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            // 左侧图标和连接线
-            Column(
-              children: [
-                // 图标
-                Container(
-                  width: 48.w,
-                  height: 48.w,
-                  decoration: BoxDecoration(
-                    color: const Color(0xFF0A84FF).withOpacity(0.15),
-                    borderRadius: BorderRadius.circular(12.r),
-                  ),
-                  child: Icon(
-                    card['icon'] as IconData,
-                    color: const Color(0xFF0A84FF),
-                    size: 28.w,
-                  ),
-                ),
-                // 连接线(最后一个不显示)
-                if (!isLast)
-                  Container(
-                    width: 2.w,
-                    height: 60.h,
-                    margin: EdgeInsets.symmetric(vertical: 8.h),
-                    decoration: BoxDecoration(
-                      color: Get.reactiveTheme.hintColor.withOpacity(0.2),
-                      borderRadius: BorderRadius.circular(1.r),
-                    ),
-                  ),
-              ],
-            ),
-
-            16.horizontalSpace,
-
-            // 右侧文字内容
-            Expanded(
-              child: Padding(
-                padding: EdgeInsets.only(top: 4.h, bottom: isLast ? 0 : 24.h),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Text(
-                      card['title'] as String,
-                      style: TextStyle(
-                        fontSize: 18.sp,
-                        color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                        fontWeight: FontWeight.w600,
-                      ),
-                    ),
-                    8.verticalSpaceFromWidth,
-                    Text(
-                      card['description'] as String,
-                      style: TextStyle(
-                        fontSize: 14.sp,
-                        color: Get.reactiveTheme.hintColor.withOpacity(0.6),
-                        height: 1.5,
-                      ),
-                    ),
-                  ],
-                ),
-              ),
-            ),
-          ],
-        );
-      }),
+    return InfoCard(
+      title: '',
+      items: [
+        InfoItem(
+          icon: IconFont.icon23,
+          title: 'Your Pre Credential',
+          description:
+              'This is your VIP credential. Please store it securely and do not share it with anyone.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+        InfoItem(
+          imageSource: Assets.preCodeEmailTipBlue,
+          title: 'Secure Email Backup',
+          description:
+              'We will send an email containing this credential to your specified email address for safekeeping.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+        InfoItem(
+          icon: IconFont.icon43,
+          title: 'Send and Save',
+          description:
+              'After the email is sent, we recommend you also save this credential to a secure location on your device.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+      ],
     );
     );
   }
   }
 }
 }

+ 68 - 140
lib/app/modules/precode/views/precode_view.dart

@@ -3,33 +3,39 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
+import '../../../../config/theme/dark_theme_colors.dart';
+import '../../../constants/assets.dart';
+import '../../../constants/iconfont/iconfont.dart';
+import '../../../widgets/ix_app_bar.dart';
+import '../../../widgets/submit_btn.dart';
 import '../controllers/precode_controller.dart';
 import '../controllers/precode_controller.dart';
 
 
 class PrecodeView extends BaseView<PrecodeController> {
 class PrecodeView extends BaseView<PrecodeController> {
   const PrecodeView({super.key});
   const PrecodeView({super.key});
 
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'My Pre Code');
+
   @override
   @override
   Widget buildContent(BuildContext context) {
   Widget buildContent(BuildContext context) {
     return Padding(
     return Padding(
       padding: EdgeInsets.symmetric(horizontal: 14.w),
       padding: EdgeInsets.symmetric(horizontal: 14.w),
       child: Column(
       child: Column(
         children: [
         children: [
-          // 自定义标题栏
-          _buildAppBar(),
-
-          40.verticalSpaceFromWidth,
+          10.verticalSpaceFromWidth,
 
 
           // 说明卡片
           // 说明卡片
           _buildInfoCard(),
           _buildInfoCard(),
 
 
-          40.verticalSpaceFromWidth,
+          30.verticalSpaceFromWidth,
 
 
           // Pre Code 显示
           // Pre Code 显示
           _buildPreCodeDisplay(),
           _buildPreCodeDisplay(),
 
 
-          20.verticalSpaceFromWidth,
+          10.verticalSpaceFromWidth,
 
 
           // Preview/Hide 和 Copy 按钮
           // Preview/Hide 和 Copy 按钮
           _buildActionButtons(),
           _buildActionButtons(),
@@ -39,19 +45,20 @@ class PrecodeView extends BaseView<PrecodeController> {
           // Send to Email 按钮
           // Send to Email 按钮
           _buildEmailButton(),
           _buildEmailButton(),
 
 
-          12.verticalSpaceFromWidth,
+          10.verticalSpaceFromWidth,
 
 
           // 说明文字
           // 说明文字
           Text(
           Text(
             'Send your Pre Code to your registered email address',
             'Send your Pre Code to your registered email address',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 14.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.6),
+              fontSize: 13.sp,
+              height: 1.4,
+              color: Get.reactiveTheme.hintColor,
             ),
             ),
           ),
           ),
 
 
-          20.verticalSpaceFromWidth,
+          10.verticalSpaceFromWidth,
 
 
           // Save Local Copy 按钮
           // Save Local Copy 按钮
           _buildSaveButton(),
           _buildSaveButton(),
@@ -63,50 +70,9 @@ class PrecodeView extends BaseView<PrecodeController> {
             'Store a copy of your Pre Code on this device',
             'Store a copy of your Pre Code on this device',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 14.sp,
-              color: Get.reactiveTheme.hintColor.withOpacity(0.6),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-
-  /// 自定义标题栏
-  Widget _buildAppBar() {
-    return Container(
-      height: 64.h,
-      alignment: Alignment.center,
-      child: Stack(
-        children: [
-          // 返回按钮
-          Positioned(
-            left: 0,
-            top: 0,
-            bottom: 0,
-            child: ClickOpacity(
-              onTap: () => Get.back(),
-              child: Container(
-                width: 48.w,
-                height: 48.w,
-                alignment: Alignment.center,
-                child: Icon(
-                  Icons.arrow_back,
-                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  size: 24.w,
-                ),
-              ),
-            ),
-          ),
-          // 标题
-          Center(
-            child: Text(
-              'My Pre Code',
-              style: TextStyle(
-                fontSize: 20.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w600,
-              ),
+              fontSize: 13.sp,
+              height: 1.4,
+              color: Get.reactiveTheme.hintColor,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -117,36 +83,28 @@ class PrecodeView extends BaseView<PrecodeController> {
   /// 说明卡片
   /// 说明卡片
   Widget _buildInfoCard() {
   Widget _buildInfoCard() {
     return Container(
     return Container(
-      padding: EdgeInsets.all(24.w),
+      width: double.infinity,
+      padding: EdgeInsets.all(10.w),
       decoration: BoxDecoration(
       decoration: BoxDecoration(
         color: Get.reactiveTheme.highlightColor,
         color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
+        borderRadius: BorderRadius.circular(8.r),
       ),
       ),
       child: Column(
       child: Column(
         children: [
         children: [
           // 铃铛图标
           // 铃铛图标
-          Container(
-            width: 48.w,
-            height: 48.w,
-            decoration: BoxDecoration(
-              color: const Color(0xFFFF9500).withOpacity(0.2),
-              shape: BoxShape.circle,
-            ),
-            child: Icon(
-              Icons.notifications,
-              color: const Color(0xFFFF9500),
-              size: 28.w,
-            ),
+          Icon(
+            IconFont.icon67,
+            color: DarkThemeColors.validTermColor,
+            size: 30.w,
           ),
           ),
-
-          20.verticalSpaceFromWidth,
+          10.verticalSpaceFromWidth,
 
 
           // 说明文字
           // 说明文字
           Text(
           Text(
             'Pre Code is your premium user credential.\nUse it to activate benefits or sync your account\non other devices.',
             'Pre Code is your premium user credential.\nUse it to activate benefits or sync your account\non other devices.',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 16.sp,
+              fontSize: 14.sp,
               color: Get.reactiveTheme.textTheme.bodyLarge!.color,
               color: Get.reactiveTheme.textTheme.bodyLarge!.color,
               height: 1.5,
               height: 1.5,
             ),
             ),
@@ -159,9 +117,9 @@ class PrecodeView extends BaseView<PrecodeController> {
             'Please store it securely !',
             'Please store it securely !',
             textAlign: TextAlign.center,
             textAlign: TextAlign.center,
             style: TextStyle(
             style: TextStyle(
-              fontSize: 16.sp,
+              fontSize: 14.sp,
               color: Get.reactiveTheme.textTheme.bodyLarge!.color,
               color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-              fontWeight: FontWeight.w600,
+              height: 1.5,
             ),
             ),
           ),
           ),
         ],
         ],
@@ -175,10 +133,10 @@ class PrecodeView extends BaseView<PrecodeController> {
       () => Text(
       () => Text(
         controller.displayCode,
         controller.displayCode,
         style: TextStyle(
         style: TextStyle(
-          fontSize: 32.sp,
+          fontSize: 34.sp,
+          height: 1.2,
           color: Get.reactiveTheme.textTheme.bodyLarge!.color,
           color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-          fontWeight: FontWeight.w700,
-          letterSpacing: 2,
+          fontWeight: FontWeight.w500,
         ),
         ),
       ),
       ),
     );
     );
@@ -199,15 +157,16 @@ class PrecodeView extends BaseView<PrecodeController> {
                 Text(
                 Text(
                   controller.isPreviewMode.value ? 'Hide' : 'Preview',
                   controller.isPreviewMode.value ? 'Hide' : 'Preview',
                   style: TextStyle(
                   style: TextStyle(
-                    fontSize: 16.sp,
+                    fontSize: 12.sp,
+                    height: 1.7,
                     color: Get.reactiveTheme.hintColor,
                     color: Get.reactiveTheme.hintColor,
                   ),
                   ),
                 ),
                 ),
                 8.horizontalSpace,
                 8.horizontalSpace,
                 Icon(
                 Icon(
                   controller.isPreviewMode.value
                   controller.isPreviewMode.value
-                      ? Icons.visibility_off_outlined
-                      : Icons.remove_red_eye_outlined,
+                      ? IconFont.icon12
+                      : IconFont.icon13,
                   color: Get.reactiveTheme.hintColor,
                   color: Get.reactiveTheme.hintColor,
                   size: 20.w,
                   size: 20.w,
                 ),
                 ),
@@ -216,15 +175,15 @@ class PrecodeView extends BaseView<PrecodeController> {
           ),
           ),
         ),
         ),
 
 
-        40.horizontalSpace,
+        20.horizontalSpace,
 
 
         Container(
         Container(
-          width: 1,
-          height: 20.h,
-          color: Get.reactiveTheme.hintColor.withOpacity(0.3),
+          width: 1.w,
+          height: 20.w,
+          color: Get.reactiveTheme.dividerColor,
         ),
         ),
 
 
-        40.horizontalSpace,
+        20.horizontalSpace,
 
 
         // Copy 按钮
         // Copy 按钮
         ClickOpacity(
         ClickOpacity(
@@ -235,12 +194,17 @@ class PrecodeView extends BaseView<PrecodeController> {
               Text(
               Text(
                 'Copy',
                 'Copy',
                 style: TextStyle(
                 style: TextStyle(
-                  fontSize: 16.sp,
+                  fontSize: 12.sp,
+                  height: 1.7,
                   color: Get.reactiveTheme.hintColor,
                   color: Get.reactiveTheme.hintColor,
                 ),
                 ),
               ),
               ),
               8.horizontalSpace,
               8.horizontalSpace,
-              Icon(Icons.copy, color: Get.reactiveTheme.hintColor, size: 20.w),
+              Icon(
+                IconFont.icon57,
+                color: Get.reactiveTheme.hintColor,
+                size: 20.w,
+              ),
             ],
             ],
           ),
           ),
         ),
         ),
@@ -250,67 +214,31 @@ class PrecodeView extends BaseView<PrecodeController> {
 
 
   /// Send to Email 按钮
   /// Send to Email 按钮
   Widget _buildEmailButton() {
   Widget _buildEmailButton() {
-    return ClickOpacity(
-      onTap: controller.sendToEmail,
-      child: Container(
-        height: 56.h,
-        decoration: BoxDecoration(
-          color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
-        ),
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            Icon(
-              Icons.email_outlined,
-              color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-              size: 24.w,
-            ),
-            12.horizontalSpace,
-            Text(
-              'Send to Email',
-              style: TextStyle(
-                fontSize: 16.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w500,
-              ),
-            ),
-          ],
-        ),
+    return SubmitButton(
+      prefixIcon: IXImage(
+        source: Assets.preCodeEmail,
+        width: 20.w,
+        height: 20.w,
+        sourceType: ImageSourceType.asset,
       ),
       ),
+      text: 'Send to Email',
+      bgColor: Get.reactiveTheme.highlightColor,
+      onPressed: controller.sendToEmail,
     );
     );
   }
   }
 
 
   /// Save Local Copy 按钮
   /// Save Local Copy 按钮
   Widget _buildSaveButton() {
   Widget _buildSaveButton() {
-    return ClickOpacity(
-      onTap: controller.saveLocalCopy,
-      child: Container(
-        height: 56.h,
-        decoration: BoxDecoration(
-          color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
-        ),
-        child: Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            Icon(
-              Icons.save_alt,
-              color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-              size: 24.w,
-            ),
-            12.horizontalSpace,
-            Text(
-              'Save Local Copy',
-              style: TextStyle(
-                fontSize: 16.sp,
-                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                fontWeight: FontWeight.w500,
-              ),
-            ),
-          ],
-        ),
+    return SubmitButton(
+      prefixIcon: IXImage(
+        source: Assets.preCodeSaveLocal,
+        width: 20.w,
+        height: 20.w,
+        sourceType: ImageSourceType.asset,
       ),
       ),
+      text: 'Save Local Copy',
+      bgColor: Get.reactiveTheme.highlightColor,
+      onPressed: controller.saveLocalCopy,
     );
     );
   }
   }
 }
 }

+ 271 - 0
lib/app/modules/precode/widgets/precode_save_dialog.dart

@@ -0,0 +1,271 @@
+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 'package:screenshot/screenshot.dart';
+import 'package:nomo/app/widgets/click_opacity.dart';
+
+import '../../../../config/theme/dark_theme_colors.dart';
+import '../../../constants/assets.dart';
+import '../../../widgets/ix_image.dart';
+import '../../../widgets/submit_btn.dart';
+import '../controllers/precode_controller.dart';
+
+/// Pre Code 保存弹窗
+class PrecodeSaveDialog extends StatelessWidget {
+  final PrecodeController controller;
+
+  const PrecodeSaveDialog({super.key, required this.controller});
+
+  @override
+  Widget build(BuildContext context) {
+    return _buildDialogContent();
+  }
+
+  /// 构建弹窗内容
+  Widget _buildDialogContent() {
+    return Padding(
+      padding: EdgeInsets.all(24.w),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Screenshot(
+            controller: controller.screenshotController,
+            child: Container(
+              width: double.infinity,
+              decoration: BoxDecoration(
+                color: Get.reactiveTheme.highlightColor,
+                borderRadius: BorderRadius.circular(20.r),
+              ),
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  // 内容区域
+                  Padding(
+                    padding: EdgeInsets.symmetric(
+                      horizontal: 10.w,
+                      vertical: 20.w,
+                    ),
+                    child: Column(
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        // Logo
+                        _buildLogo(),
+
+                        10.verticalSpaceFromWidth,
+
+                        Divider(
+                          color: Get.reactiveTheme.dividerColor,
+                          height: 1.w,
+                        ),
+
+                        10.verticalSpaceFromWidth,
+
+                        // Pre Code 标题和数字
+                        _buildPreCode(),
+
+                        10.verticalSpaceFromWidth,
+
+                        Divider(
+                          color: Get.reactiveTheme.dividerColor,
+                          height: 1.w,
+                        ),
+
+                        10.verticalSpaceFromWidth,
+
+                        // 说明文字
+                        _buildDescription(),
+
+                        20.verticalSpaceFromWidth,
+
+                        // 安全提示
+                        _buildSecurityTip(),
+
+                        20.verticalSpaceFromWidth,
+
+                        // UID 信息
+                        _buildUidInfo(),
+
+                        10.verticalSpaceFromWidth,
+
+                        // Premium 标签
+                        _buildPremiumBadge(),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          50.verticalSpaceFromWidth,
+          // Save 按钮
+          _buildSaveButton(),
+
+          20.verticalSpaceFromWidth,
+
+          // Cancel 按钮
+          _buildCancelButton(),
+        ],
+      ),
+    );
+  }
+
+  /// Logo - 盾牌形状
+  Widget _buildLogo() {
+    return IXImage(
+      source: Assets.splashLogo,
+      width: 70.w,
+      height: 94.w,
+      sourceType: ImageSourceType.asset,
+    );
+  }
+
+  /// Pre Code
+  Widget _buildPreCode() {
+    return Column(
+      children: [
+        Text(
+          controller.preCode.value,
+          style: TextStyle(
+            fontSize: 28.sp,
+            height: 1.2,
+            fontWeight: FontWeight.w500,
+            color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+            letterSpacing: 1.2,
+          ),
+        ),
+      ],
+    );
+  }
+
+  /// 说明文字
+  Widget _buildDescription() {
+    return Text(
+      'Pre Code is your premium user credential.\nUse it to activate benefits or sync your\naccount on other devices.',
+      textAlign: TextAlign.center,
+      style: TextStyle(
+        fontSize: 14.sp,
+        color: Get.reactiveTheme.hintColor,
+        height: 1.4,
+      ),
+    );
+  }
+
+  /// 安全提示
+  Widget _buildSecurityTip() {
+    return Text(
+      'Please store it securely !',
+      textAlign: TextAlign.center,
+      style: TextStyle(
+        fontSize: 14.sp,
+        color: Get.reactiveTheme.hintColor,
+        height: 1.4,
+      ),
+    );
+  }
+
+  /// UID 信息
+  Widget _buildUidInfo() {
+    final uid = controller.uid;
+    final displayUid = uid.length > 12
+        ? 'UID ${uid.substring(0, 6)}***${uid.substring(uid.length - 6)}'
+        : 'UID $uid';
+
+    return Text(
+      displayUid,
+      style: TextStyle(
+        fontSize: 14.sp,
+        color: DarkThemeColors.validTermColor,
+        height: 1.4,
+      ),
+    );
+  }
+
+  /// Premium 标签
+  Widget _buildPremiumBadge() {
+    return Obx(
+      () => IXImage(
+        source: controller.isPremium.value ? Assets.premium : Assets.free,
+        width: controller.isPremium.value ? 92.w : 64.w,
+        height: 28.w,
+        sourceType: ImageSourceType.asset,
+      ),
+    );
+  }
+
+  /// Save 按钮
+  Widget _buildSaveButton() {
+    return SubmitButton(
+      prefixIcon: IXImage(
+        source: Assets.preCodeSaveLocal,
+        width: 20.w,
+        height: 20.w,
+        sourceType: ImageSourceType.asset,
+      ),
+      text: 'Save',
+      onPressed: controller.saveToGallery,
+    );
+  }
+
+  /// Cancel 按钮
+  Widget _buildCancelButton() {
+    return ClickOpacity(
+      onTap: () {
+        Navigator.of(Get.context!).pop();
+      },
+      child: Container(
+        height: 52.w,
+        decoration: BoxDecoration(
+          borderRadius: BorderRadius.circular(12.r),
+          border: Border.all(color: Get.reactiveTheme.dividerColor, width: 1.w),
+        ),
+        alignment: Alignment.center,
+        child: Text(
+          'Cancel',
+          style: TextStyle(
+            fontSize: 14.sp,
+            fontWeight: FontWeight.w600,
+            color: Get.reactiveTheme.hintColor,
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// 盾牌形状裁剪器
+class ShieldClipper extends CustomClipper<Path> {
+  @override
+  Path getClip(Size size) {
+    final path = Path();
+    final width = size.width;
+    final height = size.height;
+
+    // 绘制盾牌形状
+    path.moveTo(width * 0.5, 0); // 顶部中心
+
+    // 右上圆弧
+    path.quadraticBezierTo(width * 0.75, 0, width, height * 0.2);
+
+    // 右侧直线
+    path.lineTo(width, height * 0.6);
+
+    // 底部曲线到中心点
+    path.quadraticBezierTo(width, height * 0.85, width * 0.5, height);
+
+    // 从底部中心到左侧
+    path.quadraticBezierTo(0, height * 0.85, 0, height * 0.6);
+
+    // 左侧直线
+    path.lineTo(0, height * 0.2);
+
+    // 左上圆弧
+    path.quadraticBezierTo(0, 0, width * 0.25, 0);
+
+    path.close();
+    return path;
+  }
+
+  @override
+  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
+}

+ 4 - 3
lib/app/modules/routingmode/views/routingmode_view.dart

@@ -6,6 +6,7 @@ import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
+import '../../../constants/iconfont/iconfont.dart';
 import '../controllers/routingmode_controller.dart';
 import '../controllers/routingmode_controller.dart';
 
 
 class RoutingmodeView extends BaseView<RoutingmodeController> {
 class RoutingmodeView extends BaseView<RoutingmodeController> {
@@ -29,7 +30,7 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
             children: [
             children: [
               _buildRoutingOption(
               _buildRoutingOption(
                 mode: RoutingMode.smart,
                 mode: RoutingMode.smart,
-                icon: Icons.flash_on,
+                icon: IconFont.icon50,
                 title: 'Smart',
                 title: 'Smart',
                 description:
                 description:
                     'The local and VPN networks coexist, and the optimal route is selected intelligently.',
                     'The local and VPN networks coexist, and the optimal route is selected intelligently.',
@@ -39,7 +40,7 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
               _buildDivider(),
               _buildDivider(),
               _buildRoutingOption(
               _buildRoutingOption(
                 mode: RoutingMode.global,
                 mode: RoutingMode.global,
-                icon: Icons.language,
+                icon: IconFont.icon51,
                 title: 'Global',
                 title: 'Global',
                 description:
                 description:
                     'All traffic is routed through the VPN server to ensure maximum privacy and security.',
                     'All traffic is routed through the VPN server to ensure maximum privacy and security.',
@@ -122,7 +123,7 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
                   color: isSelected
                   color: isSelected
                       ? Get.reactiveTheme.shadowColor
                       ? Get.reactiveTheme.shadowColor
                       : Colors.grey[400]!,
                       : Colors.grey[400]!,
-                  width: 2.w,
+                  width: 1.5.w,
                 ),
                 ),
               ),
               ),
               child: isSelected
               child: isSelected

+ 188 - 106
lib/app/modules/setting/views/setting_view.dart

@@ -9,8 +9,13 @@ import 'package:nomo/app/dialog/all_dialog.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+import 'package:nomo/utils/device_manager.dart';
 
 
+import '../../../../config/theme/dark_theme_colors.dart';
+import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/system_helper.dart';
 import '../../../../utils/system_helper.dart';
+import '../../../components/ix_snackbar.dart';
+import '../../../constants/iconfont/iconfont.dart';
 import '../../../routes/app_pages.dart';
 import '../../../routes/app_pages.dart';
 import '../../../widgets/ix_app_bar.dart';
 import '../../../widgets/ix_app_bar.dart';
 import '../controllers/setting_controller.dart';
 import '../controllers/setting_controller.dart';
@@ -51,11 +56,11 @@ class SettingView extends BaseView<SettingController> {
   Widget _buildSectionHeader(String title) {
   Widget _buildSectionHeader(String title) {
     return SliverToBoxAdapter(
     return SliverToBoxAdapter(
       child: Padding(
       child: Padding(
-        padding: EdgeInsets.fromLTRB(28.w, 20.w, 28.w, 12.w),
+        padding: EdgeInsets.fromLTRB(14.w, 10.w, 14.w, 10.w),
         child: Text(
         child: Text(
           title,
           title,
           style: TextStyle(
           style: TextStyle(
-            fontSize: 14.sp,
+            fontSize: 16.sp,
             color: Get.reactiveTheme.hintColor,
             color: Get.reactiveTheme.hintColor,
             fontWeight: FontWeight.w500,
             fontWeight: FontWeight.w500,
           ),
           ),
@@ -73,13 +78,13 @@ class SettingView extends BaseView<SettingController> {
           margin: EdgeInsets.symmetric(horizontal: 14.w),
           margin: EdgeInsets.symmetric(horizontal: 14.w),
           decoration: BoxDecoration(
           decoration: BoxDecoration(
             color: Get.reactiveTheme.highlightColor,
             color: Get.reactiveTheme.highlightColor,
-            borderRadius: BorderRadius.circular(16.r),
+            borderRadius: BorderRadius.circular(12.r),
           ),
           ),
           child: Column(
           child: Column(
             children: [
             children: [
               _buildSettingItem(
               _buildSettingItem(
-                icon: Icons.account_circle,
-                iconColor: const Color(0xFF00A8E8),
+                icon: IconFont.icon29,
+                iconColor: Get.reactiveTheme.shadowColor,
                 title: 'Account',
                 title: 'Account',
                 trailing: IXImage(
                 trailing: IXImage(
                   source: isPremium ? Assets.premium : Assets.free,
                   source: isPremium ? Assets.premium : Assets.free,
@@ -93,27 +98,38 @@ class SettingView extends BaseView<SettingController> {
               ),
               ),
               _buildDivider(),
               _buildDivider(),
               _buildSettingItem(
               _buildSettingItem(
-                icon: Icons.badge_outlined,
-                iconColor: const Color(0xFF00A8E8),
-                title: 'UID 123-456-789-101',
-                trailing: Icon(
-                  Icons.copy,
-                  size: 20.w,
-                  color: Get.reactiveTheme.hintColor,
+                icon: IconFont.icon14,
+                iconColor: Get.reactiveTheme.shadowColor,
+                title:
+                    'UID ${DeviceManager.getCacheDeviceId().length > 12 ? '${DeviceManager.getCacheDeviceId().substring(0, 6)}***${DeviceManager.getCacheDeviceId().substring(DeviceManager.getCacheDeviceId().length - 6)}' : DeviceManager.getCacheDeviceId()}',
+                showInfo: true,
+                trailing: ClickOpacity(
+                  onTap: () {
+                    Clipboard.setData(
+                      ClipboardData(text: DeviceManager.getCacheDeviceId()),
+                    );
+                    IXSnackBar.showIXSnackBar(
+                      title: Strings.copied.tr,
+                      message: Strings.copied.tr,
+                    );
+                  },
+                  child: Icon(
+                    IconFont.icon57,
+                    size: 20.w,
+                    color: Get.reactiveTheme.hintColor,
+                  ),
                 ),
                 ),
-                onTap: () {
-                  Clipboard.setData(
-                    const ClipboardData(text: '123-456-789-101'),
-                  );
-                  Get.snackbar('已复制', 'UID 已复制到剪贴板');
+                onTap: () {},
+                onInfoTap: () {
+                  AllDialog.showUidInfo();
                 },
                 },
               ),
               ),
               _buildDivider(),
               _buildDivider(),
               // 根据用户类型显示不同的时间信息
               // 根据用户类型显示不同的时间信息
               if (isPremium) ...[
               if (isPremium) ...[
                 _buildSettingItem(
                 _buildSettingItem(
-                  icon: Icons.workspace_premium,
-                  iconColor: const Color(0xFF00A8E8),
+                  icon: IconFont.icon23,
+                  iconColor: Get.reactiveTheme.shadowColor,
                   title: 'My Pre Code',
                   title: 'My Pre Code',
                   trailing: Row(
                   trailing: Row(
                     mainAxisSize: MainAxisSize.min,
                     mainAxisSize: MainAxisSize.min,
@@ -121,14 +137,14 @@ class SettingView extends BaseView<SettingController> {
                       Text(
                       Text(
                         '123***ADZ',
                         '123***ADZ',
                         style: TextStyle(
                         style: TextStyle(
-                          fontSize: 14.sp,
+                          fontSize: 13.sp,
                           color: Get.reactiveTheme.hintColor,
                           color: Get.reactiveTheme.hintColor,
                         ),
                         ),
                       ),
                       ),
                       SizedBox(width: 4.w),
                       SizedBox(width: 4.w),
                       Icon(
                       Icon(
-                        Icons.arrow_forward_ios,
-                        size: 16.w,
+                        IconFont.icon02,
+                        size: 20.w,
                         color: Get.reactiveTheme.hintColor,
                         color: Get.reactiveTheme.hintColor,
                       ),
                       ),
                     ],
                     ],
@@ -140,27 +156,16 @@ class SettingView extends BaseView<SettingController> {
                 ),
                 ),
                 _buildDivider(),
                 _buildDivider(),
                 _buildSettingItem(
                 _buildSettingItem(
-                  icon: Icons.event,
-                  iconColor: const Color(0xFF00A8E8),
+                  icon: IconFont.icon30,
+                  iconColor: Get.reactiveTheme.shadowColor,
                   title: 'Valid Term',
                   title: 'Valid Term',
-                  trailing: Row(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      Text(
-                        'Year / 2026-12-12',
-                        style: TextStyle(
-                          fontSize: 14.sp,
-                          color: const Color(0xFF00A8E8),
-                          fontWeight: FontWeight.w500,
-                        ),
-                      ),
-                      SizedBox(width: 4.w),
-                      Icon(
-                        Icons.arrow_forward_ios,
-                        size: 16.w,
-                        color: Get.reactiveTheme.hintColor,
-                      ),
-                    ],
+                  trailing: Text(
+                    'Year / 2026-12-12',
+                    style: TextStyle(
+                      fontSize: 13.sp,
+                      color: Get.reactiveTheme.primaryColor,
+                      fontWeight: FontWeight.w500,
+                    ),
                   ),
                   ),
                   onTap: () {
                   onTap: () {
                     // TODO: 跳转到有效期详情页面
                     // TODO: 跳转到有效期详情页面
@@ -168,8 +173,8 @@ class SettingView extends BaseView<SettingController> {
                 ),
                 ),
               ] else ...[
               ] else ...[
                 _buildSettingItem(
                 _buildSettingItem(
-                  icon: Icons.access_time,
-                  iconColor: const Color(0xFF00A8E8),
+                  icon: IconFont.icon30,
+                  iconColor: Get.reactiveTheme.shadowColor,
                   title: 'Free Time',
                   title: 'Free Time',
                   trailing: Text(
                   trailing: Text(
                     '01:60:59 / Days',
                     '01:60:59 / Days',
@@ -183,8 +188,8 @@ class SettingView extends BaseView<SettingController> {
               ],
               ],
               _buildDivider(),
               _buildDivider(),
               _buildSettingItem(
               _buildSettingItem(
-                icon: Icons.phone_iphone,
-                iconColor: const Color(0xFF00A8E8),
+                icon: IconFont.icon31,
+                iconColor: Get.reactiveTheme.shadowColor,
                 title: 'Device Authorization',
                 title: 'Device Authorization',
                 trailing: Row(
                 trailing: Row(
                   mainAxisSize: MainAxisSize.min,
                   mainAxisSize: MainAxisSize.min,
@@ -192,14 +197,14 @@ class SettingView extends BaseView<SettingController> {
                     Text(
                     Text(
                       isPremium ? '1/4' : '0/1',
                       isPremium ? '1/4' : '0/1',
                       style: TextStyle(
                       style: TextStyle(
-                        fontSize: 14.sp,
+                        fontSize: 13.sp,
                         color: Get.reactiveTheme.hintColor,
                         color: Get.reactiveTheme.hintColor,
                       ),
                       ),
                     ),
                     ),
                     SizedBox(width: 4.w),
                     SizedBox(width: 4.w),
                     Icon(
                     Icon(
-                      Icons.arrow_forward_ios,
-                      size: 16.w,
+                      IconFont.icon02,
+                      size: 20.w,
                       color: Get.reactiveTheme.hintColor,
                       color: Get.reactiveTheme.hintColor,
                     ),
                     ),
                   ],
                   ],
@@ -222,17 +227,17 @@ class SettingView extends BaseView<SettingController> {
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: Get.reactiveTheme.highlightColor,
           color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
+          borderRadius: BorderRadius.circular(12.r),
         ),
         ),
         child: Column(
         child: Column(
           children: [
           children: [
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.router,
-              iconColor: const Color(0xFF00A8E8),
+              icon: IconFont.icon34,
+              iconColor: Get.reactiveTheme.primaryColor,
               title: 'Routing Mode',
               title: 'Routing Mode',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -242,12 +247,12 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.share,
-              iconColor: const Color(0xFF00A8E8),
+              icon: IconFont.icon32,
+              iconColor: Get.reactiveTheme.primaryColor,
               title: 'Split Tunneling',
               title: 'Split Tunneling',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -257,8 +262,8 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.flash_on,
-              iconColor: const Color(0xFF00A8E8),
+              icon: IconFont.icon33,
+              iconColor: Get.reactiveTheme.primaryColor,
               title: 'Auto Reconnect',
               title: 'Auto Reconnect',
               trailing: Obx(
               trailing: Obx(
                 () => CupertinoSwitch(
                 () => CupertinoSwitch(
@@ -275,12 +280,12 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.restart_alt,
-              iconColor: const Color(0xFF00A8E8),
+              icon: IconFont.icon35,
+              iconColor: Get.reactiveTheme.primaryColor,
               title: 'Restore Default',
               title: 'Restore Default',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -300,13 +305,21 @@ class SettingView extends BaseView<SettingController> {
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: Get.reactiveTheme.highlightColor,
           color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
+          borderRadius: BorderRadius.circular(12.r),
         ),
         ),
         child: Column(
         child: Column(
           children: [
           children: [
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.language,
-              iconColor: const Color(0xFFFF9500),
+              icon: IconFont.icon36,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Language',
               title: 'Language',
               trailing: Row(
               trailing: Row(
                 mainAxisSize: MainAxisSize.min,
                 mainAxisSize: MainAxisSize.min,
@@ -314,14 +327,14 @@ class SettingView extends BaseView<SettingController> {
                   Text(
                   Text(
                     'English',
                     'English',
                     style: TextStyle(
                     style: TextStyle(
-                      fontSize: 14.sp,
+                      fontSize: 13.sp,
                       color: Get.reactiveTheme.hintColor,
                       color: Get.reactiveTheme.hintColor,
                     ),
                     ),
                   ),
                   ),
                   8.horizontalSpace,
                   8.horizontalSpace,
                   Icon(
                   Icon(
-                    Icons.arrow_forward_ios,
-                    size: 16.w,
+                    IconFont.icon02,
+                    size: 20.w,
                     color: Get.reactiveTheme.hintColor,
                     color: Get.reactiveTheme.hintColor,
                   ),
                   ),
                 ],
                 ],
@@ -333,12 +346,20 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.email,
-              iconColor: const Color(0xFFFF9500),
+              icon: IconFont.icon37,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Feedback',
               title: 'Feedback',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -348,12 +369,20 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.policy,
-              iconColor: const Color(0xFFFF9500),
+              icon: IconFont.icon38,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Privacy Policy',
               title: 'Privacy Policy',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -363,12 +392,20 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.description,
-              iconColor: const Color(0xFFFF9500),
+              icon: IconFont.icon38,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Terms of Service',
               title: 'Terms of Service',
               trailing: Icon(
               trailing: Icon(
-                Icons.arrow_forward_ios,
-                size: 16.w,
+                IconFont.icon02,
+                size: 20.w,
                 color: Get.reactiveTheme.hintColor,
                 color: Get.reactiveTheme.hintColor,
               ),
               ),
               onTap: () {
               onTap: () {
@@ -378,13 +415,21 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.apps,
-              iconColor: const Color(0xFFFF9500),
+              icon: IconFont.icon39,
+              iconColor: DarkThemeColors.settingAppLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingAppLinearGradientStartColor,
+                  DarkThemeColors.settingAppLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Version',
               title: 'Version',
               trailing: Text(
               trailing: Text(
                 'V1.0.0',
                 'V1.0.0',
                 style: TextStyle(
                 style: TextStyle(
-                  fontSize: 14.sp,
+                  fontSize: 13.sp,
                   color: Get.reactiveTheme.hintColor,
                   color: Get.reactiveTheme.hintColor,
                 ),
                 ),
               ),
               ),
@@ -402,13 +447,22 @@ class SettingView extends BaseView<SettingController> {
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         margin: EdgeInsets.symmetric(horizontal: 14.w),
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: Get.reactiveTheme.highlightColor,
           color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
+          borderRadius: BorderRadius.circular(12.r),
         ),
         ),
         child: Column(
         child: Column(
           children: [
           children: [
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.person_remove,
-              iconColor: const Color(0xFFFF3B30),
+              icon: IconFont.icon40,
+              iconColor:
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+                  DarkThemeColors.settingSecurityLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
               title: 'Delete Account',
               title: 'Delete Account',
               onTap: () {
               onTap: () {
                 // TODO: 删除账户
                 // TODO: 删除账户
@@ -416,10 +470,19 @@ class SettingView extends BaseView<SettingController> {
             ),
             ),
             _buildDivider(),
             _buildDivider(),
             _buildSettingItem(
             _buildSettingItem(
-              icon: Icons.logout,
-              iconColor: const Color(0xFFFF3B30),
-              title: 'logout',
-              titleColor: const Color(0xFFFF3B30),
+              icon: IconFont.icon66,
+              iconColor:
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+              iconGradient: LinearGradient(
+                colors: [
+                  DarkThemeColors.settingSecurityLinearGradientStartColor,
+                  DarkThemeColors.settingSecurityLinearGradientEndColor,
+                ],
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+              ),
+              title: 'Logout',
+              titleColor: const Color(0xFFEF0000),
               onTap: () {
               onTap: () {
                 // TODO: 退出登录
                 // TODO: 退出登录
                 AllDialog.showLogoutConfirm();
                 AllDialog.showLogoutConfirm();
@@ -435,42 +498,61 @@ class SettingView extends BaseView<SettingController> {
   Widget _buildSettingItem({
   Widget _buildSettingItem({
     required IconData icon,
     required IconData icon,
     required Color iconColor,
     required Color iconColor,
+    Gradient? iconGradient,
     required String title,
     required String title,
     Color? titleColor,
     Color? titleColor,
+    bool showInfo = false,
     Widget? trailing,
     Widget? trailing,
     VoidCallback? onTap,
     VoidCallback? onTap,
+    VoidCallback? onInfoTap,
   }) {
   }) {
     return ClickOpacity(
     return ClickOpacity(
       onTap: onTap,
       onTap: onTap,
       child: Container(
       child: Container(
         height: 56.w,
         height: 56.w,
-        padding: EdgeInsets.symmetric(horizontal: 16.w),
+        padding: EdgeInsets.symmetric(horizontal: 14.w),
         child: Row(
         child: Row(
           children: [
           children: [
             // 图标
             // 图标
             Container(
             Container(
-              width: 32.w,
-              height: 32.w,
+              width: 30.w,
+              height: 30.w,
               decoration: BoxDecoration(
               decoration: BoxDecoration(
-                color: iconColor,
+                gradient: iconGradient,
+                color: iconGradient == null ? iconColor : null,
                 borderRadius: BorderRadius.circular(8.r),
                 borderRadius: BorderRadius.circular(8.r),
               ),
               ),
               child: Icon(icon, size: 20.w, color: Colors.white),
               child: Icon(icon, size: 20.w, color: Colors.white),
             ),
             ),
-            SizedBox(width: 12.w),
+            10.horizontalSpace,
             // 标题
             // 标题
             Expanded(
             Expanded(
-              child: Text(
-                title,
-                style: TextStyle(
-                  fontSize: 16.sp,
-                  color:
-                      titleColor ??
-                      Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  fontWeight: FontWeight.w400,
-                ),
+              child: Row(
+                children: [
+                  Text(
+                    title,
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      color:
+                          titleColor ??
+                          Get.reactiveTheme.textTheme.bodyLarge!.color,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  ),
+                  4.horizontalSpace,
+                  if (showInfo)
+                    ClickOpacity(
+                      onTap: onInfoTap,
+                      child: Icon(
+                        IconFont.icon59,
+                        size: 20.w,
+                        color: Colors.white,
+                      ),
+                    ),
+                ],
               ),
               ),
             ),
             ),
+
             // 右侧内容
             // 右侧内容
             if (trailing != null) trailing,
             if (trailing != null) trailing,
           ],
           ],

+ 5 - 0
lib/app/modules/splash/controllers/splash_controller.dart

@@ -3,6 +3,7 @@ import 'package:get/get.dart';
 import 'package:nomo/app/base/base_controller.dart';
 import 'package:nomo/app/base/base_controller.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 
 
+import '../../../../utils/geo_downloader.dart';
 import '../../../../utils/log/logger.dart';
 import '../../../../utils/log/logger.dart';
 import '../../../components/country_restricted_overlay.dart';
 import '../../../components/country_restricted_overlay.dart';
 import '../../../constants/enums.dart';
 import '../../../constants/enums.dart';
@@ -71,6 +72,10 @@ class SplashController extends BaseController {
         try {
         try {
           final launch = await _apiController.launch();
           final launch = await _apiController.launch();
           handleLaunch(launch);
           handleLaunch(launch);
+          // 下载smartgeo文件
+          GeoDownloader().downloadSmartGeo(
+            smartGeo: launch.appConfig!.smartGeo!,
+          );
         } catch (e, st) {
         } catch (e, st) {
           handleLaunchError(e, st);
           handleLaunchError(e, st);
         } finally {
         } finally {

+ 6 - 18
lib/app/modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart

@@ -8,6 +8,7 @@ import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 
 import '../../../../../utils/event_bus.dart';
 import '../../../../../utils/event_bus.dart';
+import '../../../../constants/iconfont/iconfont.dart';
 import '../../../../widgets/state/state_wrapper.dart';
 import '../../../../widgets/state/state_wrapper.dart';
 import '../controllers/splittunneling_selectapp_controller.dart';
 import '../controllers/splittunneling_selectapp_controller.dart';
 
 
@@ -73,23 +74,10 @@ class SplittunnelingSelectappView
         ),
         ),
         child: Row(
         child: Row(
           children: [
           children: [
-            Container(
-              width: 20.w,
-              height: 20.w,
-              decoration: const BoxDecoration(
-                color: Colors.white,
-                shape: BoxShape.circle,
-              ),
-              child: Center(
-                child: Text(
-                  'i',
-                  style: TextStyle(
-                    fontSize: 14.sp,
-                    fontWeight: FontWeight.bold,
-                    color: Colors.black,
-                  ),
-                ),
-              ),
+            Icon(
+              IconFont.icon22,
+              size: 20.w,
+              color: Get.reactiveTheme.textTheme.bodyLarge!.color,
             ),
             ),
             SizedBox(width: 12.w),
             SizedBox(width: 12.w),
             Expanded(
             Expanded(
@@ -293,7 +281,7 @@ class SplittunnelingSelectappView
                         color: isSelected
                         color: isSelected
                             ? Get.reactiveTheme.shadowColor
                             ? Get.reactiveTheme.shadowColor
                             : Colors.grey[400]!,
                             : Colors.grey[400]!,
-                        width: 2.w,
+                        width: 1.5.w,
                       ),
                       ),
                     ),
                     ),
                     child: AnimatedScale(
                     child: AnimatedScale(

+ 26 - 44
lib/app/modules/splittunneling/views/splittunneling_view.dart

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/base/base_view.dart';
+import 'package:nomo/app/constants/iconfont/iconfont.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/app/widgets/ix_image.dart';
@@ -54,7 +55,7 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
       ),
       ),
       child: Row(
       child: Row(
         children: [
         children: [
-          Icon(Icons.notifications, size: 20.w, color: const Color(0xFFFFB800)),
+          Icon(IconFont.icon67, size: 20.w, color: const Color(0xFFFFB800)),
           SizedBox(width: 12.w),
           SizedBox(width: 12.w),
           Expanded(
           Expanded(
             child: Text(
             child: Text(
@@ -77,7 +78,7 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
       children: [
       children: [
         _buildModeCard(
         _buildModeCard(
           mode: SplitTunnelingMode.exclude,
           mode: SplitTunnelingMode.exclude,
-          icon: Icons.block,
+          icon: IconFont.icon42,
           title: 'Exclude selected apps from VPN',
           title: 'Exclude selected apps from VPN',
           description:
           description:
               'Choose apps that will connect directly without using the VPN.',
               'Choose apps that will connect directly without using the VPN.',
@@ -92,7 +93,7 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
 
 
         _buildModeCard(
         _buildModeCard(
           mode: SplitTunnelingMode.include,
           mode: SplitTunnelingMode.include,
-          icon: Icons.check,
+          icon: IconFont.icon43,
           title: 'Use VPN for selected apps only',
           title: 'Use VPN for selected apps only',
           description:
           description:
               'Choose apps that will use the VPN while others connect normally.',
               'Choose apps that will use the VPN while others connect normally.',
@@ -331,53 +332,34 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
         color: Get.reactiveTheme.canvasColor,
         color: Get.reactiveTheme.canvasColor,
         borderRadius: BorderRadius.circular(12.r),
         borderRadius: BorderRadius.circular(12.r),
       ),
       ),
-      child: Row(
-        crossAxisAlignment: CrossAxisAlignment.start,
+      child: Column(
         children: [
         children: [
-          Container(
-            width: 20.w,
-            height: 20.w,
-            decoration: BoxDecoration(
-              color: Colors.white,
-              shape: BoxShape.circle,
-            ),
-            child: Center(
-              child: Text(
-                'i',
+          Row(
+            children: [
+              Icon(
+                IconFont.icon22,
+                size: 20.w,
+                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+              ),
+              4.horizontalSpace,
+              Text(
+                'Customize your VPN',
                 style: TextStyle(
                 style: TextStyle(
                   fontSize: 14.sp,
                   fontSize: 14.sp,
-                  fontWeight: FontWeight.bold,
-                  color: Colors.black,
+                  height: 1.6,
+                  fontWeight: FontWeight.w500,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 ),
                 ),
               ),
               ),
-            ),
+            ],
           ),
           ),
-
-          8.horizontalSpace,
-
-          Expanded(
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Text(
-                  'Customize your VPN',
-                  style: TextStyle(
-                    fontSize: 14.sp,
-                    height: 1.6,
-                    fontWeight: FontWeight.w500,
-                    color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  ),
-                ),
-                10.verticalSpaceFromWidth,
-                Text(
-                  'Split tunneling lets you control which apps use the VPN connection and which connect directly. It helps you manage bandwidth and access local or foreign content without turning off the VPN.',
-                  style: TextStyle(
-                    fontSize: 12.sp,
-                    color: Get.reactiveTheme.hintColor,
-                    height: 1.6,
-                  ),
-                ),
-              ],
+          10.verticalSpaceFromWidth,
+          Text(
+            'Split tunneling lets you control which apps use the VPN connection and which connect directly. It helps you manage bandwidth and access local or foreign content without turning off the VPN.',
+            style: TextStyle(
+              fontSize: 12.sp,
+              color: Get.reactiveTheme.hintColor,
+              height: 1.6,
             ),
             ),
           ),
           ),
         ],
         ],

+ 10 - 0
lib/app/modules/subscription/bindings/subscription_binding.dart

@@ -0,0 +1,10 @@
+import 'package:get/get.dart';
+
+import '../controllers/subscription_controller.dart';
+
+class SubscriptionBinding extends Binding {
+  @override
+  List<Bind> dependencies() {
+    return [Bind.lazyPut(() => SubscriptionController())];
+  }
+}

+ 96 - 0
lib/app/modules/subscription/controllers/subscription_controller.dart

@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:video_player/video_player.dart';
+
+import '../../../../config/theme/dark_theme_colors.dart';
+import '../../../components/ix_snackbar.dart';
+import '../../../constants/assets.dart';
+
+class SubscriptionController extends GetxController {
+  // 视频播放器控制器
+  late VideoPlayerController videoController;
+  final isVideoInitialized = false.obs;
+
+  // 当前选中的订阅计划索引 (0: 年度, 1: 终身, 2: 月度, 3: 周度)
+  final selectedPlanIndex = 0.obs;
+
+  // 订阅计划列表
+  final List<Map<String, dynamic>> plans = [
+    {
+      'price': '\$40.00',
+      'period': 'Per year',
+      'title': 'Yearly Plan',
+      'badge': 'Mostly choose',
+      'badgeBgColor': DarkThemeColors.bg1,
+      'badgeTextColor': DarkThemeColors.subscriptionColor,
+      'badgeBorderColor': DarkThemeColors.dividerColor, // null 表示没有边框
+    },
+    {'price': '\$58.00', 'period': 'once', 'title': 'Life time', 'badge': null},
+    {'price': '\$58.00', 'period': 'Per', 'title': 'Month Plan', 'badge': null},
+    {
+      'price': '\$1.00',
+      'period': 'Per week',
+      'title': 'Week Plan',
+      'badge': 'Limited Time',
+      'badgeBgColor': DarkThemeColors.primaryColor,
+      'badgeTextColor': Colors.white,
+      'badgeBorderColor': null,
+    },
+  ];
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initializeVideoPlayer();
+  }
+
+  @override
+  void onClose() {
+    videoController.dispose();
+    super.onClose();
+  }
+
+  // 初始化视频播放器
+  void _initializeVideoPlayer() {
+    videoController = VideoPlayerController.asset(Assets.subscriptionBg)
+      ..initialize()
+          .then((_) {
+            isVideoInitialized.value = true;
+            videoController.setLooping(true);
+            videoController.setVolume(0); // 静音播放
+            videoController.play();
+          })
+          .catchError((error) {
+            print('视频初始化失败: $error');
+          });
+  }
+
+  // 选择订阅计划
+  void selectPlan(int index) {
+    selectedPlanIndex.value = index;
+  }
+
+  // 确认变更
+  void confirmChange() {
+    // TODO: 实现确认订阅变更逻辑
+    IXSnackBar.showIXSnackBar(
+      title: 'Success',
+      message: 'Subscription plan changed successfully',
+    );
+  }
+
+  // 恢复购买
+  void restorePurchases() {
+    // TODO: 实现恢复购买逻辑
+    IXSnackBar.showIXSnackBar(title: 'Info', message: 'Restoring purchases...');
+  }
+
+  // 支付问题
+  void handlePaymentIssue() {
+    // TODO: 实现支付问题处理逻辑
+    IXSnackBar.showIXSnackBar(
+      title: 'Info',
+      message: 'Opening payment support...',
+    );
+  }
+}

+ 497 - 0
lib/app/modules/subscription/views/subscription_view.dart

@@ -0,0 +1,497 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:nomo/app/constants/iconfont/iconfont.dart';
+import 'package:nomo/config/theme/dark_theme_colors.dart';
+import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+import 'package:video_player/video_player.dart';
+import '../../../constants/assets.dart';
+import '../../../widgets/info_card.dart';
+import '../../../widgets/ix_image.dart';
+import '../controllers/subscription_controller.dart';
+
+class SubscriptionView extends GetView<SubscriptionController> {
+  const SubscriptionView({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: DarkThemeColors.scaffoldBackgroundColor,
+      body: Stack(
+        children: [
+          // 视频背景层(只显示顶部214高度)
+          Obx(() {
+            if (controller.isVideoInitialized.value) {
+              return Positioned(
+                top: 0,
+                left: 0,
+                right: 0,
+                height: 214.w,
+                child: ClipRect(
+                  child: FittedBox(
+                    fit: BoxFit.cover,
+                    child: SizedBox(
+                      width: controller.videoController.value.size.width,
+                      height: controller.videoController.value.size.height,
+                      child: VideoPlayer(controller.videoController),
+                    ),
+                  ),
+                ),
+              );
+            }
+            return const SizedBox.shrink();
+          }),
+          // 渐变遮罩层(只在视频区域)
+          Positioned(
+            top: 0,
+            left: 0,
+            right: 0,
+            height: 214.w,
+            child: Container(
+              decoration: BoxDecoration(
+                gradient: LinearGradient(
+                  begin: Alignment.topCenter,
+                  end: Alignment.bottomCenter,
+                  colors: [Colors.black.withValues(alpha: 0.6), Colors.black],
+                  stops: const [0.0, 1.0],
+                ),
+              ),
+            ),
+          ),
+          // 内容层
+          SafeArea(
+            child: Column(
+              children: [
+                _buildAppBar(),
+                Expanded(
+                  child: SingleChildScrollView(
+                    padding: EdgeInsets.symmetric(horizontal: 20.w),
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        16.verticalSpaceFromWidth,
+                        _buildCurrentSubscription(),
+                        24.verticalSpaceFromWidth,
+                        _buildPlanOptions(),
+                        _buildPlanChangeInfo(),
+                        16.verticalSpaceFromWidth,
+                        _buildPremiumFeatures(),
+                        16.verticalSpaceFromWidth,
+                      ],
+                    ),
+                  ),
+                ),
+                _buildBottomSection(),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 顶部标题栏
+  Widget _buildAppBar() {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          SizedBox(width: 32.w),
+          Text(
+            'Subscription',
+            style: TextStyle(
+              fontSize: 16.sp,
+              height: 1.4,
+              fontWeight: FontWeight.w500,
+              color: Colors.white,
+            ),
+          ),
+          GestureDetector(
+            onTap: () => Get.back(),
+            child: Container(
+              width: 24.w,
+              height: 24.w,
+              decoration: BoxDecoration(
+                color: Colors.white.withValues(alpha: 0.1),
+                shape: BoxShape.circle,
+              ),
+              child: Icon(Icons.close_rounded, color: Colors.white, size: 16.w),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 当前订阅信息
+  Widget _buildCurrentSubscription() {
+    return Row(
+      children: [
+        // 钻石图标
+        IXImage(
+          source: Assets.subscriptionDiamond,
+          width: 92.w,
+          height: 80.w,
+          sourceType: ImageSourceType.asset,
+        ),
+        12.horizontalSpace,
+        Expanded(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                children: [
+                  IXImage(
+                    source: Assets.subscriptionWallet,
+                    width: 20.w,
+                    height: 20.w,
+                    sourceType: ImageSourceType.asset,
+                  ),
+                  4.horizontalSpace,
+                  Text(
+                    'Current subscription',
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      height: 1.4,
+                      color: DarkThemeColors.subscriptionColor,
+                      fontWeight: FontWeight.w700,
+                    ),
+                  ),
+                ],
+              ),
+              10.verticalSpaceFromWidth,
+              Text(
+                'Year Plan \$40.00 per year',
+                style: TextStyle(
+                  fontSize: 14.sp,
+                  height: 1.4,
+                  color: Colors.white,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  // 订阅计划选项
+  Widget _buildPlanOptions() {
+    return Obx(
+      () => Column(
+        children: List.generate(
+          controller.plans.length,
+          (index) => _buildPlanItem(
+            controller.plans[index],
+            index,
+            controller.selectedPlanIndex.value == index,
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildPlanItem(Map<String, dynamic> plan, int index, bool isSelected) {
+    final badge = plan['badge'] as String?;
+    final badgeBgColor = plan['badgeBgColor'] as Color?;
+    final badgeTextColor = plan['badgeTextColor'] as Color?;
+    final badgeBorderColor = plan['badgeBorderColor'] as Color?;
+
+    return GestureDetector(
+      onTap: () => controller.selectPlan(index),
+      child: Container(
+        margin: EdgeInsets.only(bottom: 18.w),
+        decoration: BoxDecoration(
+          color: DarkThemeColors.cardColor,
+          borderRadius: BorderRadius.circular(12.r),
+          border: Border.all(
+            color: isSelected
+                ? DarkThemeColors.subscriptionColor
+                : DarkThemeColors.dividerColor,
+            width: 2.w,
+          ),
+        ),
+        child: Stack(
+          clipBehavior: Clip.none,
+          children: [
+            // 主要内容
+            Padding(
+              padding: EdgeInsets.all(10.w),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        plan['price'] as String,
+                        style: TextStyle(
+                          fontSize: 18.sp,
+                          height: 1.4,
+                          color: DarkThemeColors.bodyTextColor,
+                          fontWeight: FontWeight.w600,
+                        ),
+                      ),
+                      Text(
+                        plan['period'] as String,
+                        style: TextStyle(
+                          fontSize: 12.sp,
+                          height: 1.6,
+                          color: DarkThemeColors.hintTextColor,
+                        ),
+                      ),
+                    ],
+                  ),
+                  Row(
+                    children: [
+                      Text(
+                        plan['title'] as String,
+                        style: TextStyle(
+                          fontSize: 13.sp,
+                          height: 1.4,
+                          color: DarkThemeColors.bodyTextColor,
+                        ),
+                      ),
+                      8.horizontalSpace,
+                      Container(
+                        width: 20.w,
+                        height: 20.w,
+                        decoration: BoxDecoration(
+                          shape: BoxShape.circle,
+                          border: Border.all(
+                            color: isSelected
+                                ? DarkThemeColors.primaryColor
+                                : Colors.white30,
+                            width: 1.5.w,
+                          ),
+                          color: isSelected
+                              ? DarkThemeColors.primaryColor
+                              : Colors.transparent,
+                        ),
+                        child: isSelected
+                            ? Icon(Icons.check, color: Colors.white, size: 12.w)
+                            : null,
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+            ),
+            // 标签固定在右上角,压在边框线上
+            if (badge != null)
+              Positioned(
+                top: -11.h, // 负值让标签向上移动,压在边框线上
+                right: 12.w,
+                child: Container(
+                  padding: EdgeInsets.symmetric(horizontal: 6.w),
+                  decoration: BoxDecoration(
+                    color: badgeBgColor ?? Colors.black,
+                    borderRadius: BorderRadius.circular(4.r),
+                    border: badgeBorderColor != null
+                        ? Border.all(color: badgeBorderColor, width: 1)
+                        : null,
+                  ),
+                  child: Text(
+                    badge,
+                    style: TextStyle(
+                      fontSize: 12.sp,
+                      color: badgeTextColor ?? Colors.white,
+                      height: 1.6,
+                    ),
+                  ),
+                ),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  // 计划变更信息
+  Widget _buildPlanChangeInfo() {
+    return InfoCard(
+      title: 'Plan change info',
+      items: [
+        InfoItem(
+          imageSource: Assets.subscriptionPlanChange1,
+          title: 'When it starts',
+          description: 'Your new plan begins right away.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+        InfoItem(
+          imageSource: Assets.subscriptionPlanChange2,
+          title: 'What happens to your balance',
+          description:
+              'Any unused amount from your old plan will be added to the new one.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+        InfoItem(
+          imageSource: Assets.subscriptionPlanChange3,
+          title: 'Extra time',
+          description:
+              'You\'ll get extra days based on your remaining balance.',
+          iconColor: DarkThemeColors.primaryColor,
+        ),
+      ],
+    );
+  }
+
+  // Premium 功能列表
+  Widget _buildPremiumFeatures() {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          'Premium\'s included',
+          style: TextStyle(
+            fontSize: 16.sp,
+            color: DarkThemeColors.subscriptionColor,
+            fontWeight: FontWeight.w500,
+          ),
+        ),
+        16.verticalSpace,
+        Container(
+          padding: EdgeInsets.symmetric(vertical: 4.w, horizontal: 10.w),
+          decoration: BoxDecoration(
+            color: DarkThemeColors.bgDisable,
+            borderRadius: BorderRadius.circular(12.r),
+          ),
+          child: Column(
+            children: [
+              _buildFeatureItem(IconFont.icon60, 'Unlock all free locations'),
+              _buildFeatureItem(IconFont.icon61, 'Unlock smart mode'),
+              _buildFeatureItem(IconFont.icon62, 'Unlock Multi-hop mode'),
+              _buildFeatureItem(IconFont.icon63, 'Premium can share X devices'),
+              _buildFeatureItem(IconFont.icon64, 'Own your own private server'),
+              _buildFeatureItem(IconFont.icon65, 'Close ads'),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildFeatureItem(IconData icon, String title) {
+    return SizedBox(
+      height: 44.w,
+      child: Row(
+        children: [
+          Icon(icon, color: DarkThemeColors.subscriptionColor, size: 24.w),
+          12.horizontalSpace,
+          Expanded(
+            child: Text(
+              title,
+              style: TextStyle(
+                fontSize: 13.sp,
+                color: Get.reactiveTheme.hintColor,
+              ),
+            ),
+          ),
+          Container(
+            width: 20.w,
+            height: 20.w,
+            decoration: BoxDecoration(
+              shape: BoxShape.circle,
+              color: DarkThemeColors.subscriptionSelectColor,
+            ),
+            child: Icon(Icons.check, color: Colors.white, size: 12.w),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 底部按钮区域
+  Widget _buildBottomSection() {
+    return Container(
+      padding: EdgeInsets.symmetric(vertical: 10.w, horizontal: 14.w),
+      decoration: BoxDecoration(
+        border: Border(
+          top: BorderSide(color: Colors.white.withOpacity(0.1), width: 1),
+        ),
+      ),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          // 确认按钮
+          GestureDetector(
+            onTap: controller.confirmChange,
+            child: Container(
+              width: double.infinity,
+              height: 48.h,
+              decoration: BoxDecoration(
+                color: DarkThemeColors.backgroundColor,
+                borderRadius: BorderRadius.circular(12.r),
+              ),
+              child: Center(
+                child: Text(
+                  'Confirm Change',
+                  style: TextStyle(
+                    fontSize: 16.sp,
+                    color: DarkThemeColors.subscriptionColor,
+                    fontWeight: FontWeight.w600,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          14.verticalSpaceFromWidth,
+          // 底部链接
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              GestureDetector(
+                onTap: controller.restorePurchases,
+                child: Text(
+                  'Restore Purchases',
+                  style: TextStyle(
+                    fontSize: 16.sp,
+                    color: DarkThemeColors.bodyTextColor,
+                  ),
+                ),
+              ),
+              Text(
+                '  |  ',
+                style: TextStyle(
+                  fontSize: 16.sp,
+                  color: DarkThemeColors.hintTextColor,
+                ),
+              ),
+              GestureDetector(
+                onTap: controller.handlePaymentIssue,
+                child: Text(
+                  'Payment issue',
+                  style: TextStyle(
+                    fontSize: 16.sp,
+                    color: DarkThemeColors.bodyTextColor,
+                  ),
+                ),
+              ),
+            ],
+          ),
+          14.verticalSpaceFromWidth,
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              IXImage(
+                source: Assets.subscriptionGreenShield,
+                width: 20.w,
+                height: 20.w,
+                sourceType: ImageSourceType.asset,
+              ),
+              10.horizontalSpace,
+              Text(
+                'Yearly auto-renew. Cancel anytime',
+                style: TextStyle(
+                  fontSize: 13.sp,
+                  color: DarkThemeColors.hintTextColor,
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 19 - 2
lib/app/routes/app_pages.dart

@@ -20,6 +20,8 @@ import '../modules/login/bindings/login_binding.dart';
 import '../modules/login/views/login_view.dart';
 import '../modules/login/views/login_view.dart';
 import '../modules/markdown/bindings/markdown_binding.dart';
 import '../modules/markdown/bindings/markdown_binding.dart';
 import '../modules/markdown/views/markdown_view.dart';
 import '../modules/markdown/views/markdown_view.dart';
+import '../modules/medialocation/bindings/medialocation_binding.dart';
+import '../modules/medialocation/views/medialocation_view.dart';
 import '../modules/node/bindings/node_binding.dart';
 import '../modules/node/bindings/node_binding.dart';
 import '../modules/node/views/node_view.dart';
 import '../modules/node/views/node_view.dart';
 import '../modules/precode/bindings/precode_binding.dart';
 import '../modules/precode/bindings/precode_binding.dart';
@@ -38,6 +40,8 @@ import '../modules/splittunneling/bindings/splittunneling_binding.dart';
 import '../modules/splittunneling/selectapp/bindings/splittunneling_selectapp_binding.dart';
 import '../modules/splittunneling/selectapp/bindings/splittunneling_selectapp_binding.dart';
 import '../modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart';
 import '../modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart';
 import '../modules/splittunneling/views/splittunneling_view.dart';
 import '../modules/splittunneling/views/splittunneling_view.dart';
+import '../modules/subscription/bindings/subscription_binding.dart';
+import '../modules/subscription/views/subscription_view.dart';
 import '../modules/web/bindings/web_binding.dart';
 import '../modules/web/bindings/web_binding.dart';
 import '../modules/web/views/web_view.dart';
 import '../modules/web/views/web_view.dart';
 
 
@@ -61,8 +65,7 @@ class AppPages {
       name: _Paths.HOME,
       name: _Paths.HOME,
       page: () => const HomeView(),
       page: () => const HomeView(),
       binding: HomeBinding(),
       binding: HomeBinding(),
-      transition: Transition.native,
-      curve: Curves.easeInOut,
+      transition: Transition.noTransition,
     ),
     ),
     GetPage(
     GetPage(
       name: _Paths.NODE,
       name: _Paths.NODE,
@@ -188,6 +191,20 @@ class AppPages {
       transition: Transition.native,
       transition: Transition.native,
       curve: Curves.easeInOut,
       curve: Curves.easeInOut,
     ),
     ),
+    GetPage(
+      name: _Paths.SUBSCRIPTION,
+      page: () => const SubscriptionView(),
+      binding: SubscriptionBinding(),
+      transition: Transition.downToUp,
+      curve: Curves.easeInOut,
+    ),
+    GetPage(
+      name: _Paths.MEDIALOCATION,
+      page: () => const MedialocationView(),
+      binding: MedialocationBinding(),
+      transition: Transition.downToUp,
+      curve: Curves.easeInOut,
+    ),
   ];
   ];
 
 
   /// 私有构造函数,防止被实例化
   /// 私有构造函数,防止被实例化

+ 4 - 0
lib/app/routes/app_routes.dart

@@ -23,6 +23,8 @@ abstract class Routes {
   static const SIGNUP = _Paths.SIGNUP;
   static const SIGNUP = _Paths.SIGNUP;
   static const FORGOTPWD = _Paths.FORGOTPWD;
   static const FORGOTPWD = _Paths.FORGOTPWD;
   static const LOGIN = _Paths.LOGIN;
   static const LOGIN = _Paths.LOGIN;
+  static const SUBSCRIPTION = _Paths.SUBSCRIPTION;
+  static const MEDIALOCATION = _Paths.MEDIALOCATION;
 }
 }
 
 
 abstract class _Paths {
 abstract class _Paths {
@@ -46,4 +48,6 @@ abstract class _Paths {
   static const SIGNUP = '/signup';
   static const SIGNUP = '/signup';
   static const FORGOTPWD = '/forgotpwd';
   static const FORGOTPWD = '/forgotpwd';
   static const LOGIN = '/login';
   static const LOGIN = '/login';
+  static const SUBSCRIPTION = '/subscription';
+  static const MEDIALOCATION = '/medialocation';
 }
 }

+ 48 - 0
lib/app/widgets/infintte_rotate.dart

@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../config/theme/dark_theme_colors.dart';
+import '../constants/iconfont/iconfont.dart';
+
+class InfiniteRotateIcon extends StatefulWidget {
+  const InfiniteRotateIcon({super.key});
+
+  @override
+  State<InfiniteRotateIcon> createState() => _InfiniteRotateIconState();
+}
+
+class _InfiniteRotateIconState extends State<InfiniteRotateIcon>
+    with SingleTickerProviderStateMixin {
+  late final AnimationController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+
+    // 创建动画控制器
+    _controller = AnimationController(
+      vsync: this,
+      duration: const Duration(seconds: 2), // 转一圈的时长
+    )..repeat(); // 无限循环
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose(); // 防止内存泄漏
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: RotationTransition(
+        turns: _controller, // 控制旋转角度(0~1)
+        child: Icon(
+          IconFont.icon20, // 可换成任意图标或图片
+          size: 20.w,
+          color: DarkThemeColors.subscriptionSelectColor,
+        ),
+      ),
+    );
+  }
+}

+ 184 - 0
lib/app/widgets/info_card.dart

@@ -0,0 +1,184 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
+import 'package:nomo/config/theme/dark_theme_colors.dart';
+
+/// 信息卡片组件 - 用于显示带图标的信息列表
+/// 常用于订阅说明、功能介绍等场景
+class InfoCard extends StatelessWidget {
+  final String? title;
+  final List<InfoItem> items;
+  final Color? backgroundColor;
+  final EdgeInsets? padding;
+
+  const InfoCard({
+    super.key,
+    this.title,
+    required this.items,
+    this.backgroundColor,
+    this.padding,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        if (title != null) ...[
+          Text(
+            title!,
+            style: TextStyle(
+              fontSize: 16.sp,
+              color: DarkThemeColors.hintTextColor,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+          16.verticalSpace,
+        ],
+        Container(
+          padding: padding ?? EdgeInsets.all(16.w),
+          decoration: BoxDecoration(
+            color: backgroundColor ?? DarkThemeColors.bgDisable,
+            borderRadius: BorderRadius.circular(12.r),
+          ),
+          child: Column(
+            children: items
+                .asMap()
+                .entries
+                .map(
+                  (entry) => _InfoItemWidget(
+                    item: entry.value,
+                    isLast: entry.key == items.length - 1,
+                  ),
+                )
+                .toList(),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+/// 信息项数据模型
+class InfoItem {
+  final IconData? icon;
+  final String? imageSource;
+  final String title;
+  final String description;
+  final Color? iconColor;
+  final Gradient? iconGradient;
+  final double? iconSize;
+  final double? imageWidth;
+  final double? imageHeight;
+
+  const InfoItem({
+    this.icon,
+    this.imageSource,
+    required this.title,
+    required this.description,
+    this.iconColor,
+    this.iconGradient,
+    this.iconSize,
+    this.imageWidth,
+    this.imageHeight,
+  }) : assert(
+         icon != null || imageSource != null,
+         'Either icon or imageSource must be provided',
+       );
+}
+
+/// 信息项组件(内部使用)
+class _InfoItemWidget extends StatelessWidget {
+  final InfoItem item;
+  final bool isLast;
+
+  const _InfoItemWidget({required this.item, this.isLast = false});
+
+  @override
+  Widget build(BuildContext context) {
+    return IntrinsicHeight(
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // 左侧时间线(图标/图片 + 竖线)
+          Column(
+            children: [
+              // 图标或图片容器
+              _buildIconOrImage(),
+              // 连接线(最后一个不显示)
+              if (!isLast)
+                Expanded(
+                  child: Container(
+                    width: 1,
+                    margin: EdgeInsets.only(top: 8.h, bottom: 8.h),
+                    color: Colors.white.withValues(alpha: 0.1),
+                  ),
+                ),
+            ],
+          ),
+          12.horizontalSpace,
+          // 文本内容
+          Expanded(
+            child: Padding(
+              padding: EdgeInsets.only(bottom: isLast ? 0 : 10.w),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    item.title,
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      height: 1.6,
+                      color: DarkThemeColors.bodyTextColor,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  ),
+                  4.verticalSpace,
+                  Text(
+                    item.description,
+                    style: TextStyle(
+                      fontSize: 12.sp,
+                      color: DarkThemeColors.hintTextColor,
+                      height: 1.7,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建图标或图片
+  Widget _buildIconOrImage() {
+    if (item.imageSource != null) {
+      // 显示图片
+      return IXImage(
+        source: item.imageSource!,
+        width: item.imageWidth ?? 20.w,
+        height: item.imageHeight ?? 20.w,
+        sourceType: _getImageSourceType(item.imageSource!),
+      );
+    } else {
+      // 显示图标
+      return Icon(
+        item.icon,
+        size: item.iconSize ?? 20.w,
+        color: item.iconColor ?? Colors.white,
+      );
+    }
+  }
+
+  /// 判断图片资源类型
+  ImageSourceType _getImageSourceType(String source) {
+    if (source.startsWith('http://') || source.startsWith('https://')) {
+      return ImageSourceType.network;
+    } else if (source.startsWith('assets/')) {
+      return ImageSourceType.asset;
+    } else {
+      return ImageSourceType.asset;
+    }
+  }
+}

+ 4 - 2
lib/app/widgets/ix_app_bar.dart

@@ -1,10 +1,12 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 
 
 import '../../config/translations/localization_service.dart';
 import '../../config/translations/localization_service.dart';
 import '../../config/theme/theme_extensions/theme_extension.dart';
 import '../../config/theme/theme_extensions/theme_extension.dart';
 import 'dart:math';
 import 'dart:math';
 
 
+import '../constants/iconfont/iconfont.dart';
 import 'click_opacity.dart';
 import 'click_opacity.dart';
 
 
 class IXAppBar extends StatelessWidget implements PreferredSizeWidget {
 class IXAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -49,9 +51,9 @@ class IXAppBar extends StatelessWidget implements PreferredSizeWidget {
               child: Transform.rotate(
               child: Transform.rotate(
                 angle: LocalizationService.isRTL() ? pi : 0, // 180度 = π 弧度
                 angle: LocalizationService.isRTL() ? pi : 0, // 180度 = π 弧度
                 child: Icon(
                 child: Icon(
-                  backIcon ?? Icons.arrow_back,
+                  backIcon ?? IconFont.icon01,
                   color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                   color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  size: 24,
+                  size: 24.w,
                 ),
                 ),
               ),
               ),
             )
             )

+ 9 - 7
lib/app/widgets/ix_text_field.dart

@@ -9,7 +9,7 @@ import 'click_opacity.dart';
 
 
 class IXTextField extends StatefulWidget {
 class IXTextField extends StatefulWidget {
   final String hintText;
   final String hintText;
-  final IconData prefixIcon;
+  final IconData? prefixIcon;
   final bool isPassword;
   final bool isPassword;
   final TextEditingController controller;
   final TextEditingController controller;
   final FocusNode? focusNode;
   final FocusNode? focusNode;
@@ -25,7 +25,7 @@ class IXTextField extends StatefulWidget {
   const IXTextField({
   const IXTextField({
     super.key,
     super.key,
     required this.hintText,
     required this.hintText,
-    required this.prefixIcon,
+    this.prefixIcon,
     this.isPassword = false,
     this.isPassword = false,
     required this.controller,
     required this.controller,
     this.focusNode,
     this.focusNode,
@@ -173,11 +173,13 @@ class _IXTextFieldState extends State<IXTextField> {
                 height: 1.6,
                 height: 1.6,
                 fontWeight: FontWeight.w400,
                 fontWeight: FontWeight.w400,
               ),
               ),
-              prefixIcon: Icon(
-                widget.prefixIcon,
-                color: Get.reactiveTheme.hintColor,
-                size: 20,
-              ),
+              prefixIcon: widget.prefixIcon != null
+                  ? Icon(
+                      widget.prefixIcon,
+                      color: Get.reactiveTheme.hintColor,
+                      size: 20,
+                    )
+                  : null,
               suffixIcon: _buildSuffixIcon(),
               suffixIcon: _buildSuffixIcon(),
               border: InputBorder.none,
               border: InputBorder.none,
               contentPadding: const EdgeInsets.symmetric(
               contentPadding: const EdgeInsets.symmetric(

+ 3 - 3
lib/app/widgets/submit_btn.dart

@@ -33,7 +33,7 @@ class SubmitButton extends StatelessWidget {
     this.fontSize,
     this.fontSize,
     this.isLoading = false,
     this.isLoading = false,
     this.width,
     this.width,
-    this.height = 54,
+    this.height = 52,
     this.prefixIcon,
     this.prefixIcon,
     this.fontFeatures,
     this.fontFeatures,
   });
   });
@@ -49,7 +49,7 @@ class SubmitButton extends StatelessWidget {
           height: height.w,
           height: height.w,
           decoration: BoxDecoration(
           decoration: BoxDecoration(
             color: bgColor ?? Get.reactiveTheme.primaryColor,
             color: bgColor ?? Get.reactiveTheme.primaryColor,
-            borderRadius: BorderRadius.circular(16.r),
+            borderRadius: BorderRadius.circular(12.r),
             border: borderColor != null
             border: borderColor != null
                 ? Border.all(color: borderColor!, width: 1.w)
                 ? Border.all(color: borderColor!, width: 1.w)
                 : null,
                 : null,
@@ -68,7 +68,7 @@ class SubmitButton extends StatelessWidget {
                     children: [
                     children: [
                       if (prefixIcon != null) ...[
                       if (prefixIcon != null) ...[
                         prefixIcon!,
                         prefixIcon!,
-                        4.horizontalSpace,
+                        10.horizontalSpace,
                       ],
                       ],
                       Text(
                       Text(
                         text,
                         text,

+ 11 - 0
lib/config/theme/dark_theme_colors.dart

@@ -84,4 +84,15 @@ class DarkThemeColors {
   static const Color strokes2 = Color(0xFF1C1E25);
   static const Color strokes2 = Color(0xFF1C1E25);
 
 
   static const Color errorColor = Color(0xFFEF0000);
   static const Color errorColor = Color(0xFFEF0000);
+
+  static const Color subscriptionColor = Color(0xFFF5D89F);
+  static const Color subscriptionSelectColor = Color(0xFF1FBC7B);
+
+  static const Color settingAppLinearGradientStartColor = Color(0xFFFFC262);
+  static const Color settingAppLinearGradientEndColor = Color(0xFFE09643);
+  static const Color settingSecurityLinearGradientStartColor = Color(
+    0xFFFF9994,
+  );
+  static const Color settingSecurityLinearGradientEndColor = Color(0xFFC9433C);
+  static const Color validTermColor = Color(0xFFFFCC00);
 }
 }

+ 0 - 123
lib/utils/file_cache_manager.dart

@@ -1,123 +0,0 @@
-import 'dart:io';
-import 'dart:convert';
-import 'package:crypto/crypto.dart';
-import 'package:path_provider/path_provider.dart';
-
-import 'file_stream_util.dart';
-import 'log/logger.dart';
-
-class FileCacheManager {
-  static const String TAG = 'FileCacheManager';
-  static final FileCacheManager _instance = FileCacheManager._internal();
-  factory FileCacheManager() => _instance;
-  FileCacheManager._internal();
-
-  late Directory _cacheDir;
-  bool _isInitialized = false;
-
-  /// 获取缓存目录
-  Future<Directory> _getCacheDirectory() async {
-    if (Platform.isIOS) {
-      // iOS 使用应用文档目录
-      final appDir = await getApplicationDocumentsDirectory();
-      final cacheDir = Directory('${appDir.path}/skip_geo');
-      if (!await cacheDir.exists()) {
-        await cacheDir.create(recursive: true);
-      }
-      return cacheDir;
-    } else {
-      // Android 使用外部存储目录
-      final appDir = await getExternalStorageDirectory();
-      if (appDir == null) {
-        throw Exception('Failed to get external storage directory');
-      }
-      final cacheDir = Directory('${appDir.path}/skip_geo');
-      if (!await cacheDir.exists()) {
-        await cacheDir.create(recursive: true);
-      }
-      return cacheDir;
-    }
-  }
-
-  /// 初始化缓存管理器
-  Future<void> init() async {
-    if (_isInitialized) return;
-
-    _cacheDir = await _getCacheDirectory();
-    _isInitialized = true;
-  }
-
-  /// 获取文件内容,如果本地缓存有效则使用缓存,否则下载
-  Future<String> getFileContent({
-    required String remoteUrl,
-    required String fileName,
-    required String expectedMd5,
-    required Function(double) onProgress,
-  }) async {
-    try {
-      await init();
-
-      final localFilePath = '${_cacheDir.path}/$fileName';
-      log(TAG, 'localFilePath: $localFilePath');
-      final localFile = File(localFilePath);
-
-      // 检查本地文件是否存在且MD5匹配
-      if (await localFile.exists()) {
-        final localContent = await localFile.readAsString();
-        final localMd5 = md5.convert(utf8.encode(localContent)).toString();
-
-        if (localMd5 == expectedMd5) {
-          return localContent;
-        }
-      }
-
-      // 下载文件
-      final result = await FileStreamUtils.readTextFile(
-        path: remoteUrl,
-        onProgress: onProgress,
-      );
-
-      // 验证下载文件的MD5
-      // if (result.md5Hash == expectedMd5) {
-      //   // 保存到本地
-      // }
-      await localFile.writeAsString(result.content);
-      return result.content;
-    } catch (e) {
-      log(TAG, 'FileCacheManager getFileContent error: $e');
-      return '';
-    }
-  }
-
-  /// 清除缓存
-  Future<void> clearCache() async {
-    try {
-      await init();
-      if (await _cacheDir.exists()) {
-        await _cacheDir.delete(recursive: true);
-        await _cacheDir.create();
-      }
-    } catch (e) {
-      log(TAG, 'FileCacheManager clearCache error: $e');
-    }
-  }
-
-  /// 获取缓存大小
-  Future<int> getCacheSize() async {
-    try {
-      await init();
-      if (!await _cacheDir.exists()) return 0;
-
-      int totalSize = 0;
-      await for (final file in _cacheDir.list(recursive: true)) {
-        if (file is File) {
-          totalSize += await file.length();
-        }
-      }
-      return totalSize;
-    } catch (e) {
-      log(TAG, 'FileCacheManager getCacheSize error: $e');
-      return 0;
-    }
-  }
-}

+ 0 - 179
lib/utils/file_stream_util.dart

@@ -1,179 +0,0 @@
-import 'dart:io';
-import 'dart:convert';
-import 'package:crypto/crypto.dart';
-import 'package:dio/io.dart';
-import 'package:path/path.dart' as path;
-import 'package:dio/dio.dart';
-
-import 'developer/ix_developer_tools.dart';
-
-class FileStreamUtils {
-  static const int defaultBufferSize = 64 * 1024;
-
-  static final Dio dio = Dio();
-
-  static setProxy(String proxy) {
-    dio.httpClientAdapter = IOHttpClientAdapter(
-      createHttpClient: () {
-        final client = HttpClient();
-        client.findProxy = (uri) => proxy;
-        client.badCertificateCallback =
-            (X509Certificate cert, String host, int port) => true;
-        return client;
-      },
-    );
-  }
-
-  /// 读取文件(支持本地文件和网络URL)
-  /// [path] 文件路径或URL
-  /// [encoding] 文件编码,默认UTF8
-  /// [onProgress] 读取进度回调
-  static Future<FileReadResult> readTextFile({
-    required String path,
-    Encoding encoding = utf8,
-    void Function(double progress)? onProgress,
-  }) async {
-    if (path.startsWith('http://') || path.startsWith('https://')) {
-      return _readFromUrl(
-        url: path,
-        encoding: encoding,
-        onProgress: onProgress,
-      );
-    } else {
-      return _readFromFile(
-        filePath: path,
-        encoding: encoding,
-        onProgress: onProgress,
-      );
-    }
-  }
-
-  /// 从URL读取
-  static Future<FileReadResult> _readFromUrl({
-    required String url,
-    required Encoding encoding,
-    void Function(double progress)? onProgress,
-  }) async {
-    try {
-      // 添加talker日志
-      dio.interceptors.add(SimpleNoSignApiMonitorInterceptor());
-      final response = await dio.get<List<int>>(
-        url,
-        options: Options(
-          responseType: ResponseType.bytes,
-          followRedirects: true,
-        ),
-        onReceiveProgress: (received, total) {
-          if (total != -1 && onProgress != null) {
-            onProgress(received / total);
-          }
-        },
-      );
-
-      if (response.data == null) {
-        throw Exception('No data received');
-      }
-
-      final bytes = response.data!;
-      final content = encoding.decode(bytes);
-      final md5Hash = md5.convert(bytes).toString();
-
-      return FileReadResult(
-        content: content,
-        md5Hash: md5Hash,
-        fileName: path.basename(url),
-        fileSize: bytes.length,
-      );
-    } catch (e) {
-      throw Exception('Error downloading file: $e');
-    }
-  }
-
-  /// 从本地文件读取
-  static Future<FileReadResult> _readFromFile({
-    required String filePath,
-    required Encoding encoding,
-    void Function(double progress)? onProgress,
-  }) async {
-    try {
-      final file = File(filePath);
-      if (!await file.exists()) {
-        throw FileSystemException('File not found', filePath);
-      }
-
-      final fileSize = await file.length();
-      var bytesRead = 0;
-      final List<int> allBytes = [];
-      final StringBuffer content = StringBuffer();
-
-      final stream = file.openRead();
-      await for (var data in stream) {
-        allBytes.addAll(data);
-        content.write(encoding.decode(data));
-
-        bytesRead += data.length;
-        if (onProgress != null) {
-          onProgress(bytesRead / fileSize);
-        }
-      }
-
-      final md5Hash = md5.convert(allBytes).toString();
-
-      return FileReadResult(
-        content: content.toString(),
-        md5Hash: md5Hash,
-        fileName: path.basename(filePath),
-        fileSize: fileSize,
-      );
-    } catch (e) {
-      throw FileSystemException('Error reading file: $e', filePath);
-    }
-  }
-
-  /// 验证文件或URL的MD5
-  static Future<bool> verifyMd5({
-    required String path,
-    required String expectedMd5,
-    void Function(double progress)? onProgress,
-  }) async {
-    try {
-      final result = await readTextFile(path: path, onProgress: onProgress);
-      return result.md5Hash.toLowerCase() == expectedMd5.toLowerCase();
-    } catch (e) {
-      return false;
-    }
-  }
-
-  /// 获取文件或URL的MD5
-  static Future<String?> getMd5({
-    required String path,
-    void Function(double progress)? onProgress,
-  }) async {
-    try {
-      final result = await readTextFile(path: path, onProgress: onProgress);
-      return result.md5Hash;
-    } catch (e) {
-      return null;
-    }
-  }
-}
-
-/// 文件读取结果
-class FileReadResult {
-  final String content; // 文件内容
-  final String md5Hash; // MD5值
-  final String fileName; // 文件名
-  final int fileSize; // 文件大小
-
-  FileReadResult({
-    required this.content,
-    required this.md5Hash,
-    required this.fileName,
-    required this.fileSize,
-  });
-
-  @override
-  String toString() {
-    return 'FileReadResult{fileName: $fileName, fileSize: $fileSize, md5Hash: $md5Hash}';
-  }
-}

+ 369 - 0
lib/utils/geo_downloader.dart

@@ -0,0 +1,369 @@
+import 'dart:io';
+import 'package:archive/archive.dart';
+import 'package:crypto/crypto.dart';
+import 'package:dio/dio.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+import '../app/data/models/launch/smart_geo.dart';
+import 'log/logger.dart';
+
+/// 地理数据下载工具类
+class GeoDownloader {
+  static const String TAG = 'GeoDownloader';
+  static final GeoDownloader _instance = GeoDownloader._internal();
+  factory GeoDownloader() => _instance;
+  GeoDownloader._internal();
+
+  final Dio _dio = Dio(
+    BaseOptions(
+      connectTimeout: const Duration(seconds: 30),
+      receiveTimeout: const Duration(minutes: 5),
+      sendTimeout: const Duration(seconds: 30),
+    ),
+  );
+
+  /// 获取 geo 文件目录(包名/files/geo)
+  Future<Directory> _getGeoDirectory() async {
+    final appDir = await getFilesDir();
+    // 使用 files/geo 路径
+    final geoDir = Directory(path.join(appDir.path, 'geo'));
+    if (!await geoDir.exists()) {
+      await geoDir.create(recursive: true);
+    }
+    return geoDir;
+  }
+
+  /// 获取文件目录
+  Future<Directory> getFilesDir() async {
+    if (Platform.isAndroid) {
+      return await getApplicationSupportDirectory();
+    } else {
+      // iOS fallback
+      return await getApplicationDocumentsDirectory();
+    }
+  }
+
+  /// 计算文件的 MD5
+  Future<String?> _calculateFileMd5(String filePath) async {
+    try {
+      final file = File(filePath);
+      if (!await file.exists()) {
+        return null;
+      }
+      final bytes = await file.readAsBytes();
+      final digest = md5.convert(bytes);
+      return digest.toString();
+    } catch (e) {
+      log(TAG, 'Error calculating MD5: $e');
+      return null;
+    }
+  }
+
+  /// 下载并解压 zip 文件
+  /// [url] 下载地址
+  /// [expectedMd5] 期望的 MD5 值(zip 文件的 MD5)
+  /// [targetFileName] 解压后的目标文件名(不含扩展名)
+  /// [onProgress] 下载进度回调 (0.0 - 1.0)
+  /// 返回解压后的文件路径,如果不需要下载则返回现有文件路径
+  Future<String?> downloadAndExtract({
+    required String url,
+    required String expectedMd5,
+    required String targetFileName,
+    Function(double)? onProgress,
+  }) async {
+    try {
+      final geoDir = await _getGeoDirectory();
+      final targetFilePath = path.join(geoDir.path, targetFileName);
+      final zipFilePath = path.join(geoDir.path, '$targetFileName.zip');
+      final zipFile = File(zipFilePath);
+
+      // 检查 zip 文件是否存在且 MD5 匹配
+      bool needDownload = true;
+      if (await zipFile.exists()) {
+        final zipMd5 = await _calculateFileMd5(zipFilePath);
+        log(
+          TAG,
+          '$targetFileName.zip local MD5: $zipMd5, expected: $expectedMd5',
+        );
+
+        if (zipMd5 != null &&
+            zipMd5.toLowerCase() == expectedMd5.toLowerCase()) {
+          log(
+            TAG,
+            '$targetFileName.zip already exists with correct MD5, skip download',
+          );
+          needDownload = false;
+          onProgress?.call(0.7); // 跳过下载,直接到解压阶段
+        } else {
+          log(TAG, '$targetFileName.zip MD5 mismatch, will download again');
+          // MD5 不匹配,删除旧的 zip 文件
+          await zipFile.delete();
+        }
+      }
+
+      // 下载 zip 文件(如果需要)
+      if (needDownload) {
+        log(TAG, 'Downloading $targetFileName from $url');
+
+        try {
+          final response = await _dio.download(
+            url,
+            zipFilePath,
+            onReceiveProgress: (received, total) {
+              if (total != -1 && onProgress != null) {
+                // 下载进度占 70%
+                onProgress(received / total * 0.7);
+              }
+            },
+          );
+
+          log(TAG, 'Download response status: ${response.statusCode}');
+
+          if (response.statusCode != 200) {
+            log(
+              TAG,
+              'Error: Download failed with status code ${response.statusCode}',
+            );
+            return null;
+          }
+        } catch (e) {
+          log(TAG, 'Error during download: $e');
+          // 清理可能产生的不完整文件
+          if (await zipFile.exists()) {
+            await zipFile.delete();
+          }
+          return null;
+        }
+
+        log(TAG, 'Downloaded to $zipFilePath');
+
+        // 验证下载的文件是否存在
+        if (!await zipFile.exists()) {
+          log(TAG, 'Error: Downloaded zip file does not exist');
+          return null;
+        }
+
+        // 检查文件大小
+        final fileSize = await zipFile.length();
+        log(TAG, 'Downloaded zip file size: $fileSize bytes');
+        if (fileSize == 0) {
+          log(TAG, 'Error: Downloaded zip file is empty');
+          await zipFile.delete();
+          return null;
+        }
+
+        onProgress?.call(0.7);
+      }
+
+      // 读取 zip 文件
+      final zipBytes = await zipFile.readAsBytes();
+
+      // 解压
+      log(TAG, 'Extracting $targetFileName...');
+      final archive = ZipDecoder().decodeBytes(zipBytes);
+
+      bool extracted = false;
+      // 查找目标文件(去掉 .zip 后缀的文件名)
+      for (final file in archive) {
+        if (file.isFile) {
+          final fileName = file.name;
+          log(TAG, 'Found file in archive: $fileName');
+
+          // 忽略 macOS 元数据文件和隐藏文件
+          if (fileName.startsWith('__MACOSX/') ||
+              fileName.startsWith('.') ||
+              path.basename(fileName).startsWith('._')) {
+            log(TAG, 'Skipping system file: $fileName');
+            continue;
+          }
+
+          // 只提取 .dat 文件到根目录
+          if (fileName.endsWith('.dat')) {
+            // 提取文件内容
+            final fileData = file.content as List<int>;
+            // 使用 basename 确保文件保存在根目录
+            final extractedFile = File(
+              path.join(geoDir.path, path.basename(fileName)),
+            );
+            await extractedFile.writeAsBytes(fileData);
+            log(TAG, 'Extracted to ${extractedFile.path}');
+            extracted = true;
+          }
+        }
+      }
+
+      if (!extracted) {
+        log(TAG, 'Warning: No .dat file found in archive');
+      }
+
+      onProgress?.call(0.9);
+
+      // 保留 zip 文件(不删除,下次启动可以通过 MD5 判断是否需要重新下载)
+      log(TAG, 'Keeping zip file for future MD5 validation');
+
+      // 验证解压后的文件是否存在
+      final targetFile = File(targetFilePath);
+      if (!await targetFile.exists()) {
+        log(TAG, 'Error: Target file does not exist after extraction');
+        onProgress?.call(1.0);
+        return null;
+      }
+
+      // 记录成功信息
+      final fileSize = await targetFile.length();
+      log(TAG, '$targetFileName extracted successfully, size: $fileSize bytes');
+      log(TAG, 'Zip file MD5: $expectedMd5 (validated)');
+
+      onProgress?.call(1.0);
+      return targetFilePath;
+    } catch (e) {
+      log(TAG, 'Error downloading/extracting $targetFileName: $e');
+
+      // 注意:不删除 zip 文件,因为:
+      // 1. 如果 zip 已经 MD5 验证通过,不应该删除
+      // 2. 如果下载失败,在下载环节已经清理过了
+      // 3. 保留 zip 文件便于调试和下次快速恢复
+
+      return null;
+    }
+  }
+
+  /// 下载 SmartGeo 数据(geosite 和 geoip)
+  /// [smartGeo] SmartGeo 配置数据
+  /// [onGeoSiteProgress] geosite 下载进度回调
+  /// [onGeoIpProgress] geoip 下载进度回调
+  /// 返回 Map,包含 geosite 和 geoip 的文件路径
+  Future<Map<String, String?>> downloadSmartGeo({
+    required SmartGeo smartGeo,
+    Function(double)? onGeoSiteProgress,
+    Function(double)? onGeoIpProgress,
+  }) async {
+    final result = <String, String?>{};
+
+    // 下载 geosite.dat
+    if (smartGeo.geoSiteUrl != null && smartGeo.geoSiteMd5 != null) {
+      log(TAG, 'Downloading geosite.dat...');
+      final geoSitePath = await downloadAndExtract(
+        url: smartGeo.geoSiteUrl!,
+        expectedMd5: smartGeo.geoSiteMd5!,
+        targetFileName: 'geosite.dat',
+        onProgress: onGeoSiteProgress,
+      );
+      result['geosite'] = geoSitePath;
+    }
+
+    // 下载 geoip.dat
+    if (smartGeo.geoIpUrl != null && smartGeo.geoIpMd5 != null) {
+      log(TAG, 'Downloading geoip.dat...');
+      final geoIpPath = await downloadAndExtract(
+        url: smartGeo.geoIpUrl!,
+        expectedMd5: smartGeo.geoIpMd5!,
+        targetFileName: 'geoip.dat',
+        onProgress: onGeoIpProgress,
+      );
+      result['geoip'] = geoIpPath;
+    }
+
+    return result;
+  }
+
+  /// 检查 SmartGeo 文件是否需要更新
+  /// [smartGeo] SmartGeo 配置数据
+  /// 返回 Map,key 为文件类型(geosite/geoip),value 为是否需要更新
+  /// 注意:通过检查 zip 文件的 MD5 来判断是否需要更新
+  Future<Map<String, bool>> checkNeedUpdate(SmartGeo smartGeo) async {
+    final result = <String, bool>{};
+
+    try {
+      final geoDir = await _getGeoDirectory();
+
+      // 检查 geosite.dat.zip
+      if (smartGeo.geoSiteUrl != null && smartGeo.geoSiteMd5 != null) {
+        final geoSiteZipFile = File(path.join(geoDir.path, 'geosite.dat.zip'));
+        if (await geoSiteZipFile.exists()) {
+          final localMd5 = await _calculateFileMd5(geoSiteZipFile.path);
+          // 如果 MD5 不同,则需要更新
+          result['geosite'] =
+              localMd5?.toLowerCase() != smartGeo.geoSiteMd5?.toLowerCase();
+        } else {
+          // zip 文件不存在,需要下载
+          result['geosite'] = true;
+        }
+      }
+
+      // 检查 geoip.dat.zip
+      if (smartGeo.geoIpUrl != null && smartGeo.geoIpMd5 != null) {
+        final geoIpZipFile = File(path.join(geoDir.path, 'geoip.dat.zip'));
+        if (await geoIpZipFile.exists()) {
+          final localMd5 = await _calculateFileMd5(geoIpZipFile.path);
+          // 如果 MD5 不同,则需要更新
+          result['geoip'] =
+              localMd5?.toLowerCase() != smartGeo.geoIpMd5?.toLowerCase();
+        } else {
+          // zip 文件不存在,需要下载
+          result['geoip'] = true;
+        }
+      }
+    } catch (e) {
+      log(TAG, 'Error checking update: $e');
+    }
+
+    return result;
+  }
+
+  /// 获取 geo 文件路径
+  /// [fileName] 文件名(如 'geosite.dat' 或 'geoip.dat')
+  Future<String> getGeoFilePath(String fileName) async {
+    final geoDir = await _getGeoDirectory();
+    return path.join(geoDir.path, fileName);
+  }
+
+  /// 清除所有 geo 文件
+  Future<void> clearGeoFiles() async {
+    try {
+      final geoDir = await _getGeoDirectory();
+      if (await geoDir.exists()) {
+        await geoDir.delete(recursive: true);
+        await geoDir.create(recursive: true);
+        log(TAG, 'Cleared all geo files');
+      }
+    } catch (e) {
+      log(TAG, 'Error clearing geo files: $e');
+    }
+  }
+
+  /// 诊断工具:列出 geo 目录下的所有文件
+  Future<Map<String, dynamic>> diagnose() async {
+    try {
+      final geoDir = await _getGeoDirectory();
+      final result = <String, dynamic>{
+        'geoDirectory': geoDir.path,
+        'directoryExists': await geoDir.exists(),
+        'files': <Map<String, dynamic>>[],
+      };
+
+      if (await geoDir.exists()) {
+        await for (final entity in geoDir.list()) {
+          if (entity is File) {
+            final file = entity;
+            final fileSize = await file.length();
+            final fileMd5 = await _calculateFileMd5(file.path);
+            result['files'].add({
+              'name': path.basename(file.path),
+              'path': file.path,
+              'size': fileSize,
+              'md5': fileMd5,
+            });
+          }
+        }
+      }
+
+      log(TAG, 'Diagnosis result: $result');
+      return result;
+    } catch (e) {
+      log(TAG, 'Error during diagnosis: $e');
+      return {'error': e.toString()};
+    }
+  }
+}

+ 4 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -10,6 +10,7 @@ import connectivity_plus
 import device_info_plus
 import device_info_plus
 import flutter_inappwebview_macos
 import flutter_inappwebview_macos
 import flutter_secure_storage_macos
 import flutter_secure_storage_macos
+import in_app_purchase_storekit
 import network_info_plus
 import network_info_plus
 import package_info_plus
 import package_info_plus
 import path_provider_foundation
 import path_provider_foundation
@@ -17,6 +18,7 @@ import share_plus
 import shared_preferences_foundation
 import shared_preferences_foundation
 import sqflite_darwin
 import sqflite_darwin
 import url_launcher_macos
 import url_launcher_macos
+import video_player_avfoundation
 
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
   AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
@@ -24,6 +26,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"))
   FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
   FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
+  InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
   NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
   NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
   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"))
@@ -31,4 +34,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+  FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
 }
 }

+ 96 - 0
pubspec.lock

@@ -209,6 +209,14 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "1.3.1"
     version: "1.3.1"
+  carousel_slider:
+    dependency: "direct main"
+    description:
+      name: carousel_slider
+      sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.1.1"
   change_app_package_name:
   change_app_package_name:
     dependency: "direct dev"
     dependency: "direct dev"
     description:
     description:
@@ -768,6 +776,46 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "4.5.4"
     version: "4.5.4"
+  image_gallery_saver_plus:
+    dependency: "direct main"
+    description:
+      name: image_gallery_saver_plus
+      sha256: "199b9e24f8d85e98f11e3d35571ab68ae50626ad40e2bb85c84383f69a6950ad"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.1"
+  in_app_purchase:
+    dependency: "direct main"
+    description:
+      name: in_app_purchase
+      sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.3"
+  in_app_purchase_android:
+    dependency: transitive
+    description:
+      name: in_app_purchase_android
+      sha256: "2d0e2d27b93bd7457526419d7feb07baf5608d733ecf6cdcd2e3b03ea7248915"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.0+6"
+  in_app_purchase_platform_interface:
+    dependency: transitive
+    description:
+      name: in_app_purchase_platform_interface
+      sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
+  in_app_purchase_storekit:
+    dependency: transitive
+    description:
+      name: in_app_purchase_storekit
+      sha256: bfdb8d1859b6d19a55aba1046e3a860c631b6e96d36275a358e1caf8b62cfbde
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.6+1"
   io:
   io:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1176,6 +1224,14 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "0.28.0"
     version: "0.28.0"
+  screenshot:
+    dependency: "direct main"
+    description:
+      name: screenshot
+      sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   share_plus:
   share_plus:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
@@ -1557,6 +1613,46 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "2.2.0"
     version: "2.2.0"
+  video_player:
+    dependency: "direct main"
+    description:
+      name: video_player
+      sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.10.1"
+  video_player_android:
+    dependency: transitive
+    description:
+      name: video_player_android
+      sha256: cf768d02924b91e333e2bc1ff928528f57d686445874f383bafab12d0bdfc340
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.8.17"
+  video_player_avfoundation:
+    dependency: transitive
+    description:
+      name: video_player_avfoundation
+      sha256: "03fc6d07dba2499588d30887329b399c1fe2d68ce4b7fcff0db79f44a2603f69"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.8.6"
+  video_player_platform_interface:
+    dependency: transitive
+    description:
+      name: video_player_platform_interface
+      sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.6.0"
+  video_player_web:
+    dependency: transitive
+    description:
+      name: video_player_web
+      sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
   vm_service:
   vm_service:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 8 - 0
pubspec.yaml

@@ -77,6 +77,11 @@ dependencies:
   event_bus: ^2.0.0 # 事件总线
   event_bus: ^2.0.0 # 事件总线
   animated_reorderable_list: ^1.3.0 # 可重新排序的动画列表
   animated_reorderable_list: ^1.3.0 # 可重新排序的动画列表
   freezed_annotation: ^2.4.4
   freezed_annotation: ^2.4.4
+  screenshot: ^3.0.0 # 截图
+  image_gallery_saver_plus: ^4.0.1 # 保存图片到相册
+  video_player: ^2.10.1 # 视频播放器
+  in_app_purchase: ^3.2.3 # 内购
+  carousel_slider: ^5.1.1 # 轮播图
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_test:
   flutter_test:
@@ -143,6 +148,9 @@ flutter:
           weight: 500
           weight: 500
         - asset: assets/fonts/Vazirmatn-SemiBold.ttf
         - asset: assets/fonts/Vazirmatn-SemiBold.ttf
           weight: 700
           weight: 700
+    - family: iconfont
+      fonts:
+        - asset: assets/fonts/iconfont.ttf
   # To add assets to your application, add an assets section, like this:
   # To add assets to your application, add an assets section, like this:
   # assets:
   # assets:
   #   - images/a_dot_burr.jpeg
   #   - images/a_dot_burr.jpeg