Bläddra i källkod

feat: api/routing/split/contact/policy/language

lilu 4 månader sedan
förälder
incheckning
a25954516d
97 ändrade filer med 4516 tillägg och 1073 borttagningar
  1. 4 0
      android/app/build.gradle.kts
  2. 28 2
      android/app/src/main/AndroidManifest.xml
  3. 10 6
      android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt
  4. 152 62
      android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt
  5. 1 1
      android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt
  6. 9 0
      android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt
  7. 42 41
      android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt
  8. 196 5
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  9. BIN
      assets/images/connected.png
  10. BIN
      assets/images/connecting.png
  11. BIN
      assets/images/disconnected.png
  12. BIN
      assets/images/network.png
  13. BIN
      assets/images/switch_status_connected.png
  14. BIN
      assets/images/switch_status_connecting.png
  15. BIN
      assets/images/switch_status_disconnected.png
  16. BIN
      assets/images/vpn_error.png
  17. 0 4
      assets/vectors/boost/connected.svg
  18. 0 3
      assets/vectors/boost/connecting.svg
  19. 0 3
      assets/vectors/boost/disconnected.svg
  20. 0 5
      assets/vectors/boost/error.svg
  21. 0 3
      assets/vectors/boost/gary_right.svg
  22. 0 9
      assets/vectors/boost/network.svg
  23. 0 0
      assets/vectors/boost/refersh.svg
  24. 0 3
      assets/vectors/boost/settings.svg
  25. 0 3
      assets/vectors/boost/switch_status_connected_dark.svg
  26. 0 3
      assets/vectors/boost/switch_status_connected_light.svg
  27. 0 3
      assets/vectors/boost/switch_status_connecting_dark.svg
  28. 0 3
      assets/vectors/boost/switch_status_connecting_light.svg
  29. 0 3
      assets/vectors/boost/switch_status_disconnected_dark.svg
  30. 0 3
      assets/vectors/boost/switch_status_disconnected_light.svg
  31. 0 3
      assets/vectors/boost/white_right.svg
  32. 0 8
      assets/vectors/status/refersh.svg
  33. 9 6
      ios/Runner/CoreApi.g.swift
  34. 6 9
      lib/app/api/base/base_api.dart
  35. 4 6
      lib/app/api/core/api_core.dart
  36. 6 0
      lib/app/api/core/api_core_paths.dart
  37. 267 0
      lib/app/api/file/api_file.dart
  38. 128 0
      lib/app/api/log/api_log.dart
  39. 3 3
      lib/app/app.dart
  40. 2 15
      lib/app/base/base_view.dart
  41. 1 1
      lib/app/components/country_restricted_overlay.dart
  42. 1 1
      lib/app/components/protocol_overlay.dart
  43. 516 0
      lib/app/constants/api_domains.dart
  44. 15 19
      lib/app/constants/assets.dart
  45. 20 2
      lib/app/constants/enums.dart
  46. 2 2
      lib/app/constants/keys.dart
  47. 321 1
      lib/app/controllers/api_controller.dart
  48. 149 59
      lib/app/controllers/core_controller.dart
  49. 126 0
      lib/app/data/models/vpn_message.dart
  50. 26 12
      lib/app/data/sp/ix_sp.dart
  51. 28 56
      lib/app/modules/account/views/account_view.dart
  52. 19 23
      lib/app/modules/deviceauth/views/deviceauth_view.dart
  53. 121 93
      lib/app/modules/feedback/views/feedback_view.dart
  54. 10 0
      lib/app/modules/forgotpwd/bindings/forgotpwd_binding.dart
  55. 23 0
      lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart
  56. 24 0
      lib/app/modules/forgotpwd/views/forgotpwd_view.dart
  57. 5 0
      lib/app/modules/home/controllers/home_controller.dart
  58. 62 58
      lib/app/modules/home/views/home_view.dart
  59. 71 16
      lib/app/modules/home/widgets/connection_button.dart
  60. 46 56
      lib/app/modules/language/views/language_view.dart
  61. 4 9
      lib/app/modules/markdown/views/markdown_view.dart
  62. 3 1
      lib/app/modules/node/views/node_view.dart
  63. 14 14
      lib/app/modules/node/widgets/node_list.dart
  64. 51 62
      lib/app/modules/routingmode/views/routingmode_view.dart
  65. 32 64
      lib/app/modules/setting/views/setting_view.dart
  66. 10 0
      lib/app/modules/signin/bindings/signin_binding.dart
  67. 23 0
      lib/app/modules/signin/controllers/signin_controller.dart
  68. 24 0
      lib/app/modules/signin/views/signin_view.dart
  69. 10 0
      lib/app/modules/signup/bindings/signup_binding.dart
  70. 23 0
      lib/app/modules/signup/controllers/signup_controller.dart
  71. 24 0
      lib/app/modules/signup/views/signup_view.dart
  72. 127 12
      lib/app/modules/splash/controllers/splash_controller.dart
  73. 34 40
      lib/app/modules/splash/views/splash_view.dart
  74. 121 13
      lib/app/modules/splittunneling/controllers/splittunneling_controller.dart
  75. 10 0
      lib/app/modules/splittunneling/selectapp/bindings/splittunneling_selectapp_binding.dart
  76. 269 0
      lib/app/modules/splittunneling/selectapp/controllers/splittunneling_selectapp_controller.dart
  77. 314 0
      lib/app/modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart
  78. 247 111
      lib/app/modules/splittunneling/views/splittunneling_view.dart
  79. 32 34
      lib/app/modules/web/views/web_view.dart
  80. 64 15
      lib/app/routes/app_pages.dart
  81. 9 0
      lib/app/routes/app_routes.dart
  82. 29 36
      lib/app/widgets/ix_app_bar.dart
  83. 35 1
      lib/app/widgets/ix_image.dart
  84. 1 0
      lib/config/theme/dark_theme_colors.dart
  85. 6 0
      lib/config/theme/ix_theme.dart
  86. 4 0
      lib/config/theme/light_theme_colors.dart
  87. 5 0
      lib/main.dart
  88. 21 6
      lib/utils/crypto.dart
  89. 52 36
      lib/utils/developer/ix_developer_tools.dart
  90. 3 3
      lib/utils/device_manager.dart
  91. 33 0
      lib/utils/event_bus.dart
  92. 123 0
      lib/utils/file_cache_manager.dart
  93. 179 0
      lib/utils/file_stream_util.dart
  94. 106 0
      lib/utils/network_helper.dart
  95. 2 1
      pigeons/core_api.dart
  96. 16 0
      pubspec.lock
  97. 3 1
      pubspec.yaml

+ 4 - 0
android/app/build.gradle.kts

@@ -28,6 +28,10 @@ android {
         targetSdk = flutter.targetSdkVersion
         versionCode = flutter.versionCode
         versionName = flutter.versionName
+        ndk {
+            abiFilters.add("armeabi-v7a")
+            abiFilters.add("arm64-v8a")
+        }
     }
 
     sourceSets {

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

@@ -6,8 +6,6 @@
         android:name="android.permission.CHANGE_NETWORK_STATE"/>
     <uses-permission
         android:name="android.permission.INTERNET"/>
-    <uses-permission
-        android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission
         android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission
@@ -20,6 +18,34 @@
     <uses-permission
         android:name="android.permission.PACKAGE_USAGE_STATS"
         tools:ignore="ProtectedPermissions"/>
+
+    <!-- 仅在 Android 10 及以下版本需要存储权限 -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29" />
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+            <data android:scheme="https" />
+        </intent>
+    </queries>
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.MAIN" />
+            <category android:name="android.intent.category.LAUNCHER" />
+        </intent>
+    </queries>
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.MAIN" />
+            <category android:name="android.intent.category.APP_BROWSER" />
+        </intent>
+    </queries>
+
     <application
         android:name=".App"
         android:allowBackup="false"

+ 10 - 6
android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt

@@ -57,9 +57,10 @@ private open class CoreApiPigeonCodec : StandardMessageCodec() {
 
 val CoreApiPigeonMethodCodec = StandardMethodCodec(CoreApiPigeonCodec())
 
+
 /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
 interface CoreApi {
-  fun getApps(): String?
+  fun getApps(callback: (Result<String?>) -> Unit)
   fun getSystemLocale(): String?
   fun connect(): Boolean?
   fun disconnect(): Boolean?
@@ -83,12 +84,15 @@ interface CoreApi {
         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getApps$separatedMessageChannelSuffix", codec)
         if (api != null) {
           channel.setMessageHandler { _, reply ->
-            val wrapped: List<Any?> = try {
-              listOf(api.getApps())
-            } catch (exception: Throwable) {
-              CoreApiPigeonUtils.wrapError(exception)
+            api.getApps{ result: Result<String?> ->
+              val error = result.exceptionOrNull()
+              if (error != null) {
+                reply.reply(CoreApiPigeonUtils.wrapError(error))
+              } else {
+                val data = result.getOrNull()
+                reply.reply(CoreApiPigeonUtils.wrapResult(data))
+              }
             }
-            reply.reply(wrapped)
           }
         } else {
           channel.setMessageHandler(null)

+ 152 - 62
android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt

@@ -7,13 +7,50 @@ import PigeonEventSink
 import android.app.Activity
 import android.content.Context.TELEPHONY_SERVICE
 import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
 import android.net.VpnService
 import android.telephony.TelephonyManager
+import android.util.Base64
 import android.util.Log
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_DISCONNECTING
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_PERMISSION_DENIED
+import app.xixi.nomo.XRayApi.OnVpnServiceEvent
 import com.google.gson.Gson
 import io.flutter.plugin.common.BinaryMessenger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.ByteArrayOutputStream
 import java.io.InputStream
 import java.util.UUID
+import androidx.core.graphics.createBitmap
+import java.util.Locale
+
+// 消息数据类
+data class VpnStatusMessage(
+    val type: String,
+    val status: Long,
+    val message: String
+)
+
+data class TimerUpdateMessage(
+    val type: String,
+    val currentTime: Long,
+    val mode: Int,
+    val isRunning: Boolean,
+    val isPaused: Boolean
+)
+
+data class TimerNotificationMessage(
+    val type: String,
+    val message: String
+)
 
 class CoreApiImpl(private val activity: Activity) : CoreApi {
     companion object {
@@ -46,12 +83,35 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         Log.d(TAG, "事件流处理器已注册")
     }
 
-    // 通知 Flutter 连接状态变化
-    private fun notifyConnectionStateChange(state: String) {
+    // 通知 Flutter VPN状态变化
+    private fun notifyVpnStatusChange(status: Long, message: String) {
+        val vpnMessage = VpnStatusMessage(
+            type = "vpn_status",
+            status = status,
+            message = message
+        )
+        notifyFlutter(vpnMessage)
+    }
+    
+    // 通知 Flutter 计时更新
+    private fun notifyTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean) {
+        val timerMessage = TimerUpdateMessage(
+            type = "timer_update",
+            currentTime = currentTime,
+            mode = mode,
+            isRunning = isRunning,
+            isPaused = isPaused
+        )
+        notifyFlutter(timerMessage)
+    }
+
+    // 通用通知方法
+    private fun notifyFlutter(message: Any) {
         eventSink?.let { sink ->
             try {
-                sink.success(state)
-                Log.d(TAG, "已通知 Flutter 连接状态变化: $state")
+                val json = Gson().toJson(message)
+                sink.success(json)
+                Log.d(TAG, "已通知 Flutter: $json")
             } catch (e: Exception) {
                 Log.e(TAG, "通知 Flutter 失败", e)
             }
@@ -69,20 +129,19 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     /**
      * 设置XRayApi状态监听
      */
-    fun setXRayApiStatusListener() {
-        xrayApi?.setVpnStatusEventListener { status, message ->
-            VLog.i(TAG, "接收到VPN状态变化: status=$status, message=$message")
-            // 根据状态码转换连接状态
-            val connectionState = when (status) {
-                0L -> "disconnected"
-                1L -> "connecting"
-                2L -> "connected"
-                3L -> "error"
-                else -> "unknown"
+    fun setVpnServiceEventListener() {
+        xrayApi?.setVpnServiceEventListener(object : OnVpnServiceEvent {
+            override fun onVpnStatusChange(status: Long, message: String) {
+                VLog.i(TAG, "接收到VPN状态变化: status=$status, message=$message")
+                // 同时发送详细的状态信息
+                notifyVpnStatusChange(status, message)
             }
-            // 通知Flutter连接状态变化
-            notifyConnectionStateChange(connectionState)
-        }
+            
+            override fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean) {
+                VLog.i(TAG, "接收到计时更新: time=$currentTime, mode=$mode, running=$isRunning, paused=$isPaused")
+                notifyTimerUpdate(currentTime, mode, isRunning, isPaused)
+            }
+        })
     }
 
     fun xrayInit() {
@@ -95,13 +154,80 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         xrayApi?.uninit()
     }
 
+    override fun getSystemLocale(): String? {
+        val locale: Locale = activity.resources.configuration.locales[0]
+        return locale.language
+    }
+
+    override fun getApps(callback: (Result<String?>) -> Unit) {
+        val apps = ArrayList<HashMap<String, Any>>()
+        val pm: PackageManager = activity.packageManager
 
-    override fun getApps(): String? {
-        TODO("Not yet implemented")
+        // 通过隐式 Intent,查询所有支持 ACTION_MAIN 的应用
+        val intent = Intent(Intent.ACTION_MAIN).apply {
+            addCategory(Intent.CATEGORY_LAUNCHER) // 查询所有可以启动的应用
+        }
+        val launcherApps = pm.queryIntentActivities(intent, 0)
+        // 使用协程处理所有游戏应用信息
+        CoroutineScope(Dispatchers.Default).launch {
+            for (app in launcherApps) {
+                val appInfo = getAppInfo(app.activityInfo.applicationInfo, pm)
+                if ((app.activityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 0) {
+                    apps.add(0, appInfo)
+                } else {
+                    apps.add(appInfo) 
+                }
+            }
+            withContext(Dispatchers.Main) {
+                callback(Result.success(Gson().toJson(apps)))
+            }
+        }
     }
 
-    override fun getSystemLocale(): String? {
-        TODO("Not yet implemented")
+    // 获取应用的详细信息
+    private suspend fun getAppInfo(
+        app: ApplicationInfo,
+        pm: PackageManager,
+    ): HashMap<String, Any> = withContext(Dispatchers.IO) {
+        val appInfo = HashMap<String, Any>()
+        try {
+            appInfo["packageName"] = app.packageName
+            appInfo["appName"] = pm.getApplicationLabel(app).toString()
+            try {
+                val drawable = app.loadIcon(pm)
+                val bitmap = drawable2Bitmap(drawable)
+                val bytes = bitmap2Bytes(bitmap)
+                bitmap.recycle()
+                appInfo["icon"] = Base64.encodeToString(bytes, Base64.NO_WRAP)
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+        appInfo
+    }
+
+    // Drawable转换成Bitmap
+    private fun drawable2Bitmap(drawable: Drawable): Bitmap {
+        val width = drawable.intrinsicWidth.coerceAtMost(128) // 限制最大宽度
+        val height = drawable.intrinsicHeight.coerceAtMost(128) // 限制最大高度
+        val bitmap = createBitmap(
+            width,
+            height,
+            if (drawable.opacity != PixelFormat.OPAQUE) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
+        )
+        val canvas = Canvas(bitmap)
+        drawable.setBounds(0, 0, width, height)
+        drawable.draw(canvas)
+        return bitmap
+    }
+
+    // Bitmap转换成byte[]
+    private fun bitmap2Bytes(bm: Bitmap): ByteArray {
+        val bos = ByteArrayOutputStream()
+        bm.compress(Bitmap.CompressFormat.PNG, 100, bos)
+        return bos.toByteArray()
     }
 
     override fun connect(): Boolean? {
@@ -124,23 +250,22 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         VLog.i(TAG, "============ 开始停止 V2Ray 服务 ============")
         
         // 通知 Flutter 开始断开连接
-        notifyConnectionStateChange("disconnecting")
-
+        notifyVpnStatusChange(VPN_STATE_DISCONNECTING, "")
         // 停止V2Ray服务
         VLog.i(TAG, "调用 xrayApi.stopXray()")
         xrayApi?.stopXray()
         xrayUnInit()
         VLog.i(TAG, "V2Ray 服务停止完成")
-        notifyConnectionStateChange("disconnected")
+
         return true
     }
 
     override fun getRemoteIp(): String? {
-        TODO("Not yet implemented")
+        return ""
     }
 
     override fun getAdvertisingId(): String? {
-        TODO("Not yet implemented")
+        return ""
     }
 
     override fun moveTaskToBack(): Boolean? {
@@ -201,9 +326,6 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
 
     fun startV2RayService() {
         VLog.i(TAG, "============ 开始启动 V2Ray 服务 ============")
-
-        // 通知 Flutter 开始连接
-        notifyConnectionStateChange("connecting")
         
         // 1. 初始化 XRay (启动和绑定服务)
         VLog.i(TAG, "步骤 1: 调用 xrayInit() 初始化服务")
@@ -218,38 +340,6 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
         
         VLog.i(TAG, "startV2RayService 调用完成,等待服务连接和启动")
         // 注意:实际的连接成功应该在服务启动完成后通知
-        // 这里先暂时保持原有逻辑
-        notifyConnectionStateChange("connected")
-    }
-
-    /**
-     * 查询统计信息
-     */
-    fun queryStats() {
-        Log.i(TAG, "Querying V2Ray stats...")
-    }
-    
-    /**
-     * 通知 Flutter 统计信息更新
-     */
-    fun notifyStatsUpdate(uplink: Long, downlink: Long) {
-        val statsJson = "{\"uplink\":$uplink,\"downlink\":$downlink}"
-        notifyConnectionStateChange("stats_update:$statsJson")
-    }
-    
-    /**
-     * 通知 Flutter 错误信息
-     */
-    fun notifyError(errorMessage: String) {
-        notifyConnectionStateChange("error:$errorMessage")
-    }
-    
-    /**
-     * 通知 Flutter 服务状态变化
-     */
-    fun notifyServiceStatus(isRunning: Boolean) {
-        val status = if (isRunning) "running" else "stopped"
-        notifyConnectionStateChange("service_status:$status")
     }
 
     // 启动V2Ray服务
@@ -312,7 +402,7 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
                 startV2RayService()
             } else {
                 Log.w(TAG, "VPN permission denied")
-                notifyConnectionStateChange("permission_denied")
+                notifyVpnStatusChange(VPN_STATE_PERMISSION_DENIED, "")
             }
         }
     }

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

@@ -77,7 +77,7 @@ class MainActivity: FlutterActivity() {
         coreApiImpl.initXrayApi();
         
         // 设置 XRayApi 状态监听
-        coreApiImpl.setXRayApiStatusListener()
+        coreApiImpl.setVpnServiceEventListener()
         
         // 设置 CoreApi
         CoreApi.setUp(messenger, coreApiImpl)

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

@@ -56,4 +56,13 @@ internal class TProxyService {
             // Ignore exception
         }
     }
+
+    fun getStats(): LongArray {
+        return try {
+            TProxyGetStats()
+        } catch (e: Exception) {
+            Log.w(TAG, "TProxyGetStats failed, returning empty array", e)
+            LongArray(0)
+        }
+    }
 }

+ 42 - 41
android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt

@@ -1,11 +1,9 @@
 package app.xixi.nomo
 
 import android.app.ActivityManager
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.ServiceConnection
 import android.os.Build
 import android.os.Bundle
@@ -15,7 +13,6 @@ import android.os.IBinder
 import android.os.Looper
 import android.os.Message
 import android.os.Messenger
-import androidx.core.content.ContextCompat
 import java.util.concurrent.locks.ReentrantLock
 
 class XRayApi {
@@ -28,11 +25,29 @@ class XRayApi {
     private var isInited = false
     private var firstInit = true
     private var serviceEvent: OnXrayServiceEvent? = null
-    private var vpnStatusEvent: OnVpnStatusEvent? = null
+    private var vpnServiceEvent: OnVpnServiceEvent? = null
 
     private inner class ReplyMsgHandler(looper: Looper) : Handler(looper) {
         override fun handleMessage(msg: Message) {
-            // Handle message
+            when (msg.what) {
+                XRayService.XRAY_MSG_REPLY_STATUS -> {
+                    val status = msg.data?.getLong("status") ?: -1L
+                    val message = msg.data?.getString("message") ?: ""
+                    VLog.i(TAG, "接收到VPN状态消息: status=$status, message=$message")
+                    vpnServiceEvent?.onVpnStatusChange(status, message)
+                }
+                XRayService.XRAY_MSG_REPLY_TIMER_UPDATE -> {
+                    val currentTime = msg.data?.getLong("currentTime") ?: 0L
+                    val mode = msg.data?.getInt("mode") ?: 0
+                    val isRunning = msg.data?.getBoolean("isRunning") ?: false
+                    val isPaused = msg.data?.getBoolean("isPaused") ?: false
+                    VLog.i(TAG, "接收到计时更新消息: time=$currentTime, mode=$mode, running=$isRunning, paused=$isPaused")
+                    vpnServiceEvent?.onTimerUpdate(currentTime, mode, isRunning, isPaused)
+                }
+                else -> {
+                    VLog.w(TAG, "未知的消息类型: ${msg.what}")
+                }
+            }
         }
     }
 
@@ -40,8 +55,9 @@ class XRayApi {
         fun onXrayServiceStarted()
     }
 
-    fun interface OnVpnStatusEvent {
+    interface OnVpnServiceEvent {
         fun onVpnStatusChange(status: Long, message: String)
+        fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean)
     }
 
     private val mConnection = object : ServiceConnection {
@@ -61,6 +77,7 @@ class XRayApi {
                 it.onXrayServiceStarted()
                 serviceEvent = null
             }
+            regReplyMessenger();
         }
 
         override fun onServiceDisconnected(className: ComponentName) {
@@ -76,17 +93,20 @@ class XRayApi {
         }
     }
 
-    private val messageReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            if (intent.action == XRAY_STAT_MESSAGE_ID) {
-                val status = intent.getLongExtra("status", 0L)
-                val message = intent.getStringExtra("message") ?: ""
-                VLog.i(TAG, "接收到VPN状态消息: status=$status, message=$message")
-                vpnStatusEvent?.onVpnStatusChange(status, message)
-            }
+    private fun regReplyMessenger() {
+        val msg = Message.obtain(null, XRayService.XRAY_MSG_REG_MESSENGER, 0, 0)
+        try {
+            msg.replyTo = mReplyMsgHandler
+            mService?.send(msg)
+            VLog.i(TAG, "注册replyTo消息已发送到 XRayService")
+        } catch (e: DeadObjectException) {
+            VLog.e(TAG, "发送注册replyTo消息失败:DeadObjectException", e)
+        } catch (e: Exception) {
+            VLog.e(TAG, "发送注册replyTo消息失败", e)
         }
     }
 
+
     fun init(context: Context) {
         VLog.i(TAG, "XRayApi init 开始")
         if (isInited) {
@@ -102,16 +122,6 @@ class XRayApi {
                 VLog.i(TAG, "XRay 服务状态不是 IDLE,当前状态: $xraySvrState")
                 return
             }
-            val intentFilter = IntentFilter().apply {
-                addAction(XRAY_STAT_MESSAGE_ID)
-            }
-            ContextCompat.registerReceiver(
-                this.context!!,
-                messageReceiver,
-                intentFilter,
-                ContextCompat.RECEIVER_NOT_EXPORTED
-            )
-            VLog.i(TAG, "BroadcastReceiver 已注册")
             doStartVpnService(null)
             VLog.i(TAG, "XRayApi init 完成,开始启动和绑定服务")
         } finally {
@@ -123,13 +133,6 @@ class XRayApi {
         if (!isInited) return
         VLog.i(TAG, "uninit 开始清理服务")
         
-        try {
-            context?.unregisterReceiver(messageReceiver)
-            VLog.i(TAG, "BroadcastReceiver 已注销")
-        } catch (e: Exception) {
-            VLog.e(TAG, "注销 BroadcastReceiver 失败", e)
-        }
-        
         try {
             context?.unbindService(mConnection)
             VLog.i(TAG, "Service 已解绑")
@@ -277,8 +280,8 @@ class XRayApi {
         }
     }
 
-    fun setVpnStatusEventListener(listener: OnVpnStatusEvent?) {
-        vpnStatusEvent = listener
+    fun setVpnServiceEventListener(listener: OnVpnServiceEvent?) {
+        vpnServiceEvent = listener
     }
 
     companion object {
@@ -288,14 +291,12 @@ class XRayApi {
         const val XRAY_SVR_QUERY_STATE = 2
         const val XRAY_SVR_SERVICE_WORKING = 7
 
-        const val VPN_STATE_IDLE = 0
-        const val VPN_STATE_CONNECTING = 1
-        const val VPN_STATE_CONNECTED = 2
-        const val VPN_STATE_ERROR = 3
-        const val VPN_STATE_STARTING = 4 // internal using
-        const val VPN_STATE_STOPPING = 5 // internal using
-
-        const val XRAY_STAT_MESSAGE_ID = "xray_service_stat_message"
+        const val VPN_STATE_IDLE = 0L
+        const val VPN_STATE_CONNECTING = 1L
+        const val VPN_STATE_CONNECTED = 2L
+        const val VPN_STATE_ERROR = 3L
+        const val VPN_STATE_DISCONNECTING = 4L
+        const val VPN_STATE_PERMISSION_DENIED = 403L
 
         fun isServiceRunning(mContext: Context): Boolean {
             val activityManager = mContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

+ 196 - 5
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -7,12 +7,16 @@ import android.content.Intent
 import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
 import android.net.VpnService
 import android.os.Build
+import android.os.Bundle
+import android.os.DeadObjectException
 import android.os.Handler
 import android.os.IBinder
+import android.os.Looper
 import android.os.Message
 import android.os.Messenger
 import android.os.ParcelFileDescriptor
 import androidx.core.app.NotificationCompat
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_CONNECTED
 import go.Seq
 import ixvpn_mobile.Ixvpn_mobile
 import ixvpn_mobile.ProxyConnectorHandler
@@ -23,6 +27,49 @@ class XRayService : VpnService() {
     private val mMessenger = Messenger(IncomingHandler())
     private val tunnelService = TProxyService()
     private var tunFileDescriptor: ParcelFileDescriptor? = null
+    private var replyMessenger: Messenger? = null
+
+    // 计时功能相关变量
+    private val timerHandler = Handler(Looper.getMainLooper())
+    private var isTimerRunning = false
+    private var isTimerPaused = false
+    private var currentTimerTime = 0L
+    private var timerStartTime = 0L
+    private var timerMode = 0 // 0: 普通计时, 1: 倒计时
+    
+    // 计时器Runnable
+    private val timerRunnable = object : Runnable {
+        override fun run() {
+            if (isTimerRunning && !isTimerPaused) {
+                val newTime = if (timerMode == 0) {
+                    // 普通计时:递增
+                    currentTimerTime + 1000L
+                } else {
+                    // 倒计时:递减
+                    currentTimerTime - 1000L
+                }
+                
+                currentTimerTime = newTime
+                
+                // 检查倒计时是否结束
+                if (timerMode == 1 && newTime <= 0) {
+                    VLog.i(TAG, "倒计时结束 - 关闭VPN")
+                    dealStopMsg()
+                    stopSelf();
+                    return
+                }
+                
+                // 更新通知
+                updateNotification()
+                
+                // 发送计时更新
+                sendTimerUpdate()
+                
+                // 继续下一秒
+                timerHandler.postDelayed(this, 1000L)
+            }
+        }
+    }
 
     private val vpnHandler = object : ProxyConnectorHandler {
         override fun proxyStatusChange(l: Long, msg: String) {
@@ -77,6 +124,10 @@ class XRayService : VpnService() {
             VLog.e(TAG, "stopForeground 失败", e)
         }
         
+        // 清理Messenger引用
+        replyMessenger = null
+        VLog.i(TAG, "replyMessenger 已清理")
+        
         // 清理资源
         try {
             Ixvpn_mobile.freeProxyConnector()
@@ -96,6 +147,14 @@ class XRayService : VpnService() {
     }
 
     private fun handleRemoteMessage(msg: Message) {
+        // 只在有replyTo时才记录Messenger对象
+        if (msg.replyTo != null) {
+            replyMessenger = msg.replyTo
+            VLog.i(TAG, "记录replyTo Messenger: $replyMessenger")
+        } else {
+            VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
+        }
+        
         when (msg.what) {
             XRAY_MSG_START -> dealStartMsg(msg)
             XRAY_MSG_STOP -> dealStopMsg()
@@ -157,6 +216,10 @@ class XRayService : VpnService() {
 
     private fun doStop() {
         VLog.i(TAG, "开始停止 XRay 服务")
+        
+        // 停止计时
+        stopTimer()
+        
         try {
             tunnelService.stopTunnel()
             VLog.i(TAG, "Tunnel stopped")
@@ -185,16 +248,141 @@ class XRayService : VpnService() {
 
     private fun sendStatusMessage(status: Long, message: String) {
         try {
-            val intent = Intent("xray_service_stat_message").apply {
-                putExtra("status", status)
-                putExtra("message", message)
+            if(status == VPN_STATE_CONNECTED) {
+                // 启动计时(普通计时模式)
+                startTimer(0, 0)
+//                startTimer(1, 10000L)
+            }
+            replyMessenger?.let { messenger ->
+                try {
+                    val msg = Message.obtain().apply {
+                        what = XRAY_MSG_REPLY_STATUS
+                        // 使用Bundle传递消息,避免跨进程序列化问题
+                        data = Bundle().apply {
+                            putLong("status", status)
+                            putString("message", message)
+                        }
+                    }
+                    messenger.send(msg)
+                    VLog.i(TAG, "状态消息已通过Messenger发送: status=$status, message=$message")
+                } catch (e: DeadObjectException) {
+                    VLog.w(TAG, "Messenger已失效,清理引用", e)
+                    replyMessenger = null
+                } catch (e: Exception) {
+                    VLog.e(TAG, "发送Messenger消息失败", e)
+                }
+            } ?: run {
+                VLog.w(TAG, "replyMessenger为空,无法发送状态消息")
             }
-            sendBroadcast(intent)
-            VLog.i(TAG, "状态消息已发送: status=$status, message=$message")
         } catch (e: Exception) {
             VLog.e(TAG, "发送状态消息失败", e)
         }
     }
+    
+    // 计时相关方法
+    private fun startTimer(mode: Int, initialTime: Long) {
+        if(isTimerRunning)
+            return
+        VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
+        timerMode = mode
+        currentTimerTime = initialTime
+        timerStartTime = System.currentTimeMillis()
+
+        isTimerRunning = true
+        isTimerPaused = false
+        
+        // 开始计时循环
+        timerHandler.post(timerRunnable)
+    }
+    
+    private fun stopTimer() {
+        if(!isTimerRunning)
+            return
+        VLog.i(TAG, "停止计时")
+        isTimerRunning = false
+        isTimerPaused = false
+        timerHandler.removeCallbacks(timerRunnable)
+    }
+    
+    private fun pauseTimer() {
+        if (isTimerRunning) {
+            VLog.i(TAG, "暂停计时")
+            isTimerPaused = true
+            timerHandler.removeCallbacks(timerRunnable)
+        }
+    }
+    
+    private fun resumeTimer() {
+        if (isTimerRunning && isTimerPaused) {
+            VLog.i(TAG, "恢复计时")
+            isTimerPaused = false
+            timerHandler.post(timerRunnable)
+        }
+    }
+    
+    private fun sendTimerUpdate() {
+        try {
+            replyMessenger?.let { messenger ->
+                try {
+                    val msg = Message.obtain().apply {
+                        what = XRAY_MSG_REPLY_TIMER_UPDATE
+                        data = Bundle().apply {
+                            putLong("currentTime", currentTimerTime)
+                            putInt("mode", timerMode)
+                            putBoolean("isRunning", isTimerRunning)
+                            putBoolean("isPaused", isTimerPaused)
+                        }
+                    }
+                    messenger.send(msg)
+                    VLog.i(TAG, "计时更新消息已发送: time=$currentTimerTime, mode=$timerMode")
+                } catch (e: DeadObjectException) {
+                    VLog.w(TAG, "Messenger已失效,清理引用", e)
+                    replyMessenger = null
+                } catch (e: Exception) {
+                    VLog.e(TAG, "发送计时更新消息失败", e)
+                }
+            } ?: run {
+                VLog.w(TAG, "replyMessenger为空,无法发送计时更新")
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "发送计时更新失败", e)
+        }
+    }
+    
+    private fun updateNotification() {
+        val timeText = formatTime(currentTimerTime)
+        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")
+            .setSmallIcon(R.drawable.ic_xvpn)
+            .setPriority(NotificationCompat.PRIORITY_LOW)
+            .setOngoing(true)
+            .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) {
+            String.format("%02d:%02d:%02d", hours, minutes, seconds)
+        } else {
+            String.format("00:%02d:%02d", minutes, seconds)
+        }
+    }
 
     private fun createNotificationChannel() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -248,6 +436,9 @@ class XRayService : VpnService() {
     companion object {
         const val XRAY_MSG_START = 1
         const val XRAY_MSG_STOP = 2
+        const val XRAY_MSG_REG_MESSENGER = 3
+        const val XRAY_MSG_REPLY_STATUS = 101
+        const val XRAY_MSG_REPLY_TIMER_UPDATE = 102
         private const val TAG = "ixvpn"
 
         private const val CHANNEL_ID: String = "nomo_channel_id"

BIN
assets/images/connected.png


BIN
assets/images/connecting.png


BIN
assets/images/disconnected.png


BIN
assets/images/network.png


BIN
assets/images/switch_status_connected.png


BIN
assets/images/switch_status_connecting.png


BIN
assets/images/switch_status_disconnected.png


BIN
assets/images/vpn_error.png


+ 0 - 4
assets/vectors/boost/connected.svg

@@ -1,4 +0,0 @@
-<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.25 2.69958L7.50251 1.16669L12.75 2.69958V5.84318C12.75 9.14733 10.6355 12.0807 7.50076 13.1252C4.3652 12.0807 2.25 9.14669 2.25 5.84172V2.69958Z" stroke="#90E05E" style="stroke:#90E05E;stroke:color(display-p3 0.5647 0.8784 0.3686);stroke-opacity:1;" stroke-linejoin="round"/>
-<path d="M4.875 6.70833L6.91667 8.75L10.4167 5.25" stroke="#90E05E" style="stroke:#90E05E;stroke:color(display-p3 0.5647 0.8784 0.3686);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/connecting.svg

@@ -1,3 +0,0 @@
-<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.25 2.40789L7.50251 0.875L12.75 2.40789V5.5515C12.75 8.85564 10.6355 11.789 7.50076 12.8335C4.3652 11.789 2.25 8.855 2.25 5.55004V2.40789Z" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9183 0.5969 0.0000);stroke-opacity:1;" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/disconnected.svg

@@ -1,3 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1.75 2.40789L7.00251 0.875L12.25 2.40789V5.5515C12.25 8.85564 10.1355 11.789 7.00076 12.8335C3.8652 11.789 1.75 8.855 1.75 5.55004V2.40789Z" stroke="#646776" style="stroke:#646776;stroke:color(display-p3 0.3922 0.4039 0.4627);stroke-opacity:1;" stroke-linejoin="round"/>
-</svg>

+ 0 - 5
assets/vectors/boost/error.svg

@@ -1,5 +0,0 @@
-<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.25 2.69958L7.50251 1.16669L12.75 2.69958V5.84318C12.75 9.14733 10.6355 12.0807 7.50076 13.1252C4.3652 12.0807 2.25 9.14669 2.25 5.84172V2.69958Z" stroke="#EF0000" style="stroke:#EF0000;stroke:color(display-p3 0.9371 0.0000 0.0000);stroke-opacity:1;" stroke-linejoin="round"/>
-<path d="M9.10403 5.36908L5.8042 8.66891" stroke="#EF0000" style="stroke:#EF0000;stroke:color(display-p3 0.9371 0.0000 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.8042 5.36914L9.10403 8.66897" stroke="#EF0000" style="stroke:#EF0000;stroke:color(display-p3 0.9371 0.0000 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/gary_right.svg

@@ -1,3 +0,0 @@
-<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.08301 4.16705L12.9163 10.0004L7.08301 15.8337" stroke="#646776" style="stroke:#646776;stroke:color(display-p3 0.3922 0.4039 0.4627);stroke-opacity:1;" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 9
assets/vectors/boost/network.svg

@@ -1,9 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8333 5.53153C10.8574 3.65894 8.20135 2.91837 5.6875 3.30985" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.0833 7.52469C10.2919 6.73326 9.32794 6.21957 8.3125 5.98364" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.9165 7.52471C3.30372 7.13749 3.73227 6.81675 4.18791 6.5625" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.6665 9.42488C5.08615 9.00523 5.59068 8.72226 6.12484 8.57593" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00016 11.6666C7.40287 11.6666 7.72933 11.3402 7.72933 10.9375C7.72933 10.5348 7.40287 10.2083 7.00016 10.2083C6.59746 10.2083 6.271 10.5348 6.271 10.9375C6.271 11.3402 6.59746 11.6666 7.00016 11.6666Z" fill="#EA9800" style="fill:#EA9800;fill:color(display-p3 0.9176 0.5961 0.0000);fill-opacity:1;"/>
-<path d="M11.6668 11.6666L2.3335 2.33331" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M1.1665 5.5315C1.33824 5.36875 1.51511 5.21454 1.69658 5.06891C1.85671 4.94037 2.02042 4.81852 2.18733 4.70331" stroke="#EA9800" style="stroke:#EA9800;stroke:color(display-p3 0.9176 0.5961 0.0000);stroke-opacity:1;" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 0
assets/vectors/boost/refresh.svg → assets/vectors/boost/refersh.svg


+ 0 - 3
assets/vectors/boost/settings.svg

@@ -1,3 +0,0 @@
-<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.1665 2.08398C14.4618 2.08398 14.7355 2.23967 14.8853 2.49414L19.0513 9.57812C19.2045 9.83886 19.2046 10.1622 19.0513 10.4229L14.8853 17.5059C14.7355 17.7602 14.4617 17.917 14.1665 17.917H5.8335C5.53834 17.9169 5.26446 17.7602 5.11475 17.5059L0.947754 10.4229C0.794618 10.1624 0.794847 9.83871 0.947754 9.57812L5.11475 2.49414L5.17627 2.40332C5.33287 2.20297 5.57521 2.08405 5.8335 2.08398H14.1665ZM2.6333 10L6.30908 16.25H13.6909L17.3657 10L13.6899 3.75H6.31006L2.6333 10ZM9.99951 7.08398C11.6102 7.08398 12.9163 8.38933 12.9165 10C12.9163 11.6107 11.6102 12.917 9.99951 12.917C8.389 12.9168 7.08368 11.6105 7.0835 10C7.08367 8.38947 8.38899 7.08421 9.99951 7.08398ZM9.99951 8.75C9.30947 8.75023 8.74969 9.30994 8.74951 10C8.74969 10.6901 9.30947 11.2498 9.99951 11.25C10.6897 11.25 11.2493 10.6902 11.2495 10C11.2493 9.3098 10.6897 8.75 9.99951 8.75Z" fill="white" style="fill:white;fill-opacity:1;"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_connected_dark.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4814C14.9756 10.8158 14.4121 11.184 13.8736 11.5832C12.9521 12.2665 12.1037 13.0407 11.3422 13.8924C8.71634 16.829 7.12305 20.6864 7.12305 24.9112C7.12305 34.1113 14.6789 41.5696 23.9995 41.5696C33.3201 41.5696 40.876 34.1113 40.876 24.9112C40.876 20.6864 39.2827 16.829 36.6569 13.8924C35.8953 13.0407 35.0469 12.2665 34.1254 11.5832C33.5869 11.184 33.0234 10.8158 32.4377 10.4814M23.9995 6.22998V23.9947" stroke="#1FBC7B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_connected_light.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4814C14.9756 10.8158 14.4121 11.184 13.8736 11.5832C12.9521 12.2665 12.1037 13.0407 11.3422 13.8924C8.71634 16.829 7.12305 20.6864 7.12305 24.9112C7.12305 34.1113 14.6789 41.5696 23.9995 41.5696C33.3201 41.5696 40.876 34.1113 40.876 24.9112C40.876 20.6864 39.2827 16.829 36.6569 13.8924C35.8953 13.0407 35.0469 12.2665 34.1254 11.5832C33.5869 11.184 33.0234 10.8158 32.4377 10.4814M23.9995 6.22998V23.9947" stroke="#1BBA66" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_connecting_dark.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4814C14.9756 10.8158 14.4121 11.184 13.8736 11.5832C12.9521 12.2665 12.1037 13.0407 11.3422 13.8924C8.71634 16.829 7.12305 20.6864 7.12305 24.9112C7.12305 34.1113 14.6789 41.5696 23.9995 41.5696C33.3201 41.5696 40.876 34.1113 40.876 24.9112C40.876 20.6864 39.2827 16.829 36.6569 13.8924C35.8953 13.0407 35.0469 12.2665 34.1254 11.5832C33.5869 11.184 33.0234 10.8158 32.4377 10.4814M23.9995 6.22998V23.9947" stroke="#7E6DFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_connecting_light.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4815C14.9756 10.8159 14.4121 11.1841 13.8736 11.5834C12.9521 12.2666 12.1037 13.0408 11.3422 13.8925C8.71634 16.8291 7.12305 20.6865 7.12305 24.9113C7.12305 34.1115 14.6789 41.5697 23.9995 41.5697C33.3201 41.5697 40.876 34.1115 40.876 24.9113C40.876 20.6865 39.2827 16.8291 36.6569 13.8925C35.8953 13.0408 35.0469 12.2666 34.1254 11.5834C33.5869 11.1841 33.0234 10.8159 32.4377 10.4815M23.9995 6.2301V23.9948" stroke="#6A55FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_disconnected_dark.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4814C14.9756 10.8158 14.4121 11.184 13.8736 11.5832C12.9521 12.2665 12.1037 13.0407 11.3422 13.8924C8.71634 16.829 7.12305 20.6864 7.12305 24.9112C7.12305 34.1113 14.6789 41.5696 23.9995 41.5696C33.3201 41.5696 40.876 34.1113 40.876 24.9112C40.876 20.6864 39.2827 16.829 36.6569 13.8924C35.8953 13.0407 35.0469 12.2665 34.1254 11.5832C33.5869 11.184 33.0234 10.8158 32.4377 10.4814M23.9995 6.22998V23.9947" stroke="white" stroke-opacity="0.4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/switch_status_disconnected_light.svg

@@ -1,3 +0,0 @@
-<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15.5613 10.4814C14.9756 10.8158 14.4121 11.184 13.8736 11.5832C12.9521 12.2665 12.1037 13.0407 11.3422 13.8924C8.71634 16.829 7.12305 20.6864 7.12305 24.9112C7.12305 34.1113 14.6789 41.5696 23.9995 41.5696C33.3201 41.5696 40.876 34.1113 40.876 24.9112C40.876 20.6864 39.2827 16.829 36.6569 13.8924C35.8953 13.0407 35.0469 12.2665 34.1254 11.5832C33.5869 11.184 33.0234 10.8158 32.4377 10.4814M23.9995 6.22998V23.9947" stroke="#181818" stroke-opacity="0.4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 3
assets/vectors/boost/white_right.svg

@@ -1,3 +0,0 @@
-<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.9165 5L12.9165 10L7.9165 15" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

+ 0 - 8
assets/vectors/status/refersh.svg

@@ -1,8 +0,0 @@
-<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<mask id="mask0_378_12329" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="25" height="24">
-<rect x="0.5" width="24" height="24" fill="#D9D9D9" style="fill:#D9D9D9;fill:color(display-p3 0.8510 0.8510 0.8510);fill-opacity:1;"/>
-</mask>
-<g mask="url(#mask0_378_12329)">
-<path d="M11.5 20.95C9.48333 20.7 7.8125 19.8208 6.4875 18.3125C5.1625 16.8042 4.5 15.0333 4.5 13C4.5 11.9 4.71667 10.8458 5.15 9.8375C5.58333 8.82917 6.2 7.95 7 7.2L8.425 8.625C7.79167 9.19167 7.3125 9.85 6.9875 10.6C6.6625 11.35 6.5 12.15 6.5 13C6.5 14.4667 6.96667 15.7625 7.9 16.8875C8.83333 18.0125 10.0333 18.7 11.5 18.95V20.95ZM13.5 20.95V18.95C14.95 18.6833 16.1458 17.9917 17.0875 16.875C18.0292 15.7583 18.5 14.4667 18.5 13C18.5 11.3333 17.9167 9.91667 16.75 8.75C15.5833 7.58333 14.1667 7 12.5 7H12.425L13.525 8.1L12.125 9.5L8.625 6L12.125 2.5L13.525 3.9L12.425 5H12.5C14.7333 5 16.625 5.775 18.175 7.325C19.725 8.875 20.5 10.7667 20.5 13C20.5 15.0167 19.8375 16.7792 18.5125 18.2875C17.1875 19.7958 15.5167 20.6833 13.5 20.95Z" fill="white" style="fill:white;fill-opacity:1;"/>
-</g>
-</svg>

+ 9 - 6
ios/Runner/CoreApi.g.swift

@@ -87,9 +87,10 @@ class CoreApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
 
 var coreApiPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: CoreApiPigeonCodecReaderWriter());
 
+
 /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
 protocol CoreApi {
-  func getApps() throws -> String?
+  func getApps(completion: @escaping (Result<String?, Error>) -> Void)
   func getSystemLocale() throws -> String?
   func connect() throws -> Bool?
   func disconnect() throws -> Bool?
@@ -110,11 +111,13 @@ class CoreApiSetup {
     let getAppsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getApps\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
     if let api = api {
       getAppsChannel.setMessageHandler { _, reply in
-        do {
-          let result = try api.getApps()
-          reply(wrapResult(result))
-        } catch {
-          reply(wrapError(error))
+        api.getApps { result in
+          switch result {
+          case .success(let res):
+            reply(wrapResult(res))
+          case .failure(let error):
+            reply(wrapError(error))
+          }
         }
       }
     } else {

+ 6 - 9
lib/app/api/base/base_api.dart

@@ -8,7 +8,6 @@ import '../../../utils/log/logger.dart';
 import '../../components/country_restricted_overlay.dart';
 import '../../data/models/api_result.dart';
 
-
 enum DomainType {
   api, // 普通API域名
   log, // 日志上传域名
@@ -129,14 +128,10 @@ abstract class BaseApi {
         client.autoUncompress = true;
 
         // 设置用户代理
-        client.userAgent = 'FKey/1.0';
+        client.userAgent = 'Nomo/1.0';
 
         // 设置连接工厂
-        client.connectionFactory = (
-          Uri uri,
-          String? host,
-          int? port,
-        ) async {
+        client.connectionFactory = (Uri uri, String? host, int? port) async {
           final resolvedHost = host ?? _hostIpMap[uri.host] ?? uri.host;
           final actualPort = port ?? uri.port;
           if (uri.scheme == 'https') {
@@ -392,8 +387,10 @@ abstract class BaseApi {
         retryState.isRetrying = true;
         retryState.currentRetry++;
 
-        log('BaseApi',
-            'Request failed (${domainType.name}). Retrying (${retryState.currentRetry}/${_maxRetries[domainType]})...');
+        log(
+          'BaseApi',
+          'Request failed (${domainType.name}). Retrying (${retryState.currentRetry}/${_maxRetries[domainType]})...',
+        );
 
         // 重试请求
         return rawRequest(

+ 4 - 6
lib/app/api/core/api_core.dart

@@ -33,7 +33,6 @@ class ApiCore extends BaseApi {
   void _initDio() {
     // 此处可以设置默认的DIO参数
     final options = BaseOptions();
-    // options.baseUrl = DomainManager.instance.getCurrentDomain(DomainType.api);
     options.connectTimeout = const Duration(seconds: 15);
     options.receiveTimeout = const Duration(seconds: 15);
     options.responseType = ResponseType.bytes;
@@ -153,7 +152,7 @@ class ApiCore extends BaseApi {
         options: Options(
           headers: {
             'Authorization': user.refreshToken,
-            'X-NL-Product-Code': 'fkey',
+            'X-NL-Product-Code': 'nomo',
             'X-NL-Content-Encoding': 'gzip',
           },
         ),
@@ -213,7 +212,7 @@ class ApiCore extends BaseApi {
   Map<String, dynamic>? getDefaultHeader() {
     // 可以设置自定义Header
     final headers = {
-      'X-NL-Product-Code': 'fkey',
+      'X-NL-Product-Code': 'nomo',
       'X-NL-Content-Encoding': 'gzip',
     };
     return headers;
@@ -229,12 +228,11 @@ class ApiCore extends BaseApi {
   dynamic encrypt(dynamic input) {
     try {
       final data = jsonEncode(input);
-      // decryptTest(data);
       final bytes = utf8.encode(data);
       final gzipEncoder = GZipEncoder();
       final compressedBytes = gzipEncoder.encode(bytes);
       log('ApiCore', '>>: $data');
-      return Crytpo.encryptBytesUint8(compressedBytes, Keys.aesKey, Keys.aesIv);
+      return Crypto.encryptBytesUint8(compressedBytes, Keys.aesKey, Keys.aesIv);
     } catch (error) {
       throw ApiException("-2", "encrypt error: $error");
     }
@@ -243,7 +241,7 @@ class ApiCore extends BaseApi {
   @override
   dynamic decrypt(dynamic input) {
     try {
-      final decryptedBytes = Crytpo.decryptBytes(
+      final decryptedBytes = Crypto.decryptBytes(
         base64Encode(input),
         Keys.aesKey,
         Keys.aesIv,

+ 6 - 0
lib/app/api/core/api_core_paths.dart

@@ -87,4 +87,10 @@ class ApiCorePaths {
 
   /// 文件上传
   static const String uploadFile = '$_ver/log/upload';
+
+  /// 上传图片
+  static const String uploadImage = '$_ver/issue/uploadImage';
+
+  /// 上传视频
+  static const String uploadVideo = '$_ver/issue/uploadVideo';
 }

+ 267 - 0
lib/app/api/file/api_file.dart

@@ -0,0 +1,267 @@
+import 'dart:convert';
+
+import 'package:archive/archive.dart';
+import 'package:dio/dio.dart';
+import 'package:uuid/uuid.dart';
+import '../../../utils/crypto.dart';
+import '../../../utils/developer/ix_developer_tools.dart';
+import '../../../utils/log/logger.dart';
+import '../../constants/keys.dart';
+import '../../data/models/api_exception.dart';
+import '../../data/models/api_result.dart';
+import '../../data/models/disconnect_domain.dart';
+import '../../data/sp/ix_sp.dart';
+import '../base/base_api.dart';
+
+import '../core/api_core_paths.dart';
+
+/// 核心API
+class ApiFile extends BaseApi {
+  static final _instance = ApiFile._internal();
+
+  factory ApiFile() => _instance;
+
+  ApiFile._internal() {
+    _initDio();
+  }
+
+  /// 初始化Dio
+  void _initDio() {
+    // 此处可以设置默认的DIO参数
+    final options = BaseOptions();
+    options.connectTimeout = const Duration(seconds: 90);
+    options.receiveTimeout = const Duration(seconds: 90);
+    options.responseType = ResponseType.bytes;
+    options.headers['content-type'] = 'multipart/form-data';
+
+    dio = Dio(options);
+
+    // 添加talker日志
+    dio.interceptors.add(SimpleApiMonitorInterceptor());
+
+    // 添加拦截器
+    _setupInterceptors();
+  }
+
+  // bool _isRefreshing = false;
+
+  void _setupInterceptors() {
+    dio.interceptors.add(
+      InterceptorsWrapper(
+        onRequest: (options, handler) async {
+          // 在请求发送前添加token
+          final user = IXSP.getUser();
+          if (user != null) {
+            options.headers['Authorization'] = user.accessToken;
+          }
+          return handler.next(options);
+        },
+        onError: (DioException error, handler) async {
+          IXSP.addDisconnectDomain(
+            DisconnectDomain(
+              domain: error.requestOptions.baseUrl,
+              status: error.response?.statusCode ?? -1,
+              msg: error.message ?? '',
+              startTime: DateTime.now().millisecondsSinceEpoch,
+              endTime: DateTime.now().millisecondsSinceEpoch,
+              count: 1,
+            ),
+          );
+          return handler.next(error);
+        },
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? getDefaultHeader() {
+    // 可以设置自定义Header
+    final headers = {
+      'X-NL-Product-Code': 'nomo',
+      'X-NL-Content-Encoding': 'gzip',
+    };
+    return headers;
+  }
+
+  @override
+  Map<String, dynamic>? getDefaultQuery() {
+    // 可以设置自定义Query
+    return null;
+  }
+
+  @override
+  dynamic encrypt(dynamic input) {
+    return input;
+  }
+
+  @override
+  dynamic decrypt(dynamic input) {
+    try {
+      final decryptedBytes = Crypto.decryptBytes(
+        base64Encode(input),
+        Keys.aesKey,
+        Keys.aesIv,
+      );
+      // 使用GZip解压
+      final gzipDecoder = GZipDecoder();
+      final decodeBytes = gzipDecoder.decodeBytes(decryptedBytes);
+      final data = utf8.decode(decodeBytes);
+      log('ApiCore', '<<:$data');
+      return jsonDecode(data);
+    } catch (error) {
+      throw ApiException("-2", "decrypt error: $error");
+    }
+  }
+
+  // 上传日志文件
+  Future<ApiResult> uploadLogFile(
+    String filePath,
+    String fileName,
+    String logType,
+    String data,
+  ) async {
+    final fileData = await MultipartFile.fromFile(filePath, filename: fileName);
+    const fileType = 'gzip';
+    final timestamp = DateTime.now().millisecondsSinceEpoch;
+    final sign = Crypto.encrypt(
+      '$fileType$logType$timestamp',
+      Keys.aesKey,
+      Keys.aesIv,
+    );
+    if (logType == "App_NL_Log") {
+      final logId = const Uuid().v4();
+      final userInfo = Crypto.encrypt(data, Keys.aesKey, Keys.aesIv);
+      final formData = FormData.fromMap({
+        'file': fileData,
+        'fileName': fileName,
+        'fileType': fileType,
+        'logType': logType,
+        'timestamp': timestamp,
+        'sign': sign,
+        'userInfo': userInfo,
+        'logId': logId,
+      });
+      return post(
+        ApiCorePaths.uploadFile,
+        data: formData,
+        domainType: DomainType.file,
+      );
+    } else {
+      final formData = FormData.fromMap({
+        'file': fileData,
+        'fileName': fileName,
+        'fileType': fileType,
+        'timestamp': timestamp,
+        'logType': logType,
+        'sign': sign,
+      });
+      return post(
+        ApiCorePaths.uploadFile,
+        data: formData,
+        domainType: DomainType.file,
+      );
+    }
+  }
+
+  // 上传图片文件
+  Future<ApiResult> uploadImageFile(String filePath, String fileName) async {
+    final fileData = await MultipartFile.fromFile(filePath, filename: fileName);
+    const fileType = 'image';
+    final timestamp = DateTime.now().millisecondsSinceEpoch;
+    final sign = Crypto.encrypt('$fileType$timestamp', Keys.aesKey, Keys.aesIv);
+    final formData = FormData.fromMap({
+      'file': fileData,
+      'fileType': fileType,
+      'timestamp': timestamp,
+      'sign': sign,
+    });
+    return post(
+      ApiCorePaths.uploadImage,
+      data: formData,
+      domainType: DomainType.file,
+    );
+  }
+
+  // 上传视频文件
+  Future<ApiResult> uploadVideoFile(String filePath, String fileName) async {
+    final fileData = await MultipartFile.fromFile(filePath, filename: fileName);
+    const fileType = 'video';
+    final timestamp = DateTime.now().millisecondsSinceEpoch;
+    final sign = Crypto.encrypt('$fileType$timestamp', Keys.aesKey, Keys.aesIv);
+    final formData = FormData.fromMap({
+      'file': fileData,
+      'fileType': fileType,
+      'timestamp': timestamp,
+      'sign': sign,
+    });
+    return post(
+      ApiCorePaths.uploadVideo,
+      data: formData,
+      domainType: DomainType.file,
+    );
+  }
+
+  // 上传多个图片文件
+  Future<ApiResult> uploadMultipleImageFiles(
+    List<String> filePaths,
+    List<String> fileNames,
+  ) async {
+    final List<MultipartFile> files = [];
+    for (int i = 0; i < filePaths.length; i++) {
+      final fileData = await MultipartFile.fromFile(
+        filePaths[i],
+        filename: fileNames[i],
+      );
+      files.add(fileData);
+    }
+
+    const fileType = 'image';
+    final timestamp = DateTime.now().millisecondsSinceEpoch;
+    final sign = Crypto.encrypt('$fileType$timestamp', Keys.aesKey, Keys.aesIv);
+
+    final formData = FormData.fromMap({
+      'file': files,
+      'fileType': fileType,
+      'timestamp': timestamp,
+      'sign': sign,
+    });
+
+    return post(
+      ApiCorePaths.uploadImage,
+      data: formData,
+      domainType: DomainType.file,
+    );
+  }
+
+  // 上传多个视频文件
+  Future<ApiResult> uploadMultipleVideoFiles(
+    List<String> filePaths,
+    List<String> fileNames,
+  ) async {
+    final List<MultipartFile> files = [];
+    for (int i = 0; i < filePaths.length; i++) {
+      final fileData = await MultipartFile.fromFile(
+        filePaths[i],
+        filename: fileNames[i],
+      );
+      files.add(fileData);
+    }
+
+    const fileType = 'video';
+    final timestamp = DateTime.now().millisecondsSinceEpoch;
+    final sign = Crypto.encrypt('$fileType$timestamp', Keys.aesKey, Keys.aesIv);
+
+    final formData = FormData.fromMap({
+      'file': files,
+      'fileType': fileType,
+      'timestamp': timestamp,
+      'sign': sign,
+    });
+
+    return post(
+      ApiCorePaths.uploadVideo,
+      data: formData,
+      domainType: DomainType.file,
+    );
+  }
+}

+ 128 - 0
lib/app/api/log/api_log.dart

@@ -0,0 +1,128 @@
+import 'dart:convert';
+
+import 'package:archive/archive.dart';
+import 'package:dio/dio.dart';
+
+import '../../../utils/crypto.dart';
+import '../../../utils/developer/ix_developer_tools.dart';
+import '../../../utils/log/logger.dart';
+import '../../constants/keys.dart';
+import '../../data/models/api_exception.dart';
+import '../../data/models/api_result.dart';
+import '../../data/models/disconnect_domain.dart';
+import '../../data/sp/ix_sp.dart';
+import '../base/base_api.dart';
+
+import '../core/api_core_paths.dart';
+
+/// 核心API
+class ApiLog extends BaseApi {
+  static final _instance = ApiLog._internal();
+
+  factory ApiLog() => _instance;
+
+  ApiLog._internal() {
+    _initDio();
+  }
+
+  /// 初始化Dio
+  void _initDio() {
+    // 此处可以设置默认的DIO参数
+    final options = BaseOptions();
+    options.connectTimeout = const Duration(seconds: 90);
+    options.receiveTimeout = const Duration(seconds: 90);
+    options.responseType = ResponseType.bytes;
+    options.headers['content-type'] = 'application/json';
+
+    dio = Dio(options);
+
+    dio.interceptors.add(SimpleApiMonitorInterceptor());
+
+    // 添加拦截器
+    _setupInterceptors();
+  }
+
+  // bool _isRefreshing = false;
+
+  void _setupInterceptors() {
+    dio.interceptors.add(
+      InterceptorsWrapper(
+        onRequest: (options, handler) async {
+          // 在请求发送前添加token
+          final user = IXSP.getUser();
+          if (user != null) {
+            options.headers['Authorization'] = user.accessToken;
+          }
+          return handler.next(options);
+        },
+        onError: (DioException error, handler) async {
+          IXSP.addDisconnectDomain(
+            DisconnectDomain(
+              domain: error.requestOptions.baseUrl,
+              status: error.response?.statusCode ?? -1,
+              msg: error.message ?? '',
+              startTime: DateTime.now().millisecondsSinceEpoch,
+              endTime: DateTime.now().millisecondsSinceEpoch,
+              count: 1,
+            ),
+          );
+          return handler.next(error);
+        },
+      ),
+    );
+  }
+
+  @override
+  Map<String, dynamic>? getDefaultHeader() {
+    // 可以设置自定义Header
+    final headers = {
+      'X-NL-Product-Code': 'nomo',
+      'X-NL-Content-Encoding': 'gzip',
+    };
+    return headers;
+  }
+
+  @override
+  Map<String, dynamic>? getDefaultQuery() {
+    // 可以设置自定义Query
+    return null;
+  }
+
+  @override
+  dynamic encrypt(dynamic input) {
+    try {
+      final data = jsonEncode(input);
+      final bytes = utf8.encode(data);
+      final gzipEncoder = GZipEncoder();
+      final compressedBytes = gzipEncoder.encode(bytes);
+      log('ApiLog', '>>: $data');
+      return Crypto.encryptBytesUint8(compressedBytes, Keys.aesKey, Keys.aesIv);
+    } catch (error) {
+      throw ApiException("-2", "encrypt error: $error");
+    }
+  }
+
+  @override
+  dynamic decrypt(dynamic input) {
+    try {
+      final decryptedBytes = Crypto.decryptBytes(
+        base64Encode(input),
+        Keys.aesKey,
+        Keys.aesIv,
+      );
+      // 使用GZip解压
+      final gzipDecoder = GZipDecoder();
+      final decodeBytes = gzipDecoder.decodeBytes(decryptedBytes);
+      final data = utf8.decode(decodeBytes);
+      log('ApiLog', '<<:$data');
+      return jsonDecode(data);
+    } catch (error) {
+      throw ApiException("-2", "decrypt error: $error");
+    }
+  }
+
+  /// 上传日志
+  Future<ApiResult> uploadLogs(dynamic data) async {
+    return post(ApiCorePaths.uploadLog, data: data, domainType: DomainType.log);
+  }
+}

+ 3 - 3
lib/app/app.dart

@@ -24,7 +24,7 @@ class App extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return ScreenUtilInit(
-      designSize: const Size(390, 844),
+      designSize: const Size(375, 812),
       minTextAdapt: true,
       splitScreenMode: true,
       useInheritedMediaQuery: true,
@@ -108,9 +108,9 @@ class App extends StatelessWidget {
                 ),
                 onTap: () {
                   // 使用简化版开发者工具
-                  // IXDeveloperTools.show();
+                  IXDeveloperTools.show();
                   // 切换主题
-                  IXTheme.changeTheme();
+                  // IXTheme.changeTheme();
                 },
               ),
             ),

+ 2 - 15
lib/app/base/base_view.dart

@@ -16,7 +16,7 @@ abstract class BaseView<T> extends GetView<T> {
   Color? get backgroundColor => LightThemeColors.backgroundColor;
   bool get extendBody => false;
   bool get extendBodyBehindAppBar => false;
-  bool get safeArea => true;
+  bool get safeArea => false;
   EdgeInsets? get padding => null;
   bool get resizeToAvoidBottomInset => true;
   Widget? get drawer => null;
@@ -39,19 +39,6 @@ abstract class BaseView<T> extends GetView<T> {
     // 使用 Obx 包装整个视图以响应主题变化
     Widget view = Obx(() {
       // 在 Obx 内部构建 content,确保使用 Get.reactiveTheme 的子视图能响应主题变化
-      Widget content = padding != null
-          ? safeArea
-                ? SafeArea(
-                    child: Padding(
-                      padding: padding!,
-                      child: buildContent(context),
-                    ),
-                  )
-                : Padding(padding: padding!, child: buildContent(context))
-          : safeArea
-          ? SafeArea(child: buildContent(context))
-          : buildContent(context);
-
       return Scaffold(
         backgroundColor: Get.reactiveTheme.scaffoldBackgroundColor,
         appBar: appBar,
@@ -63,7 +50,7 @@ abstract class BaseView<T> extends GetView<T> {
         resizeToAvoidBottomInset: resizeToAvoidBottomInset,
         drawer: drawer,
         endDrawer: endDrawer,
-        body: content,
+        body: buildContent(context),
       );
     });
 

+ 1 - 1
lib/app/components/country_restricted_overlay.dart

@@ -112,7 +112,7 @@ class CountryRestrictedOverlay extends StatelessWidget {
                         text: type == RestrictedType.network
                             ? Strings.refresh.tr
                             : Strings.exit.tr,
-                        svgPath: Assets.facebook,
+                        svgPath: Assets.refersh,
                         svgColor: Get.reactiveTheme.textTheme.bodyLarge!.color,
                         bgColor: Get.reactiveTheme.cardColor,
                         textColor: Get.reactiveTheme.textTheme.bodyLarge!.color,

+ 1 - 1
lib/app/components/protocol_overlay.dart

@@ -23,7 +23,7 @@ class _ProtocolOverlayState extends State<ProtocolOverlay> {
     return WillPopScope(
       onWillPop: () async => false, // 禁止返回
       child: Scaffold(
-        backgroundColor: Get.reactiveTheme.dialogTheme.backgroundColor,
+        backgroundColor: Get.reactiveTheme.scaffoldBackgroundColor,
         body: SafeArea(
           child: Column(
             mainAxisAlignment: MainAxisAlignment.center,

+ 516 - 0
lib/app/constants/api_domains.dart

@@ -0,0 +1,516 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:dio/io.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../../utils/crypto.dart';
+import '../../utils/developer/ix_developer_tools.dart';
+import '../../utils/log/logger.dart';
+import '../api/core/api_core.dart';
+import '../api/file/api_file.dart';
+import '../api/log/api_log.dart';
+import 'configs.dart';
+import '../data/models/launch/launch.dart';
+import 'keys.dart';
+
+class ApiDomains {
+  static final ApiDomains _instance = ApiDomains._internal();
+  static ApiDomains get instance => _instance;
+
+  factory ApiDomains() => _instance;
+
+  ApiDomains._internal();
+
+  // 常量定义
+  static const String _STORAGE_API_URLS = 'api_urls';
+  static const String _STORAGE_LOG_URLS = 'log_urls';
+  static const String _STORAGE_FILE_URLS = 'file_urls';
+  static const String _STORAGE_BACKUP_URLS = 'backup_api_urls';
+  static const String _STORAGE_API_URLS_INDEX = 'api_urls_index';
+  static const String _STORAGE_LOG_URLS_INDEX = 'log_urls_index';
+  static const String _STORAGE_FILE_URLS_INDEX = 'file_urls_index';
+  static const String _STORAGE_IS_HARDCODED = 'is_hardcoded';
+  static const int RETRY_COUNT_PER_LAUNCH = 2;
+
+  final Dio dio = Dio(
+    BaseOptions(
+      connectTimeout: const Duration(seconds: 30),
+      receiveTimeout: const Duration(seconds: 30),
+      sendTimeout: const Duration(seconds: 30),
+    ),
+  );
+
+  // 标记是否已经添加了监控拦截器
+  bool _monitorInterceptorAdded = false;
+
+  setProxy(String proxy) {
+    dio.httpClientAdapter = IOHttpClientAdapter(
+      createHttpClient: () {
+        final client = HttpClient();
+        client.findProxy = (uri) => proxy;
+        client.badCertificateCallback =
+            (X509Certificate cert, String host, int port) => true;
+        return client;
+      },
+    );
+  }
+
+  // 默认API URL列表
+  final List<String> _defaultApiUrls = [
+    "https://d2aphju2iq7g2g.cloudfront.net",
+    "https://api.turboaccel.website",
+    "https://d1rvevafipy7o6.cloudfront.net",
+    "https://api.speedboost.website",
+    "https://dqmkongldmyxf.cloudfront.net",
+    "https://api.fastforward.website",
+    "https://d2cvqygi5xlqp3.cloudfront.net",
+    "https://api.odyxighs.com",
+    "https://service.fkey.club",
+  ];
+
+  // 默认LOG API URL列表
+  final List<String> _defaultLogUrls = ['https://stat.fkey.win'];
+
+  // 默认FILE API URL列表
+  final List<String> _defaultFileUrls = [];
+
+  // 默认备用URL列表(txt文件地址)
+  final List<String> _defaultBackupApiUrls = [
+    'https://drive.google.com/uc?export=download&id=1YMEZiAHPhlAg_xZqD7dSDt0WjlcogwVz',
+    'https://fkey.win/.well-known/backup/backup_fk.data',
+  ];
+
+  // 当前使用的URL列表
+  List<String> _apiUrls = [];
+
+  // 当前使用的LogURL列表
+  List<String> _logUrls = [];
+
+  // 当前使用的FILE URL列表
+  List<String> _fileUrls = [];
+
+  // 当前使用的备用URL列表
+  List<String> _backupApiUrls = [];
+
+  // 当前尝试的索引
+  int _currentIndex = 0;
+
+  // 当前使用的LogURL索引
+  int _currentLogIndex = 0;
+
+  // 当前使用的FILE URL索引
+  int _currentFileIndex = 0;
+
+  // 当前正在使用的备用列表索引
+  int _currentBackupListIndex = 0;
+
+  // 添加标志位,标识是否使用的是硬编码URL
+  bool _isUsingHardcodedUrls = true;
+
+  // 添加标志位,记录硬编码请求次数
+  int _hardcodedRequestCount = 0;
+
+  // 初始化方法
+  Future<void> init() async {
+    initUrls();
+    // 尝试加载保存的URLs
+    await _loadSavedUrls();
+  }
+
+  void initUrls() {
+    if (Configs.debug) {
+      _apiUrls = [
+        // 'https://api.znomo.com', // 测试环境
+        "https://nomo-api.clickto.dev", // 开发环境
+      ];
+      _logUrls = [];
+      _fileUrls = [];
+      _backupApiUrls = [];
+    } else {
+      _apiUrls = List.from(_defaultApiUrls);
+      _logUrls = List.from(_defaultLogUrls);
+      _fileUrls = List.from(_defaultFileUrls);
+      _backupApiUrls = List.from(_defaultBackupApiUrls);
+    }
+  }
+
+  // 修改加载保存的URL方法,返回是否成功加载
+  Future<void> _loadSavedUrls() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      _currentIndex = prefs.getInt(_STORAGE_API_URLS_INDEX) ?? 0;
+      _currentLogIndex = prefs.getInt(_STORAGE_LOG_URLS_INDEX) ?? 0;
+      _currentFileIndex = prefs.getInt(_STORAGE_FILE_URLS_INDEX) ?? 0;
+      _isUsingHardcodedUrls = prefs.getBool(_STORAGE_IS_HARDCODED) ?? true;
+
+      final savedApiUrls = prefs.getStringList(_STORAGE_API_URLS);
+      final savedBackupUrls = prefs.getStringList(_STORAGE_BACKUP_URLS);
+
+      final savedLogUrls = prefs.getStringList(_STORAGE_LOG_URLS);
+
+      final savedFileUrls = prefs.getStringList(_STORAGE_FILE_URLS);
+
+      if (savedApiUrls != null && savedApiUrls.isNotEmpty) {
+        _apiUrls = savedApiUrls;
+      }
+      if (savedBackupUrls != null && savedBackupUrls.isNotEmpty) {
+        _backupApiUrls = savedBackupUrls;
+      }
+      if (savedLogUrls != null && savedLogUrls.isNotEmpty) {
+        _logUrls = savedLogUrls;
+      }
+      if (savedFileUrls != null && savedFileUrls.isNotEmpty) {
+        _fileUrls = savedFileUrls;
+      }
+      log(
+        'ApiDomains',
+        'Loaded saved URLs: ${_apiUrls.length} APIs, ${_backupApiUrls.length} backups, ${_logUrls.length} logs, ${_fileUrls.length} files',
+      );
+    } catch (e) {
+      log('ApiDomains', 'Error loading saved URLs: $e');
+    }
+  }
+
+  Future<void> setApiUrls(List<String> urls) async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setStringList(_STORAGE_API_URLS, urls);
+      _currentIndex = 0;
+      await prefs.setInt(_STORAGE_API_URLS_INDEX, _currentIndex);
+      ApiCore().setbaseUrl(urls[_currentIndex]);
+    } catch (e) {
+      log('ApiDomains', 'Error saving URLs: $e');
+    }
+  }
+
+  // 添加logUrl
+  Future<void> addLogUrls(List<String> urls) async {
+    try {
+      var logList = List<String>.from(_logUrls);
+      logList.insertAll(0, urls);
+      _logUrls = logList.toSet().toList();
+      await _saveLogUrls();
+    } catch (e) {
+      log('ApiDomains', 'Error add Log URL: $e');
+    }
+  }
+
+  // 添加fileUrl
+  Future<void> addFileUrls(List<String> urls) async {
+    try {
+      var fileList = List<String>.from(_fileUrls);
+      fileList.insertAll(0, urls);
+      _fileUrls = fileList.toSet().toList();
+      await _saveFileUrls();
+    } catch (e) {
+      log('ApiDomains', 'Error add File URL: $e');
+    }
+  }
+
+  // 保存当前URL列表到持久化存储
+  Future<void> _saveApiUrls() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setStringList(_STORAGE_API_URLS, _apiUrls);
+      await prefs.setStringList(_STORAGE_BACKUP_URLS, _backupApiUrls);
+    } catch (e) {
+      log('ApiDomains', 'Error saving URLs: $e');
+    }
+  }
+
+  // 保存当前LogURL列表到持久化存储
+  Future<void> _saveLogUrls() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setStringList(_STORAGE_LOG_URLS, _logUrls);
+    } catch (e) {
+      log('ApiDomains', 'Error saving Log URLs: $e');
+    }
+  }
+
+  // 保存当前FILE URL列表到持久化存储
+  Future<void> _saveFileUrls() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setStringList(_STORAGE_FILE_URLS, _fileUrls);
+    } catch (e) {
+      log('ApiDomains', 'Error saving File URLs: $e');
+    }
+  }
+
+  // 保存硬编码状态
+  Future<void> _saveIsHardcodedUrls() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setBool(_STORAGE_IS_HARDCODED, _isUsingHardcodedUrls);
+    } catch (e) {
+      log('ApiDomains', 'Error saving is hardcoded URLs: $e');
+    }
+  }
+
+  // 保存硬编码索引
+  Future<void> _saveApiUrlsIndex() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setInt(_STORAGE_API_URLS_INDEX, _currentIndex);
+    } catch (e) {
+      log('ApiDomains', 'Error saving hardcoded index: $e');
+    }
+  }
+
+  // 保存LogURL硬编码索引
+  Future<void> _saveLogUrlsIndex() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setInt(_STORAGE_LOG_URLS_INDEX, _currentLogIndex);
+    } catch (e) {
+      log('ApiDomains', 'Error saving Log URLs index: $e');
+    }
+  }
+
+  // 保存FILE URL硬编码索引
+  Future<void> _saveFileUrlsIndex() async {
+    try {
+      final prefs = await SharedPreferences.getInstance();
+      await prefs.setInt(_STORAGE_FILE_URLS_INDEX, _currentFileIndex);
+    } catch (e) {
+      log('ApiDomains', 'Error saving File URLs index: $e');
+    }
+  }
+
+  // 从Launch数据更新URL列表
+  Future<void> updateFromLaunch(Launch launch) async {
+    try {
+      if (launch.appConfig?.apiUrls != null &&
+          launch.appConfig!.apiUrls!.isNotEmpty) {
+        _apiUrls = List<String>.from(launch.appConfig!.apiUrls!);
+
+        // 如果ApiCore的baseUrl不在launch的apiUrls中,或者使用硬编码的url,则设置ApiCore的baseUrl为launch的apiUrls的第一个
+        if (!_apiUrls.contains(ApiCore().baseUrl) || _isUsingHardcodedUrls) {
+          ApiCore().setbaseUrl(_apiUrls[0]);
+        }
+        final baseUrl = ApiCore().baseUrl;
+        _apiUrls.insert(0, baseUrl);
+        _apiUrls = _apiUrls.toSet().toList();
+        _currentIndex = 0;
+        log('ApiDomains', 'Add ApiCore baseUrl to index 0: $baseUrl');
+      }
+
+      if (launch.appConfig?.backupApiUrls != null &&
+          launch.appConfig!.backupApiUrls!.isNotEmpty) {
+        _backupApiUrls = launch.appConfig!.backupApiUrls!;
+        _currentBackupListIndex = 0;
+      }
+
+      if (launch.appConfig?.appStatUrls != null &&
+          launch.appConfig!.appStatUrls!.isNotEmpty) {
+        _logUrls = launch.appConfig!.appStatUrls!;
+        // 设置ApiLog的baseUrl为launch的appStatUrls的第一个
+        final baseUrl = _logUrls[0];
+        ApiLog().setbaseUrl(baseUrl);
+        _currentLogIndex = 0;
+        log('ApiDomains', 'Add ApiLog baseUrl to index 0: $baseUrl');
+      }
+
+      if (launch.appConfig?.logFileUploadUrls != null &&
+          launch.appConfig!.logFileUploadUrls!.isNotEmpty) {
+        _fileUrls = launch.appConfig!.logFileUploadUrls!;
+        // 设置ApiFile的baseUrl为launch的logFileUploadUrls的第一个
+        final baseUrl = _fileUrls[0];
+        ApiFile().setbaseUrl(baseUrl);
+        _currentFileIndex = 0;
+        log('ApiDomains', 'Add ApiFile baseUrl to index 0: $baseUrl');
+      }
+
+      _isUsingHardcodedUrls = false;
+      await _saveIsHardcodedUrls();
+      await _saveApiUrls();
+      await _saveApiUrlsIndex();
+      await _saveLogUrls();
+      await _saveLogUrlsIndex();
+      await _saveFileUrls();
+      await _saveFileUrlsIndex();
+      log('ApiDomains', 'Updated URLs from launch data');
+    } catch (e) {
+      log('ApiDomains', 'Error updating URLs from launch data: $e');
+    }
+  }
+
+  // 获取当前ApiURL
+  String getApiUrl() {
+    if (_currentIndex >= _apiUrls.length) {
+      _currentIndex = 0;
+    }
+    return _apiUrls[_currentIndex];
+  }
+
+  // 获取当前LogURL
+  String getLogUrl() {
+    if (_currentLogIndex >= _logUrls.length) {
+      _currentLogIndex = 0;
+    }
+    return _logUrls[_currentLogIndex];
+  }
+
+  // 获取当前FILE URL
+  String getFileUrl() {
+    if (_currentFileIndex >= _fileUrls.length) {
+      _currentFileIndex = 0;
+    }
+    return _fileUrls[_currentFileIndex];
+  }
+
+  Future<bool> isHardcodedUrls() async {
+    return _isUsingHardcodedUrls;
+  }
+
+  // 获取下一次要尝试的URL
+  Future<String> getNextApiUrl() async {
+    // 使用硬编码URLs的情况
+    if (_isUsingHardcodedUrls) {
+      // 如果硬编码请求次数大于等于最大重试次数,返回空字符串
+      if (_hardcodedRequestCount >= RETRY_COUNT_PER_LAUNCH) {
+        _hardcodedRequestCount = 0;
+        return '';
+      }
+      // 获取下一个索引
+      int nextIndex = _currentIndex + 1;
+      if (nextIndex >= _apiUrls.length) {
+        if (_hardcodedRequestCount < RETRY_COUNT_PER_LAUNCH) {
+          final isSuccess = await _switchToBackupList();
+          if (isSuccess) {
+            return getApiUrl();
+          } else {
+            _hardcodedRequestCount = 0;
+            return '';
+          }
+        }
+        return '';
+      }
+      final url = _apiUrls[nextIndex];
+      _currentIndex++;
+      _hardcodedRequestCount++;
+      await _saveApiUrlsIndex();
+      return url;
+    }
+    // 使用Launch数据的情况
+    else {
+      // 如果当前列表已用完
+      int nextIndex = _currentIndex + 1;
+      if (nextIndex >= _apiUrls.length) {
+        // 尝试切换到备用列表
+        final isSuccess = await _switchToBackupList();
+        if (isSuccess) {
+          return getApiUrl();
+        } else {
+          return '';
+        }
+      }
+
+      // 返回当前URL
+      final url = _apiUrls[nextIndex];
+      _currentIndex++;
+      await _saveApiUrlsIndex();
+      return url;
+    }
+  }
+
+  // 获取下一次要尝试的LogURL
+  Future<String> getNextLogUrl() async {
+    // 获取下一个索引
+    int nextIndex = _currentLogIndex + 1;
+    if (nextIndex >= _logUrls.length) {
+      _currentLogIndex = 0;
+      await _saveLogUrlsIndex();
+      return '';
+    }
+
+    // 返回所有剩余的URL
+    final url = _logUrls[nextIndex];
+    _currentLogIndex++;
+    await _saveLogUrlsIndex();
+    return url;
+  }
+
+  // 获取下一次要尝试的FILE URL
+  Future<String> getNextFileUrl() async {
+    // 获取下一个索引
+    int nextIndex = _currentFileIndex + 1;
+    if (nextIndex >= _fileUrls.length) {
+      _currentFileIndex = 0;
+      await _saveFileUrlsIndex();
+      return '';
+    }
+
+    // 返回所有剩余的URL
+    final url = _fileUrls[nextIndex];
+    _currentFileIndex++;
+    await _saveFileUrlsIndex();
+    return url;
+  }
+
+  // 切换到备用列表
+  Future<bool> _switchToBackupList() async {
+    while (_currentBackupListIndex < _backupApiUrls.length) {
+      try {
+        if (_currentBackupListIndex >= _backupApiUrls.length) {
+          log('ApiDomains', 'Error: Backup URL index out of bounds');
+          break;
+        }
+        final backupUrl = _backupApiUrls[_currentBackupListIndex];
+        log('ApiDomains', 'Trying backup URL: $backupUrl');
+
+        // 添加talker日志(避免重复添加)
+        if (!_monitorInterceptorAdded) {
+          dio.interceptors.add(SimpleNoSignApiMonitorInterceptor());
+          _monitorInterceptorAdded = true;
+        }
+        final response = await dio.get(backupUrl);
+        if (response.statusCode == 200) {
+          log('Before decryption: ${response.data}');
+          final decryptedBytes = Crypto.decryptBytes(
+            response.data.toString().trim(),
+            Keys.aesKey,
+            Keys.aesIv,
+          );
+          final jsonText = utf8.decode(decryptedBytes);
+          log('Decryption results: $jsonText');
+          final Map<String, dynamic> map = jsonDecode(jsonText);
+          if (map['apiUrls'] != null) {
+            final list = List<String>.from(map['apiUrls']);
+            _apiUrls = list;
+            _currentIndex = 0;
+            // 当前备用文件成功,标志可以尝试下一个
+            _currentBackupListIndex++;
+            await _saveApiUrls();
+            await _saveApiUrlsIndex();
+
+            log('ApiDomains', 'Switched to backup list: ${list.length} URLs');
+            return true;
+          }
+        }
+        // 当前备用文件失败,尝试下一个
+        _currentBackupListIndex++;
+      } catch (e) {
+        log('ApiDomains', 'Error switching to backup list: $e');
+        _currentBackupListIndex++;
+      }
+    }
+
+    // 如果所有备用URL都失败了,重置为硬编码模式
+    _isUsingHardcodedUrls = true;
+    _currentIndex = 0;
+    _currentBackupListIndex = 0;
+    initUrls();
+    await _saveIsHardcodedUrls();
+    await _saveApiUrls();
+    await _saveApiUrlsIndex();
+    return false;
+  }
+
+  // 获取所有logUrl
+  List<String> getAllLogUrls() {
+    return _logUrls;
+  }
+}

+ 15 - 19
lib/app/constants/assets.dart

@@ -25,33 +25,29 @@ class Assets {
   static const String youtube = 'assets/vectors/social/youtube.svg';
 
   static const String settings = 'assets/vectors/boost/settings.svg';
-  static const String switchStatusDisconnectedLight =
-      'assets/vectors/boost/switch_status_disconnected_light.svg';
-  static const String switchStatusDisconnectedDark =
-      'assets/vectors/boost/switch_status_disconnected_dark.svg';
-  static const String switchStatusConnectingLight =
-      'assets/vectors/boost/switch_status_connecting_light.svg';
-  static const String switchStatusConnectingDark =
-      'assets/vectors/boost/switch_status_connecting_dark.svg';
-  static const String switchStatusConnectedLight =
-      'assets/vectors/boost/switch_status_connected_light.svg';
-  static const String switchStatusConnectedDark =
-      'assets/vectors/boost/switch_status_connected_dark.svg';
+  static const String switchStatusDisconnected =
+      'assets/images/switch_status_disconnected.png';
+  static const String switchStatusConnected =
+      'assets/images/switch_status_connected.png';
+  static const String switchStatusConnecting =
+      'assets/images/switch_status_connecting.png';
 
   // 协议
   static const String nomoLogo = 'assets/images/nomo_logo.png';
   static const String nomo = 'assets/images/nomo.png';
-  static const String refersh = 'assets/vectors/status/refersh.svg';
+  static const String refersh = 'assets/vectors/boost/refersh.svg';
 
   // 错误页
   static const String restricted = 'assets/images/restricted.png';
   static const String oops = 'assets/images/oops.png';
 
   // 连接状态
-  static const String disconnected = 'assets/vectors/boost/disconnected.svg';
-  static const String connecting = 'assets/vectors/boost/connecting.svg';
-  static const String connected = 'assets/vectors/boost/connected.svg';
-  static const String connectingError = 'assets/vectors/boost/error.svg';
-  static const String connectionNetworkError =
-      'assets/vectors/boost/network.svg';
+  static const String disconnected = 'assets/images/disconnected.png';
+  static const String connecting = 'assets/images/connecting.png';
+  static const String connected = 'assets/images/connected.png';
+  static const String connectingError = 'assets/images/vpn_error.png';
+  static const String connectionNetworkError = 'assets/images/network.png';
+
+  static const String premium = 'assets/images/premium.png';
+  static const String free = 'assets/images/free.png';
 }

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

@@ -52,12 +52,10 @@ enum FirebaseEvent {
   register,
   login,
   logout,
-  providerLogin,
   startBoost,
   cancelBoost,
   errorBoost,
   stopBoost,
-  zipLog,
 }
 
 enum ConnectionState {
@@ -67,3 +65,23 @@ enum ConnectionState {
   disconnecting, // 断开连接中状态
   error, // 连接错误状态
 }
+
+enum MemberLevel {
+  guest(1, 'Guest'),
+  normal(2, 'Normal'),
+  vip(3, 'VIP');
+
+  final int level;
+  final String label;
+
+  const MemberLevel(this.level, this.label);
+
+  // 从 int 值获取枚举
+  static MemberLevel? fromLevel(int? level) {
+    if (level == null) return null;
+    return MemberLevel.values.firstWhere(
+      (e) => e.level == level,
+      orElse: () => MemberLevel.guest,
+    );
+  }
+}

+ 2 - 2
lib/app/constants/keys.dart

@@ -1,6 +1,6 @@
 class Keys {
   Keys._();
-  static const String aesIv = "P5AeebP0s+2uzYjlipIZDQ==";
-  static const String aesKey = "7ySutTIWypkQTBOJ2u2i3E29202F2jIh9ij9Pwmho0M=";
+  static const String aesIv = "3EvggvTpyJ8E+5z1YA0yvQ==";
+  static const String aesKey = "nZrI4r+R4gEplmoUVvdNhv0MJ4+34uuX6d9btBSJzHs=";
   static const String zipPassword = "tL9#Vw@8zNp!2QxG";
 }

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

@@ -2,20 +2,49 @@ 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:nomo/app/data/sp/ix_sp.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:play_install_referrer/play_install_referrer.dart';
 
 import '../../config/translations/localization_service.dart';
+import '../../config/translations/strings_enum.dart';
 import '../../pigeons/core_api.g.dart';
 import '../../utils/device_manager.dart';
+import '../../utils/file_cache_manager.dart';
 import '../../utils/log/logger.dart';
+import '../../utils/network_helper.dart';
+import '../api/core/api_core.dart';
+import '../components/country_restricted_overlay.dart';
+import '../components/ix_snackbar.dart';
+import '../constants/api_domains.dart';
+import '../constants/configs.dart';
+import '../constants/enums.dart';
+import '../constants/errors.dart';
 import '../constants/platforms.dart';
+import '../data/models/api_exception.dart';
+import '../data/models/failure.dart';
 import '../data/models/fingerprint.dart';
+import '../data/models/launch/groups.dart';
+import '../data/models/launch/launch.dart';
 
 class ApiController extends GetxService {
   final TAG = 'ApiController';
+
+  // 记录是否已经显示禁用弹窗
+  bool isShowDisabled = false;
+
+  //是否是游客
+  final _isGuest = false.obs;
+  bool get isGuest => _isGuest.value;
+  set isGuest(bool value) => _isGuest.value = value;
+
+  //全部节点列表
+  final _nodesList = <LocationList>[].obs;
+  List<LocationList> get nodesList => _nodesList.value;
+  set nodesList(List<LocationList> value) => _nodesList.value = value;
+
   //初始化fingerprint
   Fingerprint fp = Fingerprint.empty();
 
@@ -68,7 +97,6 @@ class ApiController extends GetxService {
       log(TAG, 'get install referrer error: $e');
     }
     fp.isNewInstall = IXSP.getIsNewInstall();
-    fp.userUuid = IXSP.getUserUuid() ?? '';
     await updateFingerprintData();
     return fp;
   }
@@ -98,4 +126,296 @@ class ApiController extends GetxService {
       log(TAG, 'read app sim error: $e');
     }
   }
+
+  Future<void> initData(Launch? launch) async {
+    // 初始化是否第一次安装
+    IXSP.setIsNewInstall(false);
+    fp.userUuid = '';
+    fp.isNewInstall = false;
+    await initLaunch(launch);
+  }
+
+  Future<void> initLaunch(Launch? launch) async {
+    try {
+      if (launch != null) {
+        // 初始化用户状态
+        isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level;
+
+        // 设置路由和节点
+        nodesList = launch.groups?.normal?.list ?? [];
+
+        // 设置资源url
+        if (launch.appConfig?.assetUrls != null &&
+            launch.appConfig!.assetUrls!.isNotEmpty) {
+          Configs.assetUrl = launch.appConfig!.assetUrls![0];
+        }
+
+        // 设置官网url
+        if (launch.appConfig?.websiteUrl != null &&
+            launch.appConfig!.websiteUrl!.isNotEmpty) {
+          Configs.websiteUrl = launch.appConfig!.websiteUrl!;
+        }
+
+        // 下载ips和domains文件
+        readIpDomain();
+      }
+    } catch (e) {
+      log(TAG, 'initLaunch error: $e');
+    }
+  }
+
+  // 发送分析事件, 后续可以发送到firebase
+  Future<void> sendAnalytics(FirebaseEvent event) async {
+    try {} catch (e) {
+      log('sendAnalytics error: $e');
+    }
+  }
+
+  Future<Launch> launch({bool isCache = false}) async {
+    sendAnalytics(isCache ? FirebaseEvent.launchCache : FirebaseEvent.launch);
+    while (true) {
+      try {
+        ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
+        final request = fp.toJson();
+        if (IXSP.getDisconnectDomains().isNotEmpty) {
+          final disconnectDomainList = IXSP
+              .getDisconnectDomains()
+              .map((e) => e.toJson())
+              .toList();
+          request['disconnectDomainList'] = disconnectDomainList;
+        }
+        final result = await ApiCore().launch(request);
+
+        if (!result.success) {
+          throw Failure(
+            code: result.errorCode ?? '',
+            message: result.errorMessage ?? '',
+          );
+        }
+        sendAnalytics(
+          isCache
+              ? FirebaseEvent.launchCacheSuccess
+              : FirebaseEvent.launchSuccess,
+        );
+        if (IXSP.getDisconnectDomains().isNotEmpty) {
+          IXSP.clearDisconnectDomains();
+        }
+        // 重置禁用状态
+        IXSP.setLastIsRegionDisabled(false);
+        IXSP.setLastIsUserDisabled(false);
+
+        final launchData = Launch.fromJson(result.data);
+
+        // 设置扩展数据
+        fp.exData = launchData.exData;
+
+        // 更新URL列表
+        await ApiDomains.instance.updateFromLaunch(launchData);
+
+        // 保存Launch数据
+        await IXSP.saveLaunch(launchData);
+
+        // 初始化Launch
+        await initData(launchData);
+
+        return launchData;
+      } on ApiException catch (_) {
+        rethrow;
+      } on Failure catch (_) {
+        rethrow;
+      } on DioException catch (e) {
+        if (e.response?.statusCode == Errors.eRegionNotAvailable ||
+            e.response?.statusCode == Errors.eUserDisabled ||
+            e.response?.statusCode == Errors.eTokenExpired) {
+          rethrow;
+        } else {
+          if (await NetworkHelper.instance.isNetworkAvailable()) {
+            final url = await ApiDomains.instance.getNextApiUrl();
+            log(TAG, 'Launch request failed for URL $url: $e');
+            if (url.isEmpty) {
+              rethrow;
+            }
+            ApiCore().setbaseUrl(url);
+          } else {
+            rethrow;
+          }
+        }
+      } catch (e) {
+        final url = await ApiDomains.instance.getNextApiUrl();
+        log(TAG, 'Launch request failed for URL $url: $e');
+        if (url.isEmpty) {
+          rethrow;
+        }
+        ApiCore().setbaseUrl(url);
+      }
+    }
+  }
+
+  Future<void> asyncHandleLaunch() async {
+    try {
+      final data = await launch(isCache: true);
+      final isVpnRunning = await CoreApi().isConnected() ?? false;
+      if (!isVpnRunning) {
+        await checkUpdate();
+        switch (MemberLevel.fromLevel(data.userConfig?.memberLevel)) {
+          case MemberLevel.guest:
+            //游客禁用,必须登录
+            if (data.appConfig?.visitorDisabled ?? true) {
+            } else {
+              //允许游客访问,如果不存在Main,则跳转Main
+            }
+            break;
+          default:
+            log('login user, default not handle');
+            break;
+        }
+      }
+    } catch (e, s) {
+      if (IXSP.getLastIsUserDisabled()) {
+        if (!isShowDisabled) {
+          Get.offAll(
+            () => CountryRestrictedOverlay(
+              type: RestrictedType.user,
+              onPressed: () async {
+                // 清除LaunchData
+                await IXSP.clearLaunchData();
+                // 清除禁用状态
+                IXSP.setLastIsUserDisabled(false);
+                // 发送事件
+              },
+            ),
+            transition: Transition.fadeIn,
+          );
+        }
+        return;
+      } else if (IXSP.getLastIsRegionDisabled()) {
+        if (!isShowDisabled) {
+          Get.offAll(
+            () => const CountryRestrictedOverlay(type: RestrictedType.region),
+            transition: Transition.fadeIn,
+          );
+        }
+        return;
+      } else if (IXSP.getLastIsDeviceDisabled()) {
+        if (!isShowDisabled) {
+          Get.offAll(
+            () => const CountryRestrictedOverlay(type: RestrictedType.device),
+            transition: Transition.fadeIn,
+          );
+        }
+        return;
+      }
+      final isVpnRunning = await CoreApi().isConnected() ?? false;
+      if (!isVpnRunning) {
+        await checkUpdate();
+      }
+      handleSnackBarError(e, s);
+    }
+  }
+
+  void handleSnackBarError(dynamic error, StackTrace stackTrace) {
+    if (error is ApiException) {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: error.message,
+      );
+    } else if (error is Failure) {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: error.message ?? Strings.unknownError.tr,
+      );
+    } else if (error is DioException) {
+      switch (error.type) {
+        case DioExceptionType.connectionError:
+        case DioExceptionType.connectionTimeout:
+        case DioExceptionType.receiveTimeout:
+        case DioExceptionType.sendTimeout:
+          IXSnackBar.showIXErrorSnackBar(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectNetwork.tr,
+          );
+          break;
+        default:
+          IXSnackBar.showIXErrorSnackBar(
+            title: Strings.error.tr,
+            message: Strings.unableToConnectServer.tr,
+          );
+      }
+    } else {
+      IXSnackBar.showIXErrorSnackBar(
+        title: Strings.error.tr,
+        message: Strings.unknownError.tr,
+      );
+    }
+  }
+
+  Future<void> readIpDomain() async {
+    try {
+      final fileCacheManager = FileCacheManager();
+      final launch = IXSP.getLaunch();
+      if (launch != null && launch.appConfig?.skipGeo != null) {
+        // 读取文件并计算MD5
+        if (launch.appConfig?.skipGeo?.ipsUrl != null &&
+            launch.appConfig!.skipGeo!.ipsUrl!.isNotEmpty) {
+          final remoteUrl =
+              '${Configs.assetUrl}/${launch.appConfig?.skipGeo?.ipsUrl}';
+          await fileCacheManager.getFileContent(
+            remoteUrl: remoteUrl,
+            fileName: 'ips.txt',
+            expectedMd5: launch.appConfig?.skipGeo?.ipsMd5 ?? '',
+            onProgress: (progress) {
+              log(
+                TAG,
+                'Downloading IPs: ${(progress * 100).toStringAsFixed(2)}%',
+              );
+            },
+          );
+        }
+        if (launch.appConfig?.skipGeo?.domainsUrl != null &&
+            launch.appConfig!.skipGeo!.domainsUrl!.isNotEmpty) {
+          final remoteUrl =
+              '${Configs.assetUrl}/${launch.appConfig?.skipGeo?.domainsUrl}';
+          await fileCacheManager.getFileContent(
+            remoteUrl: remoteUrl,
+            fileName: 'domains.txt',
+            expectedMd5: launch.appConfig?.skipGeo?.domainsMd5 ?? '',
+            onProgress: (progress) {
+              log(
+                TAG,
+                'Downloading Domains: ${(progress * 100).toStringAsFixed(2)}%',
+              );
+            },
+          );
+        }
+      }
+    } catch (e) {
+      log(TAG, 'readIpsDomain error: $e');
+    }
+  }
+
+  // 更新检查 - 智能时间控制版本
+  Future<bool> checkUpdate({bool isClickCheck = false}) async {
+    try {
+      final upgrade = IXSP.getUpgrade();
+      var hasUpdate = false;
+      var hasForceUpdate = false;
+
+      if (upgrade != null) {
+        if (upgrade.upgradeType == 1) {
+          hasUpdate = true;
+        }
+        if (upgrade.forced == true) {
+          hasForceUpdate = true;
+        }
+      }
+
+      if (hasUpdate) {
+        if (hasForceUpdate) {}
+      }
+      return hasUpdate;
+    } catch (e) {
+      log(TAG, 'checkUpdate error: $e');
+    }
+    return false;
+  }
 }

+ 149 - 59
lib/app/controllers/core_controller.dart

@@ -7,6 +7,7 @@ import '../../pigeons/core_api.g.dart';
 import '../../utils/haptic_feedback_manager.dart';
 import '../../utils/log/logger.dart';
 import '../constants/enums.dart';
+import '../data/models/vpn_message.dart';
 
 class CoreController extends GetxService {
   final TAG = 'CoreController';
@@ -15,6 +16,10 @@ class CoreController extends GetxService {
   ConnectionState get state => _state.value;
   set state(ConnectionState value) => _state.value = value;
 
+  final _timer = "00:00:00".obs;
+  String get timer => _timer.value;
+  set timer(String value) => _timer.value = value;
+
   // 事件流订阅
   StreamSubscription<String>? _eventSubscription;
 
@@ -53,7 +58,6 @@ class CoreController extends GetxService {
     } else {
       // 断开连接
       state = ConnectionState.disconnecting;
-      HapticFeedbackManager.connectionDisconnected();
       CoreApi().disconnect();
     }
   }
@@ -68,74 +72,160 @@ class CoreController extends GetxService {
     );
   }
 
-  /// 处理来自 Android 的事件变化
-  void _handleEventChange(String event) {
-    log(TAG, '收到事件: $event');
-
-    if (event.startsWith('connecting')) {
-      state = ConnectionState.connecting;
-      HapticFeedbackManager.connectionStart();
-    } else if (event.startsWith('connected')) {
-      state = ConnectionState.connected;
-      HapticFeedbackManager.connectionSuccess();
-    } else if (event.startsWith('disconnecting')) {
-      state = ConnectionState.disconnecting;
-    } else if (event.startsWith('disconnected')) {
-      state = ConnectionState.disconnected;
-      HapticFeedbackManager.connectionDisconnected();
-    } else if (event.startsWith('failed')) {
-      state = ConnectionState.disconnected;
-      HapticFeedbackManager.connectionDisconnected();
-      // 可以显示错误提示
-      Get.snackbar('连接失败', '无法建立连接,请重试');
-    } else if (event.startsWith('permission_denied')) {
-      state = ConnectionState.disconnected;
-      HapticFeedbackManager.connectionDisconnected();
-      // 可以显示错误提示
-      Get.snackbar('权限拒绝', '无法建立连接,请重试');
-    } else if (event.startsWith('stats_update:')) {
-      _handleStatsUpdate(event);
-    } else if (event.startsWith('delay_measured:')) {
-      _handleDelayUpdate(event);
-    } else if (event.startsWith('error:')) {
-      _handleError(event);
-    } else if (event.startsWith('service_status:')) {
-      _handleServiceStatus(event);
+  // 处理从原生端接收到的消息
+  void _handleEventChange(String message) {
+    try {
+      final Map<String, dynamic> json = jsonDecode(message);
+      final String type = json['type'] ?? '';
+
+      switch (type) {
+        case 'vpn_status':
+          _handleVpnStatus(VpnStatusMessage.fromJson(json));
+          break;
+        case 'timer_update':
+          _handleTimerUpdate(TimerUpdateMessage.fromJson(json));
+          break;
+        default:
+          log(TAG, '未知消息类型: $type');
+      }
+    } catch (e) {
+      log(TAG, '解析消息失败: $e');
     }
   }
 
-  /// 处理统计信息更新
-  void _handleStatsUpdate(String event) {
-    try {
-      final jsonStr = event.substring('stats_update:'.length);
-      final stats = jsonDecode(jsonStr);
-      stats['uplink'] ?? 0;
-      stats['downlink'] ?? 0;
-      // TODO: 处理统计信息更新
-    } catch (e) {
-      log(TAG, '解析统计信息失败: $e');
+  void _handleVpnStatus(VpnStatusMessage message) {
+    log(TAG, 'VPN状态变化: status=${message.status}, message=${message.message}');
+
+    // 根据状态码处理不同的VPN状态
+    switch (message.status) {
+      case 0:
+        // disconnected
+        _onVpnDisconnected();
+        break;
+      case 1:
+        // connecting
+        _onVpnConnecting();
+        break;
+      case 2:
+        // connected
+        _onVpnConnected();
+        break;
+      case 3:
+        // error
+        _onVpnError(message.message);
+        break;
+      case 4:
+        // disconnecting
+        _onVpnDisconnecting();
+        break;
+      case 403:
+        // permission denied
+        _onVpnPermissionDenied();
+        break;
+      default:
+        log(TAG, '未知VPN状态: ${message.status}');
     }
   }
 
-  /// 处理延迟更新
-  void _handleDelayUpdate(String event) {
-    try {
-      event.substring('delay_measured:'.length);
-      // TODO: 处理延迟更新
-    } catch (e) {
-      log(TAG, '解析延迟信息失败: $e');
+  void _handleTimerUpdate(TimerUpdateMessage message) {
+    log(
+      TAG,
+      '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
+    );
+
+    timer = _formatTime(message.currentTime);
+
+    // 处理计时更新
+    if (message.isRunning) {
+      if (message.isPaused) {
+        _onTimerPaused(message.currentTime, message.mode);
+      } else {
+        _onTimerRunning(message.currentTime, message.mode);
+      }
+    } else {
+      _onTimerStopped();
     }
   }
 
-  /// 处理错误信息
-  void _handleError(String event) {
-    final errorMessage = event.substring('error:'.length);
-    Get.snackbar('错误', errorMessage);
+  // VPN状态处理方法
+  void _onVpnDisconnected() {
+    log(TAG, 'VPN已断开连接');
+    // 更新UI状态
+    state = ConnectionState.disconnected;
+    timer = "00:00:00";
+    HapticFeedbackManager.connectionDisconnected();
+  }
+
+  void _onVpnConnecting() {
+    log(TAG, 'VPN正在连接');
+    // 显示连接中状态
+    state = ConnectionState.connecting;
+  }
+
+  void _onVpnConnected() {
+    log(TAG, 'VPN已连接');
+    // 显示已连接状态
+    state = ConnectionState.connected;
+    HapticFeedbackManager.connectionSuccess();
+  }
+
+  void _onVpnError(String errorMessage) {
+    log(TAG, 'VPN连接错误: $errorMessage');
+    // 显示错误信息
+    state = ConnectionState.disconnected;
+    HapticFeedbackManager.connectionDisconnected();
+    // 可以显示错误提示
+    Get.snackbar('连接失败', '无法建立连接,请重试');
   }
 
-  /// 处理服务状态
-  void _handleServiceStatus(String event) {
-    final status = event.substring('service_status:'.length);
-    log(TAG, '服务状态: $status');
+  void _onVpnDisconnecting() {
+    log(TAG, 'VPN正在断开连接');
+    // 显示断开连接状态
+    state = ConnectionState.disconnecting;
+  }
+
+  void _onVpnPermissionDenied() {
+    log(TAG, 'VPN权限拒绝');
+    // 显示权限拒绝状态
+    state = ConnectionState.disconnected;
+    HapticFeedbackManager.connectionDisconnected();
+    // 可以显示错误提示
+    Get.snackbar('权限拒绝', '无法建立连接,请重试');
+  }
+
+  // 计时器状态处理方法
+  void _onTimerRunning(int currentTime, int mode) {
+    log(
+      TAG,
+      '计时器运行中: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
+    );
+  }
+
+  void _onTimerPaused(int currentTime, int mode) {
+    log(
+      TAG,
+      '计时器已暂停: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
+    );
+  }
+
+  void _onTimerStopped() {
+    log(TAG, '计时器已停止');
+  }
+
+  // 格式化时间显示
+  String _formatTime(int timeMs) {
+    final totalSeconds = (timeMs / 1000).abs().round();
+    final days = totalSeconds ~/ 86400; // 86400 = 24 * 3600
+    final hours = (totalSeconds % 86400) ~/ 3600;
+    final minutes = (totalSeconds % 3600) ~/ 60;
+    final seconds = totalSeconds % 60;
+
+    if (days > 0) {
+      return '$days days ${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+    } else if (hours > 0) {
+      return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+    } else {
+      return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+    }
   }
 }

+ 126 - 0
lib/app/data/models/vpn_message.dart

@@ -0,0 +1,126 @@
+// VPN消息数据模型
+import 'dart:convert';
+
+class VpnStatusMessage {
+  final String type;
+  final int status;
+  final String message;
+
+  VpnStatusMessage({
+    required this.type,
+    required this.status,
+    required this.message,
+  });
+
+  factory VpnStatusMessage.fromJson(Map<String, dynamic> json) {
+    return VpnStatusMessage(
+      type: json['type'] ?? '',
+      status: json['status'] ?? 0,
+      message: json['message'] ?? '',
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {'type': type, 'status': status, 'message': message};
+  }
+}
+
+class TimerUpdateMessage {
+  final String type;
+  final int currentTime;
+  final int mode;
+  final bool isRunning;
+  final bool isPaused;
+
+  TimerUpdateMessage({
+    required this.type,
+    required this.currentTime,
+    required this.mode,
+    required this.isRunning,
+    required this.isPaused,
+  });
+
+  factory TimerUpdateMessage.fromJson(Map<String, dynamic> json) {
+    return TimerUpdateMessage(
+      type: json['type'] ?? '',
+      currentTime: json['currentTime'] ?? 0,
+      mode: json['mode'] ?? 0,
+      isRunning: json['isRunning'] ?? false,
+      isPaused: json['isPaused'] ?? false,
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'type': type,
+      'currentTime': currentTime,
+      'mode': mode,
+      'isRunning': isRunning,
+      'isPaused': isPaused,
+    };
+  }
+}
+
+class TimerNotificationMessage {
+  final String type;
+  final String message;
+
+  TimerNotificationMessage({required this.type, required this.message});
+
+  factory TimerNotificationMessage.fromJson(Map<String, dynamic> json) {
+    return TimerNotificationMessage(
+      type: json['type'] ?? '',
+      message: json['message'] ?? '',
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {'type': type, 'message': message};
+  }
+}
+
+// 通用消息处理器
+class VpnMessageHandler {
+  static void handleMessage(String jsonString) {
+    try {
+      final Map<String, dynamic> json = jsonDecode(jsonString);
+      final String type = json['type'] ?? '';
+
+      switch (type) {
+        case 'vpn_status':
+          _handleVpnStatus(VpnStatusMessage.fromJson(json));
+          break;
+        case 'timer_update':
+          _handleTimerUpdate(TimerUpdateMessage.fromJson(json));
+          break;
+        case 'timer_notification':
+          _handleTimerNotification(TimerNotificationMessage.fromJson(json));
+          break;
+        default:
+          print('未知消息类型: $type');
+      }
+    } catch (e) {
+      print('解析消息失败: $e');
+    }
+  }
+
+  static void _handleVpnStatus(VpnStatusMessage message) {
+    print('VPN状态变化: status=${message.status}, message=${message.message}');
+    // 处理VPN状态变化
+    // 例如:更新UI状态、显示通知等
+  }
+
+  static void _handleTimerUpdate(TimerUpdateMessage message) {
+    print(
+      '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
+    );
+    // 处理计时更新
+    // 例如:更新计时器显示、更新UI状态等
+  }
+
+  static void _handleTimerNotification(TimerNotificationMessage message) {
+    print('计时通知: ${message.message}');
+    // 处理计时通知
+    // 例如:显示Toast、更新通知栏等
+  }
+}

+ 26 - 12
lib/app/data/sp/ix_sp.dart

@@ -8,6 +8,7 @@ import '../../../utils/log/logger.dart';
 import '../../constants/keys.dart';
 import '../models/disconnect_domain.dart';
 import '../models/launch/launch.dart';
+import '../models/launch/upgrade.dart';
 import '../models/launch/user.dart';
 
 class IXSP {
@@ -19,14 +20,13 @@ class IXSP {
 
   // STORING KEYS
   /// Key for storing the device ID
-  static const String _deviceIdKey = 'iv_device_id';
+  static const String _deviceIdKey = 'omon_device_id';
   static const String _fcmTokenKey = 'fcm_token';
   static const String _currentLocalKey = 'current_local';
   static const String _lightThemeKey = 'is_theme_light';
   static const String _launchGame = 'is_launch_game';
   static const String _ignoreVersionKey = 'ignore_version';
   static const String _isNewInstallKey = 'is_new_install';
-  static const String _userUuidKey = 'user_uuid';
   static const String _launchDataKey = 'launch_data';
   //记录网络报错的域名信息
   static const String _disconnectDomainsKey = 'disconnect_domains';
@@ -119,13 +119,6 @@ class IXSP {
   static bool getIsNewInstall() =>
       _sharedPreferences.getBool(_isNewInstallKey) ?? true;
 
-  /// Save user uuid to shared preferences
-  static Future<void> setUserUuid(String userUuid) =>
-      _sharedPreferences.setString(_userUuidKey, userUuid);
-
-  /// Get user uuid from shared preferences
-  static String? getUserUuid() => _sharedPreferences.getString(_userUuidKey);
-
   /// 保存 DisconnectDomain 列表,合并 domain+status 相同的数据
   static Future<void> addDisconnectDomain(DisconnectDomain newItem) async {
     try {
@@ -263,7 +256,7 @@ class IXSP {
       // 保存数据
       await _sharedPreferences.setString(
         _launchDataKey,
-        Crytpo.encrypt(jsonData, Keys.aesKey, Keys.aesIv),
+        Crypto.encrypt(jsonData, Keys.aesKey, Keys.aesIv),
       );
 
       // 保存保存时间
@@ -285,7 +278,7 @@ class IXSP {
       }
 
       final Map<String, dynamic> map = jsonDecode(
-        Crytpo.decrypt(jsonData, Keys.aesKey, Keys.aesIv),
+        Crypto.decrypt(jsonData, Keys.aesKey, Keys.aesIv),
       );
       return Launch.fromJson(map);
     } catch (e) {
@@ -300,7 +293,13 @@ class IXSP {
     return launch?.userConfig;
   }
 
-   /// 保存用户信息
+  /// 获取升级信息
+  static Upgrade? getUpgrade() {
+    final launch = getLaunch();
+    return launch?.upgradeConfig;
+  }
+
+  /// 保存用户信息
   static Future<void> saveUser(User user) async {
     final launch = getLaunch();
     final newLaunch = launch!.copyWith(userConfig: user);
@@ -318,4 +317,19 @@ class IXSP {
       return false;
     }
   }
+
+  /// 通用获取字符串方法
+  static String? getString(String key) {
+    return _sharedPreferences.getString(key);
+  }
+
+  /// 通用设置字符串方法
+  static Future<bool> setString(String key, String value) async {
+    try {
+      return await _sharedPreferences.setString(key, value);
+    } catch (e) {
+      log('IXSP', 'Error setting string for key $key: $e');
+      return false;
+    }
+  }
 }

+ 28 - 56
lib/app/modules/account/views/account_view.dart

@@ -7,44 +7,42 @@ import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
+import '../../../constants/assets.dart';
+import '../../../widgets/ix_image.dart';
 import '../controllers/account_controller.dart';
 
 class AccountView extends BaseView<AccountController> {
   const AccountView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Account');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Account'),
-        Expanded(
-          child: Obx(() {
-            final isPremium = controller.isPremium.value;
-            return SingleChildScrollView(
-              padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
-              child: Column(
-                children: [
-                  // Account 信息卡片
-                  _buildAccountCard(isPremium),
+    return Obx(() {
+      final isPremium = controller.isPremium.value;
+      return SingleChildScrollView(
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
+        child: Column(
+          children: [
+            // Account 信息卡片
+            _buildAccountCard(isPremium),
 
-                  20.verticalSpaceFromWidth,
+            20.verticalSpaceFromWidth,
 
-                  // Premium 功能列表
-                  _buildPremiumFeatures(isPremium),
+            // Premium 功能列表
+            _buildPremiumFeatures(isPremium),
 
-                  30.verticalSpaceFromWidth,
+            30.verticalSpaceFromWidth,
 
-                  // 底部按钮
-                  _buildBottomButtons(isPremium),
+            // 底部按钮
+            _buildBottomButtons(isPremium),
 
-                  20.verticalSpaceFromWidth,
-                ],
-              ),
-            );
-          }),
+            20.verticalSpaceFromWidth,
+          ],
         ),
-      ],
-    );
+      );
+    });
   }
 
   /// 构建账户信息卡片
@@ -101,37 +99,11 @@ class AccountView extends BaseView<AccountController> {
             ),
           ),
           // 徽章
-          Container(
-            padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
-            decoration: BoxDecoration(
-              color: isPremium
-                  ? const Color(0xFFFF9500).withOpacity(0.2)
-                  : Get.reactiveTheme.hintColor.withOpacity(0.2),
-              borderRadius: BorderRadius.circular(12.r),
-            ),
-            child: Row(
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Icon(
-                  Icons.diamond,
-                  size: 14.w,
-                  color: isPremium
-                      ? const Color(0xFFFF9500)
-                      : Get.reactiveTheme.hintColor,
-                ),
-                SizedBox(width: 4.w),
-                Text(
-                  isPremium ? 'Premium' : 'Free',
-                  style: TextStyle(
-                    fontSize: 12.sp,
-                    color: isPremium
-                        ? const Color(0xFFFF9500)
-                        : Get.reactiveTheme.hintColor,
-                    fontWeight: FontWeight.w500,
-                  ),
-                ),
-              ],
-            ),
+          IXImage(
+            source: isPremium ? Assets.premium : Assets.free,
+            width: isPremium ? 92.w : 64.w,
+            height: 28.w,
+            sourceType: ImageSourceType.asset,
           ),
         ],
       ),

+ 19 - 23
lib/app/modules/deviceauth/views/deviceauth_view.dart

@@ -13,32 +13,28 @@ import '../controllers/deviceauth_controller.dart';
 class DeviceauthView extends BaseView<DeviceauthController> {
   const DeviceauthView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Device Authorization');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Device Authorization'),
-        Expanded(
-          child: Obx(() {
-            final isPremium = controller.isPremium.value;
-            return SingleChildScrollView(
-              padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
-              child: Column(
-                children: [
-                  if (isPremium) ...[
-                    // VIP用户界面
-                    _buildVIPUserInterface(),
-                  ] else ...[
-                    // 免费用户界面
-                    _buildFreeUserInterface(),
-                  ],
-                ],
-              ),
-            );
-          }),
+    return Obx(() {
+      final isPremium = controller.isPremium.value;
+      return SingleChildScrollView(
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
+        child: Column(
+          children: [
+            if (isPremium) ...[
+              // VIP用户界面
+              _buildVIPUserInterface(),
+            ] else ...[
+              // 免费用户界面
+              _buildFreeUserInterface(),
+            ],
+          ],
         ),
-      ],
-    );
+      );
+    });
   }
 
   /// 免费用户界面 - 显示授权码和等待授权

+ 121 - 93
lib/app/modules/feedback/views/feedback_view.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_spinkit/flutter_spinkit.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
@@ -11,126 +12,153 @@ import '../controllers/feedback_controller.dart';
 class FeedbackView extends BaseView<FeedbackController> {
   const FeedbackView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Feedback');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Feedback'),
-        Expanded(
-          child: Padding(
-            padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
-            child: Column(
-              children: [
-                // 反馈内容输入区域
-                Expanded(
-                  child: Container(
-                    decoration: BoxDecoration(
-                      color: Get.reactiveTheme.highlightColor,
-                      borderRadius: BorderRadius.circular(16.r),
-                    ),
-                    child: Padding(
-                      padding: EdgeInsets.all(20.w),
-                      child: TextField(
-                        controller: controller.feedbackController,
-                        maxLines: null,
-                        expands: true,
-                        textAlignVertical: TextAlignVertical.top,
-                        style: TextStyle(fontSize: 16.sp, color: Colors.white),
-                        decoration: InputDecoration(
-                          hintText:
-                              'An English version for reference: Describe\nyour issue or suggestion...',
-                          hintStyle: TextStyle(
-                            fontSize: 16.sp,
-                            color: Get.reactiveTheme.hintColor,
-                          ),
-                          border: InputBorder.none,
-                          contentPadding: EdgeInsets.zero,
-                        ),
-                      ),
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          // 反馈内容输入区域
+          Expanded(
+            child: Container(
+              decoration: BoxDecoration(
+                color: Get.reactiveTheme.highlightColor,
+                borderRadius: BorderRadius.circular(12.r),
+                border: Border.all(
+                  color: Get.reactiveTheme.dividerColor,
+                  width: 1.w,
+                ),
+              ),
+              child: Padding(
+                padding: EdgeInsets.all(14.w),
+                child: TextField(
+                  controller: controller.feedbackController,
+                  maxLines: null,
+                  expands: true,
+                  textAlignVertical: TextAlignVertical.top,
+                  cursorHeight: 18.w,
+                  style: TextStyle(
+                    fontSize: 16.sp,
+                    height: 1.4,
+                    color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                    fontWeight: FontWeight.w400,
+                  ),
+                  decoration: InputDecoration(
+                    hintText:
+                        'An English version for reference: Describe\nyour issue or suggestion...',
+                    hintStyle: TextStyle(
+                      fontSize: 16.sp,
+                      height: 1.4,
+                      fontWeight: FontWeight.w400,
+                      color: Get.reactiveTheme.hintColor,
                     ),
+                    border: InputBorder.none,
+                    contentPadding: EdgeInsets.zero,
                   ),
                 ),
+              ),
+            ),
+          ),
 
-                SizedBox(height: 20.h),
+          20.verticalSpaceFromWidth,
 
-                // 邮箱输入区域
-                Container(
-                  decoration: BoxDecoration(
-                    color: Get.reactiveTheme.highlightColor,
-                    borderRadius: BorderRadius.circular(16.r),
-                  ),
-                  child: Padding(
-                    padding: EdgeInsets.all(20.w),
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        TextField(
-                          controller: controller.emailController,
-                          style: TextStyle(
-                            fontSize: 16.sp,
-                            color: Colors.white,
-                          ),
-                          decoration: InputDecoration(
-                            hintText: 'Enter your email',
-                            hintStyle: TextStyle(
-                              fontSize: 16.sp,
-                              color: Get.reactiveTheme.hintColor,
-                            ),
-                            border: InputBorder.none,
-                            contentPadding: EdgeInsets.zero,
-                          ),
-                        ),
-                        SizedBox(height: 8.h),
-                        Text(
-                          '• Your email address (for our reply)',
-                          style: TextStyle(
-                            fontSize: 12.sp,
-                            color: Get.reactiveTheme.hintColor,
-                          ),
-                        ),
-                      ],
-                    ),
+          // 邮箱输入区域
+          Container(
+            height: 50.w,
+            alignment: Alignment.center,
+            decoration: BoxDecoration(
+              color: Get.reactiveTheme.highlightColor,
+              borderRadius: BorderRadius.circular(12.r),
+              border: Border.all(
+                color: Get.reactiveTheme.dividerColor,
+                width: 1.w,
+              ),
+            ),
+            child: Padding(
+              padding: EdgeInsets.symmetric(horizontal: 14.w),
+              child: TextField(
+                controller: controller.emailController,
+                maxLines: 1, // 邮箱输入通常只需要一行
+                cursorHeight: 18.w,
+                scrollPadding: EdgeInsets.zero,
+                style: TextStyle(
+                  fontSize: 16.sp,
+                  height: 1.4,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                  fontWeight: FontWeight.w400,
+                ),
+                decoration: InputDecoration(
+                  hintText: 'Enter your email',
+                  hintStyle: TextStyle(
+                    fontSize: 16.sp,
+                    height: 1.4,
+                    fontWeight: FontWeight.w400,
+                    color: Get.reactiveTheme.hintColor,
                   ),
+                  border: InputBorder.none,
+                  contentPadding: EdgeInsets.zero,
                 ),
+              ),
+            ),
+          ),
 
-                SizedBox(height: 30.h),
+          10.verticalSpaceFromWidth,
 
-                // 发送按钮
-                Obx(
+          Text(
+            '• Your email address (for our reply)',
+            style: TextStyle(
+              fontSize: 13.sp,
+              height: 1.4,
+              fontWeight: FontWeight.w400,
+              color: Get.reactiveTheme.hintColor,
+            ),
+          ),
+
+          30.verticalSpaceFromWidth,
+
+          // 发送按钮
+          Expanded(
+            child: SafeArea(
+              child: Container(
+                alignment: Alignment.bottomCenter,
+                child: Obx(
                   () => ClickOpacity(
                     onTap: controller.isSubmitting.value
                         ? null
                         : controller.submitFeedback,
                     child: Container(
                       width: double.infinity,
-                      height: 52.h,
+                      height: 50.w,
                       decoration: BoxDecoration(
                         color:
                             controller.canSubmit &&
                                 !controller.isSubmitting.value
-                            ? const Color(0xFF00A8E8)
-                            : Get.reactiveTheme.hintColor.withOpacity(0.3),
-                        borderRadius: BorderRadius.circular(26.r),
+                            ? Get.reactiveTheme.shadowColor
+                            : Get.reactiveTheme.highlightColor,
+                        borderRadius: BorderRadius.circular(12.r),
                       ),
                       child: Center(
                         child: controller.isSubmitting.value
-                            ? SizedBox(
-                                width: 20.w,
-                                height: 20.w,
-                                child: CircularProgressIndicator(
-                                  strokeWidth: 2,
-                                  valueColor: AlwaysStoppedAnimation<Color>(
-                                    Colors.white,
-                                  ),
-                                ),
+                            ? SpinKitRing(
+                                size: 20.w,
+                                lineWidth: 2.w,
+                                color: Colors.white,
                               )
                             : Text(
                                 'Send',
                                 style: TextStyle(
                                   fontSize: 16.sp,
-                                  fontWeight: FontWeight.w600,
+                                  fontWeight: FontWeight.w400,
                                   color: controller.canSubmit
-                                      ? Colors.white
+                                      ? Get
+                                            .reactiveTheme
+                                            .textTheme
+                                            .bodyLarge!
+                                            .color
                                       : Get.reactiveTheme.hintColor,
                                 ),
                               ),
@@ -138,11 +166,11 @@ class FeedbackView extends BaseView<FeedbackController> {
                     ),
                   ),
                 ),
-              ],
+              ),
             ),
           ),
-        ),
-      ],
+        ],
+      ),
     );
   }
 }

+ 10 - 0
lib/app/modules/forgotpwd/bindings/forgotpwd_binding.dart

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

+ 23 - 0
lib/app/modules/forgotpwd/controllers/forgotpwd_controller.dart

@@ -0,0 +1,23 @@
+import 'package:get/get.dart';
+
+class ForgotpwdController extends GetxController {
+  //TODO: Implement ForgotpwdController
+
+  final count = 0.obs;
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  void increment() => count.value++;
+}

+ 24 - 0
lib/app/modules/forgotpwd/views/forgotpwd_view.dart

@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+
+import 'package:get/get.dart';
+
+import '../controllers/forgotpwd_controller.dart';
+
+class ForgotpwdView extends GetView<ForgotpwdController> {
+  const ForgotpwdView({super.key});
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('ForgotpwdView'),
+        centerTitle: true,
+      ),
+      body: const Center(
+        child: Text(
+          'ForgotpwdView is working',
+          style: TextStyle(fontSize: 20),
+        ),
+      ),
+    );
+  }
+}

+ 5 - 0
lib/app/modules/home/controllers/home_controller.dart

@@ -49,6 +49,11 @@ class HomeController extends BaseController {
   final _allLocations = <Locations>[].obs;
   List<Locations> get allLocations => _allLocations;
 
+  // 是否是会员
+  final _isPremium = false.obs;
+  bool get isPremium => _isPremium.value;
+  set isPremium(bool value) => _isPremium.value = value;
+
   @override
   void onInit() {
     super.onInit();

+ 62 - 58
lib/app/modules/home/views/home_view.dart

@@ -4,9 +4,9 @@ import 'package:get/get.dart';
 import '../../../../config/theme/theme_extensions/theme_extension.dart';
 import '../../../base/base_view.dart';
 import '../../../constants/assets.dart';
-import '../../../dialog/error_dialog.dart';
 import '../../../routes/app_pages.dart';
 import '../../../widgets/click_opacity.dart';
+import '../../../widgets/ix_image.dart';
 import '../widgets/connection_button.dart';
 import '../controllers/home_controller.dart';
 import 'package:flutter_svg/flutter_svg.dart';
@@ -16,9 +16,6 @@ import '../widgets/menu_list.dart';
 class HomeView extends BaseView<HomeController> {
   const HomeView({super.key});
 
-  @override
-  bool get safeArea => true;
-
   @override
   bool get isPopScope => true;
 
@@ -32,8 +29,16 @@ class HomeView extends BaseView<HomeController> {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             Row(
-              mainAxisAlignment: MainAxisAlignment.end,
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
+                Obx(
+                  () => IXImage(
+                    source: controller.isPremium ? Assets.premium : Assets.free,
+                    width: controller.isPremium ? 92.w : 64.w,
+                    height: 28.w,
+                    sourceType: ImageSourceType.asset,
+                  ),
+                ),
                 ClickOpacity(
                   child: Padding(
                     padding: EdgeInsets.all(10.w),
@@ -49,7 +54,7 @@ class HomeView extends BaseView<HomeController> {
                 ),
               ],
             ),
-            50.verticalSpaceFromWidth,
+            80.verticalSpaceFromWidth,
             Text(
               "Active time",
               style: TextStyle(
@@ -59,12 +64,14 @@ class HomeView extends BaseView<HomeController> {
               ),
             ),
             2.verticalSpaceFromWidth,
-            Text(
-              "10:00:00",
-              style: TextStyle(
-                fontSize: 28.sp,
-                height: 1.5,
-                color: Get.reactiveTheme.primaryColor,
+            Obx(
+              () => Text(
+                controller.coreController.timer,
+                style: TextStyle(
+                  fontSize: 28.sp,
+                  height: 1.5,
+                  color: Get.reactiveTheme.primaryColor,
+                ),
               ),
             ),
 
@@ -151,20 +158,20 @@ class HomeView extends BaseView<HomeController> {
                   width: 32.w,
                   height: 24.w,
                   fit: BoxFit.cover,
-                  placeholderBuilder: (context) => Container(
-                    width: 32.w,
-                    height: 24.w,
-                    decoration: BoxDecoration(
-                      borderRadius: BorderRadius.circular(4.r),
-                      color: Colors.grey[200],
-                    ),
-                    alignment: Alignment.center,
-                    child: Icon(
-                      Icons.flag,
-                      size: 16.w,
-                      color: Colors.grey[400],
-                    ),
-                  ),
+                  // placeholderBuilder: (context) => Container(
+                  //   width: 32.w,
+                  //   height: 24.w,
+                  //   decoration: BoxDecoration(
+                  //     borderRadius: BorderRadius.circular(4.r),
+                  //     color: Colors.grey[200],
+                  //   ),
+                  //   alignment: Alignment.center,
+                  //   child: Icon(
+                  //     Icons.flag,
+                  //     size: 16.w,
+                  //     color: Colors.grey[400],
+                  //   ),
+                  // ),
                 ),
               ),
               10.horizontalSpace,
@@ -206,7 +213,8 @@ class HomeView extends BaseView<HomeController> {
         ),
         child: Column(
           children: [
-            ClickOpacity(
+            GestureDetector(
+              behavior: HitTestBehavior.opaque,
               onTap: () {
                 controller.isRecentLocationsExpanded =
                     !controller.isRecentLocationsExpanded;
@@ -250,12 +258,8 @@ class HomeView extends BaseView<HomeController> {
                                   decoration: BoxDecoration(
                                     borderRadius: BorderRadius.circular(5.r),
                                     border: Border.all(
-                                      color: Get
-                                          .reactiveTheme
-                                          .textTheme
-                                          .bodyLarge!
-                                          .color!,
-                                      width: 0.7,
+                                      color: Get.reactiveTheme.canvasColor,
+                                      width: 0.4,
                                     ),
                                   ),
                                   child: ClipRRect(
@@ -267,22 +271,22 @@ class HomeView extends BaseView<HomeController> {
                                       width: 24.w,
                                       height: 16.w,
                                       fit: BoxFit.cover,
-                                      placeholderBuilder: (context) =>
-                                          Container(
-                                            width: 24.w,
-                                            height: 16.w,
-                                            decoration: BoxDecoration(
-                                              borderRadius:
-                                                  BorderRadius.circular(4.r),
-                                              color: Colors.grey[200],
-                                            ),
-                                            alignment: Alignment.center,
-                                            child: Icon(
-                                              Icons.flag,
-                                              size: 10.w,
-                                              color: Colors.grey[400],
-                                            ),
-                                          ),
+                                      //   placeholderBuilder: (context) =>
+                                      //       Container(
+                                      //         width: 24.w,
+                                      //         height: 16.w,
+                                      //         decoration: BoxDecoration(
+                                      //           borderRadius:
+                                      //               BorderRadius.circular(4.r),
+                                      //           color: Colors.grey[200],
+                                      //         ),
+                                      //         alignment: Alignment.center,
+                                      //         child: Icon(
+                                      //           Icons.flag,
+                                      //           size: 10.w,
+                                      //           color: Colors.grey[400],
+                                      //         ),
+                                      //       ),
                                     ),
                                   ),
                                 );
@@ -352,15 +356,15 @@ class HomeView extends BaseView<HomeController> {
                                         width: 28.w,
                                         height: 21.w,
                                         fit: BoxFit.cover,
-                                        placeholderBuilder: (context) =>
-                                            Container(
-                                              color: Colors.grey[200],
-                                              child: Icon(
-                                                Icons.flag,
-                                                size: 12.w,
-                                                color: Colors.grey[400],
-                                              ),
-                                            ),
+                                        // placeholderBuilder: (context) =>
+                                        //     Container(
+                                        //       color: Colors.grey[200],
+                                        //       child: Icon(
+                                        //         Icons.flag,
+                                        //         size: 12.w,
+                                        //         color: Colors.grey[400],
+                                        //       ),
+                                        //     ),
                                       ),
                                     ),
                                     SizedBox(width: 10.w),

+ 71 - 16
lib/app/modules/home/widgets/connection_button.dart

@@ -1,8 +1,8 @@
 import 'package:flutter/material.dart' hide ConnectionState;
 import 'package:flutter_screenutil/flutter_screenutil.dart';
-import 'package:flutter_svg/flutter_svg.dart';
 import 'dart:math' as math;
 import 'package:get/get.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
 
 import '../../../constants/assets.dart';
 import '../../../../config/theme/theme_extensions/theme_extension.dart';
@@ -22,6 +22,23 @@ class _ConnectionButtonState extends State<ConnectionButton>
     with TickerProviderStateMixin {
   bool _isAtTop = false; // 控制按钮位置,false=底部,true=顶部
   List<Color>? _previousGradientColor; // 保存前一个渐变色用于动画过渡
+  late AnimationController _rotationController; // 旋转动画控制器
+
+  @override
+  void initState() {
+    super.initState();
+    // 初始化旋转动画控制器
+    _rotationController = AnimationController(
+      duration: const Duration(seconds: 2),
+      vsync: this,
+    );
+  }
+
+  @override
+  void dispose() {
+    _rotationController.dispose();
+    super.dispose();
+  }
 
   void _onTap() {
     // 切换位置
@@ -43,6 +60,43 @@ class _ConnectionButtonState extends State<ConnectionButton>
     return true;
   }
 
+  Widget _buildImageWithAnimation(String imgPath) {
+    // 如果是connecting状态,添加旋转动画
+    if (widget.state == ConnectionState.connecting) {
+      // 启动旋转动画
+      if (!_rotationController.isAnimating) {
+        _rotationController.repeat();
+      }
+
+      return AnimatedBuilder(
+        animation: _rotationController,
+        builder: (context, child) {
+          return Transform.rotate(
+            angle: _rotationController.value * 2 * math.pi,
+            child: IXImage(
+              source: imgPath,
+              sourceType: ImageSourceType.asset,
+              width: 30.w,
+              height: 30.w,
+            ),
+          );
+        },
+      );
+    } else {
+      // 其他状态停止旋转动画
+      if (_rotationController.isAnimating) {
+        _rotationController.stop();
+      }
+
+      return IXImage(
+        source: imgPath,
+        sourceType: ImageSourceType.asset,
+        width: 30.w,
+        height: 30.w,
+      );
+    }
+  }
+
   @override
   Widget build(BuildContext context) {
     return GestureDetector(onTap: _onTap, child: _buildMainButton());
@@ -51,7 +105,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
   Widget _buildMainButton() {
     // 根据状态和主题获取颜色
     List<Color> gradientColor;
-    String svgPath;
+    String imgPath;
+    String statusImgPath;
     String text;
     Color textColor;
 
@@ -61,7 +116,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
           Get.reactiveTheme.highlightColor,
           Get.reactiveTheme.hintColor,
         ];
-        svgPath = Assets.switchStatusDisconnectedLight;
+        imgPath = Assets.switchStatusDisconnected;
+        statusImgPath = Assets.disconnected;
         text = "Disconnected";
         textColor = Get.reactiveTheme.hintColor;
         if (_isAtTop) {
@@ -73,7 +129,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
           Get.reactiveTheme.highlightColor,
           Get.reactiveTheme.hintColor,
         ];
-        svgPath = Assets.switchStatusConnectingLight;
+        imgPath = Assets.switchStatusConnecting;
+        statusImgPath = Assets.connecting;
         text = "Connecting";
         textColor = Get.reactiveTheme.hintColor;
         break;
@@ -82,7 +139,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
           Get.reactiveTheme.shadowColor,
           Get.reactiveTheme.primaryColor,
         ];
-        svgPath = Assets.switchStatusConnectedLight;
+        imgPath = Assets.switchStatusConnected;
+        statusImgPath = Assets.connected;
         text = "Connected";
         textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
         if (!_isAtTop) {
@@ -94,7 +152,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
           Get.reactiveTheme.highlightColor,
           Get.reactiveTheme.hintColor,
         ];
-        svgPath = Assets.switchStatusConnectingLight;
+        imgPath = Assets.switchStatusDisconnected;
+        statusImgPath = Assets.disconnected;
         text = "Disconnecting";
         textColor = Get.reactiveTheme.hintColor;
         break;
@@ -103,7 +162,8 @@ class _ConnectionButtonState extends State<ConnectionButton>
           Get.reactiveTheme.highlightColor,
           Get.reactiveTheme.hintColor,
         ];
-        svgPath = Assets.switchStatusDisconnectedLight;
+        imgPath = Assets.switchStatusDisconnected;
+        statusImgPath = Assets.error;
         text = "Error";
         textColor = Get.reactiveTheme.hintColor;
         break;
@@ -172,13 +232,7 @@ class _ConnectionButtonState extends State<ConnectionButton>
                     ),
                   );
                 },
-                child: SvgPicture.asset(
-                  svgPath,
-                  key: ValueKey(svgPath),
-                  alignment: Alignment.center,
-                  width: 30.w,
-                  height: 30.w,
-                ),
+                child: _buildImageWithAnimation(imgPath),
               ),
             ),
           ),
@@ -187,8 +241,9 @@ class _ConnectionButtonState extends State<ConnectionButton>
         Row(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
-            SvgPicture.asset(
-              Assets.switchStatusDisconnectedLight,
+            IXImage(
+              source: statusImgPath,
+              sourceType: ImageSourceType.asset,
               width: 14.w,
               height: 14.w,
             ),

+ 46 - 56
lib/app/modules/language/views/language_view.dart

@@ -11,48 +11,43 @@ import '../controllers/language_controller.dart';
 class LanguageView extends BaseView<LanguageController> {
   const LanguageView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Language');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Language'),
-        Expanded(
-          child: SingleChildScrollView(
-            child: Padding(
-              padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
-              child: Container(
-                decoration: BoxDecoration(
-                  color: Get.reactiveTheme.highlightColor,
-                  borderRadius: BorderRadius.circular(16.r),
-                ),
-                child: Obx(
-                  () => Column(
-                    children: controller.languages.asMap().entries.map((entry) {
-                      final index = entry.key;
-                      final language = entry.value;
-                      final isSelected =
-                          controller.selectedLanguage.value == language.code;
+    return SingleChildScrollView(
+      child: Padding(
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
+        child: Container(
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.highlightColor,
+            borderRadius: BorderRadius.circular(12.r),
+          ),
+          child: Obx(
+            () => Column(
+              children: controller.languages.asMap().entries.map((entry) {
+                final index = entry.key;
+                final language = entry.value;
+                final isSelected =
+                    controller.selectedLanguage.value == language.code;
 
-                      return Column(
-                        children: [
-                          _buildLanguageOption(
-                            language: language,
-                            isSelected: isSelected,
-                            onTap: () =>
-                                controller.selectLanguage(language.code),
-                          ),
-                          if (index < controller.languages.length - 1)
-                            _buildDivider(),
-                        ],
-                      );
-                    }).toList(),
-                  ),
-                ),
-              ),
+                return Column(
+                  children: [
+                    _buildLanguageOption(
+                      language: language,
+                      isSelected: isSelected,
+                      onTap: () => controller.selectLanguage(language.code),
+                    ),
+                    if (index < controller.languages.length - 1)
+                      _buildDivider(),
+                  ],
+                );
+              }).toList(),
             ),
           ),
         ),
-      ],
+      ),
     );
   }
 
@@ -65,7 +60,7 @@ class LanguageView extends BaseView<LanguageController> {
     return ClickOpacity(
       onTap: onTap,
       child: Padding(
-        padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
+        padding: EdgeInsets.all(14.w),
         child: Row(
           children: [
             // 语言信息
@@ -76,18 +71,20 @@ class LanguageView extends BaseView<LanguageController> {
                   Text(
                     language.name,
                     style: TextStyle(
-                      fontSize: 16.sp,
-                      fontWeight: FontWeight.w500,
+                      fontSize: 14.sp,
+                      height: 1.4,
+                      fontWeight: FontWeight.w600,
                       color: isSelected
-                          ? const Color(0xFF00A8E8)
-                          : Colors.white,
+                          ? Get.reactiveTheme.shadowColor
+                          : Get.reactiveTheme.textTheme.bodyLarge!.color,
                     ),
                   ),
-                  SizedBox(height: 4.h),
+
                   Text(
                     language.nativeName,
                     style: TextStyle(
-                      fontSize: 14.sp,
+                      fontSize: 12.sp,
+                      height: 1.6,
                       color: Get.reactiveTheme.hintColor,
                     ),
                   ),
@@ -97,17 +94,17 @@ class LanguageView extends BaseView<LanguageController> {
 
             // 选中指示器
             Container(
-              width: 24.w,
-              height: 24.w,
+              width: 20.w,
+              height: 20.w,
               decoration: BoxDecoration(
                 shape: BoxShape.circle,
                 color: isSelected
-                    ? const Color(0xFF00A8E8)
+                    ? Get.reactiveTheme.shadowColor
                     : Colors.transparent,
                 border: Border.all(
                   color: isSelected
-                      ? const Color(0xFF00A8E8)
-                      : Colors.white.withOpacity(0.3),
+                      ? Get.reactiveTheme.shadowColor
+                      : Colors.grey[400]!,
                   width: 2.w,
                 ),
               ),
@@ -123,13 +120,6 @@ class LanguageView extends BaseView<LanguageController> {
 
   /// 构建分割线
   Widget _buildDivider() {
-    return Padding(
-      padding: EdgeInsets.symmetric(horizontal: 20.w),
-      child: Divider(
-        color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-        height: 1.h,
-        thickness: 1.h,
-      ),
-    );
+    return Divider(color: Get.reactiveTheme.dividerColor, height: 1.w);
   }
 }

+ 4 - 9
lib/app/modules/markdown/views/markdown_view.dart

@@ -12,17 +12,12 @@ import '../controllers/markdown_controller.dart';
 class MarkdownView extends BaseView<MarkdownController> {
   const MarkdownView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: controller.title);
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(
-          title: controller.title,
-          // 不需要传递颜色参数,会自动使用响应式主题
-        ),
-        Expanded(child: _buildMarkdownContent()),
-      ],
-    );
+    return _buildMarkdownContent();
   }
 
   Widget _buildMarkdownContent() {

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

@@ -10,6 +10,9 @@ import '../widgets/node_list.dart';
 class NodeView extends BaseView<NodeController> {
   const NodeView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Select Server');
+
   @override
   Widget buildContent(BuildContext context) {
     final tabCount = controller.tabTextList.length;
@@ -21,7 +24,6 @@ class NodeView extends BaseView<NodeController> {
       initialIndex: controller.currentTabIndex, // 恢复上次的 Tab 位置
       child: Column(
         children: [
-          IXAppBar(title: 'Select Server'),
           _buildTabs(),
           Divider(color: Get.reactiveTheme.dividerColor, height: 0.5),
           Expanded(child: _buildTabBarView()),

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

@@ -206,20 +206,20 @@ class _CountrySection extends StatelessWidget {
                       width: 32.w,
                       height: 24.w,
                       fit: BoxFit.cover,
-                      placeholderBuilder: (context) => Container(
-                        width: 32.w,
-                        height: 24.w,
-                        decoration: BoxDecoration(
-                          borderRadius: BorderRadius.circular(4.r),
-                          color: Colors.grey[200],
-                        ),
-                        alignment: Alignment.center,
-                        child: Icon(
-                          Icons.flag,
-                          size: 16.w,
-                          color: Colors.grey[400],
-                        ),
-                      ),
+                      // placeholderBuilder: (context) => Container(
+                      //   width: 32.w,
+                      //   height: 24.w,
+                      //   decoration: BoxDecoration(
+                      //     borderRadius: BorderRadius.circular(4.r),
+                      //     color: Colors.grey[200],
+                      //   ),
+                      //   alignment: Alignment.center,
+                      //   child: Icon(
+                      //     Icons.flag,
+                      //     size: 16.w,
+                      //     color: Colors.grey[400],
+                      //   ),
+                      // ),
                     ),
                   ),
                   10.horizontalSpace,

+ 51 - 62
lib/app/modules/routingmode/views/routingmode_view.dart

@@ -11,49 +11,45 @@ import '../controllers/routingmode_controller.dart';
 class RoutingmodeView extends BaseView<RoutingmodeController> {
   const RoutingmodeView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Routing Mode');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Routing Mode'),
-        Padding(
-          padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
-          child: Container(
-            decoration: BoxDecoration(
-              color: Get.reactiveTheme.highlightColor,
-              borderRadius: BorderRadius.circular(16.r),
-            ),
-            child: Obx(
-              () => Column(
-                mainAxisSize: MainAxisSize.min,
-                children: [
-                  _buildRoutingOption(
-                    mode: RoutingMode.smart,
-                    icon: Icons.flash_on,
-                    title: 'Smart',
-                    description:
-                        'The local and VPN networks coexist, and the optimal route is selected intelligently.',
-                    isSelected:
-                        controller.selectedMode.value == RoutingMode.smart,
-                    onTap: () => controller.selectMode(RoutingMode.smart),
-                  ),
-                  _buildDivider(),
-                  _buildRoutingOption(
-                    mode: RoutingMode.global,
-                    icon: Icons.language,
-                    title: 'Global',
-                    description:
-                        'All traffic is routed through the VPN server to ensure maximum privacy and security.',
-                    isSelected:
-                        controller.selectedMode.value == RoutingMode.global,
-                    onTap: () => controller.selectMode(RoutingMode.global),
-                  ),
-                ],
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
+      child: Container(
+        decoration: BoxDecoration(
+          color: Get.reactiveTheme.highlightColor,
+          borderRadius: BorderRadius.circular(12.r),
+        ),
+        child: Obx(
+          () => Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              _buildRoutingOption(
+                mode: RoutingMode.smart,
+                icon: Icons.flash_on,
+                title: 'Smart',
+                description:
+                    'The local and VPN networks coexist, and the optimal route is selected intelligently.',
+                isSelected: controller.selectedMode.value == RoutingMode.smart,
+                onTap: () => controller.selectMode(RoutingMode.smart),
               ),
-            ),
+              _buildDivider(),
+              _buildRoutingOption(
+                mode: RoutingMode.global,
+                icon: Icons.language,
+                title: 'Global',
+                description:
+                    'All traffic is routed through the VPN server to ensure maximum privacy and security.',
+                isSelected: controller.selectedMode.value == RoutingMode.global,
+                onTap: () => controller.selectMode(RoutingMode.global),
+              ),
+            ],
           ),
         ),
-      ],
+      ),
     );
   }
 
@@ -71,19 +67,20 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
       child: Padding(
         padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
         child: Row(
+          crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             // 图标
             Container(
-              width: 40.w,
-              height: 40.w,
+              width: 30.w,
+              height: 30.w,
               decoration: BoxDecoration(
-                color: const Color(0xFF00A8E8),
-                borderRadius: BorderRadius.circular(12.r),
+                color: Get.reactiveTheme.shadowColor,
+                borderRadius: BorderRadius.circular(8.r),
               ),
               child: Icon(icon, color: Colors.white, size: 20.w),
             ),
 
-            SizedBox(width: 12.w),
+            10.horizontalSpace,
 
             // 标题和描述
             Expanded(
@@ -93,39 +90,38 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
                   Text(
                     title,
                     style: TextStyle(
-                      fontSize: 16.sp,
-                      fontWeight: FontWeight.w500,
-                      color: Colors.white,
+                      fontSize: 14.sp,
+                      height: 1.4,
+                      color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                     ),
                   ),
-                  SizedBox(height: 4.h),
                   Text(
                     description,
                     style: TextStyle(
                       fontSize: 12.sp,
                       color: Get.reactiveTheme.hintColor,
-                      height: 1.4,
+                      height: 1.6,
                     ),
                   ),
                 ],
               ),
             ),
 
-            SizedBox(width: 12.w),
+            10.horizontalSpace,
 
             // 选中指示器
             Container(
-              width: 24.w,
-              height: 24.w,
+              width: 20.w,
+              height: 20.w,
               decoration: BoxDecoration(
                 shape: BoxShape.circle,
                 color: isSelected
-                    ? const Color(0xFF00A8E8)
+                    ? Get.reactiveTheme.shadowColor
                     : Colors.transparent,
                 border: Border.all(
                   color: isSelected
-                      ? const Color(0xFF00A8E8)
-                      : Get.reactiveTheme.hintColor.withOpacity(0.5),
+                      ? Get.reactiveTheme.shadowColor
+                      : Colors.grey[400]!,
                   width: 2.w,
                 ),
               ),
@@ -141,13 +137,6 @@ class RoutingmodeView extends BaseView<RoutingmodeController> {
 
   /// 构建分割线
   Widget _buildDivider() {
-    return Padding(
-      padding: EdgeInsets.symmetric(horizontal: 20.w),
-      child: Divider(
-        color: Get.reactiveTheme.hintColor.withOpacity(0.1),
-        height: 1.h,
-        thickness: 1.h,
-      ),
-    );
+    return Divider(color: Get.reactiveTheme.dividerColor, height: 1.w);
   }
 }

+ 32 - 64
lib/app/modules/setting/views/setting_view.dart

@@ -1,10 +1,13 @@
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
+import 'package:nomo/app/constants/assets.dart';
 import 'package:nomo/app/dialog/all_dialog.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../../../../utils/system_helper.dart';
@@ -15,35 +18,31 @@ import '../controllers/setting_controller.dart';
 class SettingView extends BaseView<SettingController> {
   const SettingView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Settings');
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(title: 'Settings'),
-        Expanded(
-          child: CustomScrollView(
-            slivers: [
-              // Account Section
-              _buildSectionHeader('Account'),
-              _buildAccountSection(),
+    return CustomScrollView(
+      slivers: [
+        // Account Section
+        _buildSectionHeader('Account'),
+        _buildAccountSection(),
 
-              // Network Section
-              _buildSectionHeader('Network'),
-              _buildNetworkSection(),
+        // Network Section
+        _buildSectionHeader('Network'),
+        _buildNetworkSection(),
 
-              // APP Section
-              _buildSectionHeader('APP'),
-              _buildAppSection(),
+        // APP Section
+        _buildSectionHeader('APP'),
+        _buildAppSection(),
 
-              // Security Section
-              _buildSectionHeader('Security'),
-              _buildSecuritySection(),
+        // Security Section
+        _buildSectionHeader('Security'),
+        _buildSecuritySection(),
 
-              // 底部间距
-              SliverToBoxAdapter(child: 20.verticalSpaceFromWidth),
-            ],
-          ),
-        ),
+        // 底部间距
+        SliverToBoxAdapter(child: 20.verticalSpaceFromWidth),
       ],
     );
   }
@@ -82,9 +81,11 @@ class SettingView extends BaseView<SettingController> {
                 icon: Icons.account_circle,
                 iconColor: const Color(0xFF00A8E8),
                 title: 'Account',
-                trailing: _buildBadge(
-                  isPremium ? 'Premium' : 'Free',
-                  isPremium: isPremium,
+                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);
@@ -260,12 +261,15 @@ class SettingView extends BaseView<SettingController> {
               iconColor: const Color(0xFF00A8E8),
               title: 'Auto Reconnect',
               trailing: Obx(
-                () => Switch(
+                () => CupertinoSwitch(
                   value: controller.autoReconnect.value,
                   onChanged: (value) {
                     controller.autoReconnect.value = value;
                   },
-                  activeColor: const Color(0xFF00D9FF),
+                  activeTrackColor: Get.reactiveTheme.shadowColor,
+                  thumbColor: Colors.white,
+                  inactiveThumbColor: Colors.white,
+                  inactiveTrackColor: Colors.grey,
                 ),
               ),
             ),
@@ -485,40 +489,4 @@ class SettingView extends BaseView<SettingController> {
       ),
     );
   }
-
-  /// 构建徽章
-  Widget _buildBadge(String text, {bool isPremium = false}) {
-    return Container(
-      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.w),
-      decoration: BoxDecoration(
-        color: isPremium
-            ? const Color(0xFFFF9500).withOpacity(0.2)
-            : Get.reactiveTheme.hintColor.withOpacity(0.2),
-        borderRadius: BorderRadius.circular(12.r),
-      ),
-      child: Row(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          Icon(
-            isPremium ? Icons.workspace_premium : Icons.diamond,
-            size: 14.w,
-            color: isPremium
-                ? const Color(0xFFFF9500)
-                : Get.reactiveTheme.hintColor,
-          ),
-          SizedBox(width: 4.w),
-          Text(
-            text,
-            style: TextStyle(
-              fontSize: 12.sp,
-              color: isPremium
-                  ? const Color(0xFFFF9500)
-                  : Get.reactiveTheme.hintColor,
-              fontWeight: FontWeight.w500,
-            ),
-          ),
-        ],
-      ),
-    );
-  }
 }

+ 10 - 0
lib/app/modules/signin/bindings/signin_binding.dart

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

+ 23 - 0
lib/app/modules/signin/controllers/signin_controller.dart

@@ -0,0 +1,23 @@
+import 'package:get/get.dart';
+
+class SigninController extends GetxController {
+  //TODO: Implement SigninController
+
+  final count = 0.obs;
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  void increment() => count.value++;
+}

+ 24 - 0
lib/app/modules/signin/views/signin_view.dart

@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+
+import 'package:get/get.dart';
+
+import '../controllers/signin_controller.dart';
+
+class SigninView extends GetView<SigninController> {
+  const SigninView({super.key});
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('SigninView'),
+        centerTitle: true,
+      ),
+      body: const Center(
+        child: Text(
+          'SigninView is working',
+          style: TextStyle(fontSize: 20),
+        ),
+      ),
+    );
+  }
+}

+ 10 - 0
lib/app/modules/signup/bindings/signup_binding.dart

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

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

@@ -0,0 +1,23 @@
+import 'package:get/get.dart';
+
+class SignupController extends GetxController {
+  //TODO: Implement SignupController
+
+  final count = 0.obs;
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  void increment() => count.value++;
+}

+ 24 - 0
lib/app/modules/signup/views/signup_view.dart

@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+
+import 'package:get/get.dart';
+
+import '../controllers/signup_controller.dart';
+
+class SignupView extends GetView<SignupController> {
+  const SignupView({super.key});
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('SignupView'),
+        centerTitle: true,
+      ),
+      body: const Center(
+        child: Text(
+          'SignupView is working',
+          style: TextStyle(fontSize: 20),
+        ),
+      ),
+    );
+  }
+}

+ 127 - 12
lib/app/modules/splash/controllers/splash_controller.dart

@@ -1,16 +1,26 @@
+import 'package:flutter/material.dart';
 import 'package:get/get.dart';
-import 'package:nomo/app/components/protocol_overlay.dart';
+import 'package:nomo/app/base/base_controller.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 
+import '../../../../utils/log/logger.dart';
 import '../../../components/country_restricted_overlay.dart';
+import '../../../constants/enums.dart';
+import '../../../controllers/api_controller.dart';
+import '../../../data/models/launch/launch.dart';
+import '../../../data/sp/ix_sp.dart';
+import '../../../routes/app_pages.dart';
+
+class SplashController extends BaseController {
+  static const String TAG = 'SplashController';
+  final _apiController = Get.find<ApiController>();
 
-class SplashController extends GetxController {
   final _showLoading = false.obs;
 
   bool get showLoading => _showLoading.value;
   set showLoading(bool value) => _showLoading.value = value;
 
-  final _hasLogin = false.obs;
+  final _hasLogin = true.obs;
 
   bool get hasLogin => _hasLogin.value;
   set hasLogin(bool value) => _hasLogin.value = value;
@@ -24,15 +34,18 @@ class SplashController extends GetxController {
   void onInit() {
     super.onInit();
     getVersionInfo();
-    Future.delayed(const Duration(seconds: 2), () {
-      // Get.offAllNamed(Routes.HOME);
-      // Get.to(
-      //   () => CountryRestrictedOverlay(type: RestrictedType.network),
-      //   transition: Transition.fadeIn,
-      // );
-
-      Get.to(() => const ProtocolOverlay(), transition: Transition.fadeIn);
-    });
+    initApiInfo();
+    // Get.offAllNamed(Routes.HOME);
+    // Get.to(
+    //   () => CountryRestrictedOverlay(type: RestrictedType.network),
+    //   transition: Transition.fadeIn,
+    // );
+
+    // Get.to(
+    //   () => const ProtocolOverlay(),
+    //   transition: Transition.native,
+    //   curve: Curves.easeInOut,
+    // );
   }
 
   void getVersionInfo() async {
@@ -42,4 +55,106 @@ class SplashController extends GetxController {
       (value) => value.version,
     );
   }
+
+  Future<void> initApiInfo() async {
+    try {
+      await _apiController.initFingerprint();
+      final launch = IXSP.getLaunch();
+      if (launch != null) {
+        _apiController.fp.exData = launch.exData;
+        _apiController.initLaunch(launch);
+        handleLaunch(launch, isFirstRequest: false);
+        _apiController.asyncHandleLaunch();
+      } else {
+        showLoading = true;
+        try {
+          final launch = await _apiController.launch();
+          handleLaunch(launch);
+        } catch (e, st) {
+          handleLaunchError(e, st);
+        } finally {
+          showLoading = false;
+        }
+      }
+    } catch (e, s) {
+      handleLaunchError(e, s);
+    }
+  }
+
+  void handleLaunch(Launch launch, {bool isFirstRequest = true}) {
+    if (launch.userConfig != null && launch.appConfig != null) {
+      final user = launch.userConfig!;
+      final appConfig = launch.appConfig!;
+      switch (MemberLevel.fromLevel(user.memberLevel)) {
+        case MemberLevel.guest:
+          //游客禁用,必须登录
+          if (appConfig.visitorDisabled ?? true) {
+            // Get.offAllNamed(Routes.HOME);
+          } else {
+            //允许游客访问,直接进入主页
+            Get.offAllNamed(Routes.HOME);
+          }
+          break;
+        default:
+          //登录用户,直接进入主页
+          Get.offAllNamed(Routes.HOME);
+          break;
+      }
+    }
+  }
+
+  Future<void> handleLaunchError(dynamic e, StackTrace s) async {
+    if (IXSP.getLastIsUserDisabled()) {
+      if (!_apiController.isShowDisabled) {
+        Get.offAll(
+          () => CountryRestrictedOverlay(
+            type: RestrictedType.user,
+            onPressed: () async {
+              // 清除LaunchData
+              await IXSP.clearLaunchData();
+              // 清除禁用状态
+              IXSP.setLastIsUserDisabled(false);
+              // 发送事件
+            },
+          ),
+          transition: Transition.fadeIn,
+        );
+      }
+      return;
+    } else if (IXSP.getLastIsRegionDisabled()) {
+      if (!_apiController.isShowDisabled) {
+        Get.offAll(
+          () => const CountryRestrictedOverlay(type: RestrictedType.region),
+          transition: Transition.fadeIn,
+        );
+      }
+      return;
+    } else if (IXSP.getLastIsDeviceDisabled()) {
+      if (!_apiController.isShowDisabled) {
+        Get.offAll(
+          () => const CountryRestrictedOverlay(type: RestrictedType.device),
+          transition: Transition.fadeIn,
+        );
+      }
+      return;
+    }
+    handleSnackBarError(e, s);
+    log(TAG, 'initApiInfo error: $e');
+    final launch = IXSP.getLaunch();
+    if (launch != null) {
+      _apiController.initLaunch(launch);
+      handleLaunch(launch);
+    } else {
+      Get.dialog(
+        CountryRestrictedOverlay(
+          type: RestrictedType.network,
+          onPressed: () async {
+            Navigator.pop(Get.context!);
+            initApiInfo();
+          },
+        ),
+        barrierDismissible: false,
+      );
+    }
+  }
 }

+ 34 - 40
lib/app/modules/splash/views/splash_view.dart

@@ -14,9 +14,6 @@ import '../controllers/splash_controller.dart';
 class SplashView extends BaseView<SplashController> {
   const SplashView({super.key});
 
-  @override
-  bool get safeArea => false;
-
   @override
   bool get resizeToAvoidBottomInset => false;
 
@@ -65,45 +62,42 @@ class SplashView extends BaseView<SplashController> {
 
   // 构建底部内容
   Widget _buildBottomContent() {
-    return Container(
-      padding: EdgeInsets.only(bottom: 20.h),
-      child: Obx(
-        () => !controller.hasLogin
-            ? FadeInUp(
-                duration: const Duration(milliseconds: 700),
-                delay: const Duration(milliseconds: 800),
-                child: PrivacyAgreement(
-                  textColor: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                  linkColor: Get.reactiveTheme.primaryColor,
-                  height: 1.8,
-                ),
-              )
-            : FadeInUp(
-                duration: const Duration(milliseconds: 600),
-                child: Column(
-                  children: [
-                    Obx(
-                      () => controller.showLoading
-                          ? SpinKitRing(
-                              size: 24.w,
-                              lineWidth: 2.w,
-                              color: Get.reactiveTheme.primaryColor,
-                            )
-                          : const SizedBox.shrink(),
-                    ),
-                    16.verticalSpaceFromWidth,
-                    Text(
-                      'V${controller.versionName}',
-                      textAlign: TextAlign.center,
-                      style: TextStyle(
-                        color: Get.reactiveTheme.textTheme.bodyLarge!.color,
-                        fontSize: 14.sp,
-                      ),
+    return Obx(
+      () => !controller.hasLogin
+          ? FadeInUp(
+              duration: const Duration(milliseconds: 700),
+              delay: const Duration(milliseconds: 800),
+              child: PrivacyAgreement(
+                textColor: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                linkColor: Get.reactiveTheme.primaryColor,
+                height: 1.8,
+              ),
+            )
+          : FadeInUp(
+              duration: const Duration(milliseconds: 600),
+              child: Column(
+                children: [
+                  Obx(
+                    () => controller.showLoading
+                        ? SpinKitRing(
+                            size: 24.w,
+                            lineWidth: 2.w,
+                            color: Get.reactiveTheme.primaryColor,
+                          )
+                        : const SizedBox.shrink(),
+                  ),
+                  16.verticalSpaceFromWidth,
+                  Text(
+                    'V${controller.versionName}',
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                      fontSize: 14.sp,
                     ),
-                  ],
-                ),
+                  ),
+                ],
               ),
-      ),
+            ),
     );
   }
 }

+ 121 - 13
lib/app/modules/splittunneling/controllers/splittunneling_controller.dart

@@ -1,31 +1,139 @@
+import 'dart:convert';
+
 import 'package:get/get.dart';
 
-enum SplitTunnelingMode {
-  exclude, // 排除选中的应用
-  include, // 仅包含选中的应用
-}
+import '../../../../utils/event_bus.dart';
+import '../../../../utils/log/logger.dart';
+import 'package:nomo/app/data/sp/ix_sp.dart';
+import '../../../routes/app_pages.dart';
+import '../selectapp/controllers/splittunneling_selectapp_controller.dart';
+
+class SplittunnelingController extends GetxController with EventBusMixin {
+  static const String TAG = 'SplittunnelingController';
 
-class SplittunnelingController extends GetxController {
   // 当前选中的分流隧道模式
-  final selectedMode = SplitTunnelingMode.exclude.obs;
+  final selectedMode = SplitTunnelingMode.none.obs;
+
+  // 缓存键
+  static const String _selectedModeKey = 'splittunneling_selected_mode';
 
   @override
   void onInit() {
     super.onInit();
+    _loadSelectedMode();
+    // 监听 PageVisibleEvent
+    listenEvent<SplitTunnelingPageEvent>((event) {
+      final selectApps = getSelectedApps(event.mode);
+      if (selectApps.isNotEmpty) {
+        selectedMode.value = event.mode;
+        _saveSelectedMode();
+        refreshSelectedApps();
+      } else {
+        if (selectedMode.value == event.mode) {
+          selectedMode.value = SplitTunnelingMode.none;
+          _saveSelectedMode();
+          refreshSelectedApps();
+        }
+      }
+    });
   }
 
-  @override
-  void onReady() {
-    super.onReady();
+  /// 刷新选中应用显示
+  void refreshSelectedApps() {
+    // 触发UI更新
+    selectedMode.refresh();
   }
 
-  @override
-  void onClose() {
-    super.onClose();
+  /// 加载选中的模式
+  void _loadSelectedMode() {
+    final modeString = IXSP.getString(_selectedModeKey);
+    if (modeString != null) {
+      try {
+        selectedMode.value = SplitTunnelingMode.values.firstWhere(
+          (mode) => mode.toString() == modeString,
+          orElse: () => SplitTunnelingMode.none,
+        );
+      } catch (e) {
+        log(TAG, '加载选中模式失败: $e');
+      }
+    }
+  }
+
+  /// 保存选中的模式
+  void _saveSelectedMode() {
+    IXSP.setString(_selectedModeKey, selectedMode.value.toString());
   }
 
   /// 选择分流隧道模式
   void selectMode(SplitTunnelingMode mode) {
-    selectedMode.value = mode;
+    if (mode == SplitTunnelingMode.none) {
+      selectedMode.value = SplitTunnelingMode.none;
+      _saveSelectedMode();
+      refreshSelectedApps();
+    } else {
+      final selectedApps = getSelectedApps(mode);
+      if (selectedApps.isNotEmpty) {
+        if (selectedMode.value == mode) {
+          selectedMode.value = SplitTunnelingMode.none;
+        } else {
+          selectedMode.value = mode;
+        }
+        _saveSelectedMode();
+        refreshSelectedApps();
+      } else {
+        Get.toNamed(Routes.SPLITTUNNELING_SELECTAPP, arguments: mode);
+      }
+    }
+  }
+
+  void toSelectAppPage(SplitTunnelingMode mode) {
+    Get.toNamed(Routes.SPLITTUNNELING_SELECTAPP, arguments: mode);
+  }
+
+  /// 获取当前模式选中的应用(最多3个)
+  List<Map<String, dynamic>> getSelectedApps(SplitTunnelingMode mode) {
+    if (mode == SplitTunnelingMode.none) {
+      return [];
+    }
+
+    try {
+      final key = mode == SplitTunnelingMode.exclude
+          ? 'splittunneling_exclude_selected_apps'
+          : 'splittunneling_include_selected_apps';
+
+      final selectedAppsJson = IXSP.getString(key);
+      if (selectedAppsJson != null) {
+        final selectedPackageNames =
+            jsonDecode(selectedAppsJson) as List<dynamic>;
+
+        // 获取缓存的应用数据
+        final cachedAppsJson = IXSP.getString('splittunneling_cached_apps');
+        if (cachedAppsJson != null) {
+          final cachedApps = jsonDecode(cachedAppsJson) as List;
+
+          // 按照保存的顺序重建列表
+          final selectedApps = <Map<String, dynamic>>[];
+          for (final packageName in selectedPackageNames) {
+            if (selectedApps.length >= 3) break; // 最多取3个
+
+            final app = cachedApps.firstWhere(
+              (app) => app['packageName'] == packageName,
+              orElse: () => null,
+            );
+            if (app != null) {
+              selectedApps.add({
+                'name': app['name'],
+                'packageName': app['packageName'],
+                'icon': app['icon'],
+              });
+            }
+          }
+          return selectedApps;
+        }
+      }
+    } catch (e) {
+      log(TAG, '获取选中应用失败: $e');
+    }
+    return [];
   }
 }

+ 10 - 0
lib/app/modules/splittunneling/selectapp/bindings/splittunneling_selectapp_binding.dart

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

+ 269 - 0
lib/app/modules/splittunneling/selectapp/controllers/splittunneling_selectapp_controller.dart

@@ -0,0 +1,269 @@
+import 'dart:convert';
+
+import 'package:get/get.dart';
+import 'package:nomo/app/base/base_controller.dart';
+import 'package:nomo/app/data/sp/ix_sp.dart';
+import 'package:nomo/pigeons/core_api.g.dart';
+import 'package:nomo/utils/log/logger.dart';
+
+/// 分流隧道模式枚举
+enum SplitTunnelingMode {
+  exclude, // 排除选中的应用
+  include, // 仅包含选中的应用
+  none, // 不选择任何应用
+}
+
+/// 应用数据模型
+class AppInfo {
+  final String name;
+  final String packageName;
+  final String icon;
+
+  AppInfo({required this.name, required this.packageName, required this.icon});
+}
+
+class SplittunnelingSelectappController extends BaseController {
+  // 排除模式选中的应用列表
+  final RxList<AppInfo> excludeSelectedApps = <AppInfo>[].obs;
+
+  // 包含模式选中的应用列表
+  final RxList<AppInfo> includeSelectedApps = <AppInfo>[].obs;
+
+  // 所有应用列表
+  final RxList<AppInfo> allApps = <AppInfo>[].obs;
+
+  // 当前模式
+  final Rx<SplitTunnelingMode> currentMode = SplitTunnelingMode.exclude.obs;
+
+  static const String TAG = 'SplittunnelingSelectappController';
+
+  // 缓存键
+  static const String _cachedAppsKey = 'splittunneling_cached_apps';
+  static const String _excludeSelectedAppsKey =
+      'splittunneling_exclude_selected_apps';
+  static const String _includeSelectedAppsKey =
+      'splittunneling_include_selected_apps';
+
+  @override
+  void onInit() {
+    super.onInit();
+    _loadCachedData();
+    getApps();
+  }
+
+  /// 加载缓存数据
+  void _loadCachedData() {
+    // 加载缓存的应用数据
+    final cachedAppsJson = IXSP.getString(_cachedAppsKey);
+    if (cachedAppsJson != null) {
+      try {
+        final cachedApps = jsonDecode(cachedAppsJson) as List;
+        final apps = cachedApps
+            .map(
+              (app) => AppInfo(
+                name: app['name'],
+                packageName: app['packageName'],
+                icon: app['icon'],
+              ),
+            )
+            .toList();
+        allApps.value = apps;
+        setSuccess();
+      } catch (e) {
+        log(TAG, '加载缓存应用数据失败: $e');
+      }
+    }
+
+    // 加载当前模式
+    currentMode.value = Get.arguments as SplitTunnelingMode;
+
+    _loadSelectedApps(currentMode.value);
+  }
+
+  /// 初始化应用数据
+  Future<void> getApps() async {
+    // 如果已经有缓存数据,先显示缓存,然后异步更新
+    if (allApps.isNotEmpty) {
+      setSuccess();
+    } else {
+      setLoading();
+    }
+
+    final newApps = <AppInfo>[];
+    final appsJson = await CoreApi().getApps();
+    if (appsJson != null) {
+      final apps = jsonDecode(appsJson);
+      for (dynamic app in apps) {
+        if (app is Map) {
+          String appName = app['appName'];
+          String packageName = app['packageName'];
+          String icon = app['icon'];
+          newApps.add(
+            AppInfo(name: appName, packageName: packageName, icon: icon),
+          );
+        }
+      }
+
+      // 更新应用列表
+      allApps.value = newApps;
+
+      // 缓存应用数据
+      _cacheAppsData(newApps);
+
+      // 重新加载选中的应用(因为应用列表可能已更新)
+      _loadSelectedApps(currentMode.value);
+
+      if (allApps.isNotEmpty) {
+        setSuccess();
+      } else {
+        setEmpty();
+      }
+    }
+  }
+
+  /// 缓存应用数据
+  void _cacheAppsData(List<AppInfo> apps) {
+    try {
+      final appsJson = jsonEncode(
+        apps
+            .map(
+              (app) => {
+                'name': app.name,
+                'packageName': app.packageName,
+                'icon': app.icon,
+              },
+            )
+            .toList(),
+      );
+      IXSP.setString(_cachedAppsKey, appsJson);
+    } catch (e) {
+      log(TAG, '缓存应用数据失败: $e');
+    }
+  }
+
+  /// 加载选中的应用
+  void _loadSelectedApps(SplitTunnelingMode mode) {
+    final key = mode == SplitTunnelingMode.exclude
+        ? _excludeSelectedAppsKey
+        : _includeSelectedAppsKey;
+
+    final selectedAppsJson = IXSP.getString(key);
+    if (selectedAppsJson != null) {
+      try {
+        final selectedPackageNames =
+            jsonDecode(selectedAppsJson) as List<dynamic>;
+
+        // 按照保存的顺序重建列表
+        final selectedAppsList = <AppInfo>[];
+        for (final packageName in selectedPackageNames) {
+          final app = allApps.firstWhere(
+            (app) => app.packageName == packageName,
+            orElse: () => AppInfo(name: '', packageName: packageName, icon: ''),
+          );
+          if (app.name.isNotEmpty) {
+            // 只添加有效的应用
+            selectedAppsList.add(app);
+          }
+        }
+
+        if (mode == SplitTunnelingMode.exclude) {
+          excludeSelectedApps.value = selectedAppsList;
+        } else {
+          includeSelectedApps.value = selectedAppsList;
+        }
+      } catch (e) {
+        log(
+          TAG,
+          '加载${mode == SplitTunnelingMode.exclude ? "排除" : "包含"}模式选中应用数据失败: $e',
+        );
+      }
+    }
+  }
+
+  /// 保存选中的应用
+  void _saveSelectedApps(SplitTunnelingMode mode) {
+    try {
+      final selectedApps = mode == SplitTunnelingMode.exclude
+          ? excludeSelectedApps
+          : includeSelectedApps;
+
+      final selectedPackageNames = selectedApps
+          .map((app) => app.packageName)
+          .toList();
+      final selectedAppsJson = jsonEncode(selectedPackageNames);
+
+      final key = mode == SplitTunnelingMode.exclude
+          ? _excludeSelectedAppsKey
+          : _includeSelectedAppsKey;
+
+      IXSP.setString(key, selectedAppsJson);
+    } catch (e) {
+      log(
+        TAG,
+        '保存${mode == SplitTunnelingMode.exclude ? "排除" : "包含"}模式选中应用数据失败: $e',
+      );
+    }
+  }
+
+  /// 切换应用选择状态
+  void toggleAppSelection(AppInfo app) {
+    if (currentMode.value == SplitTunnelingMode.exclude) {
+      if (excludeSelectedApps.contains(app)) {
+        excludeSelectedApps.remove(app);
+      } else {
+        excludeSelectedApps.add(app);
+      }
+      _saveSelectedApps(SplitTunnelingMode.exclude);
+    } else {
+      if (includeSelectedApps.contains(app)) {
+        includeSelectedApps.remove(app);
+      } else {
+        includeSelectedApps.add(app);
+      }
+      _saveSelectedApps(SplitTunnelingMode.include);
+    }
+  }
+
+  /// 检查应用是否已选择
+  bool isAppSelected(AppInfo app) {
+    if (currentMode.value == SplitTunnelingMode.exclude) {
+      return excludeSelectedApps.contains(app);
+    } else {
+      return includeSelectedApps.contains(app);
+    }
+  }
+
+  /// 取消选择所有应用
+  void deselectAllApps() {
+    if (currentMode.value == SplitTunnelingMode.exclude) {
+      excludeSelectedApps.clear();
+      _saveSelectedApps(SplitTunnelingMode.exclude);
+    } else {
+      includeSelectedApps.clear();
+      _saveSelectedApps(SplitTunnelingMode.include);
+    }
+  }
+
+  /// 选择所有应用
+  void selectAllApps() {
+    if (currentMode.value == SplitTunnelingMode.exclude) {
+      excludeSelectedApps.value = List.from(allApps);
+      _saveSelectedApps(SplitTunnelingMode.exclude);
+    } else {
+      includeSelectedApps.value = List.from(allApps);
+      _saveSelectedApps(SplitTunnelingMode.include);
+    }
+  }
+
+  /// 获取当前模式选中的应用列表
+  List<AppInfo> get selectedApps {
+    return currentMode.value == SplitTunnelingMode.exclude
+        ? excludeSelectedApps
+        : includeSelectedApps;
+  }
+
+  /// 获取未选择的应用列表
+  List<AppInfo> get unselectedApps {
+    return allApps.where((app) => !isAppSelected(app)).toList();
+  }
+}

+ 314 - 0
lib/app/modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart

@@ -0,0 +1,314 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:nomo/app/base/base_view.dart';
+import 'package:nomo/app/widgets/click_opacity.dart';
+import 'package:nomo/app/widgets/ix_app_bar.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
+import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
+
+import '../../../../../utils/event_bus.dart';
+import '../../../../widgets/state/state_wrapper.dart';
+import '../controllers/splittunneling_selectapp_controller.dart';
+
+class SplittunnelingSelectappView
+    extends BaseView<SplittunnelingSelectappController> {
+  SplittunnelingSelectappView({super.key});
+
+  // 用于存储应用项的位置信息
+  final Map<String, GlobalKey> _selectedAppsKeys = {};
+  final Map<String, GlobalKey> _unselectedAppsKeys = {};
+
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(
+    title: 'Split Tunneling',
+    onBackPressed: () {
+      eventBus.fire(
+        SplitTunnelingPageEvent(mode: controller.currentMode.value),
+      );
+      Get.back();
+    },
+  );
+
+  @override
+  Widget buildContent(BuildContext context) {
+    return StateWrapper(
+      controller: controller,
+      onRefresh: () => controller.getApps(),
+      child: SingleChildScrollView(
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // 信息横幅
+            _buildInfoBanner(),
+
+            // 已选择应用区域
+            _buildSelectedAppsSection(context),
+
+            10.verticalSpaceFromWidth,
+
+            // 所有应用区域
+            _buildAllAppsSection(context),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 构建信息横幅
+  Widget _buildInfoBanner() {
+    return Obx(() {
+      final isExcludeMode =
+          controller.currentMode.value == SplitTunnelingMode.exclude;
+      final message = isExcludeMode
+          ? 'Select apps that will not use the VPN'
+          : 'Select apps that will use the VPN';
+
+      return Container(
+        padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.w),
+        decoration: BoxDecoration(
+          color: Get.reactiveTheme.cardColor,
+          borderRadius: BorderRadius.circular(12.r),
+        ),
+        child: Row(
+          children: [
+            Container(
+              width: 20.w,
+              height: 20.w,
+              decoration: const BoxDecoration(
+                color: Colors.white,
+                shape: BoxShape.circle,
+              ),
+              child: Center(
+                child: Text(
+                  'i',
+                  style: TextStyle(
+                    fontSize: 14.sp,
+                    fontWeight: FontWeight.bold,
+                    color: Colors.black,
+                  ),
+                ),
+              ),
+            ),
+            SizedBox(width: 12.w),
+            Expanded(
+              child: Text(
+                message,
+                style: TextStyle(
+                  fontSize: 12.sp,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+            ),
+          ],
+        ),
+      );
+    });
+  }
+
+  /// 构建已选择应用区域
+  Widget _buildSelectedAppsSection(BuildContext context) {
+    return Obx(() {
+      if (controller.selectedApps.isEmpty) {
+        return const SizedBox.shrink();
+      }
+
+      return AnimatedContainer(
+        duration: const Duration(milliseconds: 300),
+        curve: Curves.easeInOut,
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            10.verticalSpaceFromWidth,
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Text(
+                  'Select apps',
+                  style: TextStyle(
+                    fontSize: 16.sp,
+                    fontWeight: FontWeight.w500,
+                    color: Get.reactiveTheme.hintColor,
+                  ),
+                ),
+                ClickOpacity(
+                  onTap: controller.deselectAllApps,
+                  child: Text(
+                    'Deselect all',
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      color: Get.reactiveTheme.shadowColor,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+            10.verticalSpaceFromWidth,
+            Container(
+              decoration: BoxDecoration(
+                color: Get.reactiveTheme.highlightColor,
+                borderRadius: BorderRadius.circular(12.r),
+              ),
+              child: Column(
+                children: controller.selectedApps.asMap().entries.map((entry) {
+                  final key = GlobalKey();
+                  _selectedAppsKeys[entry.value.packageName] = key;
+                  return Container(
+                    key: key,
+                    child: _buildAppItem(
+                      context,
+                      entry.value,
+                      isLast: entry.key == controller.selectedApps.length - 1,
+                    ),
+                  );
+                }).toList(),
+              ),
+            ),
+          ],
+        ),
+      );
+    });
+  }
+
+  /// 构建所有应用区域
+  Widget _buildAllAppsSection(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          'All apps',
+          style: TextStyle(
+            fontSize: 16.sp,
+            fontWeight: FontWeight.w500,
+            color: Get.reactiveTheme.hintColor,
+          ),
+        ),
+        10.verticalSpaceFromWidth,
+        Obx(() {
+          final unselectedApps = controller.unselectedApps;
+          return Container(
+            decoration: BoxDecoration(
+              color: Get.reactiveTheme.highlightColor,
+              borderRadius: BorderRadius.circular(12.r),
+            ),
+            child: Column(
+              children: unselectedApps.asMap().entries.map((entry) {
+                final key = GlobalKey();
+                _unselectedAppsKeys[entry.value.packageName] = key;
+                return Container(
+                  key: key,
+                  child: _buildAppItem(
+                    context,
+                    entry.value,
+                    isLast: entry.key == unselectedApps.length - 1,
+                  ),
+                );
+              }).toList(),
+            ),
+          );
+        }),
+      ],
+    );
+  }
+
+  /// 构建应用项
+  Widget _buildAppItem(
+    BuildContext context,
+    AppInfo app, {
+    bool isLast = false,
+  }) {
+    return AnimatedContainer(
+      duration: const Duration(milliseconds: 200),
+      curve: Curves.easeInOut,
+      child: GestureDetector(
+        onTap: () {
+          // 直接切换选择状态
+          controller.toggleAppSelection(app);
+        },
+        child: AnimatedContainer(
+          duration: const Duration(milliseconds: 150),
+          curve: Curves.easeInOut,
+          child: Container(
+            height: 68.w,
+            padding: EdgeInsets.symmetric(horizontal: 14.w),
+            decoration: BoxDecoration(
+              border: isLast
+                  ? null
+                  : Border(
+                      bottom: BorderSide(
+                        color: Get.reactiveTheme.dividerColor,
+                        width: 1.w,
+                      ),
+                    ),
+            ),
+            child: Row(
+              children: [
+                // 应用图标
+                AnimatedContainer(
+                  duration: const Duration(milliseconds: 200),
+                  curve: Curves.easeInOut,
+                  child: Transform.scale(
+                    scale: 1.0,
+                    child: IXImage(
+                      key: ValueKey('app_icon_${app.packageName}'),
+                      source: app.icon,
+                      width: 40.w,
+                      height: 40.w,
+                      sourceType: ImageSourceType.memory,
+                    ),
+                  ),
+                ),
+
+                12.horizontalSpace,
+
+                // 应用名称
+                Expanded(
+                  child: Text(
+                    app.name,
+                    style: TextStyle(
+                      fontSize: 14.sp,
+                      color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                      fontWeight: FontWeight.w500,
+                    ),
+                  ),
+                ),
+
+                // 选择状态指示器
+                Obx(() {
+                  final isSelected = controller.isAppSelected(app);
+                  return AnimatedContainer(
+                    duration: const Duration(milliseconds: 300),
+                    curve: Curves.elasticOut,
+                    width: 20.w,
+                    height: 20.w,
+                    decoration: BoxDecoration(
+                      shape: BoxShape.circle,
+                      color: isSelected
+                          ? Get.reactiveTheme.shadowColor
+                          : Colors.transparent,
+                      border: Border.all(
+                        color: isSelected
+                            ? Get.reactiveTheme.shadowColor
+                            : Colors.grey[400]!,
+                        width: 2.w,
+                      ),
+                    ),
+                    child: AnimatedScale(
+                      scale: isSelected ? 1.0 : 0.0,
+                      duration: const Duration(milliseconds: 300),
+                      curve: Curves.elasticOut,
+                      child: Icon(Icons.check, color: Colors.white, size: 14.w),
+                    ),
+                  );
+                }),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 247 - 111
lib/app/modules/splittunneling/views/splittunneling_view.dart

@@ -1,43 +1,45 @@
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:nomo/app/base/base_view.dart';
 import 'package:nomo/app/widgets/click_opacity.dart';
 import 'package:nomo/app/widgets/ix_app_bar.dart';
+import 'package:nomo/app/widgets/ix_image.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 
 import '../controllers/splittunneling_controller.dart';
+import '../selectapp/controllers/splittunneling_selectapp_controller.dart';
 
 class SplittunnelingView extends BaseView<SplittunnelingController> {
   const SplittunnelingView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(title: 'Split Tunneling');
+
   @override
   Widget buildContent(BuildContext context) {
     return Column(
       children: [
-        IXAppBar(title: 'Split Tunneling'),
         Expanded(
           child: SingleChildScrollView(
-            padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
+            padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
                 // 顶部提示框
                 _buildAlertBox(),
 
-                SizedBox(height: 20.h),
+                10.verticalSpaceFromWidth,
 
                 // 模式选择区域
                 _buildModeSelection(),
-
-                SizedBox(height: 30.h),
-
-                // 底部信息框
-                _buildInfoBox(),
               ],
             ),
           ),
         ),
+        // 底部信息框
+        SafeArea(child: _buildInfoBox()),
       ],
     );
   }
@@ -45,9 +47,9 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
   /// 构建顶部提示框
   Widget _buildAlertBox() {
     return Container(
-      padding: EdgeInsets.all(16.w),
+      padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
       decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
+        color: Get.reactiveTheme.cardColor,
         borderRadius: BorderRadius.circular(12.r),
       ),
       child: Row(
@@ -58,8 +60,8 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
             child: Text(
               'Only one mode can be active at a time.',
               style: TextStyle(
-                fontSize: 14.sp,
-                color: Colors.white,
+                fontSize: 12.sp,
+                color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                 fontWeight: FontWeight.w500,
               ),
             ),
@@ -71,34 +73,36 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
 
   /// 构建模式选择区域
   Widget _buildModeSelection() {
-    return Obx(
-      () => Column(
-        children: [
-          _buildModeCard(
-            mode: SplitTunnelingMode.exclude,
-            icon: Icons.block,
-            title: 'Exclude selected apps from VPN',
-            description:
-                'Choose apps that will connect directly without using the VPN.',
-            isSelected:
-                controller.selectedMode.value == SplitTunnelingMode.exclude,
-            onTap: () => controller.selectMode(SplitTunnelingMode.exclude),
-          ),
+    return Column(
+      children: [
+        _buildModeCard(
+          mode: SplitTunnelingMode.exclude,
+          icon: Icons.block,
+          title: 'Exclude selected apps from VPN',
+          description:
+              'Choose apps that will connect directly without using the VPN.',
+          onTap: () {
+            controller.selectMode(SplitTunnelingMode.exclude);
+            // 刷新显示
+            controller.refreshSelectedApps();
+          },
+        ),
 
-          SizedBox(height: 16.h),
-
-          _buildModeCard(
-            mode: SplitTunnelingMode.include,
-            icon: Icons.check,
-            title: 'Use VPN for selected apps only',
-            description:
-                'Choose apps that will use the VPN while others connect normally.',
-            isSelected:
-                controller.selectedMode.value == SplitTunnelingMode.include,
-            onTap: () => controller.selectMode(SplitTunnelingMode.include),
-          ),
-        ],
-      ),
+        10.verticalSpaceFromWidth,
+
+        _buildModeCard(
+          mode: SplitTunnelingMode.include,
+          icon: Icons.check,
+          title: 'Use VPN for selected apps only',
+          description:
+              'Choose apps that will use the VPN while others connect normally.',
+          onTap: () {
+            controller.selectMode(SplitTunnelingMode.include);
+            // 刷新显示
+            controller.refreshSelectedApps();
+          },
+        ),
+      ],
     );
   }
 
@@ -108,81 +112,211 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
     required IconData icon,
     required String title,
     required String description,
-    required bool isSelected,
     required VoidCallback onTap,
   }) {
-    return ClickOpacity(
-      onTap: onTap,
-      child: Container(
-        padding: EdgeInsets.all(20.w),
-        decoration: BoxDecoration(
-          color: Get.reactiveTheme.highlightColor,
-          borderRadius: BorderRadius.circular(16.r),
-        ),
-        child: Row(
-          children: [
-            // 图标
-            Container(
-              width: 40.w,
-              height: 40.w,
-              decoration: BoxDecoration(
-                color: const Color(0xFF00A8E8),
-                shape: BoxShape.circle,
-              ),
-              child: Icon(icon, color: Colors.white, size: 20.w),
-            ),
-
-            SizedBox(width: 16.w),
-
-            // 标题和描述
-            Expanded(
-              child: Column(
+    return Container(
+      decoration: BoxDecoration(
+        color: Get.reactiveTheme.highlightColor,
+        borderRadius: BorderRadius.circular(12.r),
+      ),
+      child: Column(
+        children: [
+          ClickOpacity(
+            onTap: onTap,
+            child: Padding(
+              padding: EdgeInsets.all(14.w),
+              child: Row(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
-                  Text(
-                    title,
-                    style: TextStyle(
-                      fontSize: 16.sp,
-                      fontWeight: FontWeight.w500,
-                      color: Colors.white,
+                  // 图标
+                  Container(
+                    width: 30.w,
+                    height: 30.w,
+                    decoration: BoxDecoration(
+                      color: Get.reactiveTheme.shadowColor,
+                      borderRadius: BorderRadius.circular(8.r),
                     ),
+                    child: Icon(icon, color: Colors.white, size: 20.w),
                   ),
-                  SizedBox(height: 4.h),
-                  Text(
-                    description,
-                    style: TextStyle(
-                      fontSize: 14.sp,
-                      color: Get.reactiveTheme.hintColor,
-                      height: 1.4,
+
+                  10.horizontalSpace,
+
+                  // 标题和描述
+                  Expanded(
+                    child: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text(
+                          title,
+                          style: TextStyle(
+                            fontSize: 14.sp,
+                            height: 1.4,
+                            color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                          ),
+                        ),
+                        Text(
+                          description,
+                          style: TextStyle(
+                            fontSize: 12.sp,
+                            height: 1.6,
+                            color: Get.reactiveTheme.hintColor,
+                          ),
+                        ),
+                      ],
                     ),
                   ),
+
+                  10.horizontalSpace,
+
+                  // Switch 开关
+                  Obx(() {
+                    final isSelected = controller.selectedMode.value == mode;
+                    return CupertinoSwitch(
+                      value: isSelected,
+                      onChanged: (value) {
+                        if (value) {
+                          controller.selectMode(mode);
+                        } else {
+                          controller.selectMode(SplitTunnelingMode.none);
+                        }
+                        controller.refreshSelectedApps();
+                      },
+                      activeTrackColor: Get.reactiveTheme.shadowColor,
+                      thumbColor: Colors.white,
+                      inactiveThumbColor: Colors.white,
+                      inactiveTrackColor: Colors.grey,
+                    );
+                  }),
                 ],
               ),
             ),
+          ),
+          // 显示选中的应用
+          Obx(() {
+            final isSelected = controller.selectedMode.value == mode;
+            if (isSelected) {
+              final selectedApps = controller.getSelectedApps(mode);
+              if (selectedApps.isNotEmpty) {
+                return _buildSelectedAppsPreview(selectedApps);
+              }
+            }
+            return const SizedBox.shrink();
+          }),
+        ],
+      ),
+    );
+  }
 
-            SizedBox(width: 16.w),
-
-            // 单选按钮
-            Container(
-              width: 24.w,
-              height: 24.w,
-              decoration: BoxDecoration(
-                shape: BoxShape.circle,
-                color: isSelected
-                    ? const Color(0xFF00A8E8)
-                    : Colors.transparent,
-                border: Border.all(
-                  color: isSelected
-                      ? const Color(0xFF00A8E8)
-                      : Colors.white.withOpacity(0.3),
-                  width: 2.w,
+  /// 构建选中应用预览
+  Widget _buildSelectedAppsPreview(List<Map<String, dynamic>> selectedApps) {
+    return AnimatedContainer(
+      duration: const Duration(milliseconds: 300),
+      curve: Curves.easeInOut,
+      child: ClickOpacity(
+        onTap: () {
+          controller.toSelectAppPage(controller.selectedMode.value);
+        },
+        child: Container(
+          height: 44.w,
+          padding: EdgeInsets.symmetric(horizontal: 16.w),
+          decoration: BoxDecoration(
+            color: Get.reactiveTheme.cardColor,
+            borderRadius: BorderRadius.only(
+              bottomLeft: Radius.circular(12.r),
+              bottomRight: Radius.circular(12.r),
+            ),
+          ),
+          child: Row(
+            children: [
+              Text(
+                'Select apps',
+                style: TextStyle(
+                  fontSize: 12.sp,
+                  color: Get.reactiveTheme.hintColor,
+                  fontWeight: FontWeight.w500,
                 ),
               ),
-              child: isSelected
-                  ? Icon(Icons.check, color: Colors.white, size: 16.w)
-                  : null,
-            ),
-          ],
+              SizedBox(width: 8.w),
+              Expanded(
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.end,
+                  children: [
+                    // 显示应用图标
+                    ...selectedApps
+                        .take(3)
+                        .toList()
+                        .asMap()
+                        .entries
+                        .map(
+                          (entry) => AnimatedContainer(
+                            duration: Duration(
+                              milliseconds: 200 + (entry.key * 100),
+                            ),
+                            curve: Curves.easeOut,
+                            margin: EdgeInsets.only(right: 6.w),
+                            child: SlideTransition(
+                              position:
+                                  Tween<Offset>(
+                                    begin: const Offset(0, 1),
+                                    end: Offset.zero,
+                                  ).animate(
+                                    CurvedAnimation(
+                                      parent: kAlwaysCompleteAnimation,
+                                      curve: Curves.easeOut,
+                                    ),
+                                  ),
+                              child: FadeTransition(
+                                opacity: Tween<double>(begin: 0.0, end: 1.0)
+                                    .animate(
+                                      CurvedAnimation(
+                                        parent: kAlwaysCompleteAnimation,
+                                        curve: Curves.easeOut,
+                                      ),
+                                    ),
+                                child: ClipRRect(
+                                  borderRadius: BorderRadius.circular(4.r),
+                                  child: IXImage(
+                                    key: ValueKey(
+                                      'preview_${entry.value['packageName']}',
+                                    ),
+                                    source: entry.value['icon'],
+                                    width: 24.w,
+                                    height: 24.w,
+                                    sourceType: ImageSourceType.memory,
+                                  ),
+                                ),
+                              ),
+                            ),
+                          ),
+                        ),
+                    // 如果有更多应用,显示箭头
+                    if (selectedApps.length > 3) ...[
+                      SizedBox(width: 4.w),
+                      AnimatedScale(
+                        scale: 1.0,
+                        duration: const Duration(milliseconds: 300),
+                        curve: Curves.easeOut,
+                        child: Icon(
+                          Icons.arrow_forward_ios,
+                          size: 12.w,
+                          color: Get.reactiveTheme.hintColor,
+                        ),
+                      ),
+                    ],
+                  ],
+                ),
+              ),
+              AnimatedRotation(
+                turns: 0.0,
+                duration: const Duration(milliseconds: 200),
+                child: Icon(
+                  Icons.keyboard_arrow_right,
+                  size: 20.w,
+                  color: Get.reactiveTheme.hintColor,
+                ),
+              ),
+            ],
+          ),
         ),
       ),
     );
@@ -191,17 +325,18 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
   /// 构建底部信息框
   Widget _buildInfoBox() {
     return Container(
-      padding: EdgeInsets.all(20.w),
+      margin: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
+      padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
       decoration: BoxDecoration(
-        color: Get.reactiveTheme.highlightColor,
-        borderRadius: BorderRadius.circular(16.r),
+        color: Get.reactiveTheme.canvasColor,
+        borderRadius: BorderRadius.circular(12.r),
       ),
       child: Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Container(
-            width: 24.w,
-            height: 24.w,
+            width: 20.w,
+            height: 20.w,
             decoration: BoxDecoration(
               color: Colors.white,
               shape: BoxShape.circle,
@@ -218,7 +353,7 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
             ),
           ),
 
-          SizedBox(width: 12.w),
+          8.horizontalSpace,
 
           Expanded(
             child: Column(
@@ -227,18 +362,19 @@ class SplittunnelingView extends BaseView<SplittunnelingController> {
                 Text(
                   'Customize your VPN',
                   style: TextStyle(
-                    fontSize: 16.sp,
+                    fontSize: 14.sp,
+                    height: 1.6,
                     fontWeight: FontWeight.w500,
-                    color: Colors.white,
+                    color: Get.reactiveTheme.textTheme.bodyLarge!.color,
                   ),
                 ),
-                SizedBox(height: 8.h),
+                10.verticalSpaceFromWidth,
                 Text(
                   'Split tunneling lets you control which apps use the VPN connection and which connect directly. It helps you manage bandwidth and access local or foreign content without turning off the VPN.',
                   style: TextStyle(
-                    fontSize: 14.sp,
+                    fontSize: 12.sp,
                     color: Get.reactiveTheme.hintColor,
-                    height: 1.4,
+                    height: 1.6,
                   ),
                 ),
               ],

+ 32 - 34
lib/app/modules/web/views/web_view.dart

@@ -14,42 +14,40 @@ import '../controllers/web_controller.dart';
 class WebView extends BaseView<WebController> {
   const WebView({super.key});
 
+  @override
+  PreferredSizeWidget? get appBar => IXAppBar(
+    title: controller.title,
+    // 不需要传递颜色参数,会自动使用响应式主题
+    onBackPressed: () async {
+      if (await controller.checkCanGoBack()) {
+        controller.goBack();
+      } else {
+        Get.back();
+      }
+    },
+    actions: [
+      IconButton(
+        onPressed: () {
+          controller.reload();
+        },
+        icon: const Icon(Icons.refresh_rounded),
+      ),
+      Obx(
+        () => controller.canGoBack.value
+            ? IconButton(
+                onPressed: () {
+                  Get.back();
+                },
+                icon: const Icon(Icons.close_rounded),
+              )
+            : const SizedBox.shrink(),
+      ),
+    ],
+  );
+
   @override
   Widget buildContent(BuildContext context) {
-    return Column(
-      children: [
-        IXAppBar(
-          title: controller.title,
-          // 不需要传递颜色参数,会自动使用响应式主题
-          onBackPressed: () async {
-            if (await controller.checkCanGoBack()) {
-              controller.goBack();
-            } else {
-              Get.back();
-            }
-          },
-          actions: [
-            IconButton(
-              onPressed: () {
-                controller.reload();
-              },
-              icon: const Icon(Icons.refresh_rounded),
-            ),
-            Obx(
-              () => controller.canGoBack.value
-                  ? IconButton(
-                      onPressed: () {
-                        Get.back();
-                      },
-                      icon: const Icon(Icons.close_rounded),
-                    )
-                  : const SizedBox.shrink(),
-            ),
-          ],
-        ),
-        Expanded(child: _buildWebContent()),
-      ],
-    );
+    return _buildWebContent();
   }
 
   Widget _buildWebContent() {

+ 64 - 15
lib/app/routes/app_pages.dart

@@ -1,3 +1,5 @@
+import 'package:flutter/material.dart';
+
 import 'package:get/get.dart';
 
 import '../modules/about/bindings/about_binding.dart';
@@ -8,6 +10,8 @@ import '../modules/deviceauth/bindings/deviceauth_binding.dart';
 import '../modules/deviceauth/views/deviceauth_view.dart';
 import '../modules/feedback/bindings/feedback_binding.dart';
 import '../modules/feedback/views/feedback_view.dart';
+import '../modules/forgotpwd/bindings/forgotpwd_binding.dart';
+import '../modules/forgotpwd/views/forgotpwd_view.dart';
 import '../modules/home/bindings/home_binding.dart';
 import '../modules/home/views/home_view.dart';
 import '../modules/language/bindings/language_binding.dart';
@@ -24,9 +28,15 @@ import '../modules/routingmode/bindings/routingmode_binding.dart';
 import '../modules/routingmode/views/routingmode_view.dart';
 import '../modules/setting/bindings/setting_binding.dart';
 import '../modules/setting/views/setting_view.dart';
+import '../modules/signin/bindings/signin_binding.dart';
+import '../modules/signin/views/signin_view.dart';
+import '../modules/signup/bindings/signup_binding.dart';
+import '../modules/signup/views/signup_view.dart';
 import '../modules/splash/bindings/splash_binding.dart';
 import '../modules/splash/views/splash_view.dart';
 import '../modules/splittunneling/bindings/splittunneling_binding.dart';
+import '../modules/splittunneling/selectapp/bindings/splittunneling_selectapp_binding.dart';
+import '../modules/splittunneling/selectapp/views/splittunneling_selectapp_view.dart';
 import '../modules/splittunneling/views/splittunneling_view.dart';
 import '../modules/web/bindings/web_binding.dart';
 import '../modules/web/views/web_view.dart';
@@ -44,62 +54,72 @@ class AppPages {
       name: _Paths.SPLASH,
       page: () => const SplashView(),
       binding: SplashBinding(),
-      transitionDuration: const Duration(milliseconds: 0),
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.HOME,
       page: () => const HomeView(),
       binding: HomeBinding(),
-      transitionDuration: const Duration(milliseconds: 0),
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.NODE,
       page: () => const NodeView(),
       binding: NodeBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
       preventDuplicates: true, // 防止重复打开同一个页面
     ),
     GetPage(
       name: _Paths.WEB,
       page: () => const WebView(),
       binding: WebBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.ABOUT,
       page: () => const AboutView(),
       binding: AboutBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.ACCOUNT,
       page: () => const AccountView(),
       binding: AccountBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.MARKDOWN,
       page: () => const MarkdownView(),
       binding: MarkdownBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.SETTING,
       page: () => const SettingView(),
       binding: SettingBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.PRECODE,
       page: () => const PrecodeView(),
       binding: PrecodeBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
       children: [
         GetPage(
           name: _Paths.PRECODE_SENDEMAIL,
           page: () => const PrecodeSendemailView(),
           binding: PrecodeSendemailBinding(),
-          transition: Transition.cupertino,
+          transition: Transition.native,
+          curve: Curves.easeInOut,
         ),
       ],
     ),
@@ -107,31 +127,60 @@ class AppPages {
       name: _Paths.DEVICEAUTH,
       page: () => const DeviceauthView(),
       binding: DeviceauthBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.ROUTINGMODE,
       page: () => const RoutingmodeView(),
       binding: RoutingmodeBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.LANGUAGE,
       page: () => const LanguageView(),
       binding: LanguageBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.FEEDBACK,
       page: () => const FeedbackView(),
       binding: FeedbackBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
     ),
     GetPage(
       name: _Paths.SPLITTUNNELING,
       page: () => const SplittunnelingView(),
       binding: SplittunnelingBinding(),
-      transition: Transition.cupertino,
+      transition: Transition.native,
+      curve: Curves.easeInOut,
+      children: [
+        GetPage(
+          name: _Paths.SPLITTUNNELING_SELECTAPP,
+          page: () => SplittunnelingSelectappView(),
+          binding: SplittunnelingSelectappBinding(),
+          transition: Transition.native,
+          curve: Curves.easeInOut,
+        ),
+      ],
+    ),
+    GetPage(
+      name: _Paths.SIGNIN,
+      page: () => const SigninView(),
+      binding: SigninBinding(),
+    ),
+    GetPage(
+      name: _Paths.SIGNUP,
+      page: () => const SignupView(),
+      binding: SignupBinding(),
+    ),
+    GetPage(
+      name: _Paths.FORGOTPWD,
+      page: () => const ForgotpwdView(),
+      binding: ForgotpwdBinding(),
     ),
   ];
 

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

@@ -18,6 +18,11 @@ abstract class Routes {
   static const LANGUAGE = _Paths.LANGUAGE;
   static const FEEDBACK = _Paths.FEEDBACK;
   static const SPLITTUNNELING = _Paths.SPLITTUNNELING;
+  static const SPLITTUNNELING_SELECTAPP =
+      _Paths.SPLITTUNNELING + _Paths.SPLITTUNNELING_SELECTAPP;
+  static const SIGNIN = _Paths.SIGNIN;
+  static const SIGNUP = _Paths.SIGNUP;
+  static const FORGOTPWD = _Paths.FORGOTPWD;
 }
 
 abstract class _Paths {
@@ -37,4 +42,8 @@ abstract class _Paths {
   static const LANGUAGE = '/language';
   static const FEEDBACK = '/feedback';
   static const SPLITTUNNELING = '/splittunneling';
+  static const SPLITTUNNELING_SELECTAPP = '/selectapp';
+  static const SIGNIN = '/signin';
+  static const SIGNUP = '/signup';
+  static const FORGOTPWD = '/forgotpwd';
 }

+ 29 - 36
lib/app/widgets/ix_app_bar.dart

@@ -36,45 +36,38 @@ class IXAppBar extends StatelessWidget implements PreferredSizeWidget {
   @override
   Widget build(BuildContext context) {
     // 使用 Obx 包装 AppBar 以响应主题变化
-    return Obx(() {
-      // 使用响应式主题颜色作为默认值
-      final effectiveTitleColor =
-          titleColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color;
-      final effectiveBackIconColor =
-          backIconColor ?? Get.reactiveTheme.textTheme.bodyLarge!.color;
-
-      return AppBar(
-        backgroundColor: backgroundColor ?? Colors.transparent,
-        elevation: 0,
-        centerTitle: true,
-        toolbarHeight: 64,
-        scrolledUnderElevation: 0,
-        leading: showBackButton
-            ? ClickOpacity(
-                onTap: onBackPressed ?? () => Get.back(),
-                child: Transform.rotate(
-                  angle: LocalizationService.isRTL() ? pi : 0, // 180度 = π 弧度
-                  child: Icon(
-                    backIcon ?? Icons.arrow_back,
-                    color: effectiveBackIconColor,
-                    size: 24,
-                  ),
+    return AppBar(
+      backgroundColor:
+          backgroundColor ?? Get.reactiveTheme.scaffoldBackgroundColor,
+      elevation: 0,
+      centerTitle: true,
+      toolbarHeight: 60,
+      scrolledUnderElevation: 0,
+      leading: showBackButton
+          ? ClickOpacity(
+              onTap: onBackPressed ?? () => Get.back(),
+              child: Transform.rotate(
+                angle: LocalizationService.isRTL() ? pi : 0, // 180度 = π 弧度
+                child: Icon(
+                  backIcon ?? Icons.arrow_back,
+                  color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+                  size: 24,
                 ),
-              )
-            : null,
-        title: Text(
-          title,
-          style: TextStyle(
-            color: effectiveTitleColor,
-            fontSize: titleSize,
-            fontWeight: titleWeight,
-          ),
+              ),
+            )
+          : null,
+      title: Text(
+        title,
+        style: TextStyle(
+          color: Get.reactiveTheme.textTheme.bodyLarge!.color,
+          fontSize: titleSize,
+          fontWeight: titleWeight,
         ),
-        actions: actions,
-      );
-    });
+      ),
+      actions: actions,
+    );
   }
 
   @override
-  Size get preferredSize => const Size.fromHeight(64);
+  Size get preferredSize => const Size.fromHeight(60);
 }

+ 35 - 1
lib/app/widgets/ix_image.dart

@@ -1,3 +1,6 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:nomo/app/extensions/img_extension.dart';
@@ -6,9 +9,20 @@ import 'dart:io';
 
 import '../constants/configs.dart';
 
-enum ImageSourceType { network, asset, file }
+enum ImageSourceType { network, asset, file, memory }
 
 class IXImage extends StatelessWidget {
+  // 静态缓存,避免重复解码 base64 数据
+  static final Map<String, Uint8List> _memoryImageCache = {};
+
+  /// 清理内存图片缓存
+  static void clearMemoryCache() {
+    _memoryImageCache.clear();
+  }
+
+  /// 获取缓存大小
+  static int get cacheSize => _memoryImageCache.length;
+
   const IXImage({
     super.key,
     required this.source,
@@ -63,6 +77,7 @@ class IXImage extends StatelessWidget {
     }
 
     return ClipRRect(
+      key: ValueKey('ix_image_${source.hashCode}_${width}_$height'),
       clipBehavior: Clip.antiAlias,
       borderRadius: BorderRadius.circular(borderRadius ?? 0),
       child: _buildImage(context, memCacheWidth, memCacheHeight),
@@ -116,6 +131,25 @@ class IXImage extends StatelessWidget {
           errorBuilder: (context, error, stackTrace) =>
               _buildErrorWidget(context, source, error),
         );
+      case ImageSourceType.memory:
+        // 使用缓存避免重复解码
+        Uint8List imageBytes;
+        if (_memoryImageCache.containsKey(source)) {
+          imageBytes = _memoryImageCache[source]!;
+        } else {
+          imageBytes = base64Decode(source);
+          _memoryImageCache[source] = imageBytes;
+        }
+
+        return Image.memory(
+          imageBytes,
+          key: ValueKey('memory_${source.hashCode}'),
+          width: width,
+          height: height,
+          fit: fit ?? BoxFit.cover,
+          errorBuilder: (context, error, stackTrace) =>
+              _buildErrorWidget(context, source, error),
+        );
     }
   }
 

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

@@ -79,6 +79,7 @@ class DarkThemeColors {
   static const Color text1 = Color(0xFFFFFFFF);
   static const Color text2 = Color(0xFF646776);
   static const Color textDisable = Color(0xFF272A33);
+  static const Color textBrand = Color(0xFFFFFFFF);
   static const Color strokes1 = Color(0xFF32343C);
   static const Color strokes2 = Color(0xFF1C1E25);
 }

+ 6 - 0
lib/config/theme/ix_theme.dart

@@ -45,6 +45,12 @@ class IXTheme {
           ? LightThemeColors.hintTextColor
           : DarkThemeColors.hintTextColor,
 
+      canvasColor: isLight
+          ? LightThemeColors.bgDisable
+          : DarkThemeColors.bgDisable,
+
+      splashColor: isLight ? LightThemeColors.bgScrim : DarkThemeColors.bgScrim,
+
       // divider color
       dividerColor: isLight
           ? LightThemeColors.dividerColor

+ 4 - 0
lib/config/theme/light_theme_colors.dart

@@ -74,8 +74,12 @@ class LightThemeColors {
   static const Color bg1 = Color(0xFFFFFFFF);
   static const Color bg2 = Color(0xFFEFEFEF);
   static const Color bg3 = Color(0xFFF9FAFC);
+  static const Color bgDisable = Color(0xFFEFEFEF);
+  static const Color bgScrim = Color(0x99000000);
   static const Color text1 = Color(0xFF000000);
   static const Color text2 = Color(0xFF646776);
+  static const Color textDisable = Color(0xFFCECECE);
+  static const Color textBrand = Color(0xFFFFFFFF);
   static const Color strokes1 = Color(0xFFE6E7EB);
   static const Color strokes2 = Color(0xFFF2F1F4);
 }

+ 5 - 0
lib/main.dart

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 
 import 'app/app.dart';
+import 'app/constants/api_domains.dart';
 import 'app/controllers/api_controller.dart';
 import 'app/controllers/core_controller.dart';
 import 'app/data/sp/ix_sp.dart';
@@ -23,6 +24,7 @@ Future<void> main() async {
     },
     (error, stackTrace) {
       // TODO统一错误处理
+      log('main', 'Error: $error\n$stackTrace');
     },
   );
 }
@@ -41,6 +43,9 @@ Future<void> _initializeApp() async {
     // 3. 数据存储初始化
     await IXSP.init();
 
+    // 3.1 初始化ApiDomains
+    await ApiDomains.instance.init();
+
     // 4. 获取设备ID
     await DeviceManager.getDeviceId();
 

+ 21 - 6
lib/utils/crypto.dart

@@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart' as foundation;
 import 'package:crypto/crypto.dart' as crypto;
 import 'package:encrypt/encrypt.dart';
 
-class Crytpo {
+class Crypto {
   static String encrypt(String plainText, String keyText, String ivText) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
@@ -14,7 +14,10 @@ class Crytpo {
   }
 
   static Uint8List encryptUint8(
-      String plainText, String keyText, String ivText) {
+    String plainText,
+    String keyText,
+    String ivText,
+  ) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
     final cipher = Encrypter(AES(key, mode: AESMode.ctr, padding: null));
@@ -22,7 +25,10 @@ class Crytpo {
   }
 
   static String encryptBytes(
-      List<int> plainText, String keyText, String ivText) {
+    List<int> plainText,
+    String keyText,
+    String ivText,
+  ) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
     final cipher = Encrypter(AES(key, mode: AESMode.ctr, padding: null));
@@ -30,7 +36,10 @@ class Crytpo {
   }
 
   static Uint8List encryptBytesUint8(
-      List<int> plainText, String keyText, String ivText) {
+    List<int> plainText,
+    String keyText,
+    String ivText,
+  ) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
     final cipher = Encrypter(AES(key, mode: AESMode.ctr, padding: null));
@@ -46,7 +55,10 @@ class Crytpo {
   }
 
   static List<int> decryptBytes(
-      String cipherText, String keyText, String ivText) {
+    String cipherText,
+    String keyText,
+    String ivText,
+  ) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
     final cipher = Encrypter(AES(key, mode: AESMode.ctr, padding: null));
@@ -55,7 +67,10 @@ class Crytpo {
   }
 
   static List<int> decryptBytesUint8(
-      Uint8List cipherText, String keyText, String ivText) {
+    Uint8List cipherText,
+    String keyText,
+    String ivText,
+  ) {
     final key = Key(base64Decode(keyText));
     final iv = IV(base64Decode(ivText));
     final cipher = Encrypter(AES(key, mode: AESMode.ctr, padding: null));

+ 52 - 36
lib/utils/developer/ix_developer_tools.dart

@@ -13,8 +13,7 @@ import 'simple_api_card.dart';
 
 /// 简化版开发者工具 - 只包含控制台日志和API监控
 class IXDeveloperTools {
-  static final IXDeveloperTools _instance =
-      IXDeveloperTools._internal();
+  static final IXDeveloperTools _instance = IXDeveloperTools._internal();
   factory IXDeveloperTools() => _instance;
   IXDeveloperTools._internal();
 
@@ -42,8 +41,10 @@ class IXDeveloperTools {
   static void addApiRequest(ApiRequestInfo request) {
     _instance._apiRequests.insert(0, request);
     if (_instance._apiRequests.length > maxApiRequests) {
-      _instance._apiRequests
-          .removeRange(maxApiRequests, _instance._apiRequests.length);
+      _instance._apiRequests.removeRange(
+        maxApiRequests,
+        _instance._apiRequests.length,
+      );
     }
   }
 
@@ -67,8 +68,10 @@ class IXDeveloperTools {
   /// 显示开发者工具
   static void show() {
     try {
-      Get.to(() => const SimpleDeveloperToolsScreen(),
-          transition: Transition.cupertino);
+      Get.to(
+        () => const SimpleDeveloperToolsScreen(),
+        transition: Transition.cupertino,
+      );
     } catch (e) {
       log('IXDeveloperTools', 'Unable to open Developer Tools: $e');
     }
@@ -149,7 +152,9 @@ class SimpleApiMonitorInterceptor extends dio.Interceptor {
 
   @override
   void onRequest(
-      dio.RequestOptions options, dio.RequestInterceptorHandler handler) {
+    dio.RequestOptions options,
+    dio.RequestInterceptorHandler handler,
+  ) {
     final requestId = _generateRequestId();
     _requestStartTimes[requestId] = DateTime.now();
 
@@ -170,12 +175,15 @@ class SimpleApiMonitorInterceptor extends dio.Interceptor {
 
   @override
   void onResponse(
-      dio.Response response, dio.ResponseInterceptorHandler handler) {
+    dio.Response response,
+    dio.ResponseInterceptorHandler handler,
+  ) {
     final requestId = _findRequestId(response.requestOptions.uri.toString());
     if (requestId != null) {
       final startTime = _requestStartTimes.remove(requestId);
-      final duration =
-          startTime != null ? DateTime.now().difference(startTime) : null;
+      final duration = startTime != null
+          ? DateTime.now().difference(startTime)
+          : null;
 
       final updatedRequest = ApiRequestInfo(
         id: requestId,
@@ -201,8 +209,9 @@ class SimpleApiMonitorInterceptor extends dio.Interceptor {
     final requestId = _findRequestId(err.requestOptions.uri.toString());
     if (requestId != null) {
       final startTime = _requestStartTimes.remove(requestId);
-      final duration =
-          startTime != null ? DateTime.now().difference(startTime) : null;
+      final duration = startTime != null
+          ? DateTime.now().difference(startTime)
+          : null;
 
       final updatedRequest = ApiRequestInfo(
         id: requestId,
@@ -229,8 +238,11 @@ class SimpleApiMonitorInterceptor extends dio.Interceptor {
       final base64Data = base64Encode(encryptedData);
 
       // 2. AES 解密
-      final decryptedBytes =
-          Crytpo.decryptBytes(base64Data, Keys.aesKey, Keys.aesIv);
+      final decryptedBytes = Crypto.decryptBytes(
+        base64Data,
+        Keys.aesKey,
+        Keys.aesIv,
+      );
 
       // 3. GZip 解压
       final gzipDecoder = GZipDecoder();
@@ -275,7 +287,9 @@ class SimpleNoSignApiMonitorInterceptor extends dio.Interceptor {
 
   @override
   void onRequest(
-      dio.RequestOptions options, dio.RequestInterceptorHandler handler) {
+    dio.RequestOptions options,
+    dio.RequestInterceptorHandler handler,
+  ) {
     final requestId = _generateRequestId();
     _requestStartTimes[requestId] = DateTime.now();
 
@@ -294,12 +308,15 @@ class SimpleNoSignApiMonitorInterceptor extends dio.Interceptor {
 
   @override
   void onResponse(
-      dio.Response response, dio.ResponseInterceptorHandler handler) {
+    dio.Response response,
+    dio.ResponseInterceptorHandler handler,
+  ) {
     final requestId = _findRequestId(response.requestOptions.uri.toString());
     if (requestId != null) {
       final startTime = _requestStartTimes.remove(requestId);
-      final duration =
-          startTime != null ? DateTime.now().difference(startTime) : null;
+      final duration = startTime != null
+          ? DateTime.now().difference(startTime)
+          : null;
 
       final updatedRequest = ApiRequestInfo(
         id: requestId,
@@ -323,8 +340,9 @@ class SimpleNoSignApiMonitorInterceptor extends dio.Interceptor {
     final requestId = _findRequestId(err.requestOptions.uri.toString());
     if (requestId != null) {
       final startTime = _requestStartTimes.remove(requestId);
-      final duration =
-          startTime != null ? DateTime.now().difference(startTime) : null;
+      final duration = startTime != null
+          ? DateTime.now().difference(startTime)
+          : null;
 
       final updatedRequest = ApiRequestInfo(
         id: requestId,
@@ -481,18 +499,12 @@ class _SimpleDeveloperToolsScreenState extends State<SimpleDeveloperToolsScreen>
           gradient: LinearGradient(
             begin: Alignment.topCenter,
             end: Alignment.bottomCenter,
-            colors: [
-              Colors.grey,
-              Colors.blue,
-            ],
+            colors: [Colors.grey, Colors.blue],
           ),
         ),
         child: TabBarView(
           controller: _tabController,
-          children: const [
-            SimpleConsoleLogTab(),
-            SimpleApiMonitorTab(),
-          ],
+          children: const [SimpleConsoleLogTab(), SimpleApiMonitorTab()],
         ),
       ),
     );
@@ -556,13 +568,15 @@ class _SimpleConsoleLogTabState extends State<SimpleConsoleLogTab> {
         _filteredLogs = allLogs;
       } else {
         _filteredLogs = allLogs
-            .where((log) =>
-                log.message
-                    .toLowerCase()
-                    .contains(_searchController.text.toLowerCase()) ||
-                log.tag
-                    .toLowerCase()
-                    .contains(_searchController.text.toLowerCase()))
+            .where(
+              (log) =>
+                  log.message.toLowerCase().contains(
+                    _searchController.text.toLowerCase(),
+                  ) ||
+                  log.tag.toLowerCase().contains(
+                    _searchController.text.toLowerCase(),
+                  ),
+            )
             .toList();
       }
     });
@@ -777,7 +791,9 @@ class _SimpleApiMonitorTabState extends State<SimpleApiMonitorTab> {
                     const Spacer(),
                     Container(
                       padding: const EdgeInsets.symmetric(
-                          horizontal: 8, vertical: 4),
+                        horizontal: 8,
+                        vertical: 4,
+                      ),
                       decoration: BoxDecoration(
                         color: Colors.blue[100],
                         borderRadius: BorderRadius.circular(8),

+ 3 - 3
lib/utils/device_manager.dart

@@ -16,9 +16,9 @@ import 'permission_manager.dart';
 class DeviceManager {
   static const String TAG = 'DeviceManager';
   static final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
-  static const String _folderPrefix = '.fkey_cache_';
-  static const String _iosDeviceIdKey = 'fkey_device_id';
-  static const String _iosDeviceFolderKey = 'fkey_device_folder';
+  static const String _folderPrefix = '.omon_cache_';
+  static const String _iosDeviceIdKey = 'omon_device_id';
+  static const String _iosDeviceFolderKey = 'omon_device_folder';
 
   /// 获取设备唯一标识并确保文件夹存在
   static Future<String> getDeviceId() async {

+ 33 - 0
lib/utils/event_bus.dart

@@ -0,0 +1,33 @@
+import 'dart:async';
+
+import 'package:event_bus/event_bus.dart';
+import 'package:get/get.dart';
+
+import '../app/modules/splittunneling/selectapp/controllers/splittunneling_selectapp_controller.dart';
+
+final EventBus eventBus = EventBus();
+
+mixin EventBusMixin on GetxController {
+  final List<StreamSubscription> _subscriptions = [];
+
+  /// 监听某种事件类型
+  void listenEvent<T>(void Function(T event) handler) {
+    final sub = eventBus.on<T>().listen(handler);
+    _subscriptions.add(sub);
+  }
+
+  @override
+  void onClose() {
+    // 页面销毁时自动反注册
+    for (final sub in _subscriptions) {
+      sub.cancel();
+    }
+    _subscriptions.clear();
+    super.onClose();
+  }
+}
+
+class SplitTunnelingPageEvent {
+  final SplitTunnelingMode mode;
+  SplitTunnelingPageEvent({required this.mode});
+}

+ 123 - 0
lib/utils/file_cache_manager.dart

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

+ 179 - 0
lib/utils/file_stream_util.dart

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

+ 106 - 0
lib/utils/network_helper.dart

@@ -0,0 +1,106 @@
+import 'dart:io';
+
+import 'package:connectivity_plus/connectivity_plus.dart';
+import 'package:network_info_plus/network_info_plus.dart';
+
+class NetworkHelper {
+  static final NetworkHelper _instance = NetworkHelper._();
+  static NetworkHelper get instance => _instance;
+
+  final _connectivity = Connectivity();
+  final _networkInfo = NetworkInfo();
+
+  NetworkHelper._();
+
+  // 检查网络是否可用
+  Future<bool> isNetworkAvailable() async {
+    try {
+      final connectivityResult = await _connectivity.checkConnectivity();
+      if (connectivityResult.contains(ConnectivityResult.none)) {
+        return false;
+      }
+
+      // 如果是WiFi,进一步检查WiFi信息
+      if (connectivityResult.contains(ConnectivityResult.wifi)) {
+        final wifiName = await _networkInfo.getWifiName();
+        final wifiIP = await _networkInfo.getWifiIP();
+
+        // 如果无法获取WiFi名称和IP,可能表示网络异常
+        if (wifiName == null && wifiIP == null) {
+          return false;
+        }
+      }
+
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  // 获取当前网络类型
+  Future<String> getNetworkType() async {
+    try {
+      final connectivityResult = await _connectivity.checkConnectivity();
+      if (connectivityResult.contains(ConnectivityResult.mobile)) {
+        return 'Mobile';
+      } else if (connectivityResult.contains(ConnectivityResult.wifi)) {
+        final wifiName = await _networkInfo.getWifiName();
+        return 'WiFi${wifiName != null ? " ($wifiName)" : ""}';
+      } else if (connectivityResult.contains(ConnectivityResult.ethernet)) {
+        return 'Ethernet';
+      } else if (connectivityResult.contains(ConnectivityResult.vpn)) {
+        return 'VPN';
+      } else if (connectivityResult.contains(ConnectivityResult.bluetooth)) {
+        return 'Bluetooth';
+      } else if (connectivityResult.contains(ConnectivityResult.other)) {
+        return 'Other';
+      } else if (connectivityResult.contains(ConnectivityResult.none)) {
+        return 'No Network';
+      } else {
+        return 'Unknown';
+      }
+    } catch (e) {
+      return 'Error';
+    }
+  }
+
+  static final List<String> _commonVpnInterfaceNamePatterns = [
+    'tun', // Linux/Unix TUN interface
+    'tap', // Linux/Unix TAP interface
+    'ppp', // Point-to-Point Protocol
+    'pptp', // PPTP VPN
+    'l2tp', // L2TP VPN
+    'ipsec', // IPsec VPN
+    'vpn', // Generic "VPN" keyword
+    'wireguard', // WireGuard VPN
+    'openvpn', // OpenVPN VPN
+    'softether', // SoftEther VPN
+  ];
+
+  // 获取当前网络类型有没有包含VPN
+  Future<bool> isVPN() async {
+    try {
+      final interfaces = await NetworkInterface.list();
+      bool isIosDevice = Platform.isIOS;
+      return interfaces.any((interface) {
+        return _commonVpnInterfaceNamePatterns.any((pattern) {
+          if (isIosDevice &&
+              (interface.name.toLowerCase().contains('ipsec') ||
+                  interface.name.toLowerCase().contains('utun6') ||
+                  interface.name.toLowerCase().contains('ikev2') ||
+                  interface.name.toLowerCase().contains('l2tp'))) {
+            return false;
+          }
+          return interface.name.toLowerCase().contains(pattern);
+        });
+      });
+    } catch (e) {
+      return false;
+    }
+  }
+
+  // 监听网络状态变化
+  Stream<List<ConnectivityResult>> onConnectivityChanged() {
+    return _connectivity.onConnectivityChanged;
+  }
+}

+ 2 - 1
pigeons/core_api.dart

@@ -10,6 +10,7 @@ import 'package:pigeon/pigeon.dart';
 )
 @HostApi()
 abstract class CoreApi {
+  @async
   String? getApps();
   String? getSystemLocale();
   bool? connect();
@@ -22,7 +23,7 @@ abstract class CoreApi {
   bool? reconnect();
 }
 
-// 如果你需要让原生通知 Flutter 主题变化,可用 EventChannelApi
+// 如果你需要让原生通知 Flutter 事件变化,可用 EventChannelApi
 @EventChannelApi()
 abstract class CoreChangeEventApi {
   String onEventChange();

+ 16 - 0
pubspec.lock

@@ -33,6 +33,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.2.0"
+  animated_reorderable_list:
+    dependency: "direct main"
+    description:
+      name: animated_reorderable_list
+      sha256: "5de5cca556a8c9c8f7b65234ae4b683593dc6e167db498744a5e389302f24d13"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
   ansicolor:
     dependency: transitive
     description:
@@ -377,6 +385,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.0.3"
+  event_bus:
+    dependency: "direct main"
+    description:
+      name: event_bus
+      sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   fake_async:
     dependency: transitive
     description:

+ 3 - 1
pubspec.yaml

@@ -74,7 +74,9 @@ dependencies:
   play_install_referrer: ^0.5.0 # google play install referrer
   # infinite_scroll_pagination: ^5.1.1 # 无限滚动分页
   # custom_refresh_indicator: ^4.0.1 # 自定义刷新指示器
-  flutter_sticky_header: ^0.8.0
+  flutter_sticky_header: ^0.8.0 # 吸顶列表
+  event_bus: ^2.0.0 # 事件总线
+  animated_reorderable_list: ^1.3.0 # 可重新排序的动画列表
 
 dev_dependencies:
   flutter_test: