Przeglądaj źródła

feat: 修改服务类型,新增订阅接口

lilu 2 miesięcy temu
rodzic
commit
2690ec0318
66 zmienionych plików z 3715 dodań i 563 usunięć
  1. 2 2
      android/app/src/main/AndroidManifest.xml
  2. 5 0
      android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt
  3. 16 11
      android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt
  4. 1 0
      android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt
  5. 12 1
      android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt
  6. 54 51
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  7. 7 1
      android/app/src/main/res/values-night/styles.xml
  8. 3 1
      android/app/src/main/res/values/styles.xml
  9. 62 5
      assets/flags/um.svg
  10. 62 5
      assets/flags/us.svg
  11. 15 0
      lib/app/api/core/api_core.dart
  12. 10 1
      lib/app/api/core/api_core_paths.dart
  13. 2 0
      lib/app/constants/enums.dart
  14. 66 0
      lib/app/controllers/api_controller.dart
  15. 77 4
      lib/app/controllers/core_controller.dart
  16. 39 0
      lib/app/data/models/channelplan/channel_plan_list.dart
  17. 750 0
      lib/app/data/models/channelplan/channel_plan_list.freezed.dart
  18. 63 0
      lib/app/data/models/channelplan/channel_plan_list.g.dart
  19. 0 1
      lib/app/data/models/launch/groups.dart
  20. 4 25
      lib/app/data/models/launch/groups.freezed.dart
  21. 0 2
      lib/app/data/models/launch/groups.g.dart
  22. 25 2
      lib/app/data/models/launch/user.dart
  23. 502 18
      lib/app/data/models/launch/user.freezed.dart
  24. 40 2
      lib/app/data/models/launch/user.g.dart
  25. 64 1
      lib/app/data/sp/ix_sp.dart
  26. 10 0
      lib/app/dialog/all_dialog.dart
  27. 39 6
      lib/app/dialog/custom_dialog.dart
  28. 59 2
      lib/app/modules/account/controllers/account_controller.dart
  29. 340 5
      lib/app/modules/account/views/account_view.dart
  30. 70 2
      lib/app/modules/home/controllers/home_controller.dart
  31. 57 54
      lib/app/modules/home/views/home_view.dart
  32. 3 0
      lib/app/modules/login/controllers/login_controller.dart
  33. 18 0
      lib/app/modules/node/controllers/node_controller.dart
  34. 1 1
      lib/app/modules/node/views/node_view.dart
  35. 10 8
      lib/app/modules/node/widgets/node_list.dart
  36. 25 4
      lib/app/modules/setting/controllers/setting_controller.dart
  37. 116 95
      lib/app/modules/setting/views/setting_view.dart
  38. 2 0
      lib/app/modules/signup/controllers/signup_controller.dart
  39. 268 70
      lib/app/modules/subscription/controllers/subscription_controller.dart
  40. 163 118
      lib/app/modules/subscription/views/subscription_view.dart
  41. 10 1
      lib/config/translations/ar_AR/ar_ar_translation.dart
  42. 10 1
      lib/config/translations/de_DE/de_de_translation.dart
  43. 10 1
      lib/config/translations/en_US/en_us_translation.dart
  44. 10 1
      lib/config/translations/es_ES/es_es_translation.dart
  45. 10 1
      lib/config/translations/fa_IR/fa_ir_translation.dart
  46. 10 1
      lib/config/translations/fr_FR/fr_fr_translation.dart
  47. 10 1
      lib/config/translations/hi_IN/hi_in_translation.dart
  48. 10 1
      lib/config/translations/id_ID/id_id_translation.dart
  49. 10 1
      lib/config/translations/ja_JP/ja_jp_translation.dart
  50. 10 1
      lib/config/translations/ko_KR/ko_kr_translation.dart
  51. 10 1
      lib/config/translations/my_MM/my_mm_translation.dart
  52. 10 1
      lib/config/translations/pt_BR/pt_br_translation.dart
  53. 10 1
      lib/config/translations/ru_RU/ru_ru_translation.dart
  54. 8 0
      lib/config/translations/strings_enum.dart
  55. 10 1
      lib/config/translations/th_TH/th_th_translation.dart
  56. 10 1
      lib/config/translations/tk_TM/tk_tm_translation.dart
  57. 10 1
      lib/config/translations/tl_PH/tl_ph_translation.dart
  58. 10 1
      lib/config/translations/tr_TR/tr_tr_translation.dart
  59. 92 43
      lib/config/translations/vi_VN/vi_vn_translation.dart
  60. 9 1
      lib/config/translations/zh_TW/zh_tw_translation.dart
  61. 229 0
      lib/utils/boost_logger.dart
  62. 124 0
      lib/utils/boost_report_manager.dart
  63. 10 4
      lib/utils/ntp_time_service.dart
  64. 2 0
      macos/Flutter/GeneratedPluginRegistrant.swift
  65. 8 0
      pubspec.lock
  66. 1 0
      pubspec.yaml

+ 2 - 2
android/app/src/main/AndroidManifest.xml

@@ -9,7 +9,7 @@
     <uses-permission
         android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission
-        android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
+        android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
     <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
     <uses-permission
         android:name="android.permission.POST_NOTIFICATIONS"/>
@@ -81,7 +81,7 @@
             android:name=".XRayService"
             android:directBootAware="true"
             android:exported="false"
-            android:foregroundServiceType="systemExempted"
+            android:foregroundServiceType="specialUse"
             android:label="@string/app_name"
             android:permission="android.permission.BIND_VPN_SERVICE"
             android:process=":nomo_vpn_service"

+ 5 - 0
android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt

@@ -390,6 +390,11 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         VLog.i(TAG, "xrayApi.startXray() 返回结果: $result")
     }
 
+    // 解绑XRay服务
+    fun unbindXrayService() {
+        xrayApi?.unbindXrayService()
+    }
+
     fun safeStringToJsonArray(jsonString: String?): JsonArray? {
         if (jsonString.isNullOrBlank()) return null
 

+ 16 - 11
android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt

@@ -15,17 +15,22 @@ class MainActivity: FlutterActivity() {
         private const val TAG = "MainActivity"
     }
     
-    override fun provideFlutterEngine(context: android.content.Context): FlutterEngine? {
-        // 使用预初始化的Flutter引擎
-        val engine = App.getFlutterEngine()
-        if (engine != null) {
-            // 确保引擎状态正确
-            engine.lifecycleChannel.appIsResumed()
-            Log.d(TAG, "使用预初始化的Flutter引擎")
-        } else {
-            Log.w(TAG, "预初始化的Flutter引擎未找到,将创建新引擎")
-        }
-        return engine
+//    override fun provideFlutterEngine(context: android.content.Context): FlutterEngine? {
+//        // 使用预初始化的Flutter引擎
+//        val engine = App.getFlutterEngine()
+//        if (engine != null) {
+//            // 确保引擎状态正确
+//            engine.lifecycleChannel.appIsResumed()
+//            Log.d(TAG, "使用预初始化的Flutter引擎")
+//        } else {
+//            Log.w(TAG, "预初始化的Flutter引擎未找到,将创建新引擎")
+//        }
+//        return engine
+//    }
+
+    override fun detachFromFlutterEngine() {
+        super.detachFromFlutterEngine()
+        coreApiImpl.unbindXrayService()
     }
     
     override fun onCreate(savedInstanceState: android.os.Bundle?) {

+ 1 - 0
android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt

@@ -54,6 +54,7 @@ internal class TProxyService {
             TProxyStopService()
         } catch (e: Exception) {
             // Ignore exception
+            Log.w(TAG, "TProxyStopService stopTunnel failed", e)
         }
     }
 

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

@@ -177,7 +177,7 @@ class XRayApi {
             doStartService(ctx)
             val intent = Intent(ctx, XRayService::class.java)
             VLog.i(TAG, "准备绑定 xray service")
-            if (!ctx.bindService(intent, mConnection, 0)) {
+            if (!ctx.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)) {
                 VLog.e(TAG, "绑定 xray service 失败")
                 return
             }
@@ -293,6 +293,17 @@ class XRayApi {
         }
     }
 
+    // 只解绑服务,不停止服务
+    fun unbindXrayService() {
+        VLog.i(TAG, "unbindXrayService 被调用")
+        lock.lock()
+        try {
+            context?.unbindService(mConnection)
+        } finally {
+            lock.unlock()
+        }
+    }
+
     fun setVpnServiceEventListener(listener: OnVpnServiceEvent?) {
         vpnServiceEvent = listener
     }

+ 54 - 51
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -4,7 +4,7 @@ import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager
 import android.content.Intent
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
 import android.net.VpnService
 import android.os.Build
 import android.os.Bundle
@@ -35,19 +35,19 @@ class XRayService : VpnService() {
     private var isTimerRunning = false
     private var isTimerPaused = false
     private var timerMode = 0 // 0: 普通计时, 1: 倒计时
-    
+
     // 使用 SystemClock.elapsedRealtime() 确保时间准确性(包括休眠时间)
     private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
     private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
     private var timerPausedElapsed = 0L // 暂停时已经过的时间
-    
+
     // 计时器Runnable
     private val timerRunnable = object : Runnable {
         override fun run() {
             if (isTimerRunning && !isTimerPaused) {
                 // 计算从开始到现在经过的真实时间
                 val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
-                
+
                 val currentTime = if (timerMode == 0) {
                     // 普通计时:初始时间 + 经过时间
                     timerInitialTime + elapsedTime
@@ -55,7 +55,7 @@ class XRayService : VpnService() {
                     // 倒计时:初始时间 - 经过时间
                     timerInitialTime - elapsedTime
                 }
-                
+
                 // 检查倒计时是否结束
                 if (timerMode == 1 && currentTime <= 0) {
                     VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
@@ -63,13 +63,13 @@ class XRayService : VpnService() {
                     notifyStop()
                     return
                 }
-                
+
                 // 更新通知
                 updateNotification(currentTime)
-                
+
                 // 发送计时更新
                 sendTimerUpdate(currentTime)
-                
+
                 // 继续下一秒
                 timerHandler.postDelayed(this, 1000L)
             }
@@ -104,7 +104,7 @@ class XRayService : VpnService() {
         // 初始化文件日志系统
         VLog.init(applicationContext)
         VLog.i(TAG, "XRayService onCreate")
-        
+
         Seq.setContext(applicationContext)
         Ixvpn_mobile.initProxyConnector()
         createNotificationChannel()
@@ -114,7 +114,7 @@ class XRayService : VpnService() {
         super.onRevoke()
         VLog.i(TAG, "onRevoke")
         // 停止计时
-        stopTimer()
+        sendStatusMessage(VPN_STATE_ERROR, "onRevoke")
     }
 
     override fun onTaskRemoved(rootIntent: Intent?) {
@@ -132,27 +132,27 @@ class XRayService : VpnService() {
         } catch (e: Exception) {
             VLog.e(TAG, "stopForeground 失败", e)
         }
-        
+
         // 清理Messenger引用
         replyMessenger = null
         VLog.i(TAG, "replyMessenger 已清理")
-        
+
         // 清理资源
         try {
             Ixvpn_mobile.freeProxyConnector()
         } catch (e: Exception) {
             VLog.e(TAG, "freeProxyConnector 失败", e)
         }
-        
+
         // 关闭日志系统
         VLog.close()
-        
+
         super.onDestroy()
     }
 
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
         createNotification()
-        return START_NOT_STICKY
+        return START_STICKY
     }
 
     private fun handleRemoteMessage(msg: Message) {
@@ -163,7 +163,7 @@ class XRayService : VpnService() {
         } else {
             VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
         }
-        
+
         when (msg.what) {
             XRAY_MSG_START -> dealStartMsg(msg)
             XRAY_MSG_STOP -> dealStopMsg()
@@ -179,10 +179,13 @@ class XRayService : VpnService() {
         val tunnelConfig = bundle.getString("tunnelConfig")
         val allowVpnApps = bundle.getStringArrayList("allowVpnApps") ?: arrayListOf()
         val disallowVpnApps = bundle.getStringArrayList("disallowVpnApps") ?: arrayListOf()
-        
+
         VLog.i(TAG, "socksPort: $socksPort, sessionId: $sessionId")
-        VLog.i(TAG, "allowVpnApps count: ${allowVpnApps.size}, disallowVpnApps count: ${disallowVpnApps.size}")
-        
+        VLog.i(
+            TAG,
+            "allowVpnApps count: ${allowVpnApps.size}, disallowVpnApps count: ${disallowVpnApps.size}"
+        )
+
         try {
             val builder = Builder()
             val ipAddress = "192.168.34.2"
@@ -204,7 +207,7 @@ class XRayService : VpnService() {
                 builder.addDisallowedApplication(packageName)
                 VLog.i(TAG, "禁止的应用: ${disallowVpnApps.joinToString(", ")}")
             }
-            
+
             tunFileDescriptor = builder.establish()
             tunFileDescriptor?.let { tfd ->
                 VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
@@ -213,13 +216,13 @@ class XRayService : VpnService() {
                 VLog.i(TAG, "XRay proxy started successfully")
             } ?: run {
                 VLog.e(TAG, "Failed to establish VPN tunnel")
-                dealStopMsg()
-                stopSelf()
+                // 通知 Flutter 层启动失败
+                sendStatusMessage(VPN_STATE_ERROR, "Failed to establish VPN tunnel")
             }
         } catch (e: Exception) {
             VLog.e(TAG, "启动 XRay 失败", e)
-            dealStopMsg()
-            stopSelf()
+            // 通知 Flutter 层启动失败
+            sendStatusMessage(VPN_STATE_ERROR, "启动 XRay 失败: ${e.message}")
         }
     }
 
@@ -230,24 +233,24 @@ class XRayService : VpnService() {
 
     private fun doStop() {
         VLog.i(TAG, "开始停止 XRay 服务")
-        
+
         // 停止计时
         stopTimer()
-        
+
         try {
             tunnelService.stopTunnel()
             VLog.i(TAG, "Tunnel stopped")
         } catch (e: Exception) {
             VLog.e(TAG, "停止 tunnel 失败", e)
         }
-        
+
         try {
             Ixvpn_mobile.proxyConnectorStop()
             VLog.i(TAG, "Proxy connector stopped")
         } catch (e: Exception) {
             VLog.e(TAG, "停止 proxy connector 失败", e)
         }
-        
+
         tunFileDescriptor?.let { tfd ->
             try {
                 tfd.close()
@@ -257,7 +260,8 @@ class XRayService : VpnService() {
             }
             tunFileDescriptor = null
         }
-        VLog.i(TAG, "xray stopped") }
+        VLog.i(TAG, "xray stopped")
+    }
 
     private fun notifyStop() {
         replyMessenger?.let { messenger ->
@@ -280,12 +284,11 @@ class XRayService : VpnService() {
 
     private fun sendStatusMessage(status: Long, message: String) {
         try {
-            if(status == VPN_STATE_CONNECTED) {
+            if (status == VPN_STATE_CONNECTED) {
                 // 启动计时(普通计时模式)
                 startTimer(0, 0)
 //                startTimer(1, 10000L)
-            }
-            else if(status == VPN_STATE_ERROR) {
+            } else if (status == VPN_STATE_ERROR) {
                 VLog.i(TAG, "VPN 连接失败,status:$status")
             }
             replyMessenger?.let { messenger ->
@@ -313,10 +316,10 @@ class XRayService : VpnService() {
             VLog.e(TAG, "发送状态消息失败", e)
         }
     }
-    
+
     // 计时相关方法
     private fun startTimer(mode: Int, initialTime: Long) {
-        if(isTimerRunning)
+        if (isTimerRunning)
             return
         VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
         timerMode = mode
@@ -326,44 +329,44 @@ class XRayService : VpnService() {
 
         isTimerRunning = true
         isTimerPaused = false
-        
+
         // 开始计时循环
         timerHandler.post(timerRunnable)
     }
-    
+
     private fun stopTimer() {
-        if(!isTimerRunning)
+        if (!isTimerRunning)
             return
         VLog.i(TAG, "停止计时")
         isTimerRunning = false
         isTimerPaused = false
         timerHandler.removeCallbacks(timerRunnable)
     }
-    
+
     private fun pauseTimer() {
         if (isTimerRunning && !isTimerPaused) {
             // 记录暂停时已经过的时间
             val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
             timerPausedElapsed = elapsedTime
-            
+
             VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
             isTimerPaused = true
             timerHandler.removeCallbacks(timerRunnable)
         }
     }
-    
+
     private fun resumeTimer() {
         if (isTimerRunning && isTimerPaused) {
             VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
-            
+
             // 恢复时重新设置基准时间,但要减去之前已经过的时间
             timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
-            
+
             isTimerPaused = false
             timerHandler.post(timerRunnable)
         }
     }
-    
+
     private fun sendTimerUpdate(currentTime: Long) {
         try {
             replyMessenger?.let { messenger ->
@@ -390,12 +393,12 @@ class XRayService : VpnService() {
             VLog.e(TAG, "发送计时更新失败", e)
         }
     }
-    
+
     private fun updateNotification(currentTime: Long) {
         val timeText = formatTime(currentTime)
         val modeText = if (timerMode == 0) "计时中" else "倒计时"
         val statusText = if (isTimerPaused) "已暂停" else "运行中"
-        
+
         val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
             .setContentTitle("NoMo Service $modeText")
             .setContentText("$timeText - $statusText")
@@ -405,18 +408,18 @@ class XRayService : VpnService() {
             .setShowWhen(false)
             .setOnlyAlertOnce(true)
             .build()
-        
+
         val notificationManager = getSystemService(NotificationManager::class.java)
         notificationManager?.notify(1, notification)
     }
-    
+
     private fun formatTime(timeMs: Long): String {
         val totalSeconds = Math.abs(timeMs) / 1000
         val days = totalSeconds / 86400 // 86400 = 24 * 3600
         val hours = (totalSeconds % 86400) / 3600
         val minutes = (totalSeconds % 3600) / 60
         val seconds = totalSeconds % 60
-        
+
         return if (days > 0) {
             String.format("%d 天 %02d:%02d:%02d", days, hours, minutes, seconds)
         } else if (hours > 0) {
@@ -456,13 +459,13 @@ class XRayService : VpnService() {
                 .build()
 
             VLog.i(TAG, "通知创建成功,准备启动前台服务")
-            
+
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                 try {
-                    startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED)
+                    startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
                     VLog.i(TAG, "前台服务已启动 (Android 14+)")
                 } catch (e: SecurityException) {
-                    VLog.w(TAG, "使用 SYSTEM_EXEMPTED 类型失败,降级为普通前台服务", e)
+                    VLog.w(TAG, "使用 SPECIAL_USE 类型失败,降级为普通前台服务", e)
                     startForeground(1, notification)
                     VLog.i(TAG, "前台服务已启动 (降级模式)")
                 }

+ 7 - 1
android/app/src/main/res/values-night/styles.xml

@@ -17,6 +17,12 @@
 
          This Theme is only used starting with V2 of Flutter's Android embedding. -->
     <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
-        <item name="android:windowBackground">?android:colorBackground</item>
+        <!-- 设置窗口背景色为黑色,与 Flutter 暗色主题的 scaffoldBackgroundColor 一致 -->
+        <!-- 解决软键盘收起时键盘区域短暂黑屏的问题 -->
+        <item name="android:windowBackground">@android:color/black</item>
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
     </style>
 </resources>

+ 3 - 1
android/app/src/main/res/values/styles.xml

@@ -17,7 +17,9 @@
 
          This Theme is only used starting with V2 of Flutter's Android embedding. -->
     <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
-        <item name="android:windowBackground">?android:colorBackground</item>
+        <!-- 设置窗口背景色为白色,与 Flutter 亮色主题的 scaffoldBackgroundColor 一致 -->
+        <!-- 解决软键盘收起时键盘区域短暂黑屏的问题 -->
+        <item name="android:windowBackground">@android:color/white</item>
         <item name="android:forceDarkAllowed">false</item>
         <item name="android:windowFullscreen">false</item>
         <item name="android:windowDrawsSystemBarBackgrounds">false</item>

+ 62 - 5
assets/flags/um.svg

@@ -1,9 +1,66 @@
-<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-um" viewBox="0 0 640 480">
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
   <path fill="#bd3d44" d="M0 0h640v480H0"/>
   <path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
   <path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
-  <marker id="um-a" markerHeight="30" markerWidth="30">
-    <path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
-  </marker>
-  <path fill="none" marker-mid="url(#um-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/>
+  <g fill="#fff">
+    <!-- Row 1: 6 stars -->
+    <path d="m30 11 3.09 9.51H43l-8.04 5.84 3.07 9.51L30 30.02l-8.04 5.84 3.07-9.51L17 20.51h9.91z"/>
+    <path d="m91 11 3.09 9.51H104l-8.04 5.84 3.07 9.51L91 30.02l-8.04 5.84 3.07-9.51L78 20.51h9.91z"/>
+    <path d="m152 11 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 11 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 11 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 11 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 2: 5 stars -->
+    <path d="m60.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 3: 6 stars -->
+    <path d="m30 63 3.09 9.51H43l-8.04 5.84 3.07 9.51L30 82.02l-8.04 5.84 3.07-9.51L17 72.51h9.91z"/>
+    <path d="m91 63 3.09 9.51H104l-8.04 5.84 3.07 9.51L91 82.02l-8.04 5.84 3.07-9.51L78 72.51h9.91z"/>
+    <path d="m152 63 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 63 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 63 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 63 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 4: 5 stars -->
+    <path d="m60.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 5: 6 stars -->
+    <path d="m30 115 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 115 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 115 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 115 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 115 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 115 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 6: 5 stars -->
+    <path d="m60.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 7: 6 stars -->
+    <path d="m30 167 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 167 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 167 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 167 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 167 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 167 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 8: 5 stars -->
+    <path d="m60.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 9: 6 stars -->
+    <path d="m30 219 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 219 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 219 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 219 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 219 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 219 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+  </g>
 </svg>

+ 62 - 5
assets/flags/us.svg

@@ -1,9 +1,66 @@
-<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
   <path fill="#bd3d44" d="M0 0h640v480H0"/>
   <path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
   <path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
-  <marker id="us-a" markerHeight="30" markerWidth="30">
-    <path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
-  </marker>
-  <path fill="none" marker-mid="url(#us-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/>
+  <g fill="#fff">
+    <!-- Row 1: 6 stars -->
+    <path d="m30 11 3.09 9.51H43l-8.04 5.84 3.07 9.51L30 30.02l-8.04 5.84 3.07-9.51L17 20.51h9.91z"/>
+    <path d="m91 11 3.09 9.51H104l-8.04 5.84 3.07 9.51L91 30.02l-8.04 5.84 3.07-9.51L78 20.51h9.91z"/>
+    <path d="m152 11 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 11 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 11 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 11 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 2: 5 stars -->
+    <path d="m60.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 37 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 3: 6 stars -->
+    <path d="m30 63 3.09 9.51H43l-8.04 5.84 3.07 9.51L30 82.02l-8.04 5.84 3.07-9.51L17 72.51h9.91z"/>
+    <path d="m91 63 3.09 9.51H104l-8.04 5.84 3.07 9.51L91 82.02l-8.04 5.84 3.07-9.51L78 72.51h9.91z"/>
+    <path d="m152 63 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 63 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 63 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 63 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 4: 5 stars -->
+    <path d="m60.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 89 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 5: 6 stars -->
+    <path d="m30 115 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 115 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 115 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 115 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 115 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 115 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 6: 5 stars -->
+    <path d="m60.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 141 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 7: 6 stars -->
+    <path d="m30 167 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 167 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 167 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 167 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 167 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 167 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 8: 5 stars -->
+    <path d="m60.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m121.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m182.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m243.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m304.5 193 3.09 9.51h9.91l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <!-- Row 9: 6 stars -->
+    <path d="m30 219 3.09 9.51H43l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m91 219 3.09 9.51H104l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m152 219 3.09 9.51H165l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m213 219 3.09 9.51H226l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m274 219 3.09 9.51H287l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+    <path d="m335 219 3.09 9.51H348l-8.04 5.84 3.07 9.51-8.03-5.84-8.04 5.84 3.07-9.51-8.03-5.84h9.91z"/>
+  </g>
 </svg>

+ 15 - 0
lib/app/api/core/api_core.dart

@@ -346,4 +346,19 @@ class ApiCore extends BaseApi {
   Future<ApiResult> getLocations(dynamic data) async {
     return post(ApiCorePaths.getLocations, data: data);
   }
+
+  /// 获取套餐列表
+  Future<ApiResult> getChannelPlanList(dynamic data) async {
+    return post(ApiCorePaths.channelPlanList, data: data);
+  }
+
+  /// 购买套餐
+  Future<ApiResult> subscribe(dynamic data) async {
+    return post(ApiCorePaths.subscribe, data: data);
+  }
+
+  /// 恢复购买
+  Future<ApiResult> restore(dynamic data) async {
+    return post(ApiCorePaths.restore, data: data);
+  }
 }

+ 10 - 1
lib/app/api/core/api_core_paths.dart

@@ -74,8 +74,17 @@ class ApiCorePaths {
   static const String uploadVideo = '$_ver/issue/uploadVideo';
 
   /// 获取调度信息
-  static const String getDispatchInfo = '$_ver/app/getNodes';
+  static const String getDispatchInfo = '$_ver/router/getNodes';
 
   /// 获取所有节点
   static const String getLocations = '$_ver/app/getLocations';
+
+  /// 套餐列表
+  static const String channelPlanList = '$_ver/pay/channelPlanList';
+
+  /// 购买套餐
+  static const String subscribe = '$_ver/pay/subscribe';
+
+  /// 恢复购买
+  static const String restore = '$_ver/pay/restore';
 }

+ 2 - 0
lib/app/constants/enums.dart

@@ -57,6 +57,8 @@ enum FirebaseEvent {
   cancelBoost,
   errorBoost,
   stopBoost,
+  subscribe,
+  restore,
 }
 
 enum ConnectionState {

+ 66 - 0
lib/app/controllers/api_controller.dart

@@ -27,6 +27,7 @@ import '../constants/enums.dart';
 import '../constants/errors.dart';
 import '../constants/platforms.dart';
 import '../data/models/api_exception.dart';
+import '../data/models/channelplan/channel_plan_list.dart';
 import '../data/models/failure.dart';
 import '../data/models/fingerprint.dart';
 import '../data/models/launch/groups.dart';
@@ -645,6 +646,7 @@ class ApiController extends GetxService {
           );
         }
         final groups = Groups.fromJson(result.data);
+        await IXSP.saveGroups(groups);
         return groups;
       } on ApiException catch (_) {
         rethrow;
@@ -677,4 +679,68 @@ class ApiController extends GetxService {
       }
     }
   }
+
+  Future<List<ChannelPlan>> getChannelPlanList() async {
+    try {
+      final request = fp.toJson();
+      final result = await ApiCore().getChannelPlanList(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final channelPlanList = ChannelPlanList.fromJson(result.data);
+      return channelPlanList.list ?? [];
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<Launch> subscribe(Map<String, dynamic> params) async {
+    try {
+      final request = fp.toJson();
+      request.addAll(params);
+      final result = await ApiCore().subscribe(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final launchData = Launch.fromJson(result.data);
+
+      // 登出成功后上报firebase登出事件
+      sendAnalytics(FirebaseEvent.subscribe);
+
+      // 保存 Launch 数据
+      await IXSP.saveLaunch(launchData);
+      return launchData;
+    } catch (e) {
+      rethrow;
+    }
+  }
+
+  Future<Launch> restore() async {
+    try {
+      final request = fp.toJson();
+      final result = await ApiCore().restore(request);
+      if (!result.success) {
+        throw Failure(
+          code: result.errorCode ?? '',
+          message: result.errorMessage ?? '',
+        );
+      }
+      final launchData = Launch.fromJson(result.data);
+
+      // 登出成功后上报firebase登出事件
+      sendAnalytics(FirebaseEvent.restore);
+
+      // 保存 Launch 数据
+      await IXSP.saveLaunch(launchData);
+      return launchData;
+    } catch (e) {
+      rethrow;
+    }
+  }
 }

+ 77 - 4
lib/app/controllers/core_controller.dart

@@ -1,14 +1,19 @@
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io';
 
+import 'package:device_info_plus/device_info_plus.dart';
 import 'package:dio/dio.dart';
 import 'package:get/get.dart';
+import 'package:package_info_plus/package_info_plus.dart';
 import 'package:uuid/uuid.dart';
 
 import '../../config/translations/strings_enum.dart';
 import '../../pigeons/core_api.g.dart';
+import '../../utils/boost_report_manager.dart';
 import '../../utils/haptic_feedback_manager.dart';
 import '../../utils/log/logger.dart';
+import '../../utils/network_helper.dart';
 import '../components/ix_snackbar.dart';
 import '../constants/enums.dart';
 import '../data/models/api_exception.dart';
@@ -38,6 +43,11 @@ class CoreController extends GetxService {
 
   CancelToken? _cancelToken;
 
+  //全局uuid
+  final _globalUuid = Uuid().v4();
+
+  String locationSelectionType = 'auto';
+
   @override
   void onInit() {
     super.onInit();
@@ -99,7 +109,8 @@ class CoreController extends GetxService {
     // 创建新的 CancelToken
     final currentToken = CancelToken();
     _cancelToken = currentToken;
-
+    // 创建一条加速日志
+    await createBoostLog();
     try {
       final locationId = IXSP.getSelectedLocation()?['id'];
       final locationCode = IXSP.getSelectedLocation()?['code'];
@@ -205,7 +216,7 @@ class CoreController extends GetxService {
         break;
       case VpnStatus.error:
         // error
-        _onVpnError();
+        _onVpnError(message.message);
         break;
       case VpnStatus.serviceDisconnected:
         // service disconnected
@@ -261,13 +272,15 @@ class CoreController extends GetxService {
     HapticFeedbackManager.connectionSuccess();
   }
 
-  void _onVpnError() {
+  void _onVpnError(String message) {
     log(TAG, 'VPN连接错误');
     // 显示错误信息
     state = ConnectionState.disconnected;
     timer = "00:00:00";
     HapticFeedbackManager.connectionDisconnected();
-    ErrorDialog.show(message: Strings.vpnConnectionError.tr);
+    ErrorDialog.show(
+      message: message == 'null' ? Strings.vpnConnectionError.tr : message,
+    );
   }
 
   void _onVpnServiceDisconnected() {
@@ -390,4 +403,64 @@ class CoreController extends GetxService {
       ErrorDialog.show(title: Strings.error.tr, message: error.toString());
     }
   }
+
+  // 创建一条加速日志
+  Future<void> createBoostLog() async {
+    await initLog();
+    await setSessionInfoLog();
+    await setTargetInfoLog();
+  }
+
+  // 初始化日志
+  Future<void> initLog() async {
+    await BoostReportManager().init();
+  }
+
+  // 读取历史日志
+  Future<void> readHistoryLog() async {
+    await BoostReportManager().readHistoryLog();
+  }
+
+  // 初始化会话日志
+  Future<void> setSessionInfoLog() async {
+    final deviceInfoPlugin = DeviceInfoPlugin();
+    final appVersion = await PackageInfo.fromPlatform().then(
+      (value) => value.version,
+    );
+    final networkType = await NetworkHelper.instance.getNetworkType();
+    Map<String, String> deviceInfo = {};
+    if (Platform.isIOS) {
+      final iosOsInfo = await deviceInfoPlugin.iosInfo;
+      deviceInfo = {
+        'deviceModel': iosOsInfo.model,
+        'osVersion': iosOsInfo.systemVersion,
+        'appVersion': appVersion,
+        'networkType': networkType,
+        'deviceBrand': iosOsInfo.utsname.machine,
+      };
+    } else if (Platform.isAndroid) {
+      final androidOsInfo = await deviceInfoPlugin.androidInfo;
+      deviceInfo = {
+        'deviceModel': androidOsInfo.model,
+        'osVersion': androidOsInfo.version.release,
+        'appVersion': appVersion,
+        'networkType': networkType,
+        'deviceBrand': androidOsInfo.brand,
+      };
+    }
+    final boostSessionId = Uuid().v4();
+    await BoostReportManager().initSessionInfo(
+      appSessionId: _globalUuid,
+      boostSessionId: boostSessionId,
+      deviceInfo: deviceInfo,
+    );
+  }
+
+  // 初始化目标信息
+  Future<void> setTargetInfoLog() async {
+    await BoostReportManager().addTargetInfo(
+      locationSelectionType: locationSelectionType,
+      location: IXSP.getSelectedLocation(),
+    );
+  }
 }

+ 39 - 0
lib/app/data/models/channelplan/channel_plan_list.dart

@@ -0,0 +1,39 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter/foundation.dart';
+part 'channel_plan_list.freezed.dart';
+part 'channel_plan_list.g.dart';
+
+@freezed
+abstract class ChannelPlanList with _$ChannelPlanList {
+  const factory ChannelPlanList({List<ChannelPlan>? list}) = _ChannelPlanList;
+
+  factory ChannelPlanList.fromJson(Map<String, Object?> json) =>
+      _$ChannelPlanListFromJson(json);
+}
+
+@freezed
+abstract class ChannelPlan with _$ChannelPlan {
+  const factory ChannelPlan({
+    String? channelItemId,
+    String? title,
+    String? subTitle,
+    String? introduce,
+    double? orgPrice,
+    double? price,
+    String? tag,
+    int? tagType,
+    int? currency,
+    bool? recommend,
+    bool? isDefault,
+    int? sort,
+    int? deviceLimit,
+    bool? isSubscribe,
+    int? subscribeType,
+    int? subscribePeriodValue,
+    String? payoutType,
+    String? payoutData,
+  }) = _ChannelPlan;
+
+  factory ChannelPlan.fromJson(Map<String, Object?> json) =>
+      _$ChannelPlanFromJson(json);
+}

+ 750 - 0
lib/app/data/models/channelplan/channel_plan_list.freezed.dart

@@ -0,0 +1,750 @@
+// 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 'channel_plan_list.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',
+);
+
+ChannelPlanList _$ChannelPlanListFromJson(Map<String, dynamic> json) {
+  return _ChannelPlanList.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ChannelPlanList {
+  List<ChannelPlan>? get list => throw _privateConstructorUsedError;
+
+  /// Serializes this ChannelPlanList to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of ChannelPlanList
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $ChannelPlanListCopyWith<ChannelPlanList> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ChannelPlanListCopyWith<$Res> {
+  factory $ChannelPlanListCopyWith(
+    ChannelPlanList value,
+    $Res Function(ChannelPlanList) then,
+  ) = _$ChannelPlanListCopyWithImpl<$Res, ChannelPlanList>;
+  @useResult
+  $Res call({List<ChannelPlan>? list});
+}
+
+/// @nodoc
+class _$ChannelPlanListCopyWithImpl<$Res, $Val extends ChannelPlanList>
+    implements $ChannelPlanListCopyWith<$Res> {
+  _$ChannelPlanListCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of ChannelPlanList
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({Object? list = freezed}) {
+    return _then(
+      _value.copyWith(
+            list: freezed == list
+                ? _value.list
+                : list // ignore: cast_nullable_to_non_nullable
+                      as List<ChannelPlan>?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$ChannelPlanListImplCopyWith<$Res>
+    implements $ChannelPlanListCopyWith<$Res> {
+  factory _$$ChannelPlanListImplCopyWith(
+    _$ChannelPlanListImpl value,
+    $Res Function(_$ChannelPlanListImpl) then,
+  ) = __$$ChannelPlanListImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({List<ChannelPlan>? list});
+}
+
+/// @nodoc
+class __$$ChannelPlanListImplCopyWithImpl<$Res>
+    extends _$ChannelPlanListCopyWithImpl<$Res, _$ChannelPlanListImpl>
+    implements _$$ChannelPlanListImplCopyWith<$Res> {
+  __$$ChannelPlanListImplCopyWithImpl(
+    _$ChannelPlanListImpl _value,
+    $Res Function(_$ChannelPlanListImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of ChannelPlanList
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({Object? list = freezed}) {
+    return _then(
+      _$ChannelPlanListImpl(
+        list: freezed == list
+            ? _value._list
+            : list // ignore: cast_nullable_to_non_nullable
+                  as List<ChannelPlan>?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ChannelPlanListImpl
+    with DiagnosticableTreeMixin
+    implements _ChannelPlanList {
+  const _$ChannelPlanListImpl({final List<ChannelPlan>? list}) : _list = list;
+
+  factory _$ChannelPlanListImpl.fromJson(Map<String, dynamic> json) =>
+      _$$ChannelPlanListImplFromJson(json);
+
+  final List<ChannelPlan>? _list;
+  @override
+  List<ChannelPlan>? get list {
+    final value = _list;
+    if (value == null) return null;
+    if (_list is EqualUnmodifiableListView) return _list;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'ChannelPlanList(list: $list)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'ChannelPlanList'))
+      ..add(DiagnosticsProperty('list', list));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$ChannelPlanListImpl &&
+            const DeepCollectionEquality().equals(other._list, _list));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode =>
+      Object.hash(runtimeType, const DeepCollectionEquality().hash(_list));
+
+  /// Create a copy of ChannelPlanList
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$ChannelPlanListImplCopyWith<_$ChannelPlanListImpl> get copyWith =>
+      __$$ChannelPlanListImplCopyWithImpl<_$ChannelPlanListImpl>(
+        this,
+        _$identity,
+      );
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$ChannelPlanListImplToJson(this);
+  }
+}
+
+abstract class _ChannelPlanList implements ChannelPlanList {
+  const factory _ChannelPlanList({final List<ChannelPlan>? list}) =
+      _$ChannelPlanListImpl;
+
+  factory _ChannelPlanList.fromJson(Map<String, dynamic> json) =
+      _$ChannelPlanListImpl.fromJson;
+
+  @override
+  List<ChannelPlan>? get list;
+
+  /// Create a copy of ChannelPlanList
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$ChannelPlanListImplCopyWith<_$ChannelPlanListImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+ChannelPlan _$ChannelPlanFromJson(Map<String, dynamic> json) {
+  return _ChannelPlan.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ChannelPlan {
+  String? get channelItemId => throw _privateConstructorUsedError;
+  String? get title => throw _privateConstructorUsedError;
+  String? get subTitle => throw _privateConstructorUsedError;
+  String? get introduce => throw _privateConstructorUsedError;
+  double? get orgPrice => throw _privateConstructorUsedError;
+  double? get price => throw _privateConstructorUsedError;
+  String? get tag => throw _privateConstructorUsedError;
+  int? get tagType => throw _privateConstructorUsedError;
+  int? get currency => throw _privateConstructorUsedError;
+  bool? get recommend => throw _privateConstructorUsedError;
+  bool? get isDefault => throw _privateConstructorUsedError;
+  int? get sort => throw _privateConstructorUsedError;
+  int? get deviceLimit => throw _privateConstructorUsedError;
+  bool? get isSubscribe => throw _privateConstructorUsedError;
+  int? get subscribeType => throw _privateConstructorUsedError;
+  int? get subscribePeriodValue => throw _privateConstructorUsedError;
+  String? get payoutType => throw _privateConstructorUsedError;
+  String? get payoutData => throw _privateConstructorUsedError;
+
+  /// Serializes this ChannelPlan to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of ChannelPlan
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $ChannelPlanCopyWith<ChannelPlan> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ChannelPlanCopyWith<$Res> {
+  factory $ChannelPlanCopyWith(
+    ChannelPlan value,
+    $Res Function(ChannelPlan) then,
+  ) = _$ChannelPlanCopyWithImpl<$Res, ChannelPlan>;
+  @useResult
+  $Res call({
+    String? channelItemId,
+    String? title,
+    String? subTitle,
+    String? introduce,
+    double? orgPrice,
+    double? price,
+    String? tag,
+    int? tagType,
+    int? currency,
+    bool? recommend,
+    bool? isDefault,
+    int? sort,
+    int? deviceLimit,
+    bool? isSubscribe,
+    int? subscribeType,
+    int? subscribePeriodValue,
+    String? payoutType,
+    String? payoutData,
+  });
+}
+
+/// @nodoc
+class _$ChannelPlanCopyWithImpl<$Res, $Val extends ChannelPlan>
+    implements $ChannelPlanCopyWith<$Res> {
+  _$ChannelPlanCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of ChannelPlan
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? channelItemId = freezed,
+    Object? title = freezed,
+    Object? subTitle = freezed,
+    Object? introduce = freezed,
+    Object? orgPrice = freezed,
+    Object? price = freezed,
+    Object? tag = freezed,
+    Object? tagType = freezed,
+    Object? currency = freezed,
+    Object? recommend = freezed,
+    Object? isDefault = freezed,
+    Object? sort = freezed,
+    Object? deviceLimit = freezed,
+    Object? isSubscribe = freezed,
+    Object? subscribeType = freezed,
+    Object? subscribePeriodValue = freezed,
+    Object? payoutType = freezed,
+    Object? payoutData = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            channelItemId: freezed == channelItemId
+                ? _value.channelItemId
+                : channelItemId // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            title: freezed == title
+                ? _value.title
+                : title // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            subTitle: freezed == subTitle
+                ? _value.subTitle
+                : subTitle // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            introduce: freezed == introduce
+                ? _value.introduce
+                : introduce // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            orgPrice: freezed == orgPrice
+                ? _value.orgPrice
+                : orgPrice // ignore: cast_nullable_to_non_nullable
+                      as double?,
+            price: freezed == price
+                ? _value.price
+                : price // ignore: cast_nullable_to_non_nullable
+                      as double?,
+            tag: freezed == tag
+                ? _value.tag
+                : tag // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            tagType: freezed == tagType
+                ? _value.tagType
+                : tagType // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            currency: freezed == currency
+                ? _value.currency
+                : currency // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            recommend: freezed == recommend
+                ? _value.recommend
+                : recommend // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            isDefault: freezed == isDefault
+                ? _value.isDefault
+                : isDefault // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            sort: freezed == sort
+                ? _value.sort
+                : sort // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            deviceLimit: freezed == deviceLimit
+                ? _value.deviceLimit
+                : deviceLimit // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            isSubscribe: freezed == isSubscribe
+                ? _value.isSubscribe
+                : isSubscribe // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            subscribeType: freezed == subscribeType
+                ? _value.subscribeType
+                : subscribeType // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            subscribePeriodValue: freezed == subscribePeriodValue
+                ? _value.subscribePeriodValue
+                : subscribePeriodValue // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            payoutType: freezed == payoutType
+                ? _value.payoutType
+                : payoutType // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            payoutData: freezed == payoutData
+                ? _value.payoutData
+                : payoutData // ignore: cast_nullable_to_non_nullable
+                      as String?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$ChannelPlanImplCopyWith<$Res>
+    implements $ChannelPlanCopyWith<$Res> {
+  factory _$$ChannelPlanImplCopyWith(
+    _$ChannelPlanImpl value,
+    $Res Function(_$ChannelPlanImpl) then,
+  ) = __$$ChannelPlanImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    String? channelItemId,
+    String? title,
+    String? subTitle,
+    String? introduce,
+    double? orgPrice,
+    double? price,
+    String? tag,
+    int? tagType,
+    int? currency,
+    bool? recommend,
+    bool? isDefault,
+    int? sort,
+    int? deviceLimit,
+    bool? isSubscribe,
+    int? subscribeType,
+    int? subscribePeriodValue,
+    String? payoutType,
+    String? payoutData,
+  });
+}
+
+/// @nodoc
+class __$$ChannelPlanImplCopyWithImpl<$Res>
+    extends _$ChannelPlanCopyWithImpl<$Res, _$ChannelPlanImpl>
+    implements _$$ChannelPlanImplCopyWith<$Res> {
+  __$$ChannelPlanImplCopyWithImpl(
+    _$ChannelPlanImpl _value,
+    $Res Function(_$ChannelPlanImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of ChannelPlan
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? channelItemId = freezed,
+    Object? title = freezed,
+    Object? subTitle = freezed,
+    Object? introduce = freezed,
+    Object? orgPrice = freezed,
+    Object? price = freezed,
+    Object? tag = freezed,
+    Object? tagType = freezed,
+    Object? currency = freezed,
+    Object? recommend = freezed,
+    Object? isDefault = freezed,
+    Object? sort = freezed,
+    Object? deviceLimit = freezed,
+    Object? isSubscribe = freezed,
+    Object? subscribeType = freezed,
+    Object? subscribePeriodValue = freezed,
+    Object? payoutType = freezed,
+    Object? payoutData = freezed,
+  }) {
+    return _then(
+      _$ChannelPlanImpl(
+        channelItemId: freezed == channelItemId
+            ? _value.channelItemId
+            : channelItemId // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        title: freezed == title
+            ? _value.title
+            : title // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        subTitle: freezed == subTitle
+            ? _value.subTitle
+            : subTitle // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        introduce: freezed == introduce
+            ? _value.introduce
+            : introduce // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        orgPrice: freezed == orgPrice
+            ? _value.orgPrice
+            : orgPrice // ignore: cast_nullable_to_non_nullable
+                  as double?,
+        price: freezed == price
+            ? _value.price
+            : price // ignore: cast_nullable_to_non_nullable
+                  as double?,
+        tag: freezed == tag
+            ? _value.tag
+            : tag // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        tagType: freezed == tagType
+            ? _value.tagType
+            : tagType // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        currency: freezed == currency
+            ? _value.currency
+            : currency // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        recommend: freezed == recommend
+            ? _value.recommend
+            : recommend // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        isDefault: freezed == isDefault
+            ? _value.isDefault
+            : isDefault // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        sort: freezed == sort
+            ? _value.sort
+            : sort // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        deviceLimit: freezed == deviceLimit
+            ? _value.deviceLimit
+            : deviceLimit // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        isSubscribe: freezed == isSubscribe
+            ? _value.isSubscribe
+            : isSubscribe // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        subscribeType: freezed == subscribeType
+            ? _value.subscribeType
+            : subscribeType // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        subscribePeriodValue: freezed == subscribePeriodValue
+            ? _value.subscribePeriodValue
+            : subscribePeriodValue // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        payoutType: freezed == payoutType
+            ? _value.payoutType
+            : payoutType // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        payoutData: freezed == payoutData
+            ? _value.payoutData
+            : payoutData // ignore: cast_nullable_to_non_nullable
+                  as String?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ChannelPlanImpl with DiagnosticableTreeMixin implements _ChannelPlan {
+  const _$ChannelPlanImpl({
+    this.channelItemId,
+    this.title,
+    this.subTitle,
+    this.introduce,
+    this.orgPrice,
+    this.price,
+    this.tag,
+    this.tagType,
+    this.currency,
+    this.recommend,
+    this.isDefault,
+    this.sort,
+    this.deviceLimit,
+    this.isSubscribe,
+    this.subscribeType,
+    this.subscribePeriodValue,
+    this.payoutType,
+    this.payoutData,
+  });
+
+  factory _$ChannelPlanImpl.fromJson(Map<String, dynamic> json) =>
+      _$$ChannelPlanImplFromJson(json);
+
+  @override
+  final String? channelItemId;
+  @override
+  final String? title;
+  @override
+  final String? subTitle;
+  @override
+  final String? introduce;
+  @override
+  final double? orgPrice;
+  @override
+  final double? price;
+  @override
+  final String? tag;
+  @override
+  final int? tagType;
+  @override
+  final int? currency;
+  @override
+  final bool? recommend;
+  @override
+  final bool? isDefault;
+  @override
+  final int? sort;
+  @override
+  final int? deviceLimit;
+  @override
+  final bool? isSubscribe;
+  @override
+  final int? subscribeType;
+  @override
+  final int? subscribePeriodValue;
+  @override
+  final String? payoutType;
+  @override
+  final String? payoutData;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'ChannelPlan(channelItemId: $channelItemId, title: $title, subTitle: $subTitle, introduce: $introduce, orgPrice: $orgPrice, price: $price, tag: $tag, tagType: $tagType, currency: $currency, recommend: $recommend, isDefault: $isDefault, sort: $sort, deviceLimit: $deviceLimit, isSubscribe: $isSubscribe, subscribeType: $subscribeType, subscribePeriodValue: $subscribePeriodValue, payoutType: $payoutType, payoutData: $payoutData)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'ChannelPlan'))
+      ..add(DiagnosticsProperty('channelItemId', channelItemId))
+      ..add(DiagnosticsProperty('title', title))
+      ..add(DiagnosticsProperty('subTitle', subTitle))
+      ..add(DiagnosticsProperty('introduce', introduce))
+      ..add(DiagnosticsProperty('orgPrice', orgPrice))
+      ..add(DiagnosticsProperty('price', price))
+      ..add(DiagnosticsProperty('tag', tag))
+      ..add(DiagnosticsProperty('tagType', tagType))
+      ..add(DiagnosticsProperty('currency', currency))
+      ..add(DiagnosticsProperty('recommend', recommend))
+      ..add(DiagnosticsProperty('isDefault', isDefault))
+      ..add(DiagnosticsProperty('sort', sort))
+      ..add(DiagnosticsProperty('deviceLimit', deviceLimit))
+      ..add(DiagnosticsProperty('isSubscribe', isSubscribe))
+      ..add(DiagnosticsProperty('subscribeType', subscribeType))
+      ..add(DiagnosticsProperty('subscribePeriodValue', subscribePeriodValue))
+      ..add(DiagnosticsProperty('payoutType', payoutType))
+      ..add(DiagnosticsProperty('payoutData', payoutData));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$ChannelPlanImpl &&
+            (identical(other.channelItemId, channelItemId) ||
+                other.channelItemId == channelItemId) &&
+            (identical(other.title, title) || other.title == title) &&
+            (identical(other.subTitle, subTitle) ||
+                other.subTitle == subTitle) &&
+            (identical(other.introduce, introduce) ||
+                other.introduce == introduce) &&
+            (identical(other.orgPrice, orgPrice) ||
+                other.orgPrice == orgPrice) &&
+            (identical(other.price, price) || other.price == price) &&
+            (identical(other.tag, tag) || other.tag == tag) &&
+            (identical(other.tagType, tagType) || other.tagType == tagType) &&
+            (identical(other.currency, currency) ||
+                other.currency == currency) &&
+            (identical(other.recommend, recommend) ||
+                other.recommend == recommend) &&
+            (identical(other.isDefault, isDefault) ||
+                other.isDefault == isDefault) &&
+            (identical(other.sort, sort) || other.sort == sort) &&
+            (identical(other.deviceLimit, deviceLimit) ||
+                other.deviceLimit == deviceLimit) &&
+            (identical(other.isSubscribe, isSubscribe) ||
+                other.isSubscribe == isSubscribe) &&
+            (identical(other.subscribeType, subscribeType) ||
+                other.subscribeType == subscribeType) &&
+            (identical(other.subscribePeriodValue, subscribePeriodValue) ||
+                other.subscribePeriodValue == subscribePeriodValue) &&
+            (identical(other.payoutType, payoutType) ||
+                other.payoutType == payoutType) &&
+            (identical(other.payoutData, payoutData) ||
+                other.payoutData == payoutData));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    channelItemId,
+    title,
+    subTitle,
+    introduce,
+    orgPrice,
+    price,
+    tag,
+    tagType,
+    currency,
+    recommend,
+    isDefault,
+    sort,
+    deviceLimit,
+    isSubscribe,
+    subscribeType,
+    subscribePeriodValue,
+    payoutType,
+    payoutData,
+  );
+
+  /// Create a copy of ChannelPlan
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$ChannelPlanImplCopyWith<_$ChannelPlanImpl> get copyWith =>
+      __$$ChannelPlanImplCopyWithImpl<_$ChannelPlanImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$ChannelPlanImplToJson(this);
+  }
+}
+
+abstract class _ChannelPlan implements ChannelPlan {
+  const factory _ChannelPlan({
+    final String? channelItemId,
+    final String? title,
+    final String? subTitle,
+    final String? introduce,
+    final double? orgPrice,
+    final double? price,
+    final String? tag,
+    final int? tagType,
+    final int? currency,
+    final bool? recommend,
+    final bool? isDefault,
+    final int? sort,
+    final int? deviceLimit,
+    final bool? isSubscribe,
+    final int? subscribeType,
+    final int? subscribePeriodValue,
+    final String? payoutType,
+    final String? payoutData,
+  }) = _$ChannelPlanImpl;
+
+  factory _ChannelPlan.fromJson(Map<String, dynamic> json) =
+      _$ChannelPlanImpl.fromJson;
+
+  @override
+  String? get channelItemId;
+  @override
+  String? get title;
+  @override
+  String? get subTitle;
+  @override
+  String? get introduce;
+  @override
+  double? get orgPrice;
+  @override
+  double? get price;
+  @override
+  String? get tag;
+  @override
+  int? get tagType;
+  @override
+  int? get currency;
+  @override
+  bool? get recommend;
+  @override
+  bool? get isDefault;
+  @override
+  int? get sort;
+  @override
+  int? get deviceLimit;
+  @override
+  bool? get isSubscribe;
+  @override
+  int? get subscribeType;
+  @override
+  int? get subscribePeriodValue;
+  @override
+  String? get payoutType;
+  @override
+  String? get payoutData;
+
+  /// Create a copy of ChannelPlan
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$ChannelPlanImplCopyWith<_$ChannelPlanImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 63 - 0
lib/app/data/models/channelplan/channel_plan_list.g.dart

@@ -0,0 +1,63 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'channel_plan_list.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$ChannelPlanListImpl _$$ChannelPlanListImplFromJson(
+  Map<String, dynamic> json,
+) => _$ChannelPlanListImpl(
+  list: (json['list'] as List<dynamic>?)
+      ?.map((e) => ChannelPlan.fromJson(e as Map<String, dynamic>))
+      .toList(),
+);
+
+Map<String, dynamic> _$$ChannelPlanListImplToJson(
+  _$ChannelPlanListImpl instance,
+) => <String, dynamic>{'list': instance.list};
+
+_$ChannelPlanImpl _$$ChannelPlanImplFromJson(Map<String, dynamic> json) =>
+    _$ChannelPlanImpl(
+      channelItemId: json['channelItemId'] as String?,
+      title: json['title'] as String?,
+      subTitle: json['subTitle'] as String?,
+      introduce: json['introduce'] as String?,
+      orgPrice: (json['orgPrice'] as num?)?.toDouble(),
+      price: (json['price'] as num?)?.toDouble(),
+      tag: json['tag'] as String?,
+      tagType: (json['tagType'] as num?)?.toInt(),
+      currency: (json['currency'] as num?)?.toInt(),
+      recommend: json['recommend'] as bool?,
+      isDefault: json['isDefault'] as bool?,
+      sort: (json['sort'] as num?)?.toInt(),
+      deviceLimit: (json['deviceLimit'] as num?)?.toInt(),
+      isSubscribe: json['isSubscribe'] as bool?,
+      subscribeType: (json['subscribeType'] as num?)?.toInt(),
+      subscribePeriodValue: (json['subscribePeriodValue'] as num?)?.toInt(),
+      payoutType: json['payoutType'] as String?,
+      payoutData: json['payoutData'] as String?,
+    );
+
+Map<String, dynamic> _$$ChannelPlanImplToJson(_$ChannelPlanImpl instance) =>
+    <String, dynamic>{
+      'channelItemId': instance.channelItemId,
+      'title': instance.title,
+      'subTitle': instance.subTitle,
+      'introduce': instance.introduce,
+      'orgPrice': instance.orgPrice,
+      'price': instance.price,
+      'tag': instance.tag,
+      'tagType': instance.tagType,
+      'currency': instance.currency,
+      'recommend': instance.recommend,
+      'isDefault': instance.isDefault,
+      'sort': instance.sort,
+      'deviceLimit': instance.deviceLimit,
+      'isSubscribe': instance.isSubscribe,
+      'subscribeType': instance.subscribeType,
+      'subscribePeriodValue': instance.subscribePeriodValue,
+      'payoutType': instance.payoutType,
+      'payoutData': instance.payoutData,
+    };

+ 0 - 1
lib/app/data/models/launch/groups.dart

@@ -47,7 +47,6 @@ class Locations with _$Locations {
     String? icon,
     String? country,
     int? sort,
-    int? latency,
   }) = _Locations;
 
   factory Locations.fromJson(Map<String, Object?> json) =>

+ 4 - 25
lib/app/data/models/launch/groups.freezed.dart

@@ -911,7 +911,6 @@ mixin _$Locations {
   String? get icon => throw _privateConstructorUsedError;
   String? get country => throw _privateConstructorUsedError;
   int? get sort => throw _privateConstructorUsedError;
-  int? get latency => throw _privateConstructorUsedError;
 
   /// Serializes this Locations to a JSON map.
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -935,7 +934,6 @@ abstract class $LocationsCopyWith<$Res> {
     String? icon,
     String? country,
     int? sort,
-    int? latency,
   });
 }
 
@@ -960,7 +958,6 @@ class _$LocationsCopyWithImpl<$Res, $Val extends Locations>
     Object? icon = freezed,
     Object? country = freezed,
     Object? sort = freezed,
-    Object? latency = freezed,
   }) {
     return _then(
       _value.copyWith(
@@ -988,10 +985,6 @@ class _$LocationsCopyWithImpl<$Res, $Val extends Locations>
                 ? _value.sort
                 : sort // ignore: cast_nullable_to_non_nullable
                       as int?,
-            latency: freezed == latency
-                ? _value.latency
-                : latency // ignore: cast_nullable_to_non_nullable
-                      as int?,
           )
           as $Val,
     );
@@ -1014,7 +1007,6 @@ abstract class _$$LocationsImplCopyWith<$Res>
     String? icon,
     String? country,
     int? sort,
-    int? latency,
   });
 }
 
@@ -1038,7 +1030,6 @@ class __$$LocationsImplCopyWithImpl<$Res>
     Object? icon = freezed,
     Object? country = freezed,
     Object? sort = freezed,
-    Object? latency = freezed,
   }) {
     return _then(
       _$LocationsImpl(
@@ -1066,10 +1057,6 @@ class __$$LocationsImplCopyWithImpl<$Res>
             ? _value.sort
             : sort // ignore: cast_nullable_to_non_nullable
                   as int?,
-        latency: freezed == latency
-            ? _value.latency
-            : latency // ignore: cast_nullable_to_non_nullable
-                  as int?,
       ),
     );
   }
@@ -1085,7 +1072,6 @@ class _$LocationsImpl with DiagnosticableTreeMixin implements _Locations {
     this.icon,
     this.country,
     this.sort,
-    this.latency,
   });
 
   factory _$LocationsImpl.fromJson(Map<String, dynamic> json) =>
@@ -1103,12 +1089,10 @@ class _$LocationsImpl with DiagnosticableTreeMixin implements _Locations {
   final String? country;
   @override
   final int? sort;
-  @override
-  final int? latency;
 
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'Locations(id: $id, name: $name, code: $code, icon: $icon, country: $country, sort: $sort, latency: $latency)';
+    return 'Locations(id: $id, name: $name, code: $code, icon: $icon, country: $country, sort: $sort)';
   }
 
   @override
@@ -1121,8 +1105,7 @@ class _$LocationsImpl with DiagnosticableTreeMixin implements _Locations {
       ..add(DiagnosticsProperty('code', code))
       ..add(DiagnosticsProperty('icon', icon))
       ..add(DiagnosticsProperty('country', country))
-      ..add(DiagnosticsProperty('sort', sort))
-      ..add(DiagnosticsProperty('latency', latency));
+      ..add(DiagnosticsProperty('sort', sort));
   }
 
   @override
@@ -1135,14 +1118,13 @@ class _$LocationsImpl with DiagnosticableTreeMixin implements _Locations {
             (identical(other.code, code) || other.code == code) &&
             (identical(other.icon, icon) || other.icon == icon) &&
             (identical(other.country, country) || other.country == country) &&
-            (identical(other.sort, sort) || other.sort == sort) &&
-            (identical(other.latency, latency) || other.latency == latency));
+            (identical(other.sort, sort) || other.sort == sort));
   }
 
   @JsonKey(includeFromJson: false, includeToJson: false)
   @override
   int get hashCode =>
-      Object.hash(runtimeType, id, name, code, icon, country, sort, latency);
+      Object.hash(runtimeType, id, name, code, icon, country, sort);
 
   /// Create a copy of Locations
   /// with the given fields replaced by the non-null parameter values.
@@ -1166,7 +1148,6 @@ abstract class _Locations implements Locations {
     final String? icon,
     final String? country,
     final int? sort,
-    final int? latency,
   }) = _$LocationsImpl;
 
   factory _Locations.fromJson(Map<String, dynamic> json) =
@@ -1184,8 +1165,6 @@ abstract class _Locations implements Locations {
   String? get country;
   @override
   int? get sort;
-  @override
-  int? get latency;
 
   /// Create a copy of Locations
   /// with the given fields replaced by the non-null parameter values.

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

@@ -76,7 +76,6 @@ _$LocationsImpl _$$LocationsImplFromJson(Map<String, dynamic> json) =>
       icon: json['icon'] as String?,
       country: json['country'] as String?,
       sort: (json['sort'] as num?)?.toInt(),
-      latency: (json['latency'] as num?)?.toInt(),
     );
 
 Map<String, dynamic> _$$LocationsImplToJson(_$LocationsImpl instance) =>
@@ -87,5 +86,4 @@ Map<String, dynamic> _$$LocationsImplToJson(_$LocationsImpl instance) =>
       'icon': instance.icon,
       'country': instance.country,
       'sort': instance.sort,
-      'latency': instance.latency,
     };

+ 25 - 2
lib/app/data/models/launch/user.dart

@@ -7,17 +7,40 @@ part 'user.g.dart';
 class User with _$User {
   const factory User({
     String? country,
+    String? countryName,
     String? userIp,
     String? accessToken,
     String? refreshToken,
     String? accountKey,
     String? accountPassword,
-    String? createTime,
+    int? createTime, // API 返回 int 类型的时间戳
     bool? geographyEea,
     int? memberLevel, // 会员等级 1 游客 2 普通用户 3 会员
-    String? account, // 用户名
+    int? userLevel,
+    int? expireTime,
+    int? remainTime,
+    bool? isExpired,
+    bool? isTestUser,
+    bool? isSubscribeUser,
+    Account? account, // API 返回对象类型
     bool? activated, // 是否激活
   }) = _User;
 
   factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
 }
+
+@freezed
+class Account with _$Account {
+  const factory Account({
+    String? username,
+    String? phone,
+    String? email,
+    String? wechat,
+    String? qq,
+    String? google,
+    String? apple,
+  }) = _Account;
+
+  factory Account.fromJson(Map<String, Object?> json) =>
+      _$AccountFromJson(json);
+}

+ 502 - 18
lib/app/data/models/launch/user.freezed.dart

@@ -22,16 +22,24 @@ User _$UserFromJson(Map<String, dynamic> json) {
 /// @nodoc
 mixin _$User {
   String? get country => throw _privateConstructorUsedError;
+  String? get countryName => throw _privateConstructorUsedError;
   String? get userIp => throw _privateConstructorUsedError;
   String? get accessToken => throw _privateConstructorUsedError;
   String? get refreshToken => throw _privateConstructorUsedError;
   String? get accountKey => throw _privateConstructorUsedError;
   String? get accountPassword => throw _privateConstructorUsedError;
-  String? get createTime => throw _privateConstructorUsedError;
+  int? get createTime =>
+      throw _privateConstructorUsedError; // API 返回 int 类型的时间戳
   bool? get geographyEea => throw _privateConstructorUsedError;
   int? get memberLevel =>
       throw _privateConstructorUsedError; // 会员等级 1 游客 2 普通用户 3 会员
-  String? get account => throw _privateConstructorUsedError; // 用户名
+  int? get userLevel => throw _privateConstructorUsedError;
+  int? get expireTime => throw _privateConstructorUsedError;
+  int? get remainTime => throw _privateConstructorUsedError;
+  bool? get isExpired => throw _privateConstructorUsedError;
+  bool? get isTestUser => throw _privateConstructorUsedError;
+  bool? get isSubscribeUser => throw _privateConstructorUsedError;
+  Account? get account => throw _privateConstructorUsedError; // API 返回对象类型
   bool? get activated => throw _privateConstructorUsedError;
 
   /// Serializes this User to a JSON map.
@@ -50,17 +58,26 @@ abstract class $UserCopyWith<$Res> {
   @useResult
   $Res call({
     String? country,
+    String? countryName,
     String? userIp,
     String? accessToken,
     String? refreshToken,
     String? accountKey,
     String? accountPassword,
-    String? createTime,
+    int? createTime,
     bool? geographyEea,
     int? memberLevel,
-    String? account,
+    int? userLevel,
+    int? expireTime,
+    int? remainTime,
+    bool? isExpired,
+    bool? isTestUser,
+    bool? isSubscribeUser,
+    Account? account,
     bool? activated,
   });
+
+  $AccountCopyWith<$Res>? get account;
 }
 
 /// @nodoc
@@ -79,6 +96,7 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
   @override
   $Res call({
     Object? country = freezed,
+    Object? countryName = freezed,
     Object? userIp = freezed,
     Object? accessToken = freezed,
     Object? refreshToken = freezed,
@@ -87,6 +105,12 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
     Object? createTime = freezed,
     Object? geographyEea = freezed,
     Object? memberLevel = freezed,
+    Object? userLevel = freezed,
+    Object? expireTime = freezed,
+    Object? remainTime = freezed,
+    Object? isExpired = freezed,
+    Object? isTestUser = freezed,
+    Object? isSubscribeUser = freezed,
     Object? account = freezed,
     Object? activated = freezed,
   }) {
@@ -96,6 +120,10 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
                 ? _value.country
                 : country // ignore: cast_nullable_to_non_nullable
                       as String?,
+            countryName: freezed == countryName
+                ? _value.countryName
+                : countryName // ignore: cast_nullable_to_non_nullable
+                      as String?,
             userIp: freezed == userIp
                 ? _value.userIp
                 : userIp // ignore: cast_nullable_to_non_nullable
@@ -119,7 +147,7 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
             createTime: freezed == createTime
                 ? _value.createTime
                 : createTime // ignore: cast_nullable_to_non_nullable
-                      as String?,
+                      as int?,
             geographyEea: freezed == geographyEea
                 ? _value.geographyEea
                 : geographyEea // ignore: cast_nullable_to_non_nullable
@@ -128,10 +156,34 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
                 ? _value.memberLevel
                 : memberLevel // ignore: cast_nullable_to_non_nullable
                       as int?,
+            userLevel: freezed == userLevel
+                ? _value.userLevel
+                : userLevel // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            expireTime: freezed == expireTime
+                ? _value.expireTime
+                : expireTime // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            remainTime: freezed == remainTime
+                ? _value.remainTime
+                : remainTime // ignore: cast_nullable_to_non_nullable
+                      as int?,
+            isExpired: freezed == isExpired
+                ? _value.isExpired
+                : isExpired // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            isTestUser: freezed == isTestUser
+                ? _value.isTestUser
+                : isTestUser // ignore: cast_nullable_to_non_nullable
+                      as bool?,
+            isSubscribeUser: freezed == isSubscribeUser
+                ? _value.isSubscribeUser
+                : isSubscribeUser // ignore: cast_nullable_to_non_nullable
+                      as bool?,
             account: freezed == account
                 ? _value.account
                 : account // ignore: cast_nullable_to_non_nullable
-                      as String?,
+                      as Account?,
             activated: freezed == activated
                 ? _value.activated
                 : activated // ignore: cast_nullable_to_non_nullable
@@ -140,6 +192,20 @@ class _$UserCopyWithImpl<$Res, $Val extends User>
           as $Val,
     );
   }
+
+  /// Create a copy of User
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $AccountCopyWith<$Res>? get account {
+    if (_value.account == null) {
+      return null;
+    }
+
+    return $AccountCopyWith<$Res>(_value.account!, (value) {
+      return _then(_value.copyWith(account: value) as $Val);
+    });
+  }
 }
 
 /// @nodoc
@@ -152,17 +218,27 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> {
   @useResult
   $Res call({
     String? country,
+    String? countryName,
     String? userIp,
     String? accessToken,
     String? refreshToken,
     String? accountKey,
     String? accountPassword,
-    String? createTime,
+    int? createTime,
     bool? geographyEea,
     int? memberLevel,
-    String? account,
+    int? userLevel,
+    int? expireTime,
+    int? remainTime,
+    bool? isExpired,
+    bool? isTestUser,
+    bool? isSubscribeUser,
+    Account? account,
     bool? activated,
   });
+
+  @override
+  $AccountCopyWith<$Res>? get account;
 }
 
 /// @nodoc
@@ -178,6 +254,7 @@ class __$$UserImplCopyWithImpl<$Res>
   @override
   $Res call({
     Object? country = freezed,
+    Object? countryName = freezed,
     Object? userIp = freezed,
     Object? accessToken = freezed,
     Object? refreshToken = freezed,
@@ -186,6 +263,12 @@ class __$$UserImplCopyWithImpl<$Res>
     Object? createTime = freezed,
     Object? geographyEea = freezed,
     Object? memberLevel = freezed,
+    Object? userLevel = freezed,
+    Object? expireTime = freezed,
+    Object? remainTime = freezed,
+    Object? isExpired = freezed,
+    Object? isTestUser = freezed,
+    Object? isSubscribeUser = freezed,
     Object? account = freezed,
     Object? activated = freezed,
   }) {
@@ -195,6 +278,10 @@ class __$$UserImplCopyWithImpl<$Res>
             ? _value.country
             : country // ignore: cast_nullable_to_non_nullable
                   as String?,
+        countryName: freezed == countryName
+            ? _value.countryName
+            : countryName // ignore: cast_nullable_to_non_nullable
+                  as String?,
         userIp: freezed == userIp
             ? _value.userIp
             : userIp // ignore: cast_nullable_to_non_nullable
@@ -218,7 +305,7 @@ class __$$UserImplCopyWithImpl<$Res>
         createTime: freezed == createTime
             ? _value.createTime
             : createTime // ignore: cast_nullable_to_non_nullable
-                  as String?,
+                  as int?,
         geographyEea: freezed == geographyEea
             ? _value.geographyEea
             : geographyEea // ignore: cast_nullable_to_non_nullable
@@ -227,10 +314,34 @@ class __$$UserImplCopyWithImpl<$Res>
             ? _value.memberLevel
             : memberLevel // ignore: cast_nullable_to_non_nullable
                   as int?,
+        userLevel: freezed == userLevel
+            ? _value.userLevel
+            : userLevel // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        expireTime: freezed == expireTime
+            ? _value.expireTime
+            : expireTime // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        remainTime: freezed == remainTime
+            ? _value.remainTime
+            : remainTime // ignore: cast_nullable_to_non_nullable
+                  as int?,
+        isExpired: freezed == isExpired
+            ? _value.isExpired
+            : isExpired // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        isTestUser: freezed == isTestUser
+            ? _value.isTestUser
+            : isTestUser // ignore: cast_nullable_to_non_nullable
+                  as bool?,
+        isSubscribeUser: freezed == isSubscribeUser
+            ? _value.isSubscribeUser
+            : isSubscribeUser // ignore: cast_nullable_to_non_nullable
+                  as bool?,
         account: freezed == account
             ? _value.account
             : account // ignore: cast_nullable_to_non_nullable
-                  as String?,
+                  as Account?,
         activated: freezed == activated
             ? _value.activated
             : activated // ignore: cast_nullable_to_non_nullable
@@ -245,6 +356,7 @@ class __$$UserImplCopyWithImpl<$Res>
 class _$UserImpl with DiagnosticableTreeMixin implements _User {
   const _$UserImpl({
     this.country,
+    this.countryName,
     this.userIp,
     this.accessToken,
     this.refreshToken,
@@ -253,6 +365,12 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
     this.createTime,
     this.geographyEea,
     this.memberLevel,
+    this.userLevel,
+    this.expireTime,
+    this.remainTime,
+    this.isExpired,
+    this.isTestUser,
+    this.isSubscribeUser,
     this.account,
     this.activated,
   });
@@ -263,6 +381,8 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
   @override
   final String? country;
   @override
+  final String? countryName;
+  @override
   final String? userIp;
   @override
   final String? accessToken;
@@ -273,21 +393,34 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
   @override
   final String? accountPassword;
   @override
-  final String? createTime;
+  final int? createTime;
+  // API 返回 int 类型的时间戳
   @override
   final bool? geographyEea;
   @override
   final int? memberLevel;
   // 会员等级 1 游客 2 普通用户 3 会员
   @override
-  final String? account;
-  // 用户名
+  final int? userLevel;
+  @override
+  final int? expireTime;
+  @override
+  final int? remainTime;
+  @override
+  final bool? isExpired;
+  @override
+  final bool? isTestUser;
+  @override
+  final bool? isSubscribeUser;
+  @override
+  final Account? account;
+  // API 返回对象类型
   @override
   final bool? activated;
 
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
-    return 'User(country: $country, userIp: $userIp, accessToken: $accessToken, refreshToken: $refreshToken, accountKey: $accountKey, accountPassword: $accountPassword, createTime: $createTime, geographyEea: $geographyEea, memberLevel: $memberLevel, account: $account, activated: $activated)';
+    return 'User(country: $country, countryName: $countryName, userIp: $userIp, accessToken: $accessToken, refreshToken: $refreshToken, accountKey: $accountKey, accountPassword: $accountPassword, createTime: $createTime, geographyEea: $geographyEea, memberLevel: $memberLevel, userLevel: $userLevel, expireTime: $expireTime, remainTime: $remainTime, isExpired: $isExpired, isTestUser: $isTestUser, isSubscribeUser: $isSubscribeUser, account: $account, activated: $activated)';
   }
 
   @override
@@ -296,6 +429,7 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
     properties
       ..add(DiagnosticsProperty('type', 'User'))
       ..add(DiagnosticsProperty('country', country))
+      ..add(DiagnosticsProperty('countryName', countryName))
       ..add(DiagnosticsProperty('userIp', userIp))
       ..add(DiagnosticsProperty('accessToken', accessToken))
       ..add(DiagnosticsProperty('refreshToken', refreshToken))
@@ -304,6 +438,12 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
       ..add(DiagnosticsProperty('createTime', createTime))
       ..add(DiagnosticsProperty('geographyEea', geographyEea))
       ..add(DiagnosticsProperty('memberLevel', memberLevel))
+      ..add(DiagnosticsProperty('userLevel', userLevel))
+      ..add(DiagnosticsProperty('expireTime', expireTime))
+      ..add(DiagnosticsProperty('remainTime', remainTime))
+      ..add(DiagnosticsProperty('isExpired', isExpired))
+      ..add(DiagnosticsProperty('isTestUser', isTestUser))
+      ..add(DiagnosticsProperty('isSubscribeUser', isSubscribeUser))
       ..add(DiagnosticsProperty('account', account))
       ..add(DiagnosticsProperty('activated', activated));
   }
@@ -314,6 +454,8 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
         (other.runtimeType == runtimeType &&
             other is _$UserImpl &&
             (identical(other.country, country) || other.country == country) &&
+            (identical(other.countryName, countryName) ||
+                other.countryName == countryName) &&
             (identical(other.userIp, userIp) || other.userIp == userIp) &&
             (identical(other.accessToken, accessToken) ||
                 other.accessToken == accessToken) &&
@@ -329,6 +471,18 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
                 other.geographyEea == geographyEea) &&
             (identical(other.memberLevel, memberLevel) ||
                 other.memberLevel == memberLevel) &&
+            (identical(other.userLevel, userLevel) ||
+                other.userLevel == userLevel) &&
+            (identical(other.expireTime, expireTime) ||
+                other.expireTime == expireTime) &&
+            (identical(other.remainTime, remainTime) ||
+                other.remainTime == remainTime) &&
+            (identical(other.isExpired, isExpired) ||
+                other.isExpired == isExpired) &&
+            (identical(other.isTestUser, isTestUser) ||
+                other.isTestUser == isTestUser) &&
+            (identical(other.isSubscribeUser, isSubscribeUser) ||
+                other.isSubscribeUser == isSubscribeUser) &&
             (identical(other.account, account) || other.account == account) &&
             (identical(other.activated, activated) ||
                 other.activated == activated));
@@ -339,6 +493,7 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
   int get hashCode => Object.hash(
     runtimeType,
     country,
+    countryName,
     userIp,
     accessToken,
     refreshToken,
@@ -347,6 +502,12 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
     createTime,
     geographyEea,
     memberLevel,
+    userLevel,
+    expireTime,
+    remainTime,
+    isExpired,
+    isTestUser,
+    isSubscribeUser,
     account,
     activated,
   );
@@ -368,15 +529,22 @@ class _$UserImpl with DiagnosticableTreeMixin implements _User {
 abstract class _User implements User {
   const factory _User({
     final String? country,
+    final String? countryName,
     final String? userIp,
     final String? accessToken,
     final String? refreshToken,
     final String? accountKey,
     final String? accountPassword,
-    final String? createTime,
+    final int? createTime,
     final bool? geographyEea,
     final int? memberLevel,
-    final String? account,
+    final int? userLevel,
+    final int? expireTime,
+    final int? remainTime,
+    final bool? isExpired,
+    final bool? isTestUser,
+    final bool? isSubscribeUser,
+    final Account? account,
     final bool? activated,
   }) = _$UserImpl;
 
@@ -385,6 +553,8 @@ abstract class _User implements User {
   @override
   String? get country;
   @override
+  String? get countryName;
+  @override
   String? get userIp;
   @override
   String? get accessToken;
@@ -395,13 +565,25 @@ abstract class _User implements User {
   @override
   String? get accountPassword;
   @override
-  String? get createTime;
+  int? get createTime; // API 返回 int 类型的时间戳
   @override
   bool? get geographyEea;
   @override
   int? get memberLevel; // 会员等级 1 游客 2 普通用户 3 会员
   @override
-  String? get account; // 用户名
+  int? get userLevel;
+  @override
+  int? get expireTime;
+  @override
+  int? get remainTime;
+  @override
+  bool? get isExpired;
+  @override
+  bool? get isTestUser;
+  @override
+  bool? get isSubscribeUser;
+  @override
+  Account? get account; // API 返回对象类型
   @override
   bool? get activated;
 
@@ -412,3 +594,305 @@ abstract class _User implements User {
   _$$UserImplCopyWith<_$UserImpl> get copyWith =>
       throw _privateConstructorUsedError;
 }
+
+Account _$AccountFromJson(Map<String, dynamic> json) {
+  return _Account.fromJson(json);
+}
+
+/// @nodoc
+mixin _$Account {
+  String? get username => throw _privateConstructorUsedError;
+  String? get phone => throw _privateConstructorUsedError;
+  String? get email => throw _privateConstructorUsedError;
+  String? get wechat => throw _privateConstructorUsedError;
+  String? get qq => throw _privateConstructorUsedError;
+  String? get google => throw _privateConstructorUsedError;
+  String? get apple => throw _privateConstructorUsedError;
+
+  /// Serializes this Account to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of Account
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $AccountCopyWith<Account> get copyWith => throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AccountCopyWith<$Res> {
+  factory $AccountCopyWith(Account value, $Res Function(Account) then) =
+      _$AccountCopyWithImpl<$Res, Account>;
+  @useResult
+  $Res call({
+    String? username,
+    String? phone,
+    String? email,
+    String? wechat,
+    String? qq,
+    String? google,
+    String? apple,
+  });
+}
+
+/// @nodoc
+class _$AccountCopyWithImpl<$Res, $Val extends Account>
+    implements $AccountCopyWith<$Res> {
+  _$AccountCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of Account
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? username = freezed,
+    Object? phone = freezed,
+    Object? email = freezed,
+    Object? wechat = freezed,
+    Object? qq = freezed,
+    Object? google = freezed,
+    Object? apple = freezed,
+  }) {
+    return _then(
+      _value.copyWith(
+            username: freezed == username
+                ? _value.username
+                : username // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            phone: freezed == phone
+                ? _value.phone
+                : phone // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            email: freezed == email
+                ? _value.email
+                : email // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            wechat: freezed == wechat
+                ? _value.wechat
+                : wechat // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            qq: freezed == qq
+                ? _value.qq
+                : qq // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            google: freezed == google
+                ? _value.google
+                : google // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            apple: freezed == apple
+                ? _value.apple
+                : apple // ignore: cast_nullable_to_non_nullable
+                      as String?,
+          )
+          as $Val,
+    );
+  }
+}
+
+/// @nodoc
+abstract class _$$AccountImplCopyWith<$Res> implements $AccountCopyWith<$Res> {
+  factory _$$AccountImplCopyWith(
+    _$AccountImpl value,
+    $Res Function(_$AccountImpl) then,
+  ) = __$$AccountImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({
+    String? username,
+    String? phone,
+    String? email,
+    String? wechat,
+    String? qq,
+    String? google,
+    String? apple,
+  });
+}
+
+/// @nodoc
+class __$$AccountImplCopyWithImpl<$Res>
+    extends _$AccountCopyWithImpl<$Res, _$AccountImpl>
+    implements _$$AccountImplCopyWith<$Res> {
+  __$$AccountImplCopyWithImpl(
+    _$AccountImpl _value,
+    $Res Function(_$AccountImpl) _then,
+  ) : super(_value, _then);
+
+  /// Create a copy of Account
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? username = freezed,
+    Object? phone = freezed,
+    Object? email = freezed,
+    Object? wechat = freezed,
+    Object? qq = freezed,
+    Object? google = freezed,
+    Object? apple = freezed,
+  }) {
+    return _then(
+      _$AccountImpl(
+        username: freezed == username
+            ? _value.username
+            : username // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        phone: freezed == phone
+            ? _value.phone
+            : phone // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        email: freezed == email
+            ? _value.email
+            : email // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        wechat: freezed == wechat
+            ? _value.wechat
+            : wechat // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        qq: freezed == qq
+            ? _value.qq
+            : qq // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        google: freezed == google
+            ? _value.google
+            : google // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        apple: freezed == apple
+            ? _value.apple
+            : apple // ignore: cast_nullable_to_non_nullable
+                  as String?,
+      ),
+    );
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$AccountImpl with DiagnosticableTreeMixin implements _Account {
+  const _$AccountImpl({
+    this.username,
+    this.phone,
+    this.email,
+    this.wechat,
+    this.qq,
+    this.google,
+    this.apple,
+  });
+
+  factory _$AccountImpl.fromJson(Map<String, dynamic> json) =>
+      _$$AccountImplFromJson(json);
+
+  @override
+  final String? username;
+  @override
+  final String? phone;
+  @override
+  final String? email;
+  @override
+  final String? wechat;
+  @override
+  final String? qq;
+  @override
+  final String? google;
+  @override
+  final String? apple;
+
+  @override
+  String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
+    return 'Account(username: $username, phone: $phone, email: $email, wechat: $wechat, qq: $qq, google: $google, apple: $apple)';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty('type', 'Account'))
+      ..add(DiagnosticsProperty('username', username))
+      ..add(DiagnosticsProperty('phone', phone))
+      ..add(DiagnosticsProperty('email', email))
+      ..add(DiagnosticsProperty('wechat', wechat))
+      ..add(DiagnosticsProperty('qq', qq))
+      ..add(DiagnosticsProperty('google', google))
+      ..add(DiagnosticsProperty('apple', apple));
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$AccountImpl &&
+            (identical(other.username, username) ||
+                other.username == username) &&
+            (identical(other.phone, phone) || other.phone == phone) &&
+            (identical(other.email, email) || other.email == email) &&
+            (identical(other.wechat, wechat) || other.wechat == wechat) &&
+            (identical(other.qq, qq) || other.qq == qq) &&
+            (identical(other.google, google) || other.google == google) &&
+            (identical(other.apple, apple) || other.apple == apple));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+    runtimeType,
+    username,
+    phone,
+    email,
+    wechat,
+    qq,
+    google,
+    apple,
+  );
+
+  /// Create a copy of Account
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$AccountImplCopyWith<_$AccountImpl> get copyWith =>
+      __$$AccountImplCopyWithImpl<_$AccountImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$AccountImplToJson(this);
+  }
+}
+
+abstract class _Account implements Account {
+  const factory _Account({
+    final String? username,
+    final String? phone,
+    final String? email,
+    final String? wechat,
+    final String? qq,
+    final String? google,
+    final String? apple,
+  }) = _$AccountImpl;
+
+  factory _Account.fromJson(Map<String, dynamic> json) = _$AccountImpl.fromJson;
+
+  @override
+  String? get username;
+  @override
+  String? get phone;
+  @override
+  String? get email;
+  @override
+  String? get wechat;
+  @override
+  String? get qq;
+  @override
+  String? get google;
+  @override
+  String? get apple;
+
+  /// Create a copy of Account
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$AccountImplCopyWith<_$AccountImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}

+ 40 - 2
lib/app/data/models/launch/user.g.dart

@@ -8,21 +8,31 @@ part of 'user.dart';
 
 _$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
   country: json['country'] as String?,
+  countryName: json['countryName'] as String?,
   userIp: json['userIp'] as String?,
   accessToken: json['accessToken'] as String?,
   refreshToken: json['refreshToken'] as String?,
   accountKey: json['accountKey'] as String?,
   accountPassword: json['accountPassword'] as String?,
-  createTime: json['createTime'] as String?,
+  createTime: (json['createTime'] as num?)?.toInt(),
   geographyEea: json['geographyEea'] as bool?,
   memberLevel: (json['memberLevel'] as num?)?.toInt(),
-  account: json['account'] as String?,
+  userLevel: (json['userLevel'] as num?)?.toInt(),
+  expireTime: (json['expireTime'] as num?)?.toInt(),
+  remainTime: (json['remainTime'] as num?)?.toInt(),
+  isExpired: json['isExpired'] as bool?,
+  isTestUser: json['isTestUser'] as bool?,
+  isSubscribeUser: json['isSubscribeUser'] as bool?,
+  account: json['account'] == null
+      ? null
+      : Account.fromJson(json['account'] as Map<String, dynamic>),
   activated: json['activated'] as bool?,
 );
 
 Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
     <String, dynamic>{
       'country': instance.country,
+      'countryName': instance.countryName,
       'userIp': instance.userIp,
       'accessToken': instance.accessToken,
       'refreshToken': instance.refreshToken,
@@ -31,6 +41,34 @@ Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
       'createTime': instance.createTime,
       'geographyEea': instance.geographyEea,
       'memberLevel': instance.memberLevel,
+      'userLevel': instance.userLevel,
+      'expireTime': instance.expireTime,
+      'remainTime': instance.remainTime,
+      'isExpired': instance.isExpired,
+      'isTestUser': instance.isTestUser,
+      'isSubscribeUser': instance.isSubscribeUser,
       'account': instance.account,
       'activated': instance.activated,
     };
+
+_$AccountImpl _$$AccountImplFromJson(Map<String, dynamic> json) =>
+    _$AccountImpl(
+      username: json['username'] as String?,
+      phone: json['phone'] as String?,
+      email: json['email'] as String?,
+      wechat: json['wechat'] as String?,
+      qq: json['qq'] as String?,
+      google: json['google'] as String?,
+      apple: json['apple'] as String?,
+    );
+
+Map<String, dynamic> _$$AccountImplToJson(_$AccountImpl instance) =>
+    <String, dynamic>{
+      'username': instance.username,
+      'phone': instance.phone,
+      'email': instance.email,
+      'wechat': instance.wechat,
+      'qq': instance.qq,
+      'google': instance.google,
+      'apple': instance.apple,
+    };

+ 64 - 1
lib/app/data/sp/ix_sp.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:get/get.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'dart:convert';
 
@@ -8,9 +9,12 @@ import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
 import '../models/disconnect_domain.dart';
 import '../models/launch/app_config.dart';
+import '../models/launch/groups.dart';
 import '../models/launch/launch.dart';
 import '../models/launch/upgrade.dart';
 import '../models/launch/user.dart';
+import '../../modules/home/controllers/home_controller.dart';
+import '../../modules/node/controllers/node_controller.dart';
 
 class IXSP {
   // prevent making instance
@@ -316,6 +320,11 @@ class IXSP {
   /// 保存 Launch 数据
   static Future<bool> saveLaunch(Launch launch) async {
     try {
+      // 获取旧的 Launch 数据,用于比较 groups
+      final oldLaunch = getLaunch();
+      final oldGroups = oldLaunch?.groups;
+      final newGroups = launch.groups;
+
       final jsonData = jsonEncode(launch.toJson());
 
       // 保存数据
@@ -324,8 +333,14 @@ class IXSP {
         Crypto.encrypt(jsonData, Keys.aesKey, Keys.aesIv),
       );
 
-      // 保存保存时间
       log('IXSP', 'Launch data saved successfully');
+
+      // 检查 groups 是否有变化,如果有则通知更新
+      if (_isGroupsChanged(oldGroups, newGroups)) {
+        log('IXSP', 'Groups data changed, notifying controllers');
+        _notifyControllersOnGroupsChanged();
+      }
+
       return true;
     } catch (e) {
       log('IXSP', 'Error saving launch data: $e');
@@ -333,6 +348,41 @@ class IXSP {
     }
   }
 
+  /// 检查 Groups 数据是否有变化
+  static bool _isGroupsChanged(Groups? oldGroups, Groups? newGroups) {
+    // 如果两者都为 null,没有变化
+    if (oldGroups == null && newGroups == null) return false;
+    // 如果一个为 null 一个不为 null,有变化
+    if (oldGroups == null || newGroups == null) return true;
+
+    // 比较 JSON 序列化后的字符串
+    try {
+      final oldJson = jsonEncode(oldGroups.toJson());
+      final newJson = jsonEncode(newGroups.toJson());
+      return oldJson != newJson;
+    } catch (e) {
+      log('IXSP', 'Error comparing groups: $e');
+      return true; // 出错时默认认为有变化
+    }
+  }
+
+  /// 通知控制器 Groups 数据变化
+  static void _notifyControllersOnGroupsChanged() {
+    try {
+      // 通知 HomeController
+      if (Get.isRegistered<HomeController>()) {
+        Get.find<HomeController>().refreshOnLaunchChanged();
+      }
+
+      // 通知 NodeController
+      if (Get.isRegistered<NodeController>()) {
+        Get.find<NodeController>().refreshOnLaunchChanged();
+      }
+    } catch (e) {
+      log('IXSP', 'Error notifying controllers: $e');
+    }
+  }
+
   /// 获取 Launch 数据
   static Launch? getLaunch() {
     try {
@@ -358,6 +408,12 @@ class IXSP {
     return launch?.userConfig;
   }
 
+  /// 获取app配置
+  static AppConfig? getAppConfig() {
+    final launch = getLaunch();
+    return launch?.appConfig;
+  }
+
   /// 获取升级信息
   static Upgrade? getUpgrade() {
     final launch = getLaunch();
@@ -378,6 +434,13 @@ class IXSP {
     await saveLaunch(newLaunch);
   }
 
+  /// 保存 Groups 数据
+  static Future<void> saveGroups(Groups groups) async {
+    final launch = getLaunch();
+    final newLaunch = launch!.copyWith(groups: groups);
+    await saveLaunch(newLaunch);
+  }
+
   /// 清除 Launch 数据
   static Future<bool> clearLaunchData() async {
     try {

+ 10 - 0
lib/app/dialog/all_dialog.dart

@@ -9,6 +9,16 @@ import 'custom_dialog.dart';
 
 /// 弹窗使用示例
 class AllDialog {
+  /// 显示绑定邮箱/会员权益弹窗
+  static void showBindEmailMemberBenefits() {
+    CustomDialog.showInfo(
+      title: Strings.bindEmailMemberBenefits.tr,
+      message: Strings.bindingAccountEmailProtectsPreRights.tr,
+      icon: IconFont.icon23,
+      iconColor: DarkThemeColors.subscriptionColor,
+    );
+  }
+
   /// 显示Premium激活成功弹窗
   static void showPremiumActivated() {
     CustomDialog.showSuccess(

+ 39 - 6
lib/app/dialog/custom_dialog.dart

@@ -16,6 +16,8 @@ class CustomDialog {
     IconData? icon,
     String? svgPath,
     Color? iconColor,
+    Color? titleColor,
+    Color? messageColor,
   }) {
     Get.generalDialog(
       pageBuilder: (context, animation, secondaryAnimation) {
@@ -28,6 +30,8 @@ class CustomDialog {
           icon: icon ?? Icons.workspace_premium,
           svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF9500),
+          titleColor: titleColor,
+          messageColor: messageColor,
           animation: animation,
         );
       },
@@ -45,7 +49,10 @@ class CustomDialog {
     VoidCallback? onCancel,
     String? svgPath,
     Color? iconColor,
+    Color? titleColor,
+    Color? messageColor,
     Color? confirmButtonColor,
+    Color? cancelButtonColor,
   }) {
     Get.generalDialog(
       pageBuilder: (context, animation, secondaryAnimation) {
@@ -62,6 +69,10 @@ class CustomDialog {
             svgPath: svgPath,
             iconColor:
                 iconColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color!,
+            titleColor: titleColor,
+            messageColor: messageColor,
+            confirmButtonColor: confirmButtonColor,
+            cancelButtonColor: cancelButtonColor,
             animation: animation,
           ),
         );
@@ -80,6 +91,8 @@ class CustomDialog {
     IconData? icon,
     String? svgPath,
     Color? iconColor,
+    Color? titleColor,
+    Color? messageColor,
   }) {
     Get.generalDialog(
       pageBuilder: (context, animation, secondaryAnimation) {
@@ -92,6 +105,8 @@ class CustomDialog {
           icon: icon ?? Icons.mark_email_read,
           svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFF00A8E8),
+          titleColor: titleColor,
+          messageColor: messageColor,
           animation: animation,
         );
       },
@@ -111,7 +126,10 @@ class CustomDialog {
     IconData? icon,
     String? svgPath,
     Color? iconColor,
+    Color? titleColor,
+    Color? messageColor,
     Color? confirmButtonColor,
+    Color? cancelButtonColor,
     String? errorCode,
   }) {
     Get.generalDialog(
@@ -127,8 +145,11 @@ class CustomDialog {
           icon: icon ?? Icons.wifi_off,
           svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF9500),
+          titleColor: titleColor,
+          messageColor: messageColor,
           confirmButtonColor:
               confirmButtonColor ?? Get.reactiveTheme.primaryColor,
+          cancelButtonColor: cancelButtonColor,
           errorCode: errorCode,
           animation: animation,
         );
@@ -149,7 +170,10 @@ class CustomDialog {
     IconData? icon,
     String? svgPath,
     Color? iconColor,
+    Color? titleColor,
+    Color? messageColor,
     Color? confirmButtonColor,
+    Color? cancelButtonColor,
   }) {
     Get.generalDialog(
       pageBuilder: (context, animation, secondaryAnimation) {
@@ -165,7 +189,10 @@ class CustomDialog {
           icon: icon ?? Icons.info_outline,
           svgPath: svgPath,
           iconColor: iconColor ?? const Color(0xFFFF3B30),
+          titleColor: titleColor,
+          messageColor: messageColor,
           confirmButtonColor: confirmButtonColor ?? const Color(0xFFFF3B30),
+          cancelButtonColor: cancelButtonColor,
           animation: animation,
         );
       },
@@ -193,7 +220,10 @@ class _CustomDialogWidget extends StatelessWidget {
   final IconData? icon;
   final String? svgPath;
   final Color iconColor;
+  final Color? titleColor;
+  final Color? messageColor;
   final Color? confirmButtonColor;
+  final Color? cancelButtonColor;
   final String? errorCode;
   final Animation<double>? animation;
 
@@ -210,7 +240,10 @@ class _CustomDialogWidget extends StatelessWidget {
     this.icon,
     this.svgPath,
     required this.iconColor,
+    this.titleColor,
+    this.messageColor,
     this.confirmButtonColor,
+    this.cancelButtonColor,
     this.errorCode,
     this.animation,
   });
@@ -330,7 +363,7 @@ class _CustomDialogWidget extends StatelessWidget {
           style: TextStyle(
             fontSize: 22.sp,
             fontWeight: FontWeight.w500,
-            color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+            color: titleColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color,
           ),
         ),
 
@@ -341,7 +374,7 @@ class _CustomDialogWidget extends StatelessWidget {
           message,
           style: TextStyle(
             fontSize: 14.sp,
-            color: Get.reactiveTheme.hintColor,
+            color: messageColor ?? Get.reactiveTheme.hintColor,
             height: 1.4,
           ),
         ),
@@ -431,7 +464,7 @@ class _CustomDialogWidget extends StatelessWidget {
                 decoration: BoxDecoration(
                   borderRadius: BorderRadius.circular(8.r),
                   border: Border.all(
-                    color: Get.reactiveTheme.dividerColor,
+                    color: cancelButtonColor ?? Get.reactiveTheme.dividerColor,
                     width: 1.w,
                   ),
                 ),
@@ -441,7 +474,7 @@ class _CustomDialogWidget extends StatelessWidget {
                   style: TextStyle(
                     fontSize: 14.sp,
                     fontWeight: FontWeight.w600,
-                    color: Get.reactiveTheme.hintColor,
+                    color: cancelButtonColor ?? Get.reactiveTheme.hintColor,
                   ),
                 ),
               ),
@@ -492,7 +525,7 @@ class _CustomDialogWidget extends StatelessWidget {
               decoration: BoxDecoration(
                 borderRadius: BorderRadius.circular(8.r),
                 border: Border.all(
-                  color: Get.reactiveTheme.dividerColor,
+                  color: cancelButtonColor ?? Get.reactiveTheme.dividerColor,
                   width: 1.w,
                 ),
               ),
@@ -502,7 +535,7 @@ class _CustomDialogWidget extends StatelessWidget {
                 style: TextStyle(
                   fontSize: 14.sp,
                   fontWeight: FontWeight.w600,
-                  color: Get.reactiveTheme.hintColor,
+                  color: cancelButtonColor ?? Get.reactiveTheme.hintColor,
                 ),
               ),
             ),

+ 59 - 2
lib/app/modules/account/controllers/account_controller.dart

@@ -1,10 +1,25 @@
 import 'package:get/get.dart';
 
+import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/device_manager.dart';
+import '../../../constants/enums.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../data/sp/ix_sp.dart';
+import '../../../dialog/loading/loading_dialog.dart';
+import '../../../routes/app_pages.dart';
 
 class AccountController extends GetxController {
+  final _apiController = Get.find<ApiController>();
+
   // 是否是会员(true: Premium, false: Free)
-  final isPremium = true.obs;
+  final _isPremium = false.obs;
+  bool get isPremium => _isPremium.value;
+  set isPremium(bool value) => _isPremium.value = value;
+
+  //是否是游客
+  final _isGuest = false.obs;
+  bool get isGuest => _isGuest.value;
+  set isGuest(bool value) => _isGuest.value = value;
 
   // UID
   String uid = '';
@@ -21,14 +36,56 @@ class AccountController extends GetxController {
 
   /// 切换会员状态(用于测试)
   void togglePremium() {
-    isPremium.value = !isPremium.value;
+    isPremium = !isPremium;
   }
 
   @override
   void onInit() {
     super.onInit();
+    initIsGuest();
     uid = DeviceManager.getCacheDeviceId().length > 12
         ? '${DeviceManager.getCacheDeviceId().substring(0, 6)}***${DeviceManager.getCacheDeviceId().substring(DeviceManager.getCacheDeviceId().length - 6)}'
         : DeviceManager.getCacheDeviceId();
   }
+
+  void initIsGuest() {
+    final user = IXSP.getUser();
+    if (user != null) {
+      isGuest = user.memberLevel == MemberLevel.guest.level;
+    }
+  }
+
+  // 处理退出登录
+  Future<void> handleLogout() async {
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.loggingOut.tr,
+      successText: Strings.logoutSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.logout();
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.offAllNamed(Routes.HOME);
+      },
+    );
+  }
+
+  // 处理删除账户
+  Future<void> handleDeleteAccount() async {
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: Strings.deletingAccount.tr,
+      successText: Strings.deleteAccountSuccessful.tr,
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.deleteAccount();
+      },
+      onSuccess: () {
+        // 成功后的操作
+        Get.offAllNamed(Routes.HOME);
+      },
+    );
+  }
 }

+ 340 - 5
lib/app/modules/account/views/account_view.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
@@ -10,8 +11,12 @@ import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../../../../config/theme/dark_theme_colors.dart';
 import '../../../../config/translations/strings_enum.dart';
+import '../../../../utils/device_manager.dart';
+import '../../../components/ix_snackbar.dart';
 import '../../../constants/assets.dart';
 import '../../../constants/iconfont/iconfont.dart';
+import '../../../dialog/all_dialog.dart';
+import '../../../routes/app_pages.dart';
 import '../../../widgets/ix_image.dart';
 import '../controllers/account_controller.dart';
 
@@ -24,13 +29,18 @@ class AccountView extends BaseView<AccountController> {
   @override
   Widget buildContent(BuildContext context) {
     return Obx(() {
-      final isPremium = controller.isPremium.value;
+      final isPremium = controller.isPremium;
       return SingleChildScrollView(
         padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
         child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             // Account 信息卡片
-            _buildAccountCard(isPremium),
+            // _buildAccountCard(isPremium),
+            _buildAccountSection(),
+            // Security Section
+            _buildSectionHeader(Strings.securitySection.tr),
+            _buildSecuritySection(),
 
             20.verticalSpaceFromWidth,
 
@@ -350,16 +360,17 @@ class AccountView extends BaseView<AccountController> {
           20.verticalSpaceFromWidth,
           // Activate Pre Code 按钮
           _buildSecondaryButton(
-            text: Strings.activatePreCode.tr,
+            text: Strings.bindEmailMemberBenefits.tr,
             icon: IconFont.icon23,
             onTap: () {
-              // TODO: 激活兑换码
+              // TODO: 绑定邮箱
+              AllDialog.showBindEmailMemberBenefits();
             },
           ),
           10.verticalSpaceFromWidth,
           // 提示文字
           Text(
-            Strings.preCodeHint.tr,
+            Strings.bindingAccountEmailProtectsPreRights.tr,
             textAlign: TextAlign.center,
             style: TextStyle(
               fontSize: 12.sp,
@@ -408,4 +419,328 @@ class AccountView extends BaseView<AccountController> {
   Widget _buildDivider() {
     return Divider(height: 1.w, color: Get.reactiveTheme.dividerColor);
   }
+
+  /// 构建分组标题
+  Widget _buildSectionHeader(String title) {
+    return Padding(
+      padding: EdgeInsets.symmetric(vertical: 10.w),
+      child: Text(
+        title,
+        style: TextStyle(
+          fontSize: 16.sp,
+          color: Get.reactiveTheme.hintColor,
+          fontWeight: FontWeight.w500,
+        ),
+      ),
+    );
+  }
+
+  /// Account 分组
+  Widget _buildAccountSection() {
+    return Obx(() {
+      final isPremium = controller.isPremium;
+      final isGuest = controller.isGuest;
+      return Container(
+        decoration: BoxDecoration(
+          color: Get.reactiveTheme.highlightColor,
+          borderRadius: BorderRadius.circular(12.r),
+        ),
+        child: Column(
+          children: [
+            if (!isGuest)
+              _buildSettingItem(
+                icon: IconFont.icon29,
+                iconColor: Get.reactiveTheme.shadowColor,
+                title: Strings.account.tr,
+                trailing: IXImage(
+                  source: isPremium ? Assets.premium : Assets.free,
+                  width: isPremium ? 92.w : 64.w,
+                  height: 28.w,
+                  sourceType: ImageSourceType.asset,
+                ),
+                onTap: () {
+                  Get.toNamed(Routes.ACCOUNT);
+                },
+              ),
+            if (!isGuest) _buildDivider(),
+            _buildSettingItem(
+              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: () {},
+              onInfoTap: () {
+                AllDialog.showUidInfo();
+              },
+            ),
+            _buildDivider(),
+            // 根据用户类型显示不同的时间信息
+            if (isPremium) ...[
+              // _buildSettingItem(
+              //   icon: IconFont.icon23,
+              //   iconColor: Get.reactiveTheme.shadowColor,
+              //   title: Strings.myPreCode.tr,
+              //   trailing: Row(
+              //     mainAxisSize: MainAxisSize.min,
+              //     children: [
+              //       Text(
+              //         '123***ADZ',
+              //         style: TextStyle(
+              //           fontSize: 13.sp,
+              //           color: Get.reactiveTheme.hintColor,
+              //         ),
+              //       ),
+              //       SizedBox(width: 4.w),
+              //       Icon(
+              //         IconFont.icon02,
+              //         size: 20.w,
+              //         color: Get.reactiveTheme.hintColor,
+              //       ),
+              //     ],
+              //   ),
+              //   onTap: () {
+              //     // TODO: 跳转到Pre Code页面
+              //     Get.toNamed(Routes.PRECODE);
+              //   },
+              // ),
+              // _buildDivider(),
+              _buildSettingItem(
+                icon: IconFont.icon30,
+                iconColor: Get.reactiveTheme.shadowColor,
+                title: Strings.validTerm.tr,
+                trailing: Text(
+                  'Year / 2026-12-12',
+                  style: TextStyle(
+                    fontSize: 13.sp,
+                    color: Get.reactiveTheme.primaryColor,
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+                onTap: () {
+                  // TODO: 跳转到有效期详情页面
+                },
+              ),
+            ] else ...[
+              _buildSettingItem(
+                icon: IconFont.icon30,
+                iconColor: Get.reactiveTheme.shadowColor,
+                title: Strings.freeTime.tr,
+                trailing: Text(
+                  '01:60:59 / Days',
+                  style: TextStyle(
+                    fontSize: 14.sp,
+                    color: const Color(0xFFFFCC00),
+                    fontWeight: FontWeight.w500,
+                  ),
+                ),
+              ),
+            ],
+            // _buildDivider(),
+            // _buildSettingItem(
+            //   icon: IconFont.icon31,
+            //   iconColor: Get.reactiveTheme.shadowColor,
+            //   title: Strings.deviceAuthorization.tr,
+            //   trailing: Row(
+            //     mainAxisSize: MainAxisSize.min,
+            //     children: [
+            //       Text(
+            //         isPremium ? '1/4' : '0/1',
+            //         style: TextStyle(
+            //           fontSize: 13.sp,
+            //           color: Get.reactiveTheme.hintColor,
+            //         ),
+            //       ),
+            //       SizedBox(width: 4.w),
+            //       Icon(
+            //         IconFont.icon02,
+            //         size: 20.w,
+            //         color: Get.reactiveTheme.hintColor,
+            //       ),
+            //     ],
+            //   ),
+            //   onTap: () {
+            //     Get.toNamed(Routes.DEVICEAUTH);
+            //   },
+            // ),
+          ],
+        ),
+      );
+    });
+  }
+
+  /// Security 分组
+  Widget _buildSecuritySection() {
+    return Container(
+      decoration: BoxDecoration(
+        color: Get.reactiveTheme.highlightColor,
+        borderRadius: BorderRadius.circular(12.r),
+      ),
+      child: Column(
+        children: [
+          _buildSettingItem(
+            icon: IconFont.icon11,
+            iconColor: DarkThemeColors.settingSecurityLinearGradientStartColor,
+            iconGradient: LinearGradient(
+              colors: [
+                DarkThemeColors.settingSecurityLinearGradientStartColor,
+                DarkThemeColors.settingSecurityLinearGradientEndColor,
+              ],
+              begin: Alignment.topCenter,
+              end: Alignment.bottomCenter,
+            ),
+            title: Strings.changePassword.tr,
+            onTap: () {
+              // TODO: 跳转到忘记密码页面
+              Get.toNamed(Routes.FORGOTPWD);
+            },
+          ),
+          _buildDivider(),
+          _buildSettingItem(
+            icon: IconFont.icon40,
+            iconColor: DarkThemeColors.settingSecurityLinearGradientStartColor,
+            iconGradient: LinearGradient(
+              colors: [
+                DarkThemeColors.settingSecurityLinearGradientStartColor,
+                DarkThemeColors.settingSecurityLinearGradientEndColor,
+              ],
+              begin: Alignment.topCenter,
+              end: Alignment.bottomCenter,
+            ),
+            title: Strings.deleteAccount.tr,
+            onTap: () {
+              AllDialog.showDeleteAccountConfirm(() {
+                // 退出登录
+                controller.handleDeleteAccount();
+              });
+            },
+          ),
+          _buildDivider(),
+          _buildSettingItem(
+            icon: IconFont.icon66,
+            iconColor: DarkThemeColors.settingSecurityLinearGradientStartColor,
+            iconGradient: LinearGradient(
+              colors: [
+                DarkThemeColors.settingSecurityLinearGradientStartColor,
+                DarkThemeColors.settingSecurityLinearGradientEndColor,
+              ],
+              begin: Alignment.topCenter,
+              end: Alignment.bottomCenter,
+            ),
+            title: Strings.logout.tr,
+            titleColor: const Color(0xFFEF0000),
+            onTap: () {
+              AllDialog.showLogoutConfirm(() {
+                // 退出登录
+                controller.handleLogout();
+              });
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  /// 构建设置项
+  Widget _buildSettingItem({
+    IconData? icon,
+    String? svgPath,
+    required Color iconColor,
+    Gradient? iconGradient,
+    required String title,
+    Color? titleColor,
+    bool showInfo = false,
+    Widget? trailing,
+    VoidCallback? onTap,
+    VoidCallback? onInfoTap,
+  }) {
+    // 确保至少提供了 icon 或 svgPath 之一
+    assert(
+      icon != null || svgPath != null,
+      'Must provide either icon or svgPath',
+    );
+
+    return ClickOpacity(
+      onTap: onTap,
+      child: Container(
+        height: 56.w,
+        padding: EdgeInsets.symmetric(horizontal: 14.w),
+        child: Row(
+          children: [
+            // 图标
+            Container(
+              width: 30.w,
+              height: 30.w,
+              decoration: BoxDecoration(
+                gradient: iconGradient,
+                color: iconGradient == null ? iconColor : null,
+                borderRadius: BorderRadius.circular(8.r),
+              ),
+              child: svgPath != null
+                  ? Padding(
+                      padding: EdgeInsets.all(5.w),
+                      child: SvgPicture.asset(
+                        svgPath,
+                        width: 20.w,
+                        height: 20.w,
+                        colorFilter: const ColorFilter.mode(
+                          Colors.white,
+                          BlendMode.srcIn,
+                        ),
+                      ),
+                    )
+                  : Icon(icon!, size: 20.w, color: Colors.white),
+            ),
+            10.horizontalSpace,
+            // 标题
+            Expanded(
+              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,
+          ],
+        ),
+      ),
+    );
+  }
 }

+ 70 - 2
lib/app/modules/home/controllers/home_controller.dart

@@ -1,9 +1,11 @@
 import 'package:get/get.dart';
 import 'package:nomo/app/controllers/api_controller.dart';
 import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
+import '../../../../pigeons/core_api.g.dart';
 import '../../../../utils/awesome_notifications_helper.dart';
 import '../../../../utils/log/logger.dart';
 import '../../../base/base_controller.dart';
+import '../../../constants/enums.dart';
 import '../../../controllers/core_controller.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../data/sp/ix_sp.dart';
@@ -79,7 +81,22 @@ class HomeController extends BaseController {
       // 加载当前选中的节点
       final selectedLocationData = IXSP.getSelectedLocation();
       if (selectedLocationData != null) {
-        selectedLocation = Locations.fromJson(selectedLocationData);
+        final savedLocation = Locations.fromJson(selectedLocationData);
+        // 检查保存的节点是否存在于当前 groups 中
+        if (_isLocationExistsInGroups(savedLocation)) {
+          selectedLocation = savedLocation;
+        } else {
+          // 如果节点不存在于 groups 中,选中第一个可用节点
+          // 如果当前节点是连接中的状态,则断开连接
+          if (coreController.state != ConnectionState.disconnected) {
+            CoreApi().disconnect();
+          }
+          log(
+            TAG,
+            'Saved location not found in groups, selecting first available',
+          );
+          _selectFirstAvailableLocation();
+        }
       } else {
         // 如果没有保存的节点,选中第一个可用节点
         _selectFirstAvailableLocation();
@@ -97,6 +114,41 @@ class HomeController extends BaseController {
     }
   }
 
+  /// 检查节点是否存在于当前 groups 中
+  bool _isLocationExistsInGroups(Locations location) {
+    final launch = IXSP.getLaunch();
+    final groups = launch?.groups;
+    if (groups == null) return false;
+
+    // 检查 normal 列表
+    if (groups.normal?.list != null) {
+      for (var locationList in groups.normal!.list!) {
+        if (locationList.locations != null) {
+          for (var loc in locationList.locations!) {
+            if (loc.id == location.id) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+
+    // 检查 streaming 列表
+    if (groups.streaming?.list != null) {
+      for (var locationList in groups.streaming!.list!) {
+        if (locationList.locations != null) {
+          for (var loc in locationList.locations!) {
+            if (loc.id == location.id) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+
   /// 选中第一个可用节点
   void _selectFirstAvailableLocation() {
     try {
@@ -121,8 +173,12 @@ class HomeController extends BaseController {
   }
 
   /// 选择位置
-  void selectLocation(Locations location) {
+  void selectLocation(
+    Locations location, {
+    String locationSelectionType = 'auto',
+  }) {
     selectedLocation = location;
+    coreController.locationSelectionType = locationSelectionType;
 
     // 更新最近使用列表
     _updateRecentLocations(location);
@@ -182,4 +238,16 @@ class HomeController extends BaseController {
       refreshController.refreshFailed();
     }
   }
+
+  /// 当 Launch 数据更新时刷新节点
+  void refreshOnLaunchChanged() {
+    log(TAG, 'Launch data changed, refreshing locations');
+    _loadSavedLocations();
+  }
+
+  // 设置默认auto连接
+  void setDefaultAutoConnect() {
+    coreController.locationSelectionType = 'auto';
+    coreController.handleConnection();
+  }
 }

+ 57 - 54
lib/app/modules/home/views/home_view.dart

@@ -81,65 +81,68 @@ class HomeView extends BaseView<HomeController> {
                     enablePullUp: false,
                     controller: controller.refreshController,
                     onRefresh: controller.onRefresh,
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        // 80.verticalSpaceFromWidth,
-                        Padding(
-                          padding: EdgeInsets.symmetric(vertical: 20.w),
-                          child: CarouselSlider(
-                            options: CarouselOptions(
-                              height: 80.w,
-                              viewportFraction: 1.0,
+                    child: SingleChildScrollView(
+                      physics: const ClampingScrollPhysics(),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          // 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(),
                             ),
-                            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(
-                          Strings.activeTime.tr,
-                          style: TextStyle(
-                            fontSize: 18.sp,
-                            height: 1.3,
-                            color: Get.reactiveTheme.hintColor,
                           ),
-                        ),
-                        2.verticalSpaceFromWidth,
-                        Obx(
-                          () => Text(
-                            controller.coreController.timer,
+                          Text(
+                            Strings.activeTime.tr,
                             style: TextStyle(
-                              fontSize: 28.sp,
-                              height: 1.2,
-                              color: Get.reactiveTheme.primaryColor,
+                              fontSize: 18.sp,
+                              height: 1.3,
+                              color: Get.reactiveTheme.hintColor,
                             ),
                           ),
-                        ),
-
-                        20.verticalSpaceFromWidth,
-                        // 位置选择按钮和最近位置(叠在一起的效果)
-                        Stack(
-                          children: [
-                            Container(
-                              alignment: Alignment.center,
-                              margin: EdgeInsets.only(top: 138.w),
-                              child: _buildConnectionButton(),
+                          2.verticalSpaceFromWidth,
+                          Obx(
+                            () => Text(
+                              controller.coreController.timer,
+                              style: TextStyle(
+                                fontSize: 28.sp,
+                                height: 1.2,
+                                color: Get.reactiveTheme.primaryColor,
+                              ),
                             ),
-                            _buildLocationStack(),
-                          ],
-                        ),
-                      ],
+                          ),
+
+                          20.verticalSpaceFromWidth,
+                          // 位置选择按钮和最近位置(叠在一起的效果)
+                          Stack(
+                            children: [
+                              Container(
+                                alignment: Alignment.center,
+                                margin: EdgeInsets.only(top: 138.w),
+                                child: _buildConnectionButton(),
+                              ),
+                              _buildLocationStack(),
+                            ],
+                          ),
+                        ],
+                      ),
                     ),
                   ),
                 ),
@@ -162,7 +165,7 @@ class HomeView extends BaseView<HomeController> {
       () => ConnectionButton(
         state: controller.coreController.state,
         onTap: () {
-          controller.coreController.handleConnection();
+          controller.setDefaultAutoConnect();
         },
       ),
     );

+ 3 - 0
lib/app/modules/login/controllers/login_controller.dart

@@ -55,6 +55,9 @@ class LoginController extends GetxController {
     if (!isLogin) {
       return;
     }
+    // 取消输入框焦点,关闭软键盘,防止 LoadingDialog 关闭后软键盘再次弹出
+    FocusManager.instance.primaryFocus?.unfocus();
+
     final params = {
       "account": usernameController.text.trim(),
       "password": passwordController.text,

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

@@ -1,10 +1,12 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import '../../../constants/iconfont/iconfont.dart';
+import '../../../controllers/core_controller.dart';
 import '../../../data/models/launch/groups.dart';
 import '../../../data/sp/ix_sp.dart';
 
 class NodeController extends GetxController {
+  final coreController = Get.find<CoreController>();
   // Groups 数据
   final _groups = Rxn<Groups>();
   Groups? get groups => _groups.value;
@@ -24,6 +26,11 @@ class NodeController extends GetxController {
   // key 格式: "tabIndex_countryCode"
   final Map<String, bool> expandedStates = {};
 
+  // 当前选中的tab
+  String getCurrentTab() {
+    return tabTextList[currentTabIndex];
+  }
+
   @override
   void onInit() {
     super.onInit();
@@ -92,4 +99,15 @@ class NodeController extends GetxController {
     final key = '${tabIndex}_$countryCode';
     expandedStates[key] = expanded;
   }
+
+  /// 当 Launch 数据更新时刷新节点
+  void refreshOnLaunchChanged() {
+    _loadGroups();
+  }
+
+  // 设置tab选中状态
+  void setTabSelected(int tabIndex) {
+    currentTabIndex = tabIndex;
+    coreController.locationSelectionType = tabTextList[tabIndex];
+  }
 }

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

@@ -63,7 +63,7 @@ class NodeView extends BaseView<NodeController> {
       labelPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
       // 监听 Tab 切换,保存当前索引
       onTap: (index) {
-        controller.currentTabIndex = index;
+        controller.setTabSelected(index);
       },
       tabs: controller.tabTextList.map((e) {
         return Row(

+ 10 - 8
lib/app/modules/node/widgets/node_list.dart

@@ -32,18 +32,18 @@ class _NodeListState extends State<NodeList>
   bool get wantKeepAlive => true;
 
   // 获取 Controller
-  final controller = Get.find<NodeController>();
+  final nodeController = Get.find<NodeController>();
   final apiController = Get.find<ApiController>();
 
   /// 获取国家的展开状态
   bool _getExpandedState(String countryCode) {
-    return controller.getExpandedState(widget.tabIndex, countryCode);
+    return nodeController.getExpandedState(widget.tabIndex, countryCode);
   }
 
   /// 设置国家的展开状态
   void _setExpandedState(String countryCode, bool expanded) {
     setState(() {
-      controller.setExpandedState(widget.tabIndex, countryCode, expanded);
+      nodeController.setExpandedState(widget.tabIndex, countryCode, expanded);
     });
   }
 
@@ -54,7 +54,7 @@ class _NodeListState extends State<NodeList>
   void _onRefresh() async {
     try {
       final groups = await apiController.getLocations();
-      controller.updateGroups(groups);
+      nodeController.updateGroups(groups);
       _refreshController.refreshCompleted();
     } catch (e) {
       _refreshController.refreshFailed();
@@ -66,7 +66,7 @@ class _NodeListState extends State<NodeList>
     super.build(context);
 
     // 获取当前 tab 的数据
-    final data = controller.getDataByTabIndex(widget.tabIndex);
+    final data = nodeController.getDataByTabIndex(widget.tabIndex);
 
     if (data == null || data.tags == null || data.list == null) {
       return Center(
@@ -163,6 +163,7 @@ class _CountrySection extends StatelessWidget {
 
     // 获取 HomeController 并判断当前国家是否有选中的节点
     final homeController = Get.find<HomeController>();
+    final nodeController = Get.find<NodeController>();
     final hasSelectedLocation = locations.any(
       (loc) => loc.id == homeController.selectedLocation?.id,
     );
@@ -243,9 +244,10 @@ class _CountrySection extends StatelessWidget {
 
                     return ClickOpacity(
                       onTap: () {
-                        // 获取 HomeController
-                        final homeController = Get.find<HomeController>();
-                        homeController.selectLocation(location);
+                        homeController.selectLocation(
+                          location,
+                          locationSelectionType: nodeController.getCurrentTab(),
+                        );
                         homeController.handleConnect(delay: true);
                         // 返回上一页
                         Get.back();

+ 25 - 4
lib/app/modules/setting/controllers/setting_controller.dart

@@ -2,27 +2,48 @@ import 'package:get/get.dart';
 
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/awesome_notifications_helper.dart';
+import '../../../constants/enums.dart';
 import '../../../controllers/api_controller.dart';
+import '../../../data/sp/ix_sp.dart';
 import '../../../dialog/loading/loading_dialog.dart';
 
 class SettingController extends GetxController {
   final _apiController = Get.find<ApiController>();
 
   // 自动重连开关
-  final autoReconnect = true.obs;
+  final _autoReconnect = true.obs;
+  bool get autoReconnect => _autoReconnect.value;
+  set autoReconnect(bool value) => _autoReconnect.value = value;
 
-  final pushNotifications = false.obs;
+  final _pushNotifications = false.obs;
+  bool get pushNotifications => _pushNotifications.value;
+  set pushNotifications(bool value) => _pushNotifications.value = value;
 
-  final isPremium = true.obs;
+  final _isPremium = false.obs;
+  bool get isPremium => _isPremium.value;
+  set isPremium(bool value) => _isPremium.value = value;
+
+  //是否是游客
+  final _isGuest = false.obs;
+  bool get isGuest => _isGuest.value;
+  set isGuest(bool value) => _isGuest.value = value;
 
   @override
   void onInit() {
     super.onInit();
     initPushNotifications();
+    initIsGuest();
+  }
+
+  void initIsGuest() {
+    final user = IXSP.getUser();
+    if (user != null) {
+      isGuest = user.memberLevel == MemberLevel.guest.level;
+    }
   }
 
   Future<void> initPushNotifications() async {
-    pushNotifications.value =
+    pushNotifications =
         await AwesomeNotificationsHelper.checkNotificationPermission();
   }
 

+ 116 - 95
lib/app/modules/setting/views/setting_view.dart

@@ -48,8 +48,8 @@ class SettingView extends BaseView<SettingController> {
         _buildAppSection(),
 
         // Security Section
-        _buildSectionHeader(Strings.securitySection.tr),
-        _buildSecuritySection(),
+        // _buildSectionHeader(Strings.securitySection.tr),
+        // _buildSecuritySection(),
 
         // 底部间距
         SliverSafeArea(sliver: SliverToBoxAdapter(child: 0.verticalSpace)),
@@ -77,26 +77,45 @@ class SettingView extends BaseView<SettingController> {
   /// 构建登录分组
   Widget _buildLoginSection() {
     return SliverToBoxAdapter(
-      child: Container(
-        margin: EdgeInsets.only(left: 14.w, right: 14.w, bottom: 10.w),
-        decoration: BoxDecoration(
-          color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(12.r),
-        ),
-        child: _buildSettingItem(
-          icon: IconFont.icon37,
-          iconColor: Get.reactiveTheme.shadowColor,
-          title: Strings.login.tr,
-          trailing: Icon(
-            IconFont.icon02,
-            size: 20.w,
-            color: Get.reactiveTheme.hintColor,
+      child: Obx(() {
+        final isPremium = controller.isPremium;
+        final isGuest = controller.isGuest;
+        if (!isGuest) {
+          return SizedBox.shrink();
+        }
+        return Container(
+          margin: EdgeInsets.only(left: 14.w, right: 14.w, bottom: 10.w),
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.highlightColor,
+            borderRadius: BorderRadius.circular(12.r),
           ),
-          onTap: () {
-            Get.toNamed(Routes.LOGIN);
-          },
-        ),
-      ),
+          child: _buildSettingItem(
+            icon: IconFont.icon37,
+            iconColor: Get.reactiveTheme.shadowColor,
+            title: Strings.login.tr,
+            trailing: Row(
+              children: [
+                IXImage(
+                  source: isPremium ? Assets.premium : Assets.free,
+                  width: isPremium ? 92.w : 64.w,
+                  height: 28.w,
+                  sourceType: ImageSourceType.asset,
+                ),
+                4.horizontalSpace,
+                Icon(
+                  IconFont.icon02,
+                  size: 20.w,
+                  color: Get.reactiveTheme.hintColor,
+                ),
+              ],
+            ),
+
+            onTap: () {
+              Get.toNamed(Routes.LOGIN);
+            },
+          ),
+        );
+      }),
     );
   }
 
@@ -104,7 +123,8 @@ class SettingView extends BaseView<SettingController> {
   Widget _buildAccountSection() {
     return SliverToBoxAdapter(
       child: Obx(() {
-        final isPremium = controller.isPremium.value;
+        final isPremium = controller.isPremium;
+        final isGuest = controller.isGuest;
         return Container(
           margin: EdgeInsets.symmetric(horizontal: 14.w),
           decoration: BoxDecoration(
@@ -113,21 +133,22 @@ class SettingView extends BaseView<SettingController> {
           ),
           child: Column(
             children: [
-              _buildSettingItem(
-                icon: IconFont.icon29,
-                iconColor: Get.reactiveTheme.shadowColor,
-                title: Strings.account.tr,
-                trailing: IXImage(
-                  source: isPremium ? Assets.premium : Assets.free,
-                  width: isPremium ? 92.w : 64.w,
-                  height: 28.w,
-                  sourceType: ImageSourceType.asset,
+              if (!isGuest)
+                _buildSettingItem(
+                  icon: IconFont.icon29,
+                  iconColor: Get.reactiveTheme.shadowColor,
+                  title: Strings.account.tr,
+                  trailing: IXImage(
+                    source: isPremium ? Assets.premium : Assets.free,
+                    width: isPremium ? 92.w : 64.w,
+                    height: 28.w,
+                    sourceType: ImageSourceType.asset,
+                  ),
+                  onTap: () {
+                    Get.toNamed(Routes.ACCOUNT);
+                  },
                 ),
-                onTap: () {
-                  Get.toNamed(Routes.ACCOUNT);
-                },
-              ),
-              _buildDivider(),
+              if (!isGuest) _buildDivider(),
               _buildSettingItem(
                 icon: IconFont.icon14,
                 iconColor: Get.reactiveTheme.shadowColor,
@@ -158,34 +179,34 @@ class SettingView extends BaseView<SettingController> {
               _buildDivider(),
               // 根据用户类型显示不同的时间信息
               if (isPremium) ...[
-                _buildSettingItem(
-                  icon: IconFont.icon23,
-                  iconColor: Get.reactiveTheme.shadowColor,
-                  title: Strings.myPreCode.tr,
-                  trailing: Row(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      Text(
-                        '123***ADZ',
-                        style: TextStyle(
-                          fontSize: 13.sp,
-                          color: Get.reactiveTheme.hintColor,
-                        ),
-                      ),
-                      SizedBox(width: 4.w),
-                      Icon(
-                        IconFont.icon02,
-                        size: 20.w,
-                        color: Get.reactiveTheme.hintColor,
-                      ),
-                    ],
-                  ),
-                  onTap: () {
-                    // TODO: 跳转到Pre Code页面
-                    Get.toNamed(Routes.PRECODE);
-                  },
-                ),
-                _buildDivider(),
+                // _buildSettingItem(
+                //   icon: IconFont.icon23,
+                //   iconColor: Get.reactiveTheme.shadowColor,
+                //   title: Strings.myPreCode.tr,
+                //   trailing: Row(
+                //     mainAxisSize: MainAxisSize.min,
+                //     children: [
+                //       Text(
+                //         '123***ADZ',
+                //         style: TextStyle(
+                //           fontSize: 13.sp,
+                //           color: Get.reactiveTheme.hintColor,
+                //         ),
+                //       ),
+                //       SizedBox(width: 4.w),
+                //       Icon(
+                //         IconFont.icon02,
+                //         size: 20.w,
+                //         color: Get.reactiveTheme.hintColor,
+                //       ),
+                //     ],
+                //   ),
+                //   onTap: () {
+                //     // TODO: 跳转到Pre Code页面
+                //     Get.toNamed(Routes.PRECODE);
+                //   },
+                // ),
+                // _buildDivider(),
                 _buildSettingItem(
                   icon: IconFont.icon30,
                   iconColor: Get.reactiveTheme.shadowColor,
@@ -211,39 +232,39 @@ class SettingView extends BaseView<SettingController> {
                     '01:60:59 / Days',
                     style: TextStyle(
                       fontSize: 14.sp,
-                      color: const Color(0xFFFF9500),
+                      color: const Color(0xFFFFCC00),
                       fontWeight: FontWeight.w500,
                     ),
                   ),
                 ),
               ],
-              _buildDivider(),
-              _buildSettingItem(
-                icon: IconFont.icon31,
-                iconColor: Get.reactiveTheme.shadowColor,
-                title: Strings.deviceAuthorization.tr,
-                trailing: Row(
-                  mainAxisSize: MainAxisSize.min,
-                  children: [
-                    Text(
-                      isPremium ? '1/4' : '0/1',
-                      style: TextStyle(
-                        fontSize: 13.sp,
-                        color: Get.reactiveTheme.hintColor,
-                      ),
-                    ),
-                    SizedBox(width: 4.w),
-                    Icon(
-                      IconFont.icon02,
-                      size: 20.w,
-                      color: Get.reactiveTheme.hintColor,
-                    ),
-                  ],
-                ),
-                onTap: () {
-                  Get.toNamed(Routes.DEVICEAUTH);
-                },
-              ),
+              // _buildDivider(),
+              // _buildSettingItem(
+              //   icon: IconFont.icon31,
+              //   iconColor: Get.reactiveTheme.shadowColor,
+              //   title: Strings.deviceAuthorization.tr,
+              //   trailing: Row(
+              //     mainAxisSize: MainAxisSize.min,
+              //     children: [
+              //       Text(
+              //         isPremium ? '1/4' : '0/1',
+              //         style: TextStyle(
+              //           fontSize: 13.sp,
+              //           color: Get.reactiveTheme.hintColor,
+              //         ),
+              //       ),
+              //       SizedBox(width: 4.w),
+              //       Icon(
+              //         IconFont.icon02,
+              //         size: 20.w,
+              //         color: Get.reactiveTheme.hintColor,
+              //       ),
+              //     ],
+              //   ),
+              //   onTap: () {
+              //     Get.toNamed(Routes.DEVICEAUTH);
+              //   },
+              // ),
             ],
           ),
         );
@@ -300,9 +321,9 @@ class SettingView extends BaseView<SettingController> {
               title: Strings.autoReconnect.tr,
               trailing: Obx(
                 () => CupertinoSwitch(
-                  value: controller.autoReconnect.value,
+                  value: controller.autoReconnect,
                   onChanged: (value) {
-                    controller.autoReconnect.value = value;
+                    controller.autoReconnect = value;
                   },
                   activeTrackColor: Get.reactiveTheme.shadowColor,
                   thumbColor: Colors.white,
@@ -461,7 +482,7 @@ class SettingView extends BaseView<SettingController> {
               title: Strings.pushNotifications.tr,
               trailing: Obx(
                 () => CupertinoSwitch(
-                  value: controller.pushNotifications.value,
+                  value: controller.pushNotifications,
                   onChanged: (value) {
                     controller.showNotificationConfigPage();
                   },

+ 2 - 0
lib/app/modules/signup/controllers/signup_controller.dart

@@ -61,6 +61,8 @@ class SignupController extends GetxController {
       "password": passwordController.text,
       "registMode": RegisterMode.manual.value,
     };
+    // 取消输入框焦点,关闭软键盘,防止 LoadingDialog 关闭后软键盘再次弹出
+    FocusManager.instance.primaryFocus?.unfocus();
     await LoadingDialog.show(
       context: Get.context!,
       loadingText: Strings.signingUp.tr,

+ 268 - 70
lib/app/modules/subscription/controllers/subscription_controller.dart

@@ -8,10 +8,17 @@ import 'package:video_player/video_player.dart';
 import '../../../../config/theme/dark_theme_colors.dart';
 import '../../../../config/translations/strings_enum.dart';
 import '../../../../utils/in_app_purchase_util.dart';
+import '../../../../utils/log/logger.dart';
 import '../../../components/ix_snackbar.dart';
 import '../../../constants/assets.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../data/models/channelplan/channel_plan_list.dart';
+import '../../../data/sp/ix_sp.dart';
+import '../../../dialog/loading/loading_dialog.dart';
 
 class SubscriptionController extends GetxController {
+  static const String TAG = 'SubscriptionController';
+  final ApiController _apiController = Get.find<ApiController>();
   // 内购工具实例
   final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
 
@@ -19,71 +26,94 @@ class SubscriptionController extends GetxController {
   late VideoPlayerController videoController;
   final isVideoInitialized = false.obs;
 
+  // 套餐列表加载状态
+  final isLoadingPlans = false.obs;
+
   // 产品加载状态
   final isLoadingProducts = false.obs;
 
   // 购买处理中状态
   final isPurchasing = false.obs;
 
-  // 产品列表
+  // 产品列表(内购产品)
   final productDetails = <ProductDetails>[].obs;
 
-  // 当前选中的订阅计划索引 (0: 年度, 1: 终身, 2: 月度, 3: 周度)
+  // 套餐列表(API 返回)
+  final channelPlans = <ChannelPlan>[].obs;
+
+  // 当前选中的订阅计划索引
   final selectedPlanIndex = 0.obs;
 
-  // 产品ID配置
-  // iOS 测试: 使用 StoreKit Configuration 文件中配置的 ID
-  // Android 测试: 需要在 Google Play Console 中创建对应的测试产品
-  // 生产环境: 替换为你在 App Store Connect 和 Google Play Console 中创建的真实产品 ID
-  final Map<String, String> productIds = {
-    'yearly': 'com.test.yearly', // 年度订阅
-    'lifetime': 'com.test.lifetime', // 终身会员
-    'monthly': 'com.test.monthly', // 月度订阅
-    'weekly': 'com.test.weekly', // 周度订阅
-  };
-
-  // 订阅计划列表
-  List<Map<String, dynamic>> get plans => [
-    {
-      'productId': productIds['yearly'],
-      'price': _getProductPrice(productIds['yearly']!) ?? '\$40.00',
-      'period': Strings.perYear.tr,
-      'title': Strings.yearlyPlan.tr,
-      'badge': Strings.mostlyChoose.tr,
-      'badgeBgColor': DarkThemeColors.bg1,
-      'badgeTextColor': DarkThemeColors.subscriptionColor,
-      'badgeBorderColor': DarkThemeColors.dividerColor,
-    },
-    {
-      'productId': productIds['lifetime'],
-      'price': _getProductPrice(productIds['lifetime']!) ?? '\$58.00',
-      'period': Strings.once.tr,
-      'title': Strings.lifeTime.tr,
-      'badge': null,
-    },
-    {
-      'productId': productIds['monthly'],
-      'price': _getProductPrice(productIds['monthly']!) ?? '\$58.00',
-      'period': Strings.perYear.tr,
-      'title': Strings.monthPlan.tr,
-      'badge': null,
-    },
-    {
-      'productId': productIds['weekly'],
-      'price': _getProductPrice(productIds['weekly']!) ?? '\$1.00',
-      'period': Strings.perWeek.tr,
-      'title': Strings.weekPlan.tr,
-      'badge': Strings.limitedTime.tr,
-      'badgeBgColor': DarkThemeColors.primaryColor,
-      'badgeTextColor': Colors.white,
-      'badgeBorderColor': null,
-    },
-  ];
-
-  // 获取产品价格
-  String? _getProductPrice(String productId) {
-    final product = _iapUtil.getProductById(productId);
-    return product?.price;
+  /// 是否显示套餐变更信息(仅 userLevel == 3 时显示)
+  bool get showPlanChangeInfo {
+    final user = IXSP.getUser();
+    return user?.userLevel == 3;
+  }
+
+  // 获取产品价格(优先使用内购价格,否则使用 API 返回的价格)
+  String _getDisplayPrice(ChannelPlan plan) {
+    // 尝试从内购获取价格
+    final productId = plan.payoutData;
+    if (productId != null && productId.isNotEmpty) {
+      final product = _iapUtil.getProductById(productId);
+      if (product != null) {
+        return product.price;
+      }
+    }
+    // 使用 API 返回的价格
+    return _formatPrice(plan.price, plan.currency);
+  }
+
+  // 格式化价格
+  String _formatPrice(double? price, int? currency) {
+    if (price == null) return '';
+    // currency: 1=USD, 2=CNY, etc. 根据实际情况调整
+    final symbol = currency == 2 ? '¥' : '\$';
+    return '$symbol${price.toStringAsFixed(2)}';
+  }
+
+  // 获取订阅周期显示文本
+  String _getPeriodText(ChannelPlan plan) {
+    if (plan.isSubscribe != true) {
+      return Strings.once.tr; // 一次性购买(终身)
+    }
+    // 根据 subscribeType 和 subscribePeriodValue 确定周期
+    // subscribeType: 1=天, 2=周, 3=月, 4=年
+    switch (plan.subscribeType) {
+      case 1:
+        final days = plan.subscribePeriodValue ?? 1;
+        return '/$days day${days > 1 ? 's' : ''}';
+      case 2:
+        return Strings.perWeek.tr;
+      case 3:
+        return '/month';
+      case 4:
+        return Strings.perYear.tr;
+      default:
+        return '';
+    }
+  }
+
+  // 获取标签背景色
+  Color? _getBadgeBgColor(ChannelPlan plan) {
+    if (plan.tagType == 1) {
+      return DarkThemeColors.bg1;
+    }
+    if (plan.tag != null && plan.tag!.isNotEmpty) {
+      return DarkThemeColors.primaryColor;
+    }
+    return null;
+  }
+
+  // 获取标签文字颜色
+  Color? _getBadgeTextColor(ChannelPlan plan) {
+    if (plan.tagType == 1) {
+      return DarkThemeColors.subscriptionColor;
+    }
+    if (plan.tag != null && plan.tag!.isNotEmpty) {
+      return Colors.white;
+    }
+    return null;
   }
 
   @override
@@ -91,6 +121,7 @@ class SubscriptionController extends GetxController {
     super.onInit();
     _initializeVideoPlayer();
     _initializeInAppPurchase();
+    _getChannelPlanList();
   }
 
   @override
@@ -101,6 +132,47 @@ class SubscriptionController extends GetxController {
     super.onClose();
   }
 
+  /// 获取套餐列表
+  Future<void> _getChannelPlanList() async {
+    isLoadingPlans.value = true;
+    try {
+      final plans = await _apiController.getChannelPlanList();
+      channelPlans.value = plans;
+
+      // 设置默认选中项
+      final defaultIndex = plans.indexWhere((p) => p.isDefault == true);
+      if (defaultIndex >= 0) {
+        selectedPlanIndex.value = defaultIndex;
+      }
+
+      // 加载对应的内购产品
+      await _loadProductsFromPlans();
+    } catch (e) {
+      log(TAG, '获取套餐列表失败: $e');
+      IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '获取套餐列表失败');
+    } finally {
+      isLoadingPlans.value = false;
+    }
+  }
+
+  /// 购买套餐
+  Future<void> subscribe() async {
+    await LoadingDialog.show(
+      context: Get.context!,
+      loadingText: 'Purchasing...',
+      successText: 'Purchase successful',
+      onRequest: () async {
+        // 执行你的异步请求
+        await _apiController.subscribe({
+          'channelItemId': selectedPlan?.channelItemId,
+        });
+      },
+      onSuccess: () {
+        // 成功后的操作
+      },
+    );
+  }
+
   /// 初始化内购
   Future<void> _initializeInAppPurchase() async {
     try {
@@ -113,10 +185,7 @@ class SubscriptionController extends GetxController {
         onRestore: _handleRestoreSuccess,
       );
 
-      if (success) {
-        // 加载产品信息
-        await _loadProducts();
-      } else {
+      if (!success) {
         IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '内购功能不可用');
       }
     } catch (e) {
@@ -124,19 +193,29 @@ class SubscriptionController extends GetxController {
     }
   }
 
-  /// 加载产品信息
-  Future<void> _loadProducts() async {
-    isLoadingProducts.value = true;
+  /// 根据套餐列表加载内购产品
+  Future<void> _loadProductsFromPlans() async {
+    if (channelPlans.isEmpty) return;
 
+    isLoadingProducts.value = true;
     try {
-      final productIdSet = productIds.values.toSet();
-      final success = await _iapUtil.loadProducts(productIdSet);
+      // 从套餐列表中提取产品ID
+      final productIdSet = channelPlans
+          .where((p) => p.payoutData != null && p.payoutData!.isNotEmpty)
+          .map((p) => p.payoutData!)
+          .toSet();
+
+      if (productIdSet.isEmpty) {
+        debugPrint('没有需要加载的内购产品');
+        return;
+      }
 
+      final success = await _iapUtil.loadProducts(productIdSet);
       if (success) {
         productDetails.value = _iapUtil.products;
         debugPrint('加载了 ${productDetails.length} 个产品');
-      } else {
-        IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '加载产品信息失败');
+        // 刷新套餐列表以更新价格显示
+        channelPlans.refresh();
       }
     } catch (e) {
       debugPrint('加载产品失败: $e');
@@ -252,15 +331,34 @@ class SubscriptionController extends GetxController {
 
   // 选择订阅计划
   void selectPlan(int index) {
-    selectedPlanIndex.value = index;
+    if (index >= 0 && index < channelPlans.length) {
+      selectedPlanIndex.value = index;
+    }
+  }
+
+  // 获取当前选中的套餐
+  ChannelPlan? get selectedPlan {
+    if (selectedPlanIndex.value < channelPlans.length) {
+      return channelPlans[selectedPlanIndex.value];
+    }
+    return null;
+  }
+
+  /// 获取当前选中套餐的设备限制数量
+  String get selectedPlanDeviceLimit {
+    return (selectedPlan?.deviceLimit ?? 0).toString();
   }
 
   // 确认变更/购买
   void confirmChange() {
-    final plan = plans[selectedPlanIndex.value];
-    final productId = plan['productId'] as String?;
+    final plan = selectedPlan;
+    if (plan == null) {
+      IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '请选择套餐');
+      return;
+    }
 
-    if (productId == null) {
+    final productId = plan.payoutData;
+    if (productId == null || productId.isEmpty) {
       IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '产品ID未配置');
       return;
     }
@@ -281,4 +379,104 @@ class SubscriptionController extends GetxController {
       message: Strings.openingPaymentSupport.tr,
     );
   }
+
+  // ==================== 用于 UI 展示的便捷方法 ====================
+
+  /// 获取套餐数量
+  int get planCount => channelPlans.length;
+
+  /// 获取指定索引的套餐标题
+  String getPlanTitle(int index) {
+    if (index < channelPlans.length) {
+      return channelPlans[index].title ?? '';
+    }
+    return '';
+  }
+
+  /// 获取指定索引的套餐副标题
+  String getPlanSubTitle(int index) {
+    if (index < channelPlans.length) {
+      return channelPlans[index].subTitle ?? '';
+    }
+    return '';
+  }
+
+  /// 获取指定索引的套餐介绍
+  String getPlanIntroduce(int index) {
+    if (index < channelPlans.length) {
+      return channelPlans[index].introduce ?? '';
+    }
+    return '';
+  }
+
+  /// 获取指定索引的套餐价格显示
+  String getPlanPrice(int index) {
+    if (index < channelPlans.length) {
+      return _getDisplayPrice(channelPlans[index]);
+    }
+    return '';
+  }
+
+  /// 获取指定索引的套餐周期显示
+  String getPlanPeriod(int index) {
+    if (index < channelPlans.length) {
+      return _getPeriodText(channelPlans[index]);
+    }
+    return '';
+  }
+
+  /// 获取指定索引的套餐标签
+  String getPlanBadge(int index) {
+    if (index < channelPlans.length) {
+      return channelPlans[index].tag ?? '';
+    }
+    return '';
+  }
+
+  /// 获取指定索引的标签背景色
+  Color? getPlanBadgeBgColor(int index) {
+    if (index < channelPlans.length) {
+      return _getBadgeBgColor(channelPlans[index]);
+    }
+    return null;
+  }
+
+  /// 获取指定索引的标签文字颜色
+  Color? getPlanBadgeTextColor(int index) {
+    if (index < channelPlans.length) {
+      return _getBadgeTextColor(channelPlans[index]);
+    }
+    return null;
+  }
+
+  /// 获取指定索引的标签边框颜色
+  Color? getPlanBadgeBorderColor(int index) {
+    if (index < channelPlans.length) {
+      final plan = channelPlans[index];
+      if (plan.recommend == true) {
+        return DarkThemeColors.dividerColor;
+      }
+    }
+    return null;
+  }
+
+  /// 是否显示原价(有折扣时显示)
+  bool showOriginalPrice(int index) {
+    if (index < channelPlans.length) {
+      final plan = channelPlans[index];
+      return plan.orgPrice != null &&
+          plan.price != null &&
+          plan.orgPrice! > plan.price!;
+    }
+    return false;
+  }
+
+  /// 获取原价显示
+  String getPlanOriginalPrice(int index) {
+    if (index < channelPlans.length) {
+      final plan = channelPlans[index];
+      return _formatPrice(plan.orgPrice, plan.currency);
+    }
+    return '';
+  }
 }

+ 163 - 118
lib/app/modules/subscription/views/subscription_view.dart

@@ -74,7 +74,9 @@ class SubscriptionView extends GetView<SubscriptionController> {
                         _buildCurrentSubscription(),
                         24.verticalSpaceFromWidth,
                         _buildPlanOptions(),
-                        _buildPlanChangeInfo(),
+                        // 仅 userLevel == 3 时显示套餐变更信息
+                        if (controller.showPlanChangeInfo)
+                          _buildPlanChangeInfo(),
                         16.verticalSpaceFromWidth,
                         _buildPremiumFeatures(),
                         16.verticalSpaceFromWidth,
@@ -128,6 +130,7 @@ class SubscriptionView extends GetView<SubscriptionController> {
   // 当前订阅信息
   Widget _buildCurrentSubscription() {
     return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
       children: [
         // 钻石图标
         IXImage(
@@ -179,134 +182,170 @@ class SubscriptionView extends GetView<SubscriptionController> {
 
   // 订阅计划选项
   Widget _buildPlanOptions() {
-    return Obx(
-      () => Column(
-        children: List.generate(
-          controller.plans.length,
-          (index) => _buildPlanItem(
-            controller.plans[index],
-            index,
-            controller.selectedPlanIndex.value == index,
+    return Obx(() {
+      // 加载中状态
+      if (controller.isLoadingPlans.value) {
+        return Padding(
+          padding: EdgeInsets.symmetric(vertical: 40.w),
+          child: Center(
+            child: CircularProgressIndicator(
+              color: DarkThemeColors.subscriptionColor,
+            ),
+          ),
+        );
+      }
+
+      // 空数据状态
+      if (controller.planCount == 0) {
+        return Padding(
+          padding: EdgeInsets.symmetric(vertical: 40.w),
+          child: Center(
+            child: Text(
+              '暂无可用套餐',
+              style: TextStyle(
+                fontSize: 14.sp,
+                color: DarkThemeColors.hintTextColor,
+              ),
+            ),
           ),
+        );
+      }
+
+      // 套餐列表
+      return Column(
+        children: List.generate(
+          controller.planCount,
+          (index) => _buildPlanItem(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?;
+  Widget _buildPlanItem(int index) {
+    return Obx(() {
+      final isSelected = controller.selectedPlanIndex.value == index;
+      final badge = controller.getPlanBadge(index);
+      final badgeBgColor = controller.getPlanBadgeBgColor(index);
+      final badgeTextColor = controller.getPlanBadgeTextColor(index);
+      final badgeBorderColor = controller.getPlanBadgeBorderColor(index);
 
-    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,
+      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,
-                        ),
+          child: Stack(
+            clipBehavior: Clip.none,
+            children: [
+              // 主要内容
+              Padding(
+                padding: EdgeInsets.all(10.w),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    // 左侧:价格信息
+                    Expanded(
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          Text(
+                            controller.getPlanTitle(index),
+                            style: TextStyle(
+                              fontSize: 18.sp,
+                              height: 1.4,
+                              color: DarkThemeColors.bodyTextColor,
+                              fontWeight: FontWeight.w600,
+                            ),
+                          ),
+                          Text(
+                            controller.getPlanSubTitle(index),
+                            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,
+                    ),
+                    // 右侧:标题和选择框
+                    Row(
+                      children: [
+                        Text(
+                          controller.getPlanIntroduce(index),
+                          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(
+                        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.white30,
-                            width: 1.5.w,
+                                : Colors.transparent,
                           ),
-                          color: isSelected
-                              ? DarkThemeColors.primaryColor
-                              : Colors.transparent,
+                          child: isSelected
+                              ? Icon(
+                                  Icons.check,
+                                  color: Colors.white,
+                                  size: 12.w,
+                                )
+                              : null,
                         ),
-                        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,
+              // 标签固定在右上角,压在边框线上
+              if (badge.isNotEmpty)
+                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,
+                      ),
                     ),
                   ),
                 ),
-              ),
-          ],
+            ],
+          ),
         ),
-      ),
-    );
+      );
+    });
   }
 
   // 计划变更信息
@@ -364,9 +403,13 @@ class SubscriptionView extends GetView<SubscriptionController> {
               ),
               _buildFeatureItem(IconFont.icon61, Strings.unlockSmartMode.tr),
               _buildFeatureItem(IconFont.icon62, Strings.unlockMultiHopMode.tr),
-              _buildFeatureItem(
-                IconFont.icon63,
-                Strings.premiumCanShareXDevices.tr,
+              Obx(
+                () => _buildFeatureItem(
+                  IconFont.icon63,
+                  Strings.premiumCanShareXDevices.trParams({
+                    'count': controller.selectedPlanDeviceLimit,
+                  }),
+                ),
               ),
               _buildFeatureItem(
                 IconFont.icon64,
@@ -424,17 +467,19 @@ class SubscriptionView extends GetView<SubscriptionController> {
         children: [
           // 确认按钮
           GestureDetector(
-            onTap: controller.confirmChange,
+            onTap: controller.subscribe,
             child: Container(
               width: double.infinity,
-              height: 48.h,
+              height: 48.w,
               decoration: BoxDecoration(
                 color: DarkThemeColors.backgroundColor,
                 borderRadius: BorderRadius.circular(12.r),
               ),
               child: Center(
                 child: Text(
-                  Strings.confirmChange.tr,
+                  controller.showPlanChangeInfo
+                      ? Strings.confirmChange.tr
+                      : Strings.subscription.tr,
                   style: TextStyle(
                     fontSize: 16.sp,
                     color: DarkThemeColors.subscriptionColor,

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

@@ -111,7 +111,7 @@ final Map<String, String> arAR = {
   Strings.unlockAllFreeLocations: 'فتح جميع المواقع المجانية',
   Strings.unlockSmartMode: 'فتح الوضع الذكي',
   Strings.unlockMultiHopMode: 'فتح وضع Multi-hop',
-  Strings.premiumCanShareXDevices: 'يمكن لـ Premium مشاركة X أجهزة',
+  Strings.premiumCanShareXDevices: 'يمكن لـ Premium مشاركة @count أجهزة',
   Strings.ownYourOwnPrivateServer: 'امتلك خادمك الخاص',
   Strings.closeAds: 'إغلاق الإعلانات',
   Strings.confirmChange: 'تأكيد التغيير',
@@ -374,4 +374,13 @@ final Map<String, String> arAR = {
   // Push Notifications
   Strings.pushNotifications: 'الإشعارات الفورية',
   Strings.upgradeNow: 'الترقية الآن',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'ربط البريد الإلكتروني/مزايا العضوية',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'ربط الحساب/البريد الإلكتروني يحمي حقوق Pre الخاصة بك.',
+  Strings.associatedInterests: 'المزايا المرتبطة',
+  Strings.associatedInterestsDesc:
+      'يرجى ملاحظة قواعد الاشتراك التالية عند تسجيل الدخول:\n1. الحساب المجاني: سيرث الحساب الاشتراك الحالي على هذا الجهاز.\n2. حساب العضوية: سيتحول التطبيق إلى خطة ذلك الحساب الحالية.\nملاحظة: ستظل عملية الشراء على هذا الجهاز صالحة.',
+  Strings.notNow: 'ليس الآن',
 };

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

@@ -114,7 +114,7 @@ const Map<String, String> deDE = {
   Strings.unlockAllFreeLocations: 'Alle kostenlosen Standorte freischalten',
   Strings.unlockSmartMode: 'Smart-Modus freischalten',
   Strings.unlockMultiHopMode: 'Multi-Hop-Modus freischalten',
-  Strings.premiumCanShareXDevices: 'Premium kann X Geräte teilen',
+  Strings.premiumCanShareXDevices: 'Premium kann @count Geräte teilen',
   Strings.ownYourOwnPrivateServer: 'Besitzen Sie Ihren eigenen privaten Server',
   Strings.closeAds: 'Anzeigen schließen',
   Strings.confirmChange: 'Änderung bestätigen',
@@ -379,4 +379,13 @@ const Map<String, String> deDE = {
   // Push Notifications
   Strings.pushNotifications: 'Push-Benachrichtigungen',
   Strings.upgradeNow: 'Jetzt upgraden',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'E-Mail verknüpfen/Mitgliedervorteile',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Das Verknüpfen von Konto/E-Mail schützt Ihre Pre-Rechte.',
+  Strings.associatedInterests: 'Verknüpfte Vorteile',
+  Strings.associatedInterestsDesc:
+      'Bitte beachten Sie die folgenden Abonnementregeln nach der Anmeldung:\n1. Kostenloses Konto: Das Konto übernimmt das aktuelle Abonnement auf diesem Gerät.\n2. Mitgliedskonto: Die App wechselt zum bestehenden Plan dieses Kontos.\nHinweis: Der Kauf auf diesem Gerät bleibt gültig.',
+  Strings.notNow: 'Nicht jetzt',
 };

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

@@ -117,7 +117,7 @@ Map<String, String> enUs = {
   Strings.unlockAllFreeLocations: 'Unlock all free locations',
   Strings.unlockSmartMode: 'Unlock smart mode',
   Strings.unlockMultiHopMode: 'Unlock Multi-hop mode',
-  Strings.premiumCanShareXDevices: 'Premium can share X devices',
+  Strings.premiumCanShareXDevices: 'Premium can share @count devices',
   Strings.ownYourOwnPrivateServer: 'Own your own private server',
   Strings.closeAds: 'Close ads',
   Strings.confirmChange: 'Confirm Change',
@@ -390,4 +390,13 @@ Map<String, String> enUs = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Bind email/Member benefits',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Binding account/email protects your Pre rights.',
+  Strings.associatedInterests: 'Associated interests',
+  Strings.associatedInterestsDesc:
+      'Please note the following subscription rules upon login:\n1. Free Account: The account will inherit the current subscription on this device.\n2. Member Account: The app will switch to that account\'s existing plan.\nNote: The purchase on this device will remain valid.',
+  Strings.notNow: 'Not now',
 };

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

@@ -115,7 +115,7 @@ const Map<String, String> esEs = {
   Strings.unlockAllFreeLocations: 'Desbloquear todas las ubicaciones gratuitas',
   Strings.unlockSmartMode: 'Desbloquear modo inteligente',
   Strings.unlockMultiHopMode: 'Desbloquear modo Multi-salto',
-  Strings.premiumCanShareXDevices: 'Premium puede compartir X dispositivos',
+  Strings.premiumCanShareXDevices: 'Premium puede compartir @count dispositivos',
   Strings.ownYourOwnPrivateServer: 'Ten tu propio servidor privado',
   Strings.closeAds: 'Cerrar anuncios',
   Strings.confirmChange: 'Confirmar Cambio',
@@ -384,4 +384,13 @@ const Map<String, String> esEs = {
   // Push Notifications
   Strings.pushNotifications: 'Notificaciones push',
   Strings.upgradeNow: 'Actualizar ahora',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Vincular correo/Beneficios de membresía',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Vincular cuenta/correo protege tus derechos Pre.',
+  Strings.associatedInterests: 'Beneficios asociados',
+  Strings.associatedInterestsDesc:
+      'Ten en cuenta las siguientes reglas de suscripción al iniciar sesión:\n1. Cuenta gratuita: La cuenta heredará la suscripción actual en este dispositivo.\n2. Cuenta de miembro: La app cambiará al plan existente de esa cuenta.\nNota: La compra en este dispositivo seguirá siendo válida.',
+  Strings.notNow: 'Ahora no',
 };

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

@@ -115,7 +115,7 @@ const Map<String, String> faIR = {
   Strings.unlockSmartMode: 'باز کردن قفل حالت هوشمند',
   Strings.unlockMultiHopMode: 'باز کردن قفل حالت Multi-hop',
   Strings.premiumCanShareXDevices:
-      'Premium می‌تواند X دستگاه را به اشتراک بگذارد',
+      'Premium می‌تواند @count دستگاه را به اشتراک بگذارد',
   Strings.ownYourOwnPrivateServer: 'سرور خصوصی خود را داشته باشید',
   Strings.closeAds: 'بستن تبلیغات',
   Strings.confirmChange: 'تأیید تغییر',
@@ -380,4 +380,13 @@ const Map<String, String> faIR = {
   // Push Notifications
   Strings.pushNotifications: 'اعلان‌های فوری',
   Strings.upgradeNow: 'اکنون ارتقا دهید',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'اتصال ایمیل/مزایای عضویت',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'اتصال حساب/ایمیل از حقوق Pre شما محافظت می‌کند.',
+  Strings.associatedInterests: 'مزایای مرتبط',
+  Strings.associatedInterestsDesc:
+      'لطفاً به قوانین اشتراک زیر پس از ورود توجه کنید:\n1. حساب رایگان: حساب اشتراک فعلی این دستگاه را به ارث می‌برد.\n2. حساب عضویت: برنامه به طرح موجود آن حساب تغییر می‌کند.\nتوجه: خرید در این دستگاه معتبر باقی می‌ماند.',
+  Strings.notNow: 'الان نه',
 };

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

@@ -117,7 +117,7 @@ const Map<String, String> frFR = {
   Strings.unlockAllFreeLocations: 'Débloquer tous les emplacements gratuits',
   Strings.unlockSmartMode: 'Débloquer le mode intelligent',
   Strings.unlockMultiHopMode: 'Débloquer le mode Multi-saut',
-  Strings.premiumCanShareXDevices: 'Premium peut partager X appareils',
+  Strings.premiumCanShareXDevices: 'Premium peut partager @count appareils',
   Strings.ownYourOwnPrivateServer: 'Possédez votre propre serveur privé',
   Strings.closeAds: 'Fermer les publicités',
   Strings.confirmChange: 'Confirmer le changement',
@@ -385,4 +385,13 @@ const Map<String, String> frFR = {
   // Push Notifications
   Strings.pushNotifications: 'Notifications push',
   Strings.upgradeNow: 'Mettre à niveau maintenant',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Lier l\'email/Avantages membres',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Lier votre compte/email protège vos droits Pre.',
+  Strings.associatedInterests: 'Avantages associés',
+  Strings.associatedInterestsDesc:
+      'Veuillez noter les règles d\'abonnement suivantes lors de la connexion :\n1. Compte gratuit : Le compte héritera de l\'abonnement actuel sur cet appareil.\n2. Compte membre : L\'application passera au forfait existant de ce compte.\nRemarque : L\'achat sur cet appareil restera valide.',
+  Strings.notNow: 'Pas maintenant',
 };

+ 10 - 1
lib/config/translations/hi_IN/hi_in_translation.dart

@@ -84,7 +84,7 @@ Map<String, String> hiIN = {
   Strings.unlockAllFreeLocations: 'सभी मुफ्त स्थान अनलॉक करें',
   Strings.unlockSmartMode: 'स्मार्ट मोड अनलॉक करें',
   Strings.unlockMultiHopMode: 'Multi-hop मोड अनलॉक करें',
-  Strings.premiumCanShareXDevices: 'Premium X डिवाइस साझा कर सकता है',
+  Strings.premiumCanShareXDevices: 'Premium @count डिवाइस साझा कर सकता है',
   Strings.ownYourOwnPrivateServer: 'अपना खुद का प्राइवेट सर्वर रखें',
   Strings.closeAds: 'विज्ञापन बंद करें',
   Strings.confirmChange: 'परिवर्तन की पुष्टि करें',
@@ -293,5 +293,14 @@ Map<String, String> hiIN = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'ईमेल जोड़ें/सदस्य लाभ',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'खाता/ईमेल जोड़ने से आपके Pre अधिकार सुरक्षित रहते हैं।',
+  Strings.associatedInterests: 'संबंधित लाभ',
+  Strings.associatedInterestsDesc:
+      'कृपया लॉगिन के बाद निम्नलिखित सदस्यता नियमों पर ध्यान दें:\n1. मुफ्त खाता: खाता इस डिवाइस पर वर्तमान सदस्यता को प्राप्त करेगा।\n2. सदस्य खाता: ऐप उस खाते की मौजूदा योजना पर स्विच हो जाएगा।\nनोट: इस डिवाइस पर खरीदारी वैध रहेगी।',
+  Strings.notNow: 'अभी नहीं',
 };
 

+ 10 - 1
lib/config/translations/id_ID/id_id_translation.dart

@@ -84,7 +84,7 @@ Map<String, String> idID = {
   Strings.unlockAllFreeLocations: 'Buka semua lokasi gratis',
   Strings.unlockSmartMode: 'Buka mode pintar',
   Strings.unlockMultiHopMode: 'Buka mode Multi-hop',
-  Strings.premiumCanShareXDevices: 'Premium dapat berbagi X perangkat',
+  Strings.premiumCanShareXDevices: 'Premium dapat berbagi @count perangkat',
   Strings.ownYourOwnPrivateServer: 'Miliki server pribadi Anda sendiri',
   Strings.closeAds: 'Tutup iklan',
   Strings.confirmChange: 'Konfirmasi perubahan',
@@ -293,5 +293,14 @@ Map<String, String> idID = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Hubungkan email/Manfaat anggota',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Menghubungkan akun/email melindungi hak Pre Anda.',
+  Strings.associatedInterests: 'Manfaat terkait',
+  Strings.associatedInterestsDesc:
+      'Harap perhatikan aturan langganan berikut saat login:\n1. Akun Gratis: Akun akan mewarisi langganan saat ini di perangkat ini.\n2. Akun Anggota: Aplikasi akan beralih ke paket yang ada di akun tersebut.\nCatatan: Pembelian di perangkat ini akan tetap berlaku.',
+  Strings.notNow: 'Nanti saja',
 };
 

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

@@ -110,7 +110,7 @@ const Map<String, String> jaJP = {
   Strings.unlockAllFreeLocations: 'すべての無料ロケーションを解除',
   Strings.unlockSmartMode: 'スマートモードを解除',
   Strings.unlockMultiHopMode: 'マルチホップモードを解除',
-  Strings.premiumCanShareXDevices: 'PremiumはXデバイスを共有できます',
+  Strings.premiumCanShareXDevices: 'Premiumは@countデバイスを共有できます',
   Strings.ownYourOwnPrivateServer: '自分専用のプライベートサーバーを所有',
   Strings.closeAds: '広告を閉じる',
   Strings.confirmChange: '変更を確認',
@@ -359,4 +359,13 @@ const Map<String, String> jaJP = {
   // Push Notifications
   Strings.pushNotifications: 'プッシュ通知',
   Strings.upgradeNow: '今すぐアップグレード',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'メール連携/会員特典',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'アカウント/メールを連携すると、Pre特典が保護されます。',
+  Strings.associatedInterests: '関連特典',
+  Strings.associatedInterestsDesc:
+      'ログイン後の以下のサブスクリプションルールにご注意ください:\n1. 無料アカウント:アカウントはこのデバイスの現在のサブスクリプションを継承します。\n2. 会員アカウント:アプリはそのアカウントの既存プランに切り替わります。\n注意:このデバイスでの購入は有効なままです。',
+  Strings.notNow: '後で',
 };

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

@@ -107,7 +107,7 @@ const Map<String, String> koKR = {
   Strings.unlockAllFreeLocations: '모든 무료 위치 잠금 해제',
   Strings.unlockSmartMode: '스마트 모드 잠금 해제',
   Strings.unlockMultiHopMode: '멀티 홉 모드 잠금 해제',
-  Strings.premiumCanShareXDevices: 'Premium은 X 기기 공유 가능',
+  Strings.premiumCanShareXDevices: 'Premium은 @count 기기 공유 가능',
   Strings.ownYourOwnPrivateServer: '자신만의 전용 서버 소유',
   Strings.closeAds: '광고 닫기',
   Strings.confirmChange: '변경 확인',
@@ -352,4 +352,13 @@ const Map<String, String> koKR = {
   // Push Notifications
   Strings.pushNotifications: '푸시 알림',
   Strings.upgradeNow: '지금 업그레이드',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: '이메일 연결/회원 혜택',
+  Strings.bindingAccountEmailProtectsPreRights:
+      '계정/이메일을 연결하면 Pre 혜택이 보호됩니다.',
+  Strings.associatedInterests: '관련 혜택',
+  Strings.associatedInterestsDesc:
+      '로그인 후 다음 구독 규칙을 참고하세요:\n1. 무료 계정: 계정이 이 기기의 현재 구독을 상속합니다.\n2. 회원 계정: 앱이 해당 계정의 기존 요금제로 전환됩니다.\n참고: 이 기기에서의 구매는 유효합니다.',
+  Strings.notNow: '나중에',
 };

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

@@ -116,7 +116,7 @@ const Map<String, String> myMM = {
   Strings.unlockSmartMode: 'စမတ်မုဒ်ကို သော့ဖွင့်ပါ',
   Strings.unlockMultiHopMode: 'Multi-hop မုဒ်ကို သော့ဖွင့်ပါ',
   Strings.premiumCanShareXDevices:
-      'Premium သည် X စက်ပစ္စည်းများကို မျှဝေနိုင်သည်',
+      'Premium သည် @count စက်ပစ္စည်းများကို မျှဝေနိုင်သည်',
   Strings.ownYourOwnPrivateServer:
       'သင့်ကိုယ်ပိုင် ကိုယ်ပိုင်ဆာဗာကို ပိုင်ဆိုင်ပါ',
   Strings.closeAds: 'ကြော်ငြာများကို ပိတ်ပါ',
@@ -388,4 +388,13 @@ const Map<String, String> myMM = {
   // Push Notifications
   Strings.pushNotifications: 'Push အကြောင်းကြားချက်များ',
   Strings.upgradeNow: 'ယခုပင် အဆင့်မြှင့်ပါ',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'အီးမေးလ်ချိတ်ဆက်ခြင်း/အသင်းဝင်အကျိုးခံစားခွင့်များ',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'အကောင့်/အီးမေးလ်ချိတ်ဆက်ခြင်းသည် သင်၏ Pre အခွင့်အရေးများကို ကာကွယ်ပေးသည်။',
+  Strings.associatedInterests: 'ဆက်စပ်အကျိုးခံစားခွင့်များ',
+  Strings.associatedInterestsDesc:
+      'ဝင်ရောက်ပြီးနောက် အောက်ပါစာရင်းသွင်းစည်းမျဉ်းများကို သတိပြုပါ:\n1. အခမဲ့အကောင့်: အကောင့်သည် ဤစက်ပေါ်ရှိ လက်ရှိစာရင်းသွင်းမှုကို အမွေဆက်ခံမည်။\n2. အသင်းဝင်အကောင့်: အက်ပ်သည် ထိုအကောင့်၏ လက်ရှိအစီအစဉ်သို့ ပြောင်းလဲမည်။\nမှတ်ချက်: ဤစက်ပေါ်ရှိ ဝယ်ယူမှုသည် တရားဝင်ဆက်လက်ရှိနေမည်။',
+  Strings.notNow: 'အခုမဟုတ်သေးပါ',
 };

+ 10 - 1
lib/config/translations/pt_BR/pt_br_translation.dart

@@ -115,7 +115,7 @@ Map<String, String> ptBR = {
   Strings.unlockAllFreeLocations: 'Desbloquear todas as localizações grátis',
   Strings.unlockSmartMode: 'Desbloquear modo inteligente',
   Strings.unlockMultiHopMode: 'Desbloquear modo Multi-hop',
-  Strings.premiumCanShareXDevices: 'Premium pode compartilhar X dispositivos',
+  Strings.premiumCanShareXDevices: 'Premium pode compartilhar @count dispositivos',
   Strings.ownYourOwnPrivateServer: 'Tenha seu próprio servidor privado',
   Strings.closeAds: 'Fechar anúncios',
   Strings.confirmChange: 'Confirmar mudança',
@@ -423,5 +423,14 @@ Map<String, String> ptBR = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Vincular email/Benefícios de membro',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Vincular conta/email protege seus direitos Pre.',
+  Strings.associatedInterests: 'Benefícios associados',
+  Strings.associatedInterestsDesc:
+      'Por favor, observe as seguintes regras de assinatura ao fazer login:\n1. Conta Gratuita: A conta herdará a assinatura atual neste dispositivo.\n2. Conta de Membro: O app mudará para o plano existente dessa conta.\nNota: A compra neste dispositivo permanecerá válida.',
+  Strings.notNow: 'Agora não',
 };
 

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

@@ -116,7 +116,7 @@ const Map<String, String> ruRU = {
   Strings.unlockAllFreeLocations: 'Разблокировать все бесплатные локации',
   Strings.unlockSmartMode: 'Разблокировать умный режим',
   Strings.unlockMultiHopMode: 'Разблокировать режим Multi-hop',
-  Strings.premiumCanShareXDevices: 'Premium может делиться X устройствами',
+  Strings.premiumCanShareXDevices: 'Premium может делиться @count устройствами',
   Strings.ownYourOwnPrivateServer:
       'Владейте своим собственным частным сервером',
   Strings.closeAds: 'Закрыть рекламу',
@@ -384,4 +384,13 @@ const Map<String, String> ruRU = {
   // Push Notifications
   Strings.pushNotifications: 'Push-уведомления',
   Strings.upgradeNow: 'Обновить сейчас',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Привязать email/Преимущества членства',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Привязка аккаунта/email защищает ваши права Pre.',
+  Strings.associatedInterests: 'Связанные преимущества',
+  Strings.associatedInterestsDesc:
+      'Обратите внимание на следующие правила подписки при входе:\n1. Бесплатный аккаунт: Аккаунт унаследует текущую подписку на этом устройстве.\n2. Аккаунт участника: Приложение переключится на существующий план этого аккаунта.\nПримечание: Покупка на этом устройстве останется действительной.',
+  Strings.notNow: 'Не сейчас',
 };

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

@@ -432,4 +432,12 @@ class Strings {
 
   static const String pushNotifications = 'Push Notifications';
   static const String upgradeNow = 'Upgrade Now';
+
+  static const String bindEmailMemberBenefits = 'Bind email/Member benefits';
+  static const String bindingAccountEmailProtectsPreRights =
+      'Binding account/email protects your Pre rights.';
+  static const String associatedInterests = 'Associated interests';
+  static const String associatedInterestsDesc =
+      'Please note the following subscription rules upon login:\n1. Free Account: The account will inherit the current subscription on this device.\n2. Member Account: The app will switch to that account\'s existing plan.\nNote: The purchase on this device will remain valid.';
+  static const String notNow = 'Not now';
 }

+ 10 - 1
lib/config/translations/th_TH/th_th_translation.dart

@@ -84,7 +84,7 @@ Map<String, String> thTH = {
   Strings.unlockAllFreeLocations: 'ปลดล็อกตำแหน่งฟรีทั้งหมด',
   Strings.unlockSmartMode: 'ปลดล็อกโหมดอัจฉริยะ',
   Strings.unlockMultiHopMode: 'ปลดล็อกโหมด Multi-hop',
-  Strings.premiumCanShareXDevices: 'Premium สามารถแชร์ X อุปกรณ์',
+  Strings.premiumCanShareXDevices: 'Premium สามารถแชร์ @count อุปกรณ์',
   Strings.ownYourOwnPrivateServer: 'มีเซิร์ฟเวอร์ส่วนตัวของคุณเอง',
   Strings.closeAds: 'ปิดโฆษณา',
   Strings.confirmChange: 'ยืนยันการเปลี่ยนแปลง',
@@ -293,5 +293,14 @@ Map<String, String> thTH = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'เชื่อมต่ออีเมล/สิทธิประโยชน์สมาชิก',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'การเชื่อมต่อบัญชี/อีเมลจะปกป้องสิทธิ์ Pre ของคุณ',
+  Strings.associatedInterests: 'สิทธิประโยชน์ที่เกี่ยวข้อง',
+  Strings.associatedInterestsDesc:
+      'โปรดทราบกฎการสมัครสมาชิกต่อไปนี้เมื่อเข้าสู่ระบบ:\n1. บัญชีฟรี: บัญชีจะรับช่วงการสมัครสมาชิกปัจจุบันบนอุปกรณ์นี้\n2. บัญชีสมาชิก: แอปจะเปลี่ยนไปใช้แผนที่มีอยู่ของบัญชีนั้น\nหมายเหตุ: การซื้อบนอุปกรณ์นี้จะยังคงใช้ได้',
+  Strings.notNow: 'ไม่ใช่ตอนนี้',
 };
 

+ 10 - 1
lib/config/translations/tk_TM/tk_tm_translation.dart

@@ -114,7 +114,7 @@ Map<String, String> tkTM = {
   Strings.unlockAllFreeLocations: 'Ähli mugt ýerleri aç',
   Strings.unlockSmartMode: 'Akylly rejäni aç',
   Strings.unlockMultiHopMode: 'Köp hoppy rejäni aç',
-  Strings.premiumCanShareXDevices: 'Premium X enjam paýlaşyp biler',
+  Strings.premiumCanShareXDevices: 'Premium @count enjam paýlaşyp biler',
   Strings.ownYourOwnPrivateServer: 'Öz hususy serweriňiz bolsun',
   Strings.closeAds: 'Mahabatlary ýap',
   Strings.confirmChange: 'Üýtgetmäni tassykla',
@@ -420,4 +420,13 @@ Map<String, String> tkTM = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'E-poçta birikdirmek/Agza artykmaçlyklary',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Hasap/e-poçta birikdirmek Pre hukuklaryňyzy goraýar.',
+  Strings.associatedInterests: 'Baglanyşykly artykmaçlyklar',
+  Strings.associatedInterestsDesc:
+      'Giriş edeňizde aşakdaky abuna ýazgy düzgünlerini belläň:\n1. Mugt hasap: Hasap bu enjamda häzirki abuna ýazgyny miras alar.\n2. Agza hasap: Programma şol hasabyň bar bolan meýilnamasyna geçer.\nBellik: Bu enjamda satyn alyş güýjini saklar.',
+  Strings.notNow: 'Häzir däl',
 };

+ 10 - 1
lib/config/translations/tl_PH/tl_ph_translation.dart

@@ -84,7 +84,7 @@ Map<String, String> tlPH = {
   Strings.unlockAllFreeLocations: 'I-unlock ang lahat ng libreng lokasyon',
   Strings.unlockSmartMode: 'I-unlock ang smart mode',
   Strings.unlockMultiHopMode: 'I-unlock ang Multi-hop mode',
-  Strings.premiumCanShareXDevices: 'Maaaring ibahagi ng Premium ang X devices',
+  Strings.premiumCanShareXDevices: 'Maaaring ibahagi ng Premium ang @count devices',
   Strings.ownYourOwnPrivateServer: 'Magkaroon ng sariling private server',
   Strings.closeAds: 'Isara ang ads',
   Strings.confirmChange: 'Kumpirmahin ang pagbabago',
@@ -293,5 +293,14 @@ Map<String, String> tlPH = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'I-link ang email/Mga benepisyo ng miyembro',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Ang pag-link ng account/email ay nagpoprotekta sa iyong mga karapatan sa Pre.',
+  Strings.associatedInterests: 'Mga kaugnay na benepisyo',
+  Strings.associatedInterestsDesc:
+      'Pakitandaan ang mga sumusunod na patakaran sa subscription sa pag-login:\n1. Libreng Account: Mamanahin ng account ang kasalukuyang subscription sa device na ito.\n2. Member Account: Lilipat ang app sa umiiral na plano ng account na iyon.\nTandaan: Mananatiling valid ang pagbili sa device na ito.',
+  Strings.notNow: 'Hindi ngayon',
 };
 

+ 10 - 1
lib/config/translations/tr_TR/tr_tr_translation.dart

@@ -84,7 +84,7 @@ Map<String, String> trTR = {
   Strings.unlockAllFreeLocations: 'Tüm ücretsiz konumların kilidini aç',
   Strings.unlockSmartMode: 'Akıllı modun kilidini aç',
   Strings.unlockMultiHopMode: 'Multi-hop modunun kilidini aç',
-  Strings.premiumCanShareXDevices: 'Premium X cihaz paylaşabilir',
+  Strings.premiumCanShareXDevices: 'Premium @count cihaz paylaşabilir',
   Strings.ownYourOwnPrivateServer: 'Kendi özel sunucunuza sahip olun',
   Strings.closeAds: 'Reklamları kapat',
   Strings.confirmChange: 'Değişikliği onayla',
@@ -293,5 +293,14 @@ Map<String, String> trTR = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'E-posta bağla/Üyelik avantajları',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Hesap/e-posta bağlamak Pre haklarınızı korur.',
+  Strings.associatedInterests: 'İlişkili avantajlar',
+  Strings.associatedInterestsDesc:
+      'Giriş yaptıktan sonra lütfen aşağıdaki abonelik kurallarını dikkate alın:\n1. Ücretsiz Hesap: Hesap, bu cihazdaki mevcut aboneliği devralacaktır.\n2. Üye Hesap: Uygulama, o hesabın mevcut planına geçecektir.\nNot: Bu cihazdaki satın alma geçerli kalacaktır.',
+  Strings.notNow: 'Şimdi değil',
 };
 

+ 92 - 43
lib/config/translations/vi_VN/vi_vn_translation.dart

@@ -16,8 +16,10 @@ Map<String, String> viVN = {
   Strings.noData: 'Không có dữ liệu',
   Strings.refresh: 'Làm mới',
   Strings.unableToConnectNetwork: 'Vui lòng kiểm tra kết nối internet',
-  Strings.unableToConnectServer: 'Máy chủ tạm thời không khả dụng, vui lòng thử lại sau',
-  Strings.regionRestricted: 'Do luật pháp và quy định địa phương, dịch vụ FKey không khả dụng ở khu vực của bạn.',
+  Strings.unableToConnectServer:
+      'Máy chủ tạm thời không khả dụng, vui lòng thử lại sau',
+  Strings.regionRestricted:
+      'Do luật pháp và quy định địa phương, dịch vụ FKey không khả dụng ở khu vực của bạn.',
   Strings.updateNow: 'Cập nhật ngay',
   Strings.newVersionAvailable: 'Có phiên bản mới',
   Strings.eUtilOpenEmail: 'Lỗi khi mở email',
@@ -26,7 +28,8 @@ Map<String, String> viVN = {
   Strings.error: 'Lỗi',
   Strings.terms: 'Điều khoản',
   Strings.privacy: 'Bảo mật',
-  Strings.termsAgreementPrefix: 'Bằng việc đăng ký hoặc tiếp tục, bạn đồng ý với ',
+  Strings.termsAgreementPrefix:
+      'Bằng việc đăng ký hoặc tiếp tục, bạn đồng ý với ',
   Strings.termsAgreementConnector: ' và ',
   Strings.account: 'Tài khoản',
   Strings.processing: 'Đang xử lý...',
@@ -72,25 +75,29 @@ Map<String, String> viVN = {
   Strings.currentSubscription: 'Đăng ký hiện tại',
   Strings.upgradeToPremium: 'Nâng cấp lên Premium',
   Strings.activatePreCode: 'Kích hoạt Pre Code',
-  Strings.preCodeHint: 'Nếu bạn có Pre code, vui lòng nhập để nhận quyền lợi Pre.',
+  Strings.preCodeHint:
+      'Nếu bạn có Pre code, vui lòng nhập để nhận quyền lợi Pre.',
   Strings.planChangeInfo: 'Thông tin thay đổi gói',
   Strings.whenItStarts: 'Khi nào bắt đầu',
   Strings.whatHappensToYourBalance: 'Điều gì xảy ra với số dư của bạn',
   Strings.extraTime: 'Thời gian thêm',
   Strings.yourNewPlanBeginsRightAway: 'Gói mới của bạn bắt đầu ngay.',
-  Strings.anyUnusedAmountFromYourOldPlan: 'Bất kỳ số tiền chưa sử dụng từ gói cũ sẽ được thêm vào gói mới.',
-  Strings.youllGetExtraDays: 'Bạn sẽ nhận được thêm ngày dựa trên số dư còn lại.',
+  Strings.anyUnusedAmountFromYourOldPlan:
+      'Bất kỳ số tiền chưa sử dụng từ gói cũ sẽ được thêm vào gói mới.',
+  Strings.youllGetExtraDays:
+      'Bạn sẽ nhận được thêm ngày dựa trên số dư còn lại.',
   Strings.premiumsIncluded: 'Bao gồm trong Premium',
   Strings.unlockAllFreeLocations: 'Mở khóa tất cả vị trí miễn phí',
   Strings.unlockSmartMode: 'Mở khóa chế độ thông minh',
   Strings.unlockMultiHopMode: 'Mở khóa chế độ Multi-hop',
-  Strings.premiumCanShareXDevices: 'Premium có thể chia sẻ X thiết bị',
+  Strings.premiumCanShareXDevices: 'Premium có thể chia sẻ @count thiết bị',
   Strings.ownYourOwnPrivateServer: 'Sở hữu máy chủ riêng của bạn',
   Strings.closeAds: 'Tắt quảng cáo',
   Strings.confirmChange: 'Xác nhận thay đổi',
   Strings.restorePurchases: 'Khôi phục mua hàng',
   Strings.paymentIssue: 'Vấn đề thanh toán',
-  Strings.yearlyAutoRenewCancelAnytime: 'Tự động gia hạn hàng năm. Hủy bất cứ lúc nào',
+  Strings.yearlyAutoRenewCancelAnytime:
+      'Tự động gia hạn hàng năm. Hủy bất cứ lúc nào',
   Strings.recent: 'Gần đây',
   Strings.moviesAndTV: 'Phim & TV',
   Strings.social: 'Xã hội',
@@ -100,38 +107,51 @@ Map<String, String> viVN = {
   Strings.game: 'Trò chơi',
   Strings.sorry: 'Xin lỗi',
   Strings.unableToLoadData: 'Không thể tải dữ liệu',
-  Strings.dueLawsAndRegulations: 'Do luật pháp và quy định địa phương, \ndịch vụ NOMOVPN không khả dụng ở \nkhu vực hiện tại của bạn.',
+  Strings.dueLawsAndRegulations:
+      'Do luật pháp và quy định địa phương, \ndịch vụ NOMOVPN không khả dụng ở \nkhu vực hiện tại của bạn.',
   Strings.sendPreCodeToEmail: 'Gửi Pre Code qua Email',
   Strings.selectServer: 'Chọn máy chủ',
   Strings.premiumActivated: 'Premium đã kích hoạt thành công!',
-  Strings.premiumActivatedMessage: 'Bạn đã được nâng cấp lên Premium. Tận hưởng tất cả tính năng nâng cao và trải nghiệm duyệt web được cải thiện.',
+  Strings.premiumActivatedMessage:
+      'Bạn đã được nâng cấp lên Premium. Tận hưởng tất cả tính năng nâng cao và trải nghiệm duyệt web được cải thiện.',
   Strings.gotIt: 'Đã hiểu',
   Strings.emailSent: 'Email đã gửi thành công',
-  Strings.emailSentMessage: 'Pre Code của bạn đã được gửi đến email.\nVui lòng kiểm tra hộp thư đến (và thư rác).',
+  Strings.emailSentMessage:
+      'Pre Code của bạn đã được gửi đến email.\nVui lòng kiểm tra hộp thư đến (và thư rác).',
   Strings.noInternetConnection: 'Không có kết nối internet',
-  Strings.noInternetMessage: 'Có vẻ như bạn đang ngoại tuyến. Vui lòng kiểm tra kết nối internet và thử lại.',
+  Strings.noInternetMessage:
+      'Có vẻ như bạn đang ngoại tuyến. Vui lòng kiểm tra kết nối internet và thử lại.',
   Strings.logOut: 'Đăng xuất',
-  Strings.logOutConfirmMessage: 'Bạn có chắc muốn đăng xuất? Bạn sẽ cần đăng nhập lại để truy cập các tính năng Premium.',
+  Strings.logOutConfirmMessage:
+      'Bạn có chắc muốn đăng xuất? Bạn sẽ cần đăng nhập lại để truy cập các tính năng Premium.',
   Strings.thankYouFeedback: 'Cảm ơn phản hồi của bạn!',
-  Strings.feedbackMessage: 'Chúng tôi rất tiếc vì bạn không hài lòng với trải nghiệm. Chúng tôi sẽ cố gắng cải thiện sớm.',
+  Strings.feedbackMessage:
+      'Chúng tôi rất tiếc vì bạn không hài lòng với trải nghiệm. Chúng tôi sẽ cố gắng cải thiện sớm.',
   Strings.done: 'Xong',
   Strings.whatIsUid: 'UID là gì?',
-  Strings.uidMessage: 'ID thiết bị (UID) Đây là mã định danh duy nhất của thiết bị của bạn. Cung cấp ID này giúp đội ngũ hỗ trợ xác minh thiết bị và giải quyết vấn đề nhanh hơn.',
+  Strings.uidMessage:
+      'ID thiết bị (UID) Đây là mã định danh duy nhất của thiết bị của bạn. Cung cấp ID này giúp đội ngũ hỗ trợ xác minh thiết bị và giải quyết vấn đề nhanh hơn.',
   Strings.confirm: 'Xác nhận',
   Strings.copy: 'Sao chép',
   Strings.pleaseKeepPageOpen: 'Vui lòng giữ trang này mở.',
   Strings.authorizationCode: 'Mã ủy quyền',
-  Strings.authorizationCodeDesc: 'Mã 6 chữ số này cho phép người dùng VIP liên kết thiết bị của bạn. Nó làm mới mỗi 15 phút.',
+  Strings.authorizationCodeDesc:
+      'Mã 6 chữ số này cho phép người dùng VIP liên kết thiết bị của bạn. Nó làm mới mỗi 15 phút.',
   Strings.shareWithPreUser: 'Chia sẻ với người dùng Pre',
-  Strings.shareWithPreUserDesc: 'Cho người dùng VIP biết mã này để họ có thể nhập trên thiết bị của họ để ủy quyền cho bạn.',
+  Strings.shareWithPreUserDesc:
+      'Cho người dùng VIP biết mã này để họ có thể nhập trên thiết bị của họ để ủy quyền cho bạn.',
   Strings.waitingForAuthorization: 'Đang chờ ủy quyền',
-  Strings.waitingForAuthorizationDesc: 'Vui lòng giữ trang này mở.\nSau khi được phê duyệt, tài khoản của bạn sẽ tự động nâng cấp và kết nối lại.',
+  Strings.waitingForAuthorizationDesc:
+      'Vui lòng giữ trang này mở.\nSau khi được phê duyệt, tài khoản của bạn sẽ tự động nâng cấp và kết nối lại.',
   Strings.enterCode: 'Nhập mã',
-  Strings.enterCodeDesc: 'Nhập mã 6 chữ số hiển thị trên thiết bị khác (người dùng miễn phí). Mã này làm mới mỗi 15 phút.',
+  Strings.enterCodeDesc:
+      'Nhập mã 6 chữ số hiển thị trên thiết bị khác (người dùng miễn phí). Mã này làm mới mỗi 15 phút.',
   Strings.verifyDevice: 'Xác minh thiết bị',
-  Strings.verifyDeviceDesc: 'Chúng tôi sẽ kiểm tra xem mã đã nhập có khớp với thiết bị đang chờ ủy quyền không.',
+  Strings.verifyDeviceDesc:
+      'Chúng tôi sẽ kiểm tra xem mã đã nhập có khớp với thiết bị đang chờ ủy quyền không.',
   Strings.authorizationSuccessful: 'Ủy quyền thành công',
-  Strings.authorizationSuccessfulDesc: 'Sau khi xác nhận, thiết bị sẽ tự động nâng cấp và liên kết với tài khoản của bạn.',
+  Strings.authorizationSuccessfulDesc:
+      'Sau khi xác nhận, thiết bị sẽ tự động nâng cấp và liên kết với tài khoản của bạn.',
   Strings.deviceLimitReached: 'Đã đạt giới hạn thiết bị',
   Strings.deviceLimitMessage: 'Bạn chỉ có thể ủy quyền tối đa',
   Strings.devices: 'thiết bị',
@@ -139,28 +159,36 @@ Map<String, String> viVN = {
   Strings.deviceAuthorizedMessage: 'Thiết bị mới đã được ủy quyền thành công',
   Strings.relieveDevice: 'Gỡ bỏ thiết bị',
   Strings.relieveDeviceMessage: 'Bạn có chắc muốn gỡ bỏ',
-  Strings.relieveDeviceLoseAccess: 'Thiết bị này sẽ mất quyền truy cập Premium.',
+  Strings.relieveDeviceLoseAccess:
+      'Thiết bị này sẽ mất quyền truy cập Premium.',
   Strings.deviceRelieved: 'Thiết bị đã được gỡ bỏ',
-  Strings.deviceRelievedMessage: 'đã được gỡ bỏ khỏi các thiết bị được ủy quyền',
+  Strings.deviceRelievedMessage:
+      'đã được gỡ bỏ khỏi các thiết bị được ủy quyền',
   Strings.currentDevice: 'Thiết bị hiện tại',
   Strings.androidDevices: 'Thiết bị Android',
   Strings.authCodeCopied: 'Mã ủy quyền đã được sao chép',
   Strings.invalidAuthorizationCode: 'Mã ủy quyền không hợp lệ',
-  Strings.invalidAuthorizationCodeMessage: 'Mã bạn nhập không chính xác hoặc đã hết hạn.\nVui lòng kiểm tra mã 6 chữ số trên thiết bị khác và thử lại.',
+  Strings.invalidAuthorizationCodeMessage:
+      'Mã bạn nhập không chính xác hoặc đã hết hạn.\nVui lòng kiểm tra mã 6 chữ số trên thiết bị khác và thử lại.',
   Strings.invalidAuthorizationCodeButton: 'Thử lại',
   Strings.codeBackedUpMessage: 'Mã của bạn sẽ được sao lưu vào email này.',
   Strings.enterYourEmail: 'Nhập email của bạn',
   Strings.sendYourEmail: 'Gửi email của bạn',
   Strings.yourPreCredential: 'Thông tin xác thực Pre của bạn',
-  Strings.yourPreCredentialDesc: 'Đây là thông tin xác thực VIP của bạn. Vui lòng lưu trữ an toàn và không chia sẻ với bất kỳ ai.',
+  Strings.yourPreCredentialDesc:
+      'Đây là thông tin xác thực VIP của bạn. Vui lòng lưu trữ an toàn và không chia sẻ với bất kỳ ai.',
   Strings.secureEmailBackup: 'Sao lưu email an toàn',
-  Strings.secureEmailBackupDesc: 'Chúng tôi sẽ gửi email chứa thông tin xác thực này đến địa chỉ email bạn chỉ định để lưu giữ an toàn.',
+  Strings.secureEmailBackupDesc:
+      'Chúng tôi sẽ gửi email chứa thông tin xác thực này đến địa chỉ email bạn chỉ định để lưu giữ an toàn.',
   Strings.sendAndSave: 'Gửi và lưu',
-  Strings.sendAndSaveDesc: 'Sau khi email được gửi, chúng tôi khuyên bạn cũng nên lưu thông tin xác thực này vào vị trí an toàn trên thiết bị.',
+  Strings.sendAndSaveDesc:
+      'Sau khi email được gửi, chúng tôi khuyên bạn cũng nên lưu thông tin xác thực này vào vị trí an toàn trên thiết bị.',
   Strings.smart: 'Thông minh',
-  Strings.smartModeDesc: 'Mạng cục bộ và VPN cùng tồn tại, và tuyến đường tối ưu được chọn một cách thông minh.',
+  Strings.smartModeDesc:
+      'Mạng cục bộ và VPN cùng tồn tại, và tuyến đường tối ưu được chọn một cách thông minh.',
   Strings.global: 'Toàn cầu',
-  Strings.globalModeDesc: 'Tất cả lưu lượng được định tuyến qua máy chủ VPN để đảm bảo quyền riêng tư và bảo mật tối đa.',
+  Strings.globalModeDesc:
+      'Tất cả lưu lượng được định tuyến qua máy chủ VPN để đảm bảo quyền riêng tư và bảo mật tối đa.',
   Strings.perYear: 'Mỗi năm',
   Strings.yearlyPlan: 'Gói hàng năm',
   Strings.mostlyChoose: 'Được chọn nhiều nhất',
@@ -190,10 +218,12 @@ Map<String, String> viVN = {
   Strings.yourDataIsSafe: 'Dữ liệu của bạn an toàn…',
   Strings.login: 'Đăng nhập',
   Strings.loginButton: 'Đăng nhập',
-  Strings.loginDescription: 'Sau khi đăng nhập thành công, thời gian dùng thử miễn phí sẽ được áp dụng và thời gian thành viên còn lại sẽ được đồng bộ với tài khoản để sử dụng trên tất cả thiết bị liên kết.',
+  Strings.loginDescription:
+      'Sau khi đăng nhập thành công, thời gian dùng thử miễn phí sẽ được áp dụng và thời gian thành viên còn lại sẽ được đồng bộ với tài khoản để sử dụng trên tất cả thiết bị liên kết.',
   Strings.signup: 'Đăng ký NOMO',
   Strings.signupButton: 'Đăng ký',
-  Strings.signupDescription: 'Sau khi đăng ký, thời gian dùng thử miễn phí sẽ bị trừ và thời gian thành viên khác sẽ chuyển sang tài khoản để sử dụng nhiều thiết bị.',
+  Strings.signupDescription:
+      'Sau khi đăng ký, thời gian dùng thử miễn phí sẽ bị trừ và thời gian thành viên khác sẽ chuyển sang tài khoản để sử dụng nhiều thiết bị.',
   Strings.username: 'Tên đăng nhập',
   Strings.password: 'Mật khẩu',
   Strings.usernamePasswordRule: '6-20 ký tự (chữ cái hoặc số)',
@@ -202,15 +232,19 @@ Map<String, String> viVN = {
   Strings.alreadyHaveAccount: 'Đã có tài khoản? ',
   Strings.loginNow: ' Đăng nhập ngay',
   Strings.feedbackPlaceholder: 'Mô tả vấn đề hoặc đề xuất của bạn...',
-  Strings.emailAddressForReply: '• Địa chỉ email của bạn (để chúng tôi trả lời)',
+  Strings.emailAddressForReply:
+      '• Địa chỉ email của bạn (để chúng tôi trả lời)',
   Strings.send: 'Gửi',
   Strings.changeSubscription: 'Thay đổi đăng ký',
   Strings.awaitingActivation: 'Đang chờ kích hoạt',
   Strings.relieve: 'Gỡ bỏ',
   Strings.configureAuthorizedDevices: 'Cấu hình thiết bị được ủy quyền...',
-  Strings.authorizeUpTo4DevicesAsPremium: 'Ủy quyền tối đa @max thiết bị là Premium (@current/@max)',
-  Strings.youCanAuthorizeOtherDevices: 'Bạn có thể ủy quyền các thiết bị khác là người dùng Premium (@current/@max)',
-  Strings.preCodeInfoMessage: 'Pre Code là thông tin xác thực người dùng premium.\nSử dụng nó để kích hoạt quyền lợi hoặc đồng bộ tài khoản\ntrên các thiết bị khác.',
+  Strings.authorizeUpTo4DevicesAsPremium:
+      'Ủy quyền tối đa @max thiết bị là Premium (@current/@max)',
+  Strings.youCanAuthorizeOtherDevices:
+      'Bạn có thể ủy quyền các thiết bị khác là người dùng Premium (@current/@max)',
+  Strings.preCodeInfoMessage:
+      'Pre Code là thông tin xác thực người dùng premium.\nSử dụng nó để kích hoạt quyền lợi hoặc đồng bộ tài khoản\ntrên các thiết bị khác.',
   Strings.pleaseStoreSecurely: 'Vui lòng lưu trữ an toàn!',
   Strings.sendPreCodeEmailDesc: 'Gửi Pre Code đến địa chỉ email đã đăng ký',
   Strings.storeLocalCopyDesc: 'Lưu bản sao Pre Code trên thiết bị này',
@@ -219,12 +253,17 @@ Map<String, String> viVN = {
   Strings.sendToEmail: 'Gửi qua email',
   Strings.saveLocalCopy: 'Lưu bản sao cục bộ',
   Strings.secureYourConnection: 'Bảo mật kết nối của bạn',
-  Strings.secureYourConnectionDesc: 'Bạn có thể đăng nhập hoặc đăng ký tài khoản để chia sẻ thành viên trên các thiết bị khác nhau.',
+  Strings.secureYourConnectionDesc:
+      'Bạn có thể đăng nhập hoặc đăng ký tài khoản để chia sẻ thành viên trên các thiết bị khác nhau.',
   Strings.save: 'Lưu',
-  Strings.onlyOneModeActive: 'Chỉ một chế độ có thể hoạt động tại một thời điểm.',
-  Strings.chooseAppsExcludeDesc: 'Chọn các ứng dụng sẽ kết nối trực tiếp mà không sử dụng VPN.',
-  Strings.chooseAppsIncludeDesc: 'Chọn các ứng dụng sẽ sử dụng VPN trong khi các ứng dụng khác kết nối bình thường.',
-  Strings.splitTunnelingDesc: 'Chia tách đường hầm cho phép bạn kiểm soát ứng dụng nào sử dụng kết nối VPN và ứng dụng nào kết nối trực tiếp. Nó giúp quản lý băng thông và truy cập nội dung địa phương hoặc nước ngoài mà không cần tắt VPN.',
+  Strings.onlyOneModeActive:
+      'Chỉ một chế độ có thể hoạt động tại một thời điểm.',
+  Strings.chooseAppsExcludeDesc:
+      'Chọn các ứng dụng sẽ kết nối trực tiếp mà không sử dụng VPN.',
+  Strings.chooseAppsIncludeDesc:
+      'Chọn các ứng dụng sẽ sử dụng VPN trong khi các ứng dụng khác kết nối bình thường.',
+  Strings.splitTunnelingDesc:
+      'Chia tách đường hầm cho phép bạn kiểm soát ứng dụng nào sử dụng kết nối VPN và ứng dụng nào kết nối trực tiếp. Nó giúp quản lý băng thông và truy cập nội dung địa phương hoặc nước ngoài mà không cần tắt VPN.',
   Strings.selectAppsExclude: 'Chọn ứng dụng không sử dụng VPN',
   Strings.selectAppsInclude: 'Chọn ứng dụng sử dụng VPN',
   Strings.deselectAll: 'Bỏ chọn tất cả',
@@ -255,7 +294,8 @@ Map<String, String> viVN = {
   Strings.worthRecommending: 'Đáng giới thiệu',
   Strings.loveTheDesign: 'Tôi yêu thiết kế này',
   Strings.changePassword: 'Đổi mật khẩu',
-  Strings.changePasswordDescription: 'Bạn có thể đổi mật khẩu bất cứ lúc nào để đảm bảo an toàn. Không giới hạn số lần đổi mật khẩu',
+  Strings.changePasswordDescription:
+      'Bạn có thể đổi mật khẩu bất cứ lúc nào để đảm bảo an toàn. Không giới hạn số lần đổi mật khẩu',
   Strings.enterNewPassword: 'Nhập mật khẩu mới',
   Strings.enterConfirmPassword: 'Nhập xác nhận mật khẩu',
   Strings.confirmPasswordMustBeTheSame: 'Mật khẩu nhập hai lần không khớp',
@@ -270,7 +310,8 @@ Map<String, String> viVN = {
   Strings.changePasswordSuccessful: 'Đổi mật khẩu thành công',
   Strings.deletingAccount: 'Đang xóa tài khoản...',
   Strings.deleteAccountSuccessful: 'Xóa tài khoản thành công',
-  Strings.deleteAccountConfirmMessage: 'Xóa tài khoản sẽ xóa vĩnh viễn dữ liệu và thông tin thành viên của bạn. Hành động này không thể hoàn tác.',
+  Strings.deleteAccountConfirmMessage:
+      'Xóa tài khoản sẽ xóa vĩnh viễn dữ liệu và thông tin thành viên của bạn. Hành động này không thể hoàn tác.',
   Strings.deleteAccountConfirmButton: 'Xóa',
   Strings.pushNotifications: 'Thông báo đẩy',
   Strings.upgradeNow: 'Nâng cấp ngay',
@@ -293,5 +334,13 @@ Map<String, String> viVN = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
-};
 
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: 'Liên kết email/Quyền lợi thành viên',
+  Strings.bindingAccountEmailProtectsPreRights:
+      'Liên kết tài khoản/email bảo vệ quyền Pre của bạn.',
+  Strings.associatedInterests: 'Quyền lợi liên quan',
+  Strings.associatedInterestsDesc:
+      'Vui lòng lưu ý các quy tắc đăng ký sau khi đăng nhập:\n1. Tài khoản Miễn phí: Tài khoản sẽ kế thừa gói đăng ký hiện tại trên thiết bị này.\n2. Tài khoản Thành viên: Ứng dụng sẽ chuyển sang gói hiện có của tài khoản đó.\nLưu ý: Giao dịch mua trên thiết bị này sẽ vẫn có hiệu lực.',
+  Strings.notNow: 'Để sau',
+};

+ 9 - 1
lib/config/translations/zh_TW/zh_tw_translation.dart

@@ -109,7 +109,7 @@ Map<String, String> zhTW = {
   Strings.unlockAllFreeLocations: '解鎖所有免費位置',
   Strings.unlockSmartMode: '解鎖智慧模式',
   Strings.unlockMultiHopMode: '解鎖多跳模式',
-  Strings.premiumCanShareXDevices: 'Premium 可分享 X 台裝置',
+  Strings.premiumCanShareXDevices: 'Premium 可分享 @count 台裝置',
   Strings.ownYourOwnPrivateServer: '擁有您自己的私人伺服器',
   Strings.closeAds: '關閉廣告',
   Strings.confirmChange: '確認變更',
@@ -386,4 +386,12 @@ Map<String, String> zhTW = {
   Strings.thLang: 'ไทย',
   Strings.hiLang: 'हिन्दी',
   Strings.trLang: 'Türkçe',
+
+  // Bind email / Member benefits
+  Strings.bindEmailMemberBenefits: '綁定郵箱/會員權益',
+  Strings.bindingAccountEmailProtectsPreRights: '綁定帳號/郵箱可保護您的Pre權益。',
+  Strings.associatedInterests: '關聯權益',
+  Strings.associatedInterestsDesc:
+      '請注意登入後的以下訂閱規則:\n1. 免費帳號:帳號將繼承此設備上的當前訂閱。\n2. 會員帳號:應用將切換到該帳號的現有方案。\n注意:此設備上的購買將保持有效。',
+  Strings.notNow: '暫不',
 };

+ 229 - 0
lib/utils/boost_logger.dart

@@ -0,0 +1,229 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:path_provider/path_provider.dart';
+import 'package:flutter_app_group_directory/flutter_app_group_directory.dart';
+
+import 'log/logger.dart';
+
+class BoostLogger {
+  static const String LOG_FOLDER = 'boost_logs';
+  static const String APP_LOG_FILE = 'app.json';
+  static const String CORE_LOG_FILE = 'core.json';
+  static const String TAG = 'BoostLogger';
+
+  Map<String, dynamic> _currentSession = {};
+  String? _logPath;
+
+  /// 单例模式实现
+  static final BoostLogger _instance = BoostLogger._internal();
+  factory BoostLogger() => _instance;
+  BoostLogger._internal();
+
+  /// 获取日志目录
+  Future<Directory> _getLogDirectory() async {
+    if (Platform.isIOS) {
+      try {
+        // iOS 使用 App Group 目录
+        final Directory? appGroupDirectory =
+            await FlutterAppGroupDirectory.getAppGroupDirectory(
+              'group.app.xixi.nomo',
+            );
+
+        // 验证 App Group 目录是否有效
+        if (appGroupDirectory != null) {
+          final logDir = Directory('${appGroupDirectory.path}/$LOG_FOLDER');
+          if (!await logDir.exists()) {
+            await logDir.create(recursive: true);
+          }
+          return logDir;
+        } else {
+          // App Group 目录不可用,回退到应用文档目录
+          log(
+            TAG,
+            'App Group directory not available, falling back to documents directory',
+          );
+          final appDir = await getApplicationDocumentsDirectory();
+          final logDir = Directory('${appDir.path}/$LOG_FOLDER');
+          if (!await logDir.exists()) {
+            await logDir.create(recursive: true);
+          }
+          return logDir;
+        }
+      } catch (e) {
+        // 如果 App Group 出现错误,回退到应用文档目录
+        log(
+          TAG,
+          'Error with App Group directory: $e, falling back to documents directory',
+        );
+        final appDir = await getApplicationDocumentsDirectory();
+        final logDir = Directory('${appDir.path}/$LOG_FOLDER');
+        if (!await logDir.exists()) {
+          await logDir.create(recursive: true);
+        }
+        return logDir;
+      }
+    } else {
+      // Android 使用外部存储目录
+      final appDir = await getExternalStorageDirectory();
+      if (appDir == null) {
+        throw Exception('Failed to get external storage directory');
+      }
+      final logDir = Directory('${appDir.path}/$LOG_FOLDER');
+      if (!await logDir.exists()) {
+        await logDir.create(recursive: true);
+      }
+      return logDir;
+    }
+  }
+
+  /// 初始化日志系统
+  Future<void> init() async {
+    try {
+      final logDir = await _getLogDirectory();
+      _logPath = logDir.path;
+
+      // 清除旧的app日志文件
+      final appFile = File('${logDir.path}/$APP_LOG_FILE');
+      if (await appFile.exists()) {
+        await appFile.delete();
+      }
+
+      // 清除旧的内核日志文件
+      final coreFile = File('${logDir.path}/$CORE_LOG_FILE');
+      if (await coreFile.exists()) {
+        await coreFile.delete();
+      }
+
+      // 初始化空的会话数据
+      _currentSession = {};
+      await _saveSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger init error: $e');
+    }
+  }
+
+  //读取历史日志
+  Future<void> readHistoryLog() async {
+    try {
+      final logDir = await _getLogDirectory();
+      _logPath = logDir.path;
+
+      getCurrentSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger readHistoryLog error: $e');
+    }
+  }
+
+  /// 设置整个部分的数据
+  Future<void> setSection(String section, Map<String, dynamic> data) async {
+    try {
+      _currentSession[section] = data;
+      await _saveSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger setSection error: $e');
+    }
+  }
+
+  /// 设置整个部分的数据
+  Future<void> setSectionObj(String section, dynamic data) async {
+    try {
+      _currentSession[section] = data;
+      await _saveSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger setSectionObj error: $e');
+    }
+  }
+
+  /// 更新部分中的特定字段
+  Future<void> updateField(String section, String field, dynamic value) async {
+    try {
+      final sectionData = _currentSession[section] as Map<String, dynamic>;
+      sectionData[field] = value;
+      _currentSession[section] = sectionData;
+
+      await _saveSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger updateField error: $e');
+    }
+  }
+
+  /// 向数组添加数据
+  Future<void> appendToArray(
+    String section,
+    String arrayField,
+    Map<String, dynamic> data,
+  ) async {
+    try {
+      final sectionData =
+          _currentSession[section] as Map<String, dynamic>? ?? {};
+      final array = sectionData[arrayField] as List<dynamic>? ?? [];
+      array.add(data);
+      sectionData[arrayField] = array;
+      _currentSession[section] = sectionData;
+      await _saveSession();
+    } catch (e) {
+      log(TAG, 'BoostLogger appendToArray error: $e');
+    }
+  }
+
+  /// 保存会话数据到文件
+  Future<void> _saveSession() async {
+    if (_logPath == null) return;
+
+    try {
+      final file = File('$_logPath/$APP_LOG_FILE');
+      // 使用 writeAsStringSync 确保写入完成
+      final jsonString = const JsonEncoder.withIndent(
+        '  ',
+      ).convert(_currentSession);
+      await file.writeAsString(jsonString, flush: true);
+
+      // 验证文件写入
+      final content = await file.readAsString();
+      final savedData = jsonDecode(content) as Map<String, dynamic>;
+      log(TAG, 'BoostLogger verify saved data: $savedData');
+    } catch (e) {
+      log(TAG, 'BoostLogger save session error: $e');
+    }
+  }
+
+  /// 获取当前会话数据
+  Map<String, dynamic> getCurrentSession() {
+    try {
+      // 从文件读取最新数据
+      final file = File('$_logPath/$APP_LOG_FILE');
+      if (file.existsSync()) {
+        final content = file.readAsStringSync();
+        _currentSession = jsonDecode(content) as Map<String, dynamic>;
+      }
+      return Map<String, dynamic>.from(_currentSession);
+    } catch (e) {
+      log(TAG, 'BoostLogger getCurrentSession error: $e');
+      return {};
+    }
+  }
+
+  /// 获取日志文件路径
+  Future<String?> getAppLogFilePath() async {
+    return _logPath != null ? '$_logPath/$APP_LOG_FILE' : null;
+  }
+
+  /// 获取核心日志文件路径
+  Future<String?> getCoreLogFilePath() async {
+    return _logPath != null ? '$_logPath/$CORE_LOG_FILE' : null;
+  }
+
+  /// 清理资源
+  Future<void> release() async {
+    try {
+      // 检查 _currentSession 是否为空
+      if (_currentSession.isEmpty) {
+        return;
+      }
+      await _saveSession();
+      _currentSession.clear();
+    } catch (e) {
+      log(TAG, 'BoostLogger release error: $e');
+    }
+  }
+}

+ 124 - 0
lib/utils/boost_report_manager.dart

@@ -0,0 +1,124 @@
+import 'boost_logger.dart';
+import 'log/logger.dart';
+import 'ntp_time_service.dart';
+
+class BoostReportManager {
+  final BoostLogger _logger = BoostLogger();
+  static const String TAG = 'BoostReportManager';
+  static final BoostReportManager _instance = BoostReportManager._internal();
+  factory BoostReportManager() => _instance;
+  BoostReportManager._internal();
+
+  /// 初始化报告管理器
+  Future<void> init() async {
+    try {
+      await _logger.init();
+
+      // 初始化基本结构
+      await _logger.setSection('sessionInfo', {});
+      await _logger.setSection('targetInfo', {});
+      await _logger.setSectionObj('generatedTime', _getCurrentTimestamp());
+    } catch (e) {
+      log(TAG, 'BoostReportManager init error: $e');
+    }
+  }
+
+  int _getCurrentTimestamp() {
+    return NtpTimeService().getCurrentTimestamp();
+  }
+
+  /// 读取历史日志
+  Future<void> readHistoryLog() async {
+    try {
+      await _logger.readHistoryLog();
+    } catch (e) {
+      log(TAG, 'BoostReportManager readHistoryLog error: $e');
+    }
+  }
+
+  /// 初始化会话信息
+  Future<void> initSessionInfo({
+    required String appSessionId,
+    required String boostSessionId,
+    required Map<String, String> deviceInfo,
+  }) async {
+    try {
+      await _logger.setSection('sessionInfo', {
+        'appSessionId': appSessionId,
+        'boostSessionId': boostSessionId,
+        'success': false,
+        'errorCode': -1,
+        'boostStartTime': _getCurrentTimestamp(),
+        'boostStopTime': 0,
+        'boostDuration': 0,
+        'userDeviceInfo': {
+          'deviceModel': deviceInfo['deviceModel'] ?? '',
+          'osVersion': deviceInfo['osVersion'] ?? '',
+          'appVersion': deviceInfo['appVersion'] ?? '',
+          'networkType': deviceInfo['networkType'] ?? '',
+          'deviceBrand': deviceInfo['deviceBrand'] ?? '',
+        },
+      });
+    } catch (e) {
+      log(TAG, 'BoostReportManager initSessionInfo error: $e');
+    }
+  }
+
+  /// 添加 targetInfo
+  Future<void> addTargetInfo({
+    required String locationSelectionType,
+    required dynamic location,
+  }) async {
+    try {
+      await _logger.updateField(
+        'targetInfo',
+        'locationSelectionType',
+        locationSelectionType,
+      );
+      await _logger.updateField('targetInfo', 'location', location);
+    } catch (e) {
+      log(TAG, 'BoostReportManager addTargetInfo error: $e');
+    }
+  }
+
+  /// 添加 latencyList
+  Future<void> addLatencyList({required Map<String, int> latencyList}) async {
+    try {
+      await _logger.setSectionObj('latencys', latencyList);
+    } catch (e) {
+      log(TAG, 'BoostReportManager addLatencyList error: $e');
+    }
+  }
+
+  // 获取 boostStartTime
+  int getBoostStartTime() {
+    try {
+      final session = _logger.getCurrentSession();
+      final sessionInfo = session['sessionInfo'] as Map<String, dynamic>;
+      return sessionInfo['boostStartTime'] as int;
+    } catch (e) {
+      log(TAG, 'BoostReportManager getBoostStartTime error: $e');
+      return 0;
+    }
+  }
+
+  /// 获取当前报告
+  Map<String, dynamic> getCurrentReport() {
+    return _logger.getCurrentSession();
+  }
+
+  /// 获取核心日志文件路径
+  Future<String?> getCoreLogFilePath() {
+    return _logger.getCoreLogFilePath();
+  }
+
+  /// 获取报告文件路径
+  Future<String?> getAppLogFilePath() {
+    return _logger.getAppLogFilePath();
+  }
+
+  /// 释放资源
+  Future<void> release() async {
+    await _logger.release();
+  }
+}

+ 10 - 4
lib/utils/ntp_time_service.dart

@@ -1,3 +1,4 @@
+import 'package:nomo/app/data/sp/ix_sp.dart';
 import 'package:ntp/ntp.dart';
 import 'package:system_clock/system_clock.dart';
 
@@ -99,7 +100,13 @@ class NtpTimeService {
 
   /// 获取当前"网络时间",通过系统运行时间 + NTP 差值计算
   DateTime getCurrentTime() {
-    if (!isInitialized) return DateTime.now();
+    if (!isInitialized) {
+      final appConfig = IXSP.getAppConfig();
+      if (appConfig != null && appConfig.serverTime != null) {
+        return DateTime.fromMillisecondsSinceEpoch(appConfig.serverTime!);
+      }
+      return DateTime.now();
+    }
     final currentElapsedRealtime = SystemClock.elapsedRealtime();
     final elapsed = currentElapsedRealtime - _elapsedRealtimeInitial!;
     return _ntpInitialTime!.add(elapsed);
@@ -113,10 +120,9 @@ class NtpTimeService {
 
   int getNtpInitialTime() {
     if (!isInitialized) {
-      return DateTime.now().millisecondsSinceEpoch;
+      return getCurrentTimestamp();
     }
-    return _ntpInitialTime?.millisecondsSinceEpoch ??
-        DateTime.now().millisecondsSinceEpoch;
+    return _ntpInitialTime?.millisecondsSinceEpoch ?? getCurrentTimestamp();
   }
 
   int getElapsedRealtimeInitial() {

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -9,6 +9,7 @@ import app_links
 import awesome_notifications
 import connectivity_plus
 import device_info_plus
+import flutter_app_group_directory
 import flutter_inappwebview_macos
 import flutter_secure_storage_macos
 import in_app_purchase_storekit
@@ -26,6 +27,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin"))
   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
+  FlutterAppGroupDirectoryPlugin.register(with: registry.registrar(forPlugin: "FlutterAppGroupDirectoryPlugin"))
   InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
   FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
   InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))

+ 8 - 0
pubspec.lock

@@ -446,6 +446,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_app_group_directory:
+    dependency: "direct main"
+    description:
+      name: flutter_app_group_directory
+      sha256: "680ef9b2dee84c237cd7bb7fc78bc45867b32556a8a5f0de61278078b9fefd05"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
   flutter_cache_manager:
     dependency: transitive
     description:

+ 1 - 0
pubspec.yaml

@@ -84,6 +84,7 @@ dependencies:
   carousel_slider: ^5.1.1 # 轮播图
   pull_to_refresh_flutter3: ^2.0.2 # 下拉刷新
   awesome_notifications: ^0.10.1 # 通知
+  flutter_app_group_directory: ^1.1.0 # App Group目录
 
 dev_dependencies:
   flutter_test: