Quellcode durchsuchen

fix: 调整首页布局、更新连接按钮、去掉服务暂停计时

lilu vor 1 Monat
Ursprung
Commit
e46e9f8e7a

BIN
android/app/libs/libxray.aar


+ 3 - 7
android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt

@@ -48,8 +48,6 @@ data class TimerUpdateMessage(
     val type: String,
     val currentTime: Long,
     val mode: Int,
-    val isRunning: Boolean,
-    val isPaused: Boolean
 )
 
 data class BoostResultMessage(
@@ -106,13 +104,11 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
     }
     
     // 通知 Flutter 计时更新
-    private fun notifyTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean) {
+    private fun notifyTimerUpdate(currentTime: Long, mode: Int) {
         val timerMessage = TimerUpdateMessage(
             type = "timer_update",
             currentTime = currentTime,
             mode = mode,
-            isRunning = isRunning,
-            isPaused = isPaused
         )
         notifyFlutter(timerMessage)
     }
@@ -167,8 +163,8 @@ class CoreApiImpl(private val activity: Activity) : CoreApi {
                 notifyVpnStatusChange(status, code, message)
             }
             
-            override fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean) {
-                notifyTimerUpdate(currentTime, mode, isRunning, isPaused)
+            override fun onTimerUpdate(currentTime: Long, mode: Int) {
+                notifyTimerUpdate(currentTime, mode)
             }
 
             override fun onBoostResult(

+ 201 - 0
android/app/src/main/kotlin/app/xixi/nomo/CoreLogger.kt

@@ -0,0 +1,201 @@
+package app.xixi.nomo
+
+import android.annotation.SuppressLint
+import android.content.Context
+import org.json.JSONObject
+import java.io.File
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Core日志记录器 - 用于记录核心模块的stats和version信息
+ * 功能:
+ * 1. 记录 stats 统计信息
+ * 2. 记录 version 版本信息
+ * 3. 线程安全的文件读写
+ */
+class CoreLogger private constructor(context: Context) {
+    
+    private val context: Context = context.applicationContext
+    
+    // 全局文件锁,防止多个方法同时读写文件
+    private val fileLock = ReentrantLock()
+    
+    companion object {
+        private const val LOG_FOLDER = "boost_logs"
+        private const val CORE_LOG_FILE = "core.json"
+        private const val BUFFER_SIZE = 8192
+
+        @SuppressLint("StaticFieldLeak")
+        @Volatile
+        private var instance: CoreLogger? = null
+        
+        fun getInstance(context: Context): CoreLogger {
+            return instance ?: synchronized(this) {
+                instance ?: CoreLogger(context).also { instance = it }
+            }
+        }
+    }
+    
+    /**
+     * 获取core.json文件路径
+     */
+    private fun getCoreLogFile(): File {
+        val logDir = File(context.filesDir, LOG_FOLDER)
+        if (!logDir.exists()) {
+            logDir.mkdirs()
+        }
+        return File(logDir, CORE_LOG_FILE)
+    }
+    
+    /**
+     * 读取core.json文件内容
+     */
+    private fun readCoreJsonFile(): JSONObject {
+        val coreLogFile = getCoreLogFile()
+        if (!coreLogFile.exists()) {
+            return JSONObject()
+        }
+        
+        val content = coreLogFile.bufferedReader(bufferSize = BUFFER_SIZE).use { it.readText() }
+        
+        if (content.isEmpty()) {
+            return JSONObject()
+        }
+        
+        return try {
+            JSONObject(content)
+        } catch (e: Exception) {
+            VLog.e(TAG, "readCoreJsonFile parse error: ${e.message}")
+            JSONObject()
+        }
+    }
+    
+    /**
+     * 写入core.json文件内容
+     */
+    private fun writeCoreJsonFile(coreData: JSONObject) {
+        val coreLogFile = getCoreLogFile()
+        coreLogFile.bufferedWriter().use { writer ->
+            writer.write(coreData.toString(2))
+            writer.flush()
+        }
+    }
+    
+    /**
+     * 安全地更新core.json文件
+     */
+    private inline fun safeUpdateCoreJson(operationName: String, crossinline update: (JSONObject) -> Unit) {
+        fileLock.withLock {
+            try {
+                val coreData = readCoreJsonFile()
+                
+                // 执行具体的更新操作
+                try {
+                    update(coreData)
+                } catch (e: Exception) {
+                    VLog.e(TAG, "$operationName Error in update callback: ${e.message}")
+                    return
+                }
+                
+                // 保存更新后的文件
+                writeCoreJsonFile(coreData)
+                VLog.i(TAG, "$operationName successfully updated core.json")
+                
+            } catch (e: Exception) {
+                VLog.e(TAG, "$operationName Error updating core.json: ${e.message}")
+            }
+        }
+    }
+    
+    /**
+     * 安全地更新JSONObject
+     */
+    private fun safePut(jsonObject: JSONObject?, key: String, value: Any?, operationName: String) {
+        if (jsonObject == null) {
+            VLog.e(TAG, "$operationName JSONObject is null, cannot put key: $key")
+            return
+        }
+        
+        try {
+            jsonObject.put(key, value)
+        } catch (e: Exception) {
+            VLog.e(TAG, "$operationName Failed to put key '$key' with value '$value': ${e.message}")
+        }
+    }
+    
+    /**
+     * 解析stats字符串并将内容拆解存储到coreData中
+     * @param statsStr stats的JSON字符串
+     * @param coreData 目标JSONObject
+     * @param operationName 操作名称(用于日志)
+     */
+    private fun parseAndPutStats(statsStr: String?, coreData: JSONObject, operationName: String) {
+        if (statsStr.isNullOrEmpty()) {
+            VLog.w(TAG, "$operationName stats is null or empty")
+            return
+        }
+        
+        try {
+            val statsJson = JSONObject(statsStr)
+            // 遍历stats中的所有字段,直接拆解到coreData中
+            val keys = statsJson.keys()
+            while (keys.hasNext()) {
+                val key = keys.next()
+                val value = statsJson.get(key)
+                safePut(coreData, key, value, operationName)
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "$operationName Failed to parse stats: ${e.message}")
+        }
+    }
+
+    /**
+     * 同时更新stats和version信息
+     * @param stats 统计信息JSON字符串,内容会被拆解存储
+     * @param version 版本信息int
+     */
+    fun updateStatsAndVersion(stats: String?, version: Long) {
+        safeUpdateCoreJson("updateStatsAndVersion") { coreData ->
+            val currentTime = System.currentTimeMillis()
+            parseAndPutStats(stats, coreData, "updateStatsAndVersion")
+            safePut(coreData, "version", version, "updateStatsAndVersion")
+            safePut(coreData, "generatedTime", currentTime, "updateStatsAndVersion")
+        }
+    }
+    
+    /**
+     * 获取当前core.json的内容
+     * @return JSON字符串,如果文件不存在或读取失败返回null
+     */
+    fun getCoreLogContent(): String? {
+        return fileLock.withLock {
+            try {
+                val coreData = readCoreJsonFile()
+                coreData.toString(2)
+            } catch (e: Exception) {
+                VLog.e(TAG, "getCoreLogContent error: ${e.message}")
+                null
+            }
+        }
+    }
+    
+    /**
+     * 清空core.json文件
+     */
+    fun clearCoreLog() {
+        fileLock.withLock {
+            try {
+                val coreLogFile = getCoreLogFile()
+                if (coreLogFile.exists()) {
+                    coreLogFile.delete()
+                }
+                VLog.i(TAG, "clearCoreLog successfully cleared core.json")
+            } catch (e: Exception) {
+                VLog.e(TAG, "clearCoreLog error: ${e.message}")
+            }
+        }
+    }
+}
+
+private const val TAG = "CoreLogger"

+ 4 - 8
android/app/src/main/kotlin/app/xixi/nomo/NetworkReporter.kt

@@ -161,7 +161,7 @@ class NetworkReporter private constructor() {
             }
 
             if (!stats.isNullOrEmpty()) {
-                parseStatsJson(config, stats)
+                parseMaxSpeedJson(config, stats)
             }
             configureItemsParams(module, config)
             VLog.i(TAG, "Successfully parsed realtimeSpeed JSON")
@@ -184,7 +184,7 @@ class NetworkReporter private constructor() {
             }
 
             if (!stats.isNullOrEmpty()) {
-                parseStatsJson(config, stats)
+                parseMaxSpeedJson(config, stats)
                 parseConnectionHistory(config, stats)
             }
             configureItemsParams(module, config)
@@ -194,7 +194,7 @@ class NetworkReporter private constructor() {
         }
     }
 
-    private fun parseStatsJson(config: ItemsParamConfig, stats: String) {
+    private fun parseMaxSpeedJson(config: ItemsParamConfig, stats: String) {
         val jsonObject = JSONObject(stats)
         if (jsonObject.has("maxSpeed")) {
             val maxSpeedsObj = jsonObject.getJSONObject("maxSpeed")
@@ -299,10 +299,6 @@ class NetworkReporter private constructor() {
         }
     }
 
-    fun report(endpoint: String, jsonParams: String?, callback: ReportCallback?) {
-        report(endpoint, "FK_PeekLog", jsonParams, callback)
-    }
-
     fun buildRequestParams(module: String): String? {
         return try {
             val jsonObject = JSONObject()
@@ -323,7 +319,7 @@ class NetworkReporter private constructor() {
 
             connection.requestMethod = "POST"
             connection.setRequestProperty("Content-Type", "application/json")
-            connection.setRequestProperty("X-NL-Product-Code", "fkey")
+            connection.setRequestProperty("X-NL-Product-Code", "nomo")
             connection.setRequestProperty("X-NL-Content-Encoding", "gzip")
 
             token?.takeIf { it.isNotEmpty() }?.let {

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

@@ -23,7 +23,7 @@ class XRayApi {
 
     interface OnVpnServiceEvent {
         fun onVpnStatusChange(status: Long, code: Long, message: String)
-        fun onTimerUpdate(currentTime: Long, mode: Int, isRunning: Boolean, isPaused: Boolean)
+        fun onTimerUpdate(currentTime: Long, mode: Int)
         fun onBoostResult(param: String, success: Boolean, locationCode: String, nodeId: String)
     }
 
@@ -42,9 +42,7 @@ class XRayApi {
                 TIMER_BROADCAST -> {
                     val currentTime = intent.getLongExtra("currentTime", 0L)
                     val mode = intent.getIntExtra("mode", 0)
-                    val isRunning = intent.getBooleanExtra("isRunning", false)
-                    val isPaused = intent.getBooleanExtra("isPaused", false)
-                    vpnServiceEvent?.onTimerUpdate(currentTime, mode, isRunning, isPaused)
+                    vpnServiceEvent?.onTimerUpdate(currentTime, mode)
                 }
                 BOOST_RESULT_BROADCAST -> {
                     val param = intent.getStringExtra("param") ?: ""

+ 102 - 83
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -45,8 +45,7 @@ class XRayService : LifecycleVpnService() {
     private var timerMode = TIMER_MODE_NORMAL // 0: 普通计时, 1: 倒计时
     private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
     private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
-    private var timerPausedElapsed = 0L // 暂停时已经过的时间
-    private var isTimerPaused = false
+    private var peekElapsedSeconds = 0L // Peek 上报计数器(秒)
 
     private var xrayConfig: XrayConfig? = null
 
@@ -241,8 +240,9 @@ class XRayService : LifecycleVpnService() {
         val configJson = bundle.getString("config")
         xrayConfig = Gson().fromJson(configJson, XrayConfig::class.java)
 
-        if(xrayConfig == null) {
+        if (xrayConfig == null) {
             updateStatus(ServiceStatus.Failed)
+            // 未启动tun,通知flutter并且调用stopSelf关闭服务
             sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "参数异常")
             stopSelfService()
             return
@@ -297,8 +297,17 @@ class XRayService : LifecycleVpnService() {
         tunFd = builder.establish()
         tunFd?.let { tfd ->
             VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
-            tunnelService.startTunnel(this, xrayConfig!!.socksPort, xrayConfig!!.tunnelConfig, tfd.fd)
-            Ixvpn_mobile.proxyConnectorStart(xrayConfig!!.sessionId, xrayConfig!!.startOptions, vpnHandler)
+            tunnelService.startTunnel(
+                this,
+                xrayConfig!!.socksPort,
+                xrayConfig!!.tunnelConfig,
+                tfd.fd
+            )
+            Ixvpn_mobile.proxyConnectorStart(
+                xrayConfig!!.sessionId,
+                xrayConfig!!.startOptions,
+                vpnHandler
+            )
             VLog.i(TAG, "XRay proxy started successfully")
         } ?: run {
             VLog.e(TAG, "Failed to establish VPN tunnel")
@@ -333,7 +342,6 @@ class XRayService : LifecycleVpnService() {
             startTimer(mode, time)
             uploadBoostResult(true, code)
             AppLogger.getInstance(this).updateAppJsonStatusInfo(true, code)
-            dealStat()
         } else if (status == VPN_STATE_ERROR) {
             AppLogger.getInstance(this).updateAppJsonStatusInfo(false, code)
             uploadBoostResult(false, code)
@@ -366,7 +374,7 @@ class XRayService : LifecycleVpnService() {
                 setPackage(packageName)
                 putExtra("param", param)
                 putExtra("success", success)
-                putExtra("locationCode",  NetworkReporter.getInstance().getLocationCode())
+                putExtra("locationCode", NetworkReporter.getInstance().getLocationCode())
                 putExtra("nodeId", NetworkReporter.getInstance().getConnectedNodeId())
             }
             sendBroadcast(intent)
@@ -418,22 +426,29 @@ class XRayService : LifecycleVpnService() {
         // 停止计时
         stopTimer()
 
-        tunFd?.let { tfd ->
-            try {
-                tfd.close()
-                VLog.i(TAG, "TUN file descriptor closed")
-            } catch (e: Exception) {
-                VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
-            }
-            tunFd = null
-        }
-        VLog.i(TAG, "xray stopped")
+        dealStat()
 
+        // 注意:必须先停止 tunnel,再关闭 fd
+        // 否则 hev-socks5-tunnel 可能在读写 fd 时卡住
         try {
-            tunnelService.stopTunnel()
-            VLog.i(TAG, "Tunnel stopped")
+            // 使用单独线程执行 stopTunnel,避免阻塞,并添加超时
+            val stopThread = Thread {
+                try {
+                    tunnelService.stopTunnel()
+                    VLog.i(TAG, "Tunnel stopped")
+                } catch (e: Exception) {
+                    VLog.e(TAG, "停止 tunnel 失败", e)
+                }
+            }
+            stopThread.start()
+            // 等待最多 3 秒
+            stopThread.join(3000)
+            if (stopThread.isAlive) {
+                VLog.w(TAG, "stopTunnel 超时,强制继续")
+                stopThread.interrupt()
+            }
         } catch (e: Exception) {
-            VLog.e(TAG, "停止 tunnel 失败", e)
+            VLog.e(TAG, "停止 tunnel 异常", e)
         }
 
         try {
@@ -442,6 +457,18 @@ class XRayService : LifecycleVpnService() {
         } catch (e: Exception) {
             VLog.e(TAG, "停止 proxy connector 失败", e)
         }
+
+         // 关闭 TUN fd
+         tunFd?.let { tfd ->
+            try {
+                tfd.close()
+                VLog.i(TAG, "TUN file descriptor closed")
+            } catch (e: Exception) {
+                VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
+            }
+            tunFd = null
+        }
+        VLog.i(TAG, "xray stopped")
     }
 
 
@@ -475,11 +502,13 @@ class XRayService : LifecycleVpnService() {
         timerMode = mode
         timerInitialTime = initialTime
         timerBaseRealtime = SystemClock.elapsedRealtime()
-        timerPausedElapsed = 0L
-        isTimerPaused = false
+        peekElapsedSeconds = 0L // 重置 Peek 计数器
+
+        val peekInterval = xrayConfig?.peekTimeInterval ?: 0
+        VLog.i(TAG, "Peek 上报间隔: ${peekInterval}秒")
 
         timerJob = lifecycleScope.launch {
-            while (isActive && !isTimerPaused) {
+            while (isActive) {
                 // 计算从开始到现在经过的真实时间
                 val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
 
@@ -513,6 +542,16 @@ class XRayService : LifecycleVpnService() {
                 // 发送计时更新广播
                 sendTimerUpdate(currentTime)
 
+                // Peek 定时上报(屏幕关闭时不上报,节省资源)
+                peekElapsedSeconds++
+                if (peekInterval > 0 && peekElapsedSeconds >= peekInterval) {
+                    peekElapsedSeconds = 0L
+                    if (isScreenOn) {
+                        AppLogger.getInstance(this@XRayService).updateAppJsonStopTime()
+                        callPeekInterface()
+                    }
+                }
+
                 // 等待1秒
                 delay(1000L)
             }
@@ -528,59 +567,6 @@ class XRayService : LifecycleVpnService() {
         }
         timerJob?.cancel()
         timerJob = null
-        isTimerPaused = false
-        timerPausedElapsed = 0L
-    }
-
-    /**
-     * 暂停计时
-     */
-    private fun pauseTimer() {
-        if (timerJob?.isActive == true && !isTimerPaused) {
-            val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
-            timerPausedElapsed = elapsedTime
-
-            VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
-            isTimerPaused = true
-            timerJob?.cancel()
-            timerJob = null
-        }
-    }
-
-    /**
-     * 恢复计时
-     */
-    private fun resumeTimer() {
-        if (isTimerPaused) {
-            VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
-
-            // 恢复时重新设置基准时间,但要减去之前已经过的时间
-            timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
-            isTimerPaused = false
-
-            timerJob = lifecycleScope.launch {
-                while (isActive && !isTimerPaused) {
-                    val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
-
-                    val currentTime = if (timerMode == TIMER_MODE_NORMAL) {
-                        timerInitialTime + elapsedTime
-                    } else {
-                        timerInitialTime - elapsedTime
-                    }
-
-                    if (timerMode == TIMER_MODE_COUNTDOWN && currentTime <= 0) {
-                        VLog.i(TAG, "倒计时结束 - 关闭VPN")
-                        sendTimerUpdate(0L)
-                        stop()
-                        return@launch
-                    }
-
-                    updateTimerNotification(currentTime)
-                    sendTimerUpdate(currentTime)
-                    delay(1000L)
-                }
-            }
-        }
     }
 
     /**
@@ -597,8 +583,6 @@ class XRayService : LifecycleVpnService() {
             setPackage(packageName)  // 必须设置包名,否则 RECEIVER_NOT_EXPORTED 接收不到
             putExtra("currentTime", currentTime)
             putExtra("mode", timerMode)
-            putExtra("isRunning", timerJob?.isActive == true)
-            putExtra("isPaused", isTimerPaused)
         }
         sendBroadcast(intent)
     }
@@ -608,14 +592,11 @@ class XRayService : LifecycleVpnService() {
      */
     private fun updateTimerNotification(currentTime: Long) {
         val timeText = formatTime(currentTime)
-        val modeText = if (timerMode == TIMER_MODE_NORMAL) "计时中" else "倒计时"
-        val statusText = if (isTimerPaused) "已暂停" else "运行中"
-
         val notification = createConnectionNotification(
             this,
             NOTIFICATION_CHANNEL_ID,
-            "NOMO VPN $modeText",
-            "$timeText - $statusText",
+            "NOMO VPN",
+            "Running - $timeText",
         )
 
         val notificationManager = getSystemService(NotificationManager::class.java)
@@ -631,7 +612,45 @@ class XRayService : LifecycleVpnService() {
     private fun dealStat() {
         val queryTypes = byteArrayOf(1, 2, 3)
         val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
+        val version = Ixvpn_mobile.proxyConnectorVersion()
         VLog.i(Constant.TAG, "stats = $stats")
+        
+        // 将stats和version写入core.json日志文件
+        CoreLogger.getInstance(this).updateStatsAndVersion(stats, version)
+        // 更新停止时间
+        AppLogger.getInstance(this).updateAppJsonStopTime()
+    }
+
+    /**
+     * 调用 Peek 接口上报数据
+     */
+    private fun callPeekInterface() {
+        try {
+            // 获取实时流量数据
+            // 更新 实时流量
+            val queryTypes = byteArrayOf(3)
+            val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
+            VLog.i(TAG, "peek stats : $stats")
+
+            // 设置 Peek Log 参数
+            NetworkReporter.getInstance().addPeekLogParams(
+                "NM_PeekLog",
+                stats,
+                xrayConfig!!.sessionId
+            )
+
+            // 上报数据
+            NetworkReporter.getInstance().report(
+                "/api/v1/event",
+                "NM_PeekLog",
+                null,
+                null
+            )
+
+            VLog.i(TAG, "Peek log reported successfully")
+        } catch (e: Exception) {
+            VLog.e(TAG, "callPeekInterface error: ${e.message}")
+        }
     }
 
     /**

BIN
assets/images/identity/free.png


BIN
assets/images/identity/premium.png


BIN
assets/images/round/connected_round.png


BIN
assets/images/round/connected_switch.png


BIN
assets/images/round/connecting_round.png


BIN
assets/images/round/disconnected_round.png


BIN
assets/images/round/disconnected_switch.png


BIN
assets/images/subscription_bg.mp4


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

@@ -107,4 +107,16 @@ class Assets {
   static const String pushNotifications =
       'assets/vectors/push_notifications.svg';
   static const String update = 'assets/vectors/update.svg';
+
+  // 圆形连接按钮资源
+  static const String connectedRound =
+      'assets/images/round/connected_round.png';
+  static const String connectingRound =
+      'assets/images/round/connecting_round.png';
+  static const String disconnectedRound =
+      'assets/images/round/disconnected_round.png';
+  static const String connectedSwitch =
+      'assets/images/round/connected_switch.png';
+  static const String disconnectedSwitch =
+      'assets/images/round/disconnected_switch.png';
 }

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

@@ -524,6 +524,9 @@ class ApiController extends GetxService with WidgetsBindingObserver {
         final request = fp.toJson();
         request['locationId'] = locationId;
         request['locationCode'] = locationCode;
+        // 获取选中的路由模式
+        final routingMode = IXSP.getString("routing_mode_selected") ?? "smart";
+        request['routingMode'] = routingMode;
         final result = await ApiRouter().getDispatchInfo(
           request,
           cancelToken: cancelToken,

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

@@ -318,27 +318,15 @@ class CoreController extends GetxService {
   }
 
   void _handleTimerUpdate(TimerUpdateMessage message) {
-    // log(
-    //   TAG,
-    //   '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
-    // );
-
+    log(TAG, '计时更新: time=${message.currentTime}, mode=${message.mode}');
     timer = _formatTime(message.currentTime);
-
-    // 处理计时更新
-    if (message.isRunning) {
-      if (message.isPaused) {
-        _onTimerPaused(message.currentTime, message.mode);
-      } else {
-        _onTimerRunning(message.currentTime, message.mode);
-      }
-    } else {
-      _onTimerStopped();
-    }
   }
 
   void _handleBoostResult(BoostResultMessage message) async {
-    log(TAG, '加速结果: ${message.toString()}');
+    log(
+      TAG,
+      '加速结果: locationCode=${message.locationCode}, nodeId=${message.nodeId}, success=${message.success}',
+    );
     if (message.success) {
       try {
         await _apiController.connected({
@@ -427,25 +415,6 @@ class CoreController extends GetxService {
     }
   }
 
-  // 计时器状态处理方法
-  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();

+ 2 - 16
lib/app/data/models/vpn_message.dart

@@ -32,15 +32,11 @@ 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) {
@@ -48,19 +44,11 @@ class 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,
-    };
+    return {'type': type, 'currentTime': currentTime, 'mode': mode};
   }
 }
 
@@ -150,9 +138,7 @@ class VpnMessageHandler {
   }
 
   static void _handleTimerUpdate(TimerUpdateMessage message) {
-    print(
-      '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
-    );
+    print('计时更新: time=${message.currentTime}, mode=${message.mode}');
     // 处理计时更新
     // 例如:更新计时器显示、更新UI状态等
   }

+ 45 - 60
lib/app/modules/home/views/home_view.dart

@@ -14,9 +14,9 @@ import '../../../routes/app_pages.dart';
 import '../../../widgets/click_opacity.dart';
 import '../../../widgets/country_icon.dart';
 import '../../../widgets/ix_image.dart';
-import '../widgets/connection_button.dart';
 import '../controllers/home_controller.dart';
 
+import '../widgets/connection_round_button.dart';
 import '../widgets/menu_list.dart';
 
 class HomeView extends BaseView<HomeController> {
@@ -54,6 +54,20 @@ class HomeView extends BaseView<HomeController> {
                         ),
                       ),
                     ),
+                    Obx(
+                      () => Text(
+                        controller.apiController.isGuest &&
+                                !controller.apiController.isPremium &&
+                                controller.apiController.remainTimeSeconds > 0
+                            ? controller.apiController.remainTimeFormatted
+                            : controller.coreController.timer,
+                        style: TextStyle(
+                          fontSize: 28.sp,
+                          height: 1.2,
+                          color: Get.reactiveTheme.primaryColor,
+                        ),
+                      ),
+                    ),
                     ClickOpacity(
                       child: Padding(
                         padding: EdgeInsets.only(
@@ -78,7 +92,7 @@ class HomeView extends BaseView<HomeController> {
                     ),
                   ],
                 ),
-
+                20.verticalSpaceFromWidth,
                 Expanded(
                   child: SmartRefresher(
                     enablePullDown: true,
@@ -90,63 +104,8 @@ class HomeView extends BaseView<HomeController> {
                       child: Column(
                         crossAxisAlignment: CrossAxisAlignment.start,
                         children: [
-                          // 80.verticalSpaceFromWidth,
-                          Padding(
-                            padding: EdgeInsets.symmetric(vertical: 20.w),
-                            child: CarouselSlider(
-                              options: CarouselOptions(
-                                height: 80.w,
-                                viewportFraction: 1.0,
-                              ),
-                              items: [1, 2, 3, 4, 5].map((i) {
-                                return Builder(
-                                  builder: (BuildContext context) {
-                                    return IXImage(
-                                      source: Assets.bannerTest,
-                                      width: double.infinity,
-                                      height: 80.w,
-                                      sourceType: ImageSourceType.asset,
-                                      borderRadius: 14.r,
-                                    );
-                                  },
-                                );
-                              }).toList(),
-                            ),
-                          ),
-                          Text(
-                            controller.apiController.isGuest &&
-                                    !controller.apiController.isPremium &&
-                                    controller.apiController.remainTimeSeconds >
-                                        0
-                                ? Strings.remainTime.tr
-                                : Strings.activeTime.tr,
-                            style: TextStyle(
-                              fontSize: 18.sp,
-                              height: 1.3,
-                              color: Get.reactiveTheme.hintColor,
-                            ),
-                          ),
-                          2.verticalSpaceFromWidth,
-                          Obx(
-                            () => Text(
-                              controller.apiController.isGuest &&
-                                      !controller.apiController.isPremium &&
-                                      controller
-                                              .apiController
-                                              .remainTimeSeconds >
-                                          0
-                                  ? controller.apiController.remainTimeFormatted
-                                  : controller.coreController.timer,
-                              style: TextStyle(
-                                fontSize: 28.sp,
-                                height: 1.2,
-                                color: Get.reactiveTheme.primaryColor,
-                              ),
-                            ),
-                          ),
-
-                          20.verticalSpaceFromWidth,
                           // 位置选择按钮和最近位置(叠在一起的效果)
+                          20.verticalSpaceFromWidth,
                           Stack(
                             children: [
                               Container(
@@ -168,7 +127,33 @@ class HomeView extends BaseView<HomeController> {
               bottom: Platform.isAndroid ? 10.w : 0,
               left: 0,
               right: 0,
-              child: MenuList(),
+              child: Column(
+                children: [
+                  Padding(
+                    padding: EdgeInsets.symmetric(vertical: 14.w),
+                    child: CarouselSlider(
+                      options: CarouselOptions(
+                        height: 80.w,
+                        viewportFraction: 1.0,
+                      ),
+                      items: [1, 2, 3, 4, 5].map((i) {
+                        return Builder(
+                          builder: (BuildContext context) {
+                            return IXImage(
+                              source: Assets.bannerTest,
+                              width: double.infinity,
+                              height: 80.w,
+                              sourceType: ImageSourceType.asset,
+                              borderRadius: 14.r,
+                            );
+                          },
+                        );
+                      }).toList(),
+                    ),
+                  ),
+                  MenuList(),
+                ],
+              ),
             ),
           ],
         ),
@@ -178,7 +163,7 @@ class HomeView extends BaseView<HomeController> {
 
   Widget _buildConnectionButton() {
     return Obx(
-      () => ConnectionButton(
+      () => ConnectionRoundButton(
         state: controller.coreController.state,
         onTap: () {
           controller.setDefaultAutoConnect();

+ 2 - 0
lib/app/modules/home/widgets/connection_button.dart

@@ -318,6 +318,7 @@ class _ConnectionButtonState extends State<ConnectionButton>
           child: Row(
             key: ValueKey(text), // 使用文本作为 key,确保文字改变时触发动画
             mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.center,
             children: [
               IXImage(
                 source: statusImgPath,
@@ -331,6 +332,7 @@ class _ConnectionButtonState extends State<ConnectionButton>
                 style: TextStyle(
                   fontSize: 14.sp,
                   fontWeight: FontWeight.w500,
+                  height: 1.4,
                   color: textColor,
                 ),
               ),

+ 370 - 0
lib/app/modules/home/widgets/connection_round_button.dart

@@ -0,0 +1,370 @@
+import 'package:flutter/material.dart' hide ConnectionState;
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'dart:math' as math;
+import 'dart:async';
+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';
+import '../../../../config/translations/strings_enum.dart';
+import '../../../constants/enums.dart';
+
+class ConnectionRoundButton extends StatefulWidget {
+  final ConnectionState state;
+  final VoidCallback? onTap;
+
+  const ConnectionRoundButton({super.key, required this.state, this.onTap});
+
+  @override
+  State<ConnectionRoundButton> createState() => _ConnectionRoundButtonState();
+}
+
+class _ConnectionRoundButtonState extends State<ConnectionRoundButton>
+    with TickerProviderStateMixin {
+  late AnimationController _rotationController; // 旋转动画控制器
+  late AnimationController _fadeController; // 淡入淡出控制器
+  Timer? _connectingTimer; // 连接中状态的计时器
+  int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
+  ConnectionState? _previousState; // 保存前一个连接状态
+  bool _isStoppingRotation = false; // 是否正在停止旋转
+
+  @override
+  void initState() {
+    super.initState();
+    // 初始化旋转动画控制器
+    _rotationController = AnimationController(
+      duration: const Duration(milliseconds: 500),
+      vsync: this,
+    );
+
+    // 初始化淡入淡出控制器
+    _fadeController = AnimationController(
+      duration: const Duration(milliseconds: 600),
+      vsync: this,
+      value: 1.0,
+    );
+
+    // 如果初始状态是 connecting,启动动画
+    if (widget.state == ConnectionState.connecting) {
+      _rotationController.repeat();
+      _startConnectingTimer();
+    }
+  }
+
+  @override
+  void didUpdateWidget(ConnectionRoundButton oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    // 处理状态变化
+    if (oldWidget.state != widget.state) {
+      _handleStateChange(oldWidget.state);
+    }
+  }
+
+  void _handleStateChange(ConnectionState oldState) {
+    if (widget.state == ConnectionState.connecting) {
+      _isStoppingRotation = false;
+      if (!_rotationController.isAnimating) {
+        _rotationController.repeat();
+      }
+      if (_connectingTimer == null || !_connectingTimer!.isActive) {
+        _startConnectingTimer();
+      }
+    } else {
+      // 从连接中切换到其他状态时,平滑停止旋转
+      if (_rotationController.isAnimating && !_isStoppingRotation) {
+        _isStoppingRotation = true;
+        // 计算剩余角度,让动画平滑停止在顶部(0度位置)
+        final currentValue = _rotationController.value;
+        // 停止重复,然后平滑减速到完整的一圈
+        _rotationController.stop();
+        _rotationController
+            .animateTo(
+              1.0,
+              duration: Duration(
+                milliseconds: ((1.0 - currentValue) * 400).toInt().clamp(
+                  100,
+                  400,
+                ),
+              ),
+              curve: Curves.easeOutCubic,
+            )
+            .then((_) {
+              if (mounted) {
+                _rotationController.reset();
+                _isStoppingRotation = false;
+              }
+            });
+      }
+      _stopConnectingTimer();
+    }
+  }
+
+  @override
+  void dispose() {
+    _rotationController.dispose();
+    _fadeController.dispose();
+    _connectingTimer?.cancel();
+    super.dispose();
+  }
+
+  void _onTap() {
+    if (widget.onTap != null) {
+      widget.onTap!();
+    }
+  }
+
+  // 启动连接中状态的文本轮播计时器
+  void _startConnectingTimer() {
+    _connectingTimer?.cancel();
+    _connectingTextIndex = 0;
+    _connectingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+      if (mounted) {
+        setState(() {
+          // 每秒切换到下一个文本,循环显示0-4
+          _connectingTextIndex = (_connectingTextIndex + 1) % 5;
+        });
+      }
+    });
+  }
+
+  // 停止连接中状态的文本轮播计时器
+  void _stopConnectingTimer() {
+    _connectingTimer?.cancel();
+    _connectingTimer = null;
+    _connectingTextIndex = 0;
+  }
+
+  // 根据索引获取对应的连接文本
+  String _getConnectingText() {
+    switch (_connectingTextIndex) {
+      case 0:
+        return Strings.securingData.tr;
+      case 1:
+        return Strings.encryptingTraffic.tr;
+      case 2:
+        return Strings.protectingPrivacy.tr;
+      case 3:
+        return Strings.safeConnection.tr;
+      case 4:
+        return Strings.yourDataIsSafe.tr;
+      default:
+        return Strings.connecting.tr;
+    }
+  }
+
+  // 构建旋转的圆环
+  Widget _buildRoundRing(String svgPath, bool shouldRotate) {
+    final ringWidget = IXImage(
+      key: ValueKey('ring_$svgPath'),
+      source: svgPath,
+      sourceType: ImageSourceType.asset,
+      width: 170.w,
+      height: 170.w,
+    );
+
+    if (shouldRotate) {
+      return AnimatedBuilder(
+        key: const ValueKey('rotating_ring'),
+        animation: _rotationController,
+        builder: (context, child) {
+          return Transform.rotate(
+            angle: _rotationController.value * 2 * math.pi,
+            child: child,
+          );
+        },
+        child: ringWidget,
+      );
+    }
+
+    return ringWidget;
+  }
+
+  // 构建电源图标
+  Widget _buildPowerIcon(bool isConnected) {
+    return IXImage(
+      key: ValueKey('power_icon_$isConnected'),
+      source: isConnected ? Assets.connectedSwitch : Assets.disconnectedSwitch,
+      sourceType: ImageSourceType.asset,
+      width: 48.w,
+      height: 48.w,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(onTap: _onTap, child: _buildMainButton());
+  }
+
+  Widget _buildMainButton() {
+    // 根据状态获取对应的资源和样式
+    String ringPath;
+    String statusImgPath;
+    String text;
+    Color textColor;
+    bool isConnected;
+    bool shouldRotate;
+
+    switch (widget.state) {
+      case ConnectionState.disconnected:
+        ringPath = Assets.disconnectedRound;
+        statusImgPath = Assets.disconnected;
+        text = Strings.disconnected.tr;
+        textColor = Get.reactiveTheme.hintColor;
+        isConnected = false;
+        shouldRotate = false;
+        break;
+      case ConnectionState.connecting:
+        ringPath = Assets.connectingRound;
+        statusImgPath = Assets.connecting;
+        text = _getConnectingText(); // 使用轮播文本
+        textColor = Get.reactiveTheme.hintColor;
+        isConnected = false;
+        shouldRotate = true;
+        break;
+      case ConnectionState.connected:
+        ringPath = Assets.connectedRound;
+        statusImgPath = Assets.connected;
+        text = Strings.connected.tr;
+        textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
+        isConnected = true;
+        shouldRotate = false;
+        break;
+      case ConnectionState.error:
+        ringPath = Assets.disconnectedRound;
+        statusImgPath = Assets.error;
+        text = Strings.error.tr;
+        textColor = Get.reactiveTheme.hintColor;
+        isConnected = false;
+        shouldRotate = false;
+        break;
+    }
+
+    // 更新前一个状态
+    if (_previousState != widget.state) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (mounted) {
+          _previousState = widget.state;
+        }
+      });
+    }
+
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        // 圆形按钮区域
+        SizedBox(
+          width: 170.w,
+          height: 170.w,
+          child: Stack(
+            alignment: Alignment.center,
+            children: [
+              // 圆环背景 - 使用纯淡入淡出,无缩放,更自然
+              AnimatedSwitcher(
+                duration: const Duration(milliseconds: 600),
+                switchInCurve: Curves.easeInOut,
+                switchOutCurve: Curves.easeInOut,
+                transitionBuilder: (Widget child, Animation<double> animation) {
+                  return FadeTransition(
+                    opacity: CurvedAnimation(
+                      parent: animation,
+                      curve: Curves.easeInOut,
+                    ),
+                    child: child,
+                  );
+                },
+                layoutBuilder: (currentChild, previousChildren) {
+                  return Stack(
+                    alignment: Alignment.center,
+                    children: [
+                      ...previousChildren,
+                      if (currentChild != null) currentChild,
+                    ],
+                  );
+                },
+                child: _buildRoundRing(ringPath, shouldRotate),
+              ),
+              // 电源图标 - 纯淡入淡出
+              AnimatedSwitcher(
+                duration: const Duration(milliseconds: 500),
+                switchInCurve: Curves.easeInOut,
+                switchOutCurve: Curves.easeInOut,
+                transitionBuilder: (Widget child, Animation<double> animation) {
+                  return FadeTransition(
+                    opacity: CurvedAnimation(
+                      parent: animation,
+                      curve: Curves.easeInOut,
+                    ),
+                    child: child,
+                  );
+                },
+                layoutBuilder: (currentChild, previousChildren) {
+                  return Stack(
+                    alignment: Alignment.center,
+                    children: [
+                      ...previousChildren,
+                      if (currentChild != null) currentChild,
+                    ],
+                  );
+                },
+                child: _buildPowerIcon(isConnected),
+              ),
+            ],
+          ),
+        ),
+        20.verticalSpaceFromWidth,
+        // 状态文字
+        AnimatedSwitcher(
+          duration: const Duration(milliseconds: 350),
+          switchInCurve: Curves.easeOutCubic,
+          switchOutCurve: Curves.easeInCubic,
+          transitionBuilder: (Widget child, Animation<double> animation) {
+            return FadeTransition(
+              opacity: CurvedAnimation(
+                parent: animation,
+                curve: Curves.easeInOut,
+              ),
+              child: SlideTransition(
+                position:
+                    Tween<Offset>(
+                      begin: const Offset(0, 0.15),
+                      end: Offset.zero,
+                    ).animate(
+                      CurvedAnimation(
+                        parent: animation,
+                        curve: Curves.easeOutCubic,
+                      ),
+                    ),
+                child: child,
+              ),
+            );
+          },
+          child: Row(
+            key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画
+            mainAxisAlignment: MainAxisAlignment.center,
+            crossAxisAlignment: CrossAxisAlignment.center,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              IXImage(
+                source: statusImgPath,
+                sourceType: ImageSourceType.asset,
+                width: 14.w,
+                height: 14.w,
+              ),
+              4.horizontalSpace,
+              Text(
+                text,
+                style: TextStyle(
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w500,
+                  height: 1.4,
+                  color: textColor,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 20 - 6
lib/app/modules/routingmode/controllers/routingmode_controller.dart

@@ -1,28 +1,42 @@
 import 'package:get/get.dart';
+import 'package:nomo/app/data/sp/ix_sp.dart';
 
 enum RoutingMode { smart, global }
 
 class RoutingmodeController extends GetxController {
+  // 缓存键
+  static const String _selectedModeKey = 'routing_mode_selected';
+
   // 当前选中的路由模式,默认为Smart
   final selectedMode = RoutingMode.smart.obs;
 
   @override
   void onInit() {
     super.onInit();
+    _loadSelectedMode();
   }
 
-  @override
-  void onReady() {
-    super.onReady();
+  /// 加载保存的模式
+  void _loadSelectedMode() {
+    final modeString = IXSP.getString(_selectedModeKey);
+    if (modeString != null) {
+      selectedMode.value = modeString == 'global'
+          ? RoutingMode.global
+          : RoutingMode.smart;
+    }
   }
 
-  @override
-  void onClose() {
-    super.onClose();
+  /// 保存选中的模式
+  void _saveSelectedMode() {
+    final modeString = selectedMode.value == RoutingMode.global
+        ? 'global'
+        : 'smart';
+    IXSP.setString(_selectedModeKey, modeString);
   }
 
   /// 选择路由模式
   void selectMode(RoutingMode mode) {
     selectedMode.value = mode;
+    _saveSelectedMode();
   }
 }

+ 17 - 17
lib/app/modules/setting/views/setting_view.dart

@@ -315,23 +315,23 @@ class SettingView extends BaseView<SettingController> {
               ),
               _buildDivider(),
             ],
-            _buildSettingItem(
-              icon: IconFont.icon33,
-              iconColor: Get.reactiveTheme.primaryColor,
-              title: Strings.autoReconnect.tr,
-              trailing: Obx(
-                () => CupertinoSwitch(
-                  value: controller.autoReconnect,
-                  onChanged: (value) {
-                    controller.autoReconnect = value;
-                  },
-                  activeTrackColor: Get.reactiveTheme.shadowColor,
-                  thumbColor: Colors.white,
-                  inactiveThumbColor: Colors.white,
-                  inactiveTrackColor: Colors.grey,
-                ),
-              ),
-            ),
+            // _buildSettingItem(
+            //   icon: IconFont.icon33,
+            //   iconColor: Get.reactiveTheme.primaryColor,
+            //   title: Strings.autoReconnect.tr,
+            //   trailing: Obx(
+            //     () => CupertinoSwitch(
+            //       value: controller.autoReconnect,
+            //       onChanged: (value) {
+            //         controller.autoReconnect = value;
+            //       },
+            //       activeTrackColor: Get.reactiveTheme.shadowColor,
+            //       thumbColor: Colors.white,
+            //       inactiveThumbColor: Colors.white,
+            //       inactiveTrackColor: Colors.grey,
+            //     ),
+            //   ),
+            // ),
             _buildDivider(),
             _buildSettingItem(
               icon: IconFont.icon35,

+ 2 - 0
pubspec.yaml

@@ -119,6 +119,8 @@ flutter:
   assets:
     - assets/images/
     - assets/images/streaming/
+    - assets/images/identity/
+    - assets/images/round/
     - assets/images/html/
     - assets/vectors/
     - assets/vectors/boost/