diff --git a/flutter_app/.flutter-plugins-dependencies b/flutter_app/.flutter-plugins-dependencies
new file mode 100644
index 0000000..e7d553b
--- /dev/null
+++ b/flutter_app/.flutter-plugins-dependencies
@@ -0,0 +1 @@
+{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera_avfoundation","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_avfoundation-0.9.23+2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_darwin","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_darwin-7.0.3\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.7\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.24.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"camera_android_camerax","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_android_camerax-0.6.30\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_android-7.0.4\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_plugin_android_lifecycle-2.0.34\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_android-4.6.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.2.23\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-12.1.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.23\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.29\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"usb_serial","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\usb_serial-0.5.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_android","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_android-4.10.15\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_blue_plus_darwin","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_darwin-7.0.3\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_apple-2.3.13\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"webview_flutter_wkwebview","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\webview_flutter_wkwebview-3.24.2\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_blue_plus_linux","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_linux-7.0.3\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_pty","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"flutter_pty","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_pty-0.4.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_windows-0.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"camera_web","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\camera_web-0.3.5+3\\\\","dependencies":[],"dev_dependency":false},{"name":"flutter_blue_plus_web","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\flutter_blue_plus_web-7.0.2\\\\","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\geolocator_web-4.1.3\\\\","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.3+5\\\\","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\YouTube\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.2\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"camera","dependencies":["camera_android_camerax","camera_avfoundation","camera_web","flutter_plugin_android_lifecycle"]},{"name":"camera_android_camerax","dependencies":[]},{"name":"camera_avfoundation","dependencies":[]},{"name":"camera_web","dependencies":[]},{"name":"flutter_blue_plus","dependencies":["flutter_blue_plus_android","flutter_blue_plus_darwin","flutter_blue_plus_linux","flutter_blue_plus_web"]},{"name":"flutter_blue_plus_android","dependencies":[]},{"name":"flutter_blue_plus_darwin","dependencies":[]},{"name":"flutter_blue_plus_linux","dependencies":[]},{"name":"flutter_blue_plus_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_pty","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]},{"name":"usb_serial","dependencies":[]},{"name":"webview_flutter","dependencies":["webview_flutter_android","webview_flutter_wkwebview"]},{"name":"webview_flutter_android","dependencies":[]},{"name":"webview_flutter_wkwebview","dependencies":[]}],"date_created":"2026-03-31 11:11:33.051186","version":"3.41.6","swift_package_manager_enabled":{"ios":false,"macos":false}}
\ No newline at end of file
diff --git a/flutter_app/android/app/build.gradle b/flutter_app/android/app/build.gradle
index fd928c9..bd373e6 100644
--- a/flutter_app/android/app/build.gradle
+++ b/flutter_app/android/app/build.gradle
@@ -35,7 +35,7 @@ def hasKeystore = keystoreProperties.containsKey('storeFile') &&
android {
namespace = "com.nxg.openclawproot"
- compileSdk = 35
+ compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
diff --git a/flutter_app/android/app/jniLibs/arm64-v8a/libproot.so b/flutter_app/android/app/jniLibs/arm64-v8a/libproot.so
new file mode 100644
index 0000000..591fc56
Binary files /dev/null and b/flutter_app/android/app/jniLibs/arm64-v8a/libproot.so differ
diff --git a/flutter_app/android/app/jniLibs/arm64-v8a/libprootloader.so b/flutter_app/android/app/jniLibs/arm64-v8a/libprootloader.so
new file mode 100644
index 0000000..f123455
Binary files /dev/null and b/flutter_app/android/app/jniLibs/arm64-v8a/libprootloader.so differ
diff --git a/flutter_app/android/app/jniLibs/arm64-v8a/libtalloc.so b/flutter_app/android/app/jniLibs/arm64-v8a/libtalloc.so
new file mode 100644
index 0000000..e69de29
diff --git a/flutter_app/android/app/jniLibs/armeabi-v7a/libproot.so b/flutter_app/android/app/jniLibs/armeabi-v7a/libproot.so
new file mode 100644
index 0000000..a28666c
Binary files /dev/null and b/flutter_app/android/app/jniLibs/armeabi-v7a/libproot.so differ
diff --git a/flutter_app/android/app/jniLibs/armeabi-v7a/libprootloader.so b/flutter_app/android/app/jniLibs/armeabi-v7a/libprootloader.so
new file mode 100644
index 0000000..e7449aa
Binary files /dev/null and b/flutter_app/android/app/jniLibs/armeabi-v7a/libprootloader.so differ
diff --git a/flutter_app/android/app/jniLibs/armeabi-v7a/libtalloc.so b/flutter_app/android/app/jniLibs/armeabi-v7a/libtalloc.so
new file mode 100644
index 0000000..e69de29
diff --git a/flutter_app/android/app/jniLibs/x86_64/libproot.so b/flutter_app/android/app/jniLibs/x86_64/libproot.so
new file mode 100644
index 0000000..65e93a0
Binary files /dev/null and b/flutter_app/android/app/jniLibs/x86_64/libproot.so differ
diff --git a/flutter_app/android/app/jniLibs/x86_64/libprootloader.so b/flutter_app/android/app/jniLibs/x86_64/libprootloader.so
new file mode 100644
index 0000000..391b6b5
Binary files /dev/null and b/flutter_app/android/app/jniLibs/x86_64/libprootloader.so differ
diff --git a/flutter_app/android/app/jniLibs/x86_64/libtalloc.so b/flutter_app/android/app/jniLibs/x86_64/libtalloc.so
new file mode 100644
index 0000000..e69de29
diff --git a/flutter_app/android/app/src/main/AndroidManifest.xml b/flutter_app/android/app/src/main/AndroidManifest.xml
index 63c1511..9b8c648 100644
--- a/flutter_app/android/app/src/main/AndroidManifest.xml
+++ b/flutter_app/android/app/src/main/AndroidManifest.xml
@@ -65,6 +65,11 @@
android:exported="false"
android:foregroundServiceType="specialUse" />
+
+
{
result.success(TerminalSessionService.isRunning)
}
+ "startNineRouterService" -> {
+ try {
+ NineRouterService.start(applicationContext)
+ result.success(true)
+ } catch (e: Exception) {
+ result.error("SERVICE_ERROR", e.message, null)
+ }
+ }
+ "stopNineRouterService" -> {
+ try {
+ NineRouterService.stop(applicationContext)
+ result.success(true)
+ } catch (e: Exception) {
+ result.error("SERVICE_ERROR", e.message, null)
+ }
+ }
+ "isNineRouterServiceRunning" -> {
+ result.success(NineRouterService.isRunning)
+ }
"startNodeService" -> {
try {
NodeForegroundService.start(applicationContext)
diff --git a/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/NineRouterService.kt b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/NineRouterService.kt
new file mode 100644
index 0000000..f20343f
--- /dev/null
+++ b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/NineRouterService.kt
@@ -0,0 +1,110 @@
+package com.nxg.openclawproot
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+
+class NineRouterService : Service() {
+ companion object {
+ const val NOTIFICATION_ID = 7
+ var isRunning = false
+ private set
+
+ fun start(context: Context) {
+ val intent = Intent(context, NineRouterService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+
+ fun stop(context: Context) {
+ val intent = Intent(context, NineRouterService::class.java)
+ context.stopService(intent)
+ }
+ }
+
+ private var wakeLock: PowerManager.WakeLock? = null
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ startForeground(NOTIFICATION_ID, buildNotification())
+ if (isRunning) return START_STICKY
+ isRunning = true
+ acquireWakeLock()
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ isRunning = false
+ releaseWakeLock()
+ super.onDestroy()
+ }
+
+ private fun acquireWakeLock() {
+ releaseWakeLock()
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+ wakeLock = pm.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "OpenClaw::NineRouterWakeLock"
+ )
+ wakeLock?.acquire(24 * 60 * 60 * 1000L)
+ }
+
+ private fun releaseWakeLock() {
+ wakeLock?.let { if (it.isHeld) it.release() }
+ wakeLock = null
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ GatewayService.CHANNEL_ID,
+ "OpenClaw Gateway",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun buildNotification(): Notification {
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification.Builder(this, GatewayService.CHANNEL_ID)
+ .setContentTitle("9Router")
+ .setContentText("9Router is running on port 20128")
+ .setSmallIcon(android.R.drawable.ic_menu_manage)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .build()
+ } else {
+ @Suppress("DEPRECATION")
+ Notification.Builder(this)
+ .setContentTitle("9Router")
+ .setContentText("9Router is running on port 20128")
+ .setSmallIcon(android.R.drawable.ic_menu_manage)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .build()
+ }
+ }
+}
diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.jar b/flutter_app/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/flutter_app/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties
index 7bb2df6..efdcc4a 100644
--- a/flutter_app/android/gradle/wrapper/gradle-wrapper.properties
+++ b/flutter_app/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
diff --git a/flutter_app/android/gradlew b/flutter_app/android/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/flutter_app/android/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/flutter_app/android/gradlew.bat b/flutter_app/android/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/flutter_app/android/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/flutter_app/android/settings.gradle b/flutter_app/android/settings.gradle
index 54b690d..b507b94 100644
--- a/flutter_app/android/settings.gradle
+++ b/flutter_app/android/settings.gradle
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.1.0" apply false
- id "org.jetbrains.kotlin.android" version "1.9.0" apply false
+ id "com.android.application" version "8.9.1" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"
diff --git a/flutter_app/lib/app.dart b/flutter_app/lib/app.dart
index 5732c6a..9512211 100644
--- a/flutter_app/lib/app.dart
+++ b/flutter_app/lib/app.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'providers/setup_provider.dart';
@@ -95,7 +95,7 @@ class OpenClawApp extends StatelessWidget {
color: Colors.white,
),
),
- cardTheme: CardTheme(
+ cardTheme: CardThemeData(
elevation: 0,
color: AppColors.darkSurface,
shape: RoundedRectangleBorder(
@@ -165,7 +165,7 @@ class OpenClawApp extends StatelessWidget {
color: AppColors.darkBorder,
space: 1,
),
- dialogTheme: DialogTheme(
+ dialogTheme: DialogThemeData(
backgroundColor: AppColors.darkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -226,7 +226,7 @@ class OpenClawApp extends StatelessWidget {
color: const Color(0xFF0A0A0A),
),
),
- cardTheme: CardTheme(
+ cardTheme: CardThemeData(
elevation: 0,
color: AppColors.lightBg,
shape: RoundedRectangleBorder(
@@ -296,7 +296,7 @@ class OpenClawApp extends StatelessWidget {
color: AppColors.lightBorder,
space: 1,
),
- dialogTheme: DialogTheme(
+ dialogTheme: DialogThemeData(
backgroundColor: AppColors.lightBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
diff --git a/flutter_app/lib/constants.dart b/flutter_app/lib/constants.dart
index 3beefbd..44619cf 100644
--- a/flutter_app/lib/constants.dart
+++ b/flutter_app/lib/constants.dart
@@ -31,7 +31,7 @@ class AppConstants {
static const String rootfsArmhf = '${ubuntuRootfsUrl}armhf.tar.gz';
static const String rootfsAmd64 = '${ubuntuRootfsUrl}amd64.tar.gz';
- // Node.js binary tarball — downloaded directly by Flutter, extracted by Java.
+ // Node.js binary tarball ?downloaded directly by Flutter, extracted by Java.
// Bypasses curl/gpg/NodeSource which fail inside proot.
static const String nodeVersion = '22.14.0';
static const String nodeBaseUrl =
diff --git a/flutter_app/lib/l10n/app_en.arb b/flutter_app/lib/l10n/app_en.arb
new file mode 100644
index 0000000..fa1840b
--- /dev/null
+++ b/flutter_app/lib/l10n/app_en.arb
@@ -0,0 +1,171 @@
+{
+ "@@locale": "en",
+ "appName": "OpenClaw",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "retry": "Retry",
+ "done": "Done",
+ "save": "Save",
+ "remove": "Remove",
+ "install": "Install",
+ "loading": "Loading...",
+ "error": "Error",
+ "copy": "Copy",
+ "paste": "Paste",
+ "openUrl": "Open URL",
+ "screenshot": "Screenshot",
+ "restart": "Restart",
+ "later": "Later",
+ "download": "Download",
+ "copied": "Copied to clipboard",
+ "noUrlFound": "No URL found in selection",
+ "linkCopied": "Link copied",
+ "openLink": "Open Link",
+ "aiGatewayForAndroid": "AI Gateway for Android",
+ "checkingSetup": "Checking setup status...",
+ "repairingBypass": "Repairing bionic bypass...",
+ "reinstallingNode": "Reinstalling Node.js...",
+ "reinstallingOpenClaw": "Reinstalling OpenClaw...",
+ "quickActions": "QUICK ACTIONS",
+ "terminal": "Terminal",
+ "terminalSubtitle": "Open Ubuntu shell with OpenClaw",
+ "webDashboard": "Web Dashboard",
+ "webDashboardSubtitle": "Open OpenClaw dashboard in browser",
+ "startGatewayFirst": "Start gateway first",
+ "onboarding": "Onboarding",
+ "onboardingSubtitle": "Configure API keys and binding",
+ "configure": "Configure",
+ "configureSubtitle": "Manage gateway settings",
+ "aiProviders": "AI Providers",
+ "aiProvidersSubtitle": "Configure models and API keys",
+ "packages": "Packages",
+ "packagesSubtitle": "Install optional tools (Go, Homebrew, SSH)",
+ "sshAccess": "SSH Access",
+ "sshAccessSubtitle": "Remote terminal access via SSH",
+ "logs": "Logs",
+ "logsSubtitle": "View gateway output and errors",
+ "snapshot": "Snapshot",
+ "snapshotSubtitle": "Backup or restore your config",
+ "node": "Node",
+ "nodeConnected": "Connected to gateway",
+ "nodeCapabilities": "Device capabilities for AI",
+ "dashboardUrlCopied": "Dashboard URL copied",
+ "copyDashboardUrl": "Copy dashboard URL",
+ "cliProxy": "CLIProxy Manager",
+ "cliProxySubtitle": "Manage free AI account proxy",
+ "gateway": "Gateway",
+ "startGateway": "Start Gateway",
+ "stopGateway": "Stop Gateway",
+ "viewLogs": "View Logs",
+ "urlCopied": "URL copied to clipboard",
+ "copyUrl": "Copy URL",
+ "openDashboard": "Open dashboard",
+ "gatewayRunning": "Running",
+ "gatewayStarting": "Starting",
+ "gatewayError": "Error",
+ "gatewayStopped": "Stopped",
+ "enableNode": "Enable Node",
+ "disableNode": "Disable Node",
+ "reconnect": "Reconnect",
+ "nodePaired": "Paired",
+ "nodeConnecting": "Connecting",
+ "nodeDisconnected": "Disconnected",
+ "nodeDisabled": "Disabled",
+ "nodeConfigure": "Configure",
+ "settings": "Settings",
+ "general": "GENERAL",
+ "autoStartGateway": "Auto-start gateway",
+ "autoStartSubtitle": "Start the gateway when the app opens",
+ "batteryOptimization": "Battery Optimization",
+ "batteryOptimized": "Optimized (may kill background sessions)",
+ "batteryUnrestricted": "Unrestricted (recommended)",
+ "setupStorage": "Setup Storage",
+ "storageGranted": "Granted — proot can access /sdcard. Revoke if not needed.",
+ "storageNotGranted": "Allow access to shared storage",
+ "nodeSection": "NODE",
+ "enableNodeTitle": "Enable Node",
+ "enableNodeSubtitle": "Provide device capabilities to the gateway",
+ "nodeConfiguration": "Node Configuration",
+ "nodeConfigSubtitle": "Connection, pairing, and capabilities",
+ "systemInfo": "SYSTEM INFO",
+ "architecture": "Architecture",
+ "prootPath": "PRoot path",
+ "rootfs": "Rootfs",
+ "installed": "Installed",
+ "notInstalled": "Not installed",
+ "maintenance": "MAINTENANCE",
+ "exportSnapshot": "Export Snapshot",
+ "exportSnapshotSubtitle": "Backup config to Downloads",
+ "importSnapshot": "Import Snapshot",
+ "importSnapshotSubtitle": "Restore config from backup",
+ "rerunSetup": "Re-run setup",
+ "rerunSetupSubtitle": "Reinstall or repair the environment",
+ "about": "ABOUT",
+ "checkForUpdates": "Check for Updates",
+ "checkUpdatesSubtitle": "Check GitHub for a newer release",
+ "developer": "Developer",
+ "github": "GitHub",
+ "contact": "Contact",
+ "license": "License",
+ "updateAvailable": "Update Available",
+ "currentVersion": "Current",
+ "latestVersion": "Latest",
+ "alreadyLatest": "You're on the latest version",
+ "checkUpdateFailed": "Could not check for updates",
+ "snapshotSaved": "Snapshot saved to",
+ "exportFailed": "Export failed",
+ "noSnapshotFound": "No snapshot found at",
+ "snapshotRestored": "Snapshot restored successfully. Restart the gateway to apply.",
+ "importFailed": "Import failed",
+ "setupOpenClaw": "Setup OpenClaw",
+ "setupRunning": "Setting up the environment. This may take several minutes.",
+ "setupDescription": "This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.",
+ "downloadRootfs": "Download Ubuntu rootfs",
+ "extractRootfs": "Extract rootfs",
+ "installNode": "Install Node.js",
+ "installOpenClaw": "Install OpenClaw",
+ "configureBionicBypass": "Configure Bionic Bypass",
+ "setupComplete": "Setup complete!",
+ "configureApiKeys": "Configure API Keys",
+ "beginSetup": "Begin Setup",
+ "retrySetup": "Retry Setup",
+ "storageRequired": "Requires ~500MB of storage and an internet connection",
+ "optionalPackages": "OPTIONAL PACKAGES",
+ "gatewayLogs": "Gateway Logs",
+ "filterLogs": "Filter logs...",
+ "noLogsYet": "No logs yet. Start the gateway.",
+ "noMatchingLogs": "No matching logs.",
+ "copyAllLogs": "Copy all logs",
+ "autoScrollOn": "Auto-scroll on",
+ "autoScrollOff": "Auto-scroll off",
+ "activeModel": "Active Model",
+ "selectProvider": "Select a provider to configure its API key and model.",
+ "active": "Active",
+ "configured": "Configured",
+ "apiKey": "API Key",
+ "model": "Model",
+ "customModel": "Custom...",
+ "customModelHint": "e.g. meta/llama-3.3-70b-instruct",
+ "customModelLabel": "Custom model name",
+ "saveAndActivate": "Save & Activate",
+ "removeConfiguration": "Remove Configuration",
+ "apiKeyEmpty": "API key cannot be empty",
+ "modelEmpty": "Model name cannot be empty",
+ "configuredAndActivated": "configured and activated",
+ "saveFailed": "Failed to save",
+ "removeFailed": "Failed to remove",
+ "removeProvider": "Remove",
+ "removeProviderContent": "This will delete the API key and deactivate the model.",
+ "startingTerminal": "Starting terminal...",
+ "failedToStartTerminal": "Failed to start terminal",
+ "openClawOnboarding": "OpenClaw Onboarding",
+ "startingOnboarding": "Starting onboarding...",
+ "failedToStartOnboarding": "Failed to start onboarding",
+ "goToDashboard": "Go to Dashboard",
+ "cliProxyManagement": "CLIProxy Management",
+ "cliProxyNotRunning": "CLIProxy service is not running",
+ "openInBrowser": "Open in browser",
+ "refresh": "Refresh",
+ "reconnectProxy": "Reconnect",
+ "installedBadge": "Installed"
+}
diff --git a/flutter_app/lib/l10n/app_localizations.dart b/flutter_app/lib/l10n/app_localizations.dart
new file mode 100644
index 0000000..186d611
--- /dev/null
+++ b/flutter_app/lib/l10n/app_localizations.dart
@@ -0,0 +1,1141 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:intl/intl.dart' as intl;
+
+import 'app_localizations_en.dart';
+import 'app_localizations_zh.dart';
+
+// ignore_for_file: type=lint
+
+/// Callers can lookup localized strings with an instance of AppLocalizations
+/// returned by `AppLocalizations.of(context)`.
+///
+/// Applications need to include `AppLocalizations.delegate()` in their app's
+/// `localizationDelegates` list, and the locales they support in the app's
+/// `supportedLocales` list. For example:
+///
+/// ```dart
+/// import 'l10n/app_localizations.dart';
+///
+/// return MaterialApp(
+/// localizationsDelegates: AppLocalizations.localizationsDelegates,
+/// supportedLocales: AppLocalizations.supportedLocales,
+/// home: MyApplicationHome(),
+/// );
+/// ```
+///
+/// ## Update pubspec.yaml
+///
+/// Please make sure to update your pubspec.yaml to include the following
+/// packages:
+///
+/// ```yaml
+/// dependencies:
+/// # Internationalization support.
+/// flutter_localizations:
+/// sdk: flutter
+/// intl: any # Use the pinned version from flutter_localizations
+///
+/// # Rest of dependencies
+/// ```
+///
+/// ## iOS Applications
+///
+/// iOS applications define key application metadata, including supported
+/// locales, in an Info.plist file that is built into the application bundle.
+/// To configure the locales supported by your app, you’ll need to edit this
+/// file.
+///
+/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
+/// Then, in the Project Navigator, open the Info.plist file under the Runner
+/// project’s Runner folder.
+///
+/// Next, select the Information Property List item, select Add Item from the
+/// Editor menu, then select Localizations from the pop-up menu.
+///
+/// Select and expand the newly-created Localizations item then, for each
+/// locale your application supports, add a new item and select the locale
+/// you wish to add from the pop-up menu in the Value field. This list should
+/// be consistent with the languages listed in the AppLocalizations.supportedLocales
+/// property.
+abstract class AppLocalizations {
+ AppLocalizations(String locale)
+ : localeName = intl.Intl.canonicalizedLocale(locale.toString());
+
+ final String localeName;
+
+ static AppLocalizations? of(BuildContext context) {
+ return Localizations.of(context, AppLocalizations);
+ }
+
+ static const LocalizationsDelegate delegate =
+ _AppLocalizationsDelegate();
+
+ /// A list of this localizations delegate along with the default localizations
+ /// delegates.
+ ///
+ /// Returns a list of localizations delegates containing this delegate along with
+ /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
+ /// and GlobalWidgetsLocalizations.delegate.
+ ///
+ /// Additional delegates can be added by appending to this list in
+ /// MaterialApp. This list does not have to be used at all if a custom list
+ /// of delegates is preferred or required.
+ static const List> localizationsDelegates =
+ >[
+ delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ];
+
+ /// A list of this localizations delegate's supported locales.
+ static const List supportedLocales = [
+ Locale('en'),
+ Locale('zh')
+ ];
+
+ /// No description provided for @appName.
+ ///
+ /// In en, this message translates to:
+ /// **'OpenClaw'**
+ String get appName;
+
+ /// No description provided for @cancel.
+ ///
+ /// In en, this message translates to:
+ /// **'Cancel'**
+ String get cancel;
+
+ /// No description provided for @confirm.
+ ///
+ /// In en, this message translates to:
+ /// **'Confirm'**
+ String get confirm;
+
+ /// No description provided for @retry.
+ ///
+ /// In en, this message translates to:
+ /// **'Retry'**
+ String get retry;
+
+ /// No description provided for @done.
+ ///
+ /// In en, this message translates to:
+ /// **'Done'**
+ String get done;
+
+ /// No description provided for @save.
+ ///
+ /// In en, this message translates to:
+ /// **'Save'**
+ String get save;
+
+ /// No description provided for @remove.
+ ///
+ /// In en, this message translates to:
+ /// **'Remove'**
+ String get remove;
+
+ /// No description provided for @install.
+ ///
+ /// In en, this message translates to:
+ /// **'Install'**
+ String get install;
+
+ /// No description provided for @loading.
+ ///
+ /// In en, this message translates to:
+ /// **'Loading...'**
+ String get loading;
+
+ /// No description provided for @error.
+ ///
+ /// In en, this message translates to:
+ /// **'Error'**
+ String get error;
+
+ /// No description provided for @copy.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy'**
+ String get copy;
+
+ /// No description provided for @paste.
+ ///
+ /// In en, this message translates to:
+ /// **'Paste'**
+ String get paste;
+
+ /// No description provided for @openUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'Open URL'**
+ String get openUrl;
+
+ /// No description provided for @screenshot.
+ ///
+ /// In en, this message translates to:
+ /// **'Screenshot'**
+ String get screenshot;
+
+ /// No description provided for @restart.
+ ///
+ /// In en, this message translates to:
+ /// **'Restart'**
+ String get restart;
+
+ /// No description provided for @later.
+ ///
+ /// In en, this message translates to:
+ /// **'Later'**
+ String get later;
+
+ /// No description provided for @download.
+ ///
+ /// In en, this message translates to:
+ /// **'Download'**
+ String get download;
+
+ /// No description provided for @copied.
+ ///
+ /// In en, this message translates to:
+ /// **'Copied to clipboard'**
+ String get copied;
+
+ /// No description provided for @noUrlFound.
+ ///
+ /// In en, this message translates to:
+ /// **'No URL found in selection'**
+ String get noUrlFound;
+
+ /// No description provided for @linkCopied.
+ ///
+ /// In en, this message translates to:
+ /// **'Link copied'**
+ String get linkCopied;
+
+ /// No description provided for @openLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Open Link'**
+ String get openLink;
+
+ /// No description provided for @aiGatewayForAndroid.
+ ///
+ /// In en, this message translates to:
+ /// **'AI Gateway for Android'**
+ String get aiGatewayForAndroid;
+
+ /// No description provided for @checkingSetup.
+ ///
+ /// In en, this message translates to:
+ /// **'Checking setup status...'**
+ String get checkingSetup;
+
+ /// No description provided for @repairingBypass.
+ ///
+ /// In en, this message translates to:
+ /// **'Repairing bionic bypass...'**
+ String get repairingBypass;
+
+ /// No description provided for @reinstallingNode.
+ ///
+ /// In en, this message translates to:
+ /// **'Reinstalling Node.js...'**
+ String get reinstallingNode;
+
+ /// No description provided for @reinstallingOpenClaw.
+ ///
+ /// In en, this message translates to:
+ /// **'Reinstalling OpenClaw...'**
+ String get reinstallingOpenClaw;
+
+ /// No description provided for @quickActions.
+ ///
+ /// In en, this message translates to:
+ /// **'QUICK ACTIONS'**
+ String get quickActions;
+
+ /// No description provided for @terminal.
+ ///
+ /// In en, this message translates to:
+ /// **'Terminal'**
+ String get terminal;
+
+ /// No description provided for @terminalSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Open Ubuntu shell with OpenClaw'**
+ String get terminalSubtitle;
+
+ /// No description provided for @webDashboard.
+ ///
+ /// In en, this message translates to:
+ /// **'Web Dashboard'**
+ String get webDashboard;
+
+ /// No description provided for @webDashboardSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Open OpenClaw dashboard in browser'**
+ String get webDashboardSubtitle;
+
+ /// No description provided for @startGatewayFirst.
+ ///
+ /// In en, this message translates to:
+ /// **'Start gateway first'**
+ String get startGatewayFirst;
+
+ /// No description provided for @onboarding.
+ ///
+ /// In en, this message translates to:
+ /// **'Onboarding'**
+ String get onboarding;
+
+ /// No description provided for @onboardingSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure API keys and binding'**
+ String get onboardingSubtitle;
+
+ /// No description provided for @configure.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure'**
+ String get configure;
+
+ /// No description provided for @configureSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Manage gateway settings'**
+ String get configureSubtitle;
+
+ /// No description provided for @aiProviders.
+ ///
+ /// In en, this message translates to:
+ /// **'AI Providers'**
+ String get aiProviders;
+
+ /// No description provided for @aiProvidersSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure models and API keys'**
+ String get aiProvidersSubtitle;
+
+ /// No description provided for @packages.
+ ///
+ /// In en, this message translates to:
+ /// **'Packages'**
+ String get packages;
+
+ /// No description provided for @packagesSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Install optional tools (Go, Homebrew, SSH)'**
+ String get packagesSubtitle;
+
+ /// No description provided for @sshAccess.
+ ///
+ /// In en, this message translates to:
+ /// **'SSH Access'**
+ String get sshAccess;
+
+ /// No description provided for @sshAccessSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Remote terminal access via SSH'**
+ String get sshAccessSubtitle;
+
+ /// No description provided for @logs.
+ ///
+ /// In en, this message translates to:
+ /// **'Logs'**
+ String get logs;
+
+ /// No description provided for @logsSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'View gateway output and errors'**
+ String get logsSubtitle;
+
+ /// No description provided for @snapshot.
+ ///
+ /// In en, this message translates to:
+ /// **'Snapshot'**
+ String get snapshot;
+
+ /// No description provided for @snapshotSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Backup or restore your config'**
+ String get snapshotSubtitle;
+
+ /// No description provided for @node.
+ ///
+ /// In en, this message translates to:
+ /// **'Node'**
+ String get node;
+
+ /// No description provided for @nodeConnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Connected to gateway'**
+ String get nodeConnected;
+
+ /// No description provided for @nodeCapabilities.
+ ///
+ /// In en, this message translates to:
+ /// **'Device capabilities for AI'**
+ String get nodeCapabilities;
+
+ /// No description provided for @dashboardUrlCopied.
+ ///
+ /// In en, this message translates to:
+ /// **'Dashboard URL copied'**
+ String get dashboardUrlCopied;
+
+ /// No description provided for @copyDashboardUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy dashboard URL'**
+ String get copyDashboardUrl;
+
+ /// No description provided for @cliProxy.
+ ///
+ /// In en, this message translates to:
+ /// **'CLIProxy Manager'**
+ String get cliProxy;
+
+ /// No description provided for @cliProxySubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Manage free AI account proxy'**
+ String get cliProxySubtitle;
+
+ /// No description provided for @gateway.
+ ///
+ /// In en, this message translates to:
+ /// **'Gateway'**
+ String get gateway;
+
+ /// No description provided for @startGateway.
+ ///
+ /// In en, this message translates to:
+ /// **'Start Gateway'**
+ String get startGateway;
+
+ /// No description provided for @stopGateway.
+ ///
+ /// In en, this message translates to:
+ /// **'Stop Gateway'**
+ String get stopGateway;
+
+ /// No description provided for @viewLogs.
+ ///
+ /// In en, this message translates to:
+ /// **'View Logs'**
+ String get viewLogs;
+
+ /// No description provided for @urlCopied.
+ ///
+ /// In en, this message translates to:
+ /// **'URL copied to clipboard'**
+ String get urlCopied;
+
+ /// No description provided for @copyUrl.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy URL'**
+ String get copyUrl;
+
+ /// No description provided for @openDashboard.
+ ///
+ /// In en, this message translates to:
+ /// **'Open dashboard'**
+ String get openDashboard;
+
+ /// No description provided for @gatewayRunning.
+ ///
+ /// In en, this message translates to:
+ /// **'Running'**
+ String get gatewayRunning;
+
+ /// No description provided for @gatewayStarting.
+ ///
+ /// In en, this message translates to:
+ /// **'Starting'**
+ String get gatewayStarting;
+
+ /// No description provided for @gatewayError.
+ ///
+ /// In en, this message translates to:
+ /// **'Error'**
+ String get gatewayError;
+
+ /// No description provided for @gatewayStopped.
+ ///
+ /// In en, this message translates to:
+ /// **'Stopped'**
+ String get gatewayStopped;
+
+ /// No description provided for @enableNode.
+ ///
+ /// In en, this message translates to:
+ /// **'Enable Node'**
+ String get enableNode;
+
+ /// No description provided for @disableNode.
+ ///
+ /// In en, this message translates to:
+ /// **'Disable Node'**
+ String get disableNode;
+
+ /// No description provided for @reconnect.
+ ///
+ /// In en, this message translates to:
+ /// **'Reconnect'**
+ String get reconnect;
+
+ /// No description provided for @nodePaired.
+ ///
+ /// In en, this message translates to:
+ /// **'Paired'**
+ String get nodePaired;
+
+ /// No description provided for @nodeConnecting.
+ ///
+ /// In en, this message translates to:
+ /// **'Connecting'**
+ String get nodeConnecting;
+
+ /// No description provided for @nodeDisconnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Disconnected'**
+ String get nodeDisconnected;
+
+ /// No description provided for @nodeDisabled.
+ ///
+ /// In en, this message translates to:
+ /// **'Disabled'**
+ String get nodeDisabled;
+
+ /// No description provided for @nodeConfigure.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure'**
+ String get nodeConfigure;
+
+ /// No description provided for @settings.
+ ///
+ /// In en, this message translates to:
+ /// **'Settings'**
+ String get settings;
+
+ /// No description provided for @general.
+ ///
+ /// In en, this message translates to:
+ /// **'GENERAL'**
+ String get general;
+
+ /// No description provided for @autoStartGateway.
+ ///
+ /// In en, this message translates to:
+ /// **'Auto-start gateway'**
+ String get autoStartGateway;
+
+ /// No description provided for @autoStartSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Start the gateway when the app opens'**
+ String get autoStartSubtitle;
+
+ /// No description provided for @batteryOptimization.
+ ///
+ /// In en, this message translates to:
+ /// **'Battery Optimization'**
+ String get batteryOptimization;
+
+ /// No description provided for @batteryOptimized.
+ ///
+ /// In en, this message translates to:
+ /// **'Optimized (may kill background sessions)'**
+ String get batteryOptimized;
+
+ /// No description provided for @batteryUnrestricted.
+ ///
+ /// In en, this message translates to:
+ /// **'Unrestricted (recommended)'**
+ String get batteryUnrestricted;
+
+ /// No description provided for @setupStorage.
+ ///
+ /// In en, this message translates to:
+ /// **'Setup Storage'**
+ String get setupStorage;
+
+ /// No description provided for @storageGranted.
+ ///
+ /// In en, this message translates to:
+ /// **'Granted — proot can access /sdcard. Revoke if not needed.'**
+ String get storageGranted;
+
+ /// No description provided for @storageNotGranted.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow access to shared storage'**
+ String get storageNotGranted;
+
+ /// No description provided for @nodeSection.
+ ///
+ /// In en, this message translates to:
+ /// **'NODE'**
+ String get nodeSection;
+
+ /// No description provided for @enableNodeTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Enable Node'**
+ String get enableNodeTitle;
+
+ /// No description provided for @enableNodeSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Provide device capabilities to the gateway'**
+ String get enableNodeSubtitle;
+
+ /// No description provided for @nodeConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Node Configuration'**
+ String get nodeConfiguration;
+
+ /// No description provided for @nodeConfigSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Connection, pairing, and capabilities'**
+ String get nodeConfigSubtitle;
+
+ /// No description provided for @systemInfo.
+ ///
+ /// In en, this message translates to:
+ /// **'SYSTEM INFO'**
+ String get systemInfo;
+
+ /// No description provided for @architecture.
+ ///
+ /// In en, this message translates to:
+ /// **'Architecture'**
+ String get architecture;
+
+ /// No description provided for @prootPath.
+ ///
+ /// In en, this message translates to:
+ /// **'PRoot path'**
+ String get prootPath;
+
+ /// No description provided for @rootfs.
+ ///
+ /// In en, this message translates to:
+ /// **'Rootfs'**
+ String get rootfs;
+
+ /// No description provided for @installed.
+ ///
+ /// In en, this message translates to:
+ /// **'Installed'**
+ String get installed;
+
+ /// No description provided for @notInstalled.
+ ///
+ /// In en, this message translates to:
+ /// **'Not installed'**
+ String get notInstalled;
+
+ /// No description provided for @maintenance.
+ ///
+ /// In en, this message translates to:
+ /// **'MAINTENANCE'**
+ String get maintenance;
+
+ /// No description provided for @exportSnapshot.
+ ///
+ /// In en, this message translates to:
+ /// **'Export Snapshot'**
+ String get exportSnapshot;
+
+ /// No description provided for @exportSnapshotSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Backup config to Downloads'**
+ String get exportSnapshotSubtitle;
+
+ /// No description provided for @importSnapshot.
+ ///
+ /// In en, this message translates to:
+ /// **'Import Snapshot'**
+ String get importSnapshot;
+
+ /// No description provided for @importSnapshotSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Restore config from backup'**
+ String get importSnapshotSubtitle;
+
+ /// No description provided for @rerunSetup.
+ ///
+ /// In en, this message translates to:
+ /// **'Re-run setup'**
+ String get rerunSetup;
+
+ /// No description provided for @rerunSetupSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Reinstall or repair the environment'**
+ String get rerunSetupSubtitle;
+
+ /// No description provided for @about.
+ ///
+ /// In en, this message translates to:
+ /// **'ABOUT'**
+ String get about;
+
+ /// No description provided for @checkForUpdates.
+ ///
+ /// In en, this message translates to:
+ /// **'Check for Updates'**
+ String get checkForUpdates;
+
+ /// No description provided for @checkUpdatesSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Check GitHub for a newer release'**
+ String get checkUpdatesSubtitle;
+
+ /// No description provided for @developer.
+ ///
+ /// In en, this message translates to:
+ /// **'Developer'**
+ String get developer;
+
+ /// No description provided for @github.
+ ///
+ /// In en, this message translates to:
+ /// **'GitHub'**
+ String get github;
+
+ /// No description provided for @contact.
+ ///
+ /// In en, this message translates to:
+ /// **'Contact'**
+ String get contact;
+
+ /// No description provided for @license.
+ ///
+ /// In en, this message translates to:
+ /// **'License'**
+ String get license;
+
+ /// No description provided for @updateAvailable.
+ ///
+ /// In en, this message translates to:
+ /// **'Update Available'**
+ String get updateAvailable;
+
+ /// No description provided for @currentVersion.
+ ///
+ /// In en, this message translates to:
+ /// **'Current'**
+ String get currentVersion;
+
+ /// No description provided for @latestVersion.
+ ///
+ /// In en, this message translates to:
+ /// **'Latest'**
+ String get latestVersion;
+
+ /// No description provided for @alreadyLatest.
+ ///
+ /// In en, this message translates to:
+ /// **'You\'re on the latest version'**
+ String get alreadyLatest;
+
+ /// No description provided for @checkUpdateFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not check for updates'**
+ String get checkUpdateFailed;
+
+ /// No description provided for @snapshotSaved.
+ ///
+ /// In en, this message translates to:
+ /// **'Snapshot saved to'**
+ String get snapshotSaved;
+
+ /// No description provided for @exportFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Export failed'**
+ String get exportFailed;
+
+ /// No description provided for @noSnapshotFound.
+ ///
+ /// In en, this message translates to:
+ /// **'No snapshot found at'**
+ String get noSnapshotFound;
+
+ /// No description provided for @snapshotRestored.
+ ///
+ /// In en, this message translates to:
+ /// **'Snapshot restored successfully. Restart the gateway to apply.'**
+ String get snapshotRestored;
+
+ /// No description provided for @importFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Import failed'**
+ String get importFailed;
+
+ /// No description provided for @setupOpenClaw.
+ ///
+ /// In en, this message translates to:
+ /// **'Setup OpenClaw'**
+ String get setupOpenClaw;
+
+ /// No description provided for @setupRunning.
+ ///
+ /// In en, this message translates to:
+ /// **'Setting up the environment. This may take several minutes.'**
+ String get setupRunning;
+
+ /// No description provided for @setupDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.'**
+ String get setupDescription;
+
+ /// No description provided for @downloadRootfs.
+ ///
+ /// In en, this message translates to:
+ /// **'Download Ubuntu rootfs'**
+ String get downloadRootfs;
+
+ /// No description provided for @extractRootfs.
+ ///
+ /// In en, this message translates to:
+ /// **'Extract rootfs'**
+ String get extractRootfs;
+
+ /// No description provided for @installNode.
+ ///
+ /// In en, this message translates to:
+ /// **'Install Node.js'**
+ String get installNode;
+
+ /// No description provided for @installOpenClaw.
+ ///
+ /// In en, this message translates to:
+ /// **'Install OpenClaw'**
+ String get installOpenClaw;
+
+ /// No description provided for @configureBionicBypass.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure Bionic Bypass'**
+ String get configureBionicBypass;
+
+ /// No description provided for @setupComplete.
+ ///
+ /// In en, this message translates to:
+ /// **'Setup complete!'**
+ String get setupComplete;
+
+ /// No description provided for @configureApiKeys.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure API Keys'**
+ String get configureApiKeys;
+
+ /// No description provided for @beginSetup.
+ ///
+ /// In en, this message translates to:
+ /// **'Begin Setup'**
+ String get beginSetup;
+
+ /// No description provided for @retrySetup.
+ ///
+ /// In en, this message translates to:
+ /// **'Retry Setup'**
+ String get retrySetup;
+
+ /// No description provided for @storageRequired.
+ ///
+ /// In en, this message translates to:
+ /// **'Requires ~500MB of storage and an internet connection'**
+ String get storageRequired;
+
+ /// No description provided for @optionalPackages.
+ ///
+ /// In en, this message translates to:
+ /// **'OPTIONAL PACKAGES'**
+ String get optionalPackages;
+
+ /// No description provided for @gatewayLogs.
+ ///
+ /// In en, this message translates to:
+ /// **'Gateway Logs'**
+ String get gatewayLogs;
+
+ /// No description provided for @filterLogs.
+ ///
+ /// In en, this message translates to:
+ /// **'Filter logs...'**
+ String get filterLogs;
+
+ /// No description provided for @noLogsYet.
+ ///
+ /// In en, this message translates to:
+ /// **'No logs yet. Start the gateway.'**
+ String get noLogsYet;
+
+ /// No description provided for @noMatchingLogs.
+ ///
+ /// In en, this message translates to:
+ /// **'No matching logs.'**
+ String get noMatchingLogs;
+
+ /// No description provided for @copyAllLogs.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy all logs'**
+ String get copyAllLogs;
+
+ /// No description provided for @autoScrollOn.
+ ///
+ /// In en, this message translates to:
+ /// **'Auto-scroll on'**
+ String get autoScrollOn;
+
+ /// No description provided for @autoScrollOff.
+ ///
+ /// In en, this message translates to:
+ /// **'Auto-scroll off'**
+ String get autoScrollOff;
+
+ /// No description provided for @activeModel.
+ ///
+ /// In en, this message translates to:
+ /// **'Active Model'**
+ String get activeModel;
+
+ /// No description provided for @selectProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Select a provider to configure its API key and model.'**
+ String get selectProvider;
+
+ /// No description provided for @active.
+ ///
+ /// In en, this message translates to:
+ /// **'Active'**
+ String get active;
+
+ /// No description provided for @configured.
+ ///
+ /// In en, this message translates to:
+ /// **'Configured'**
+ String get configured;
+
+ /// No description provided for @apiKey.
+ ///
+ /// In en, this message translates to:
+ /// **'API Key'**
+ String get apiKey;
+
+ /// No description provided for @model.
+ ///
+ /// In en, this message translates to:
+ /// **'Model'**
+ String get model;
+
+ /// No description provided for @customModel.
+ ///
+ /// In en, this message translates to:
+ /// **'Custom...'**
+ String get customModel;
+
+ /// No description provided for @customModelHint.
+ ///
+ /// In en, this message translates to:
+ /// **'e.g. meta/llama-3.3-70b-instruct'**
+ String get customModelHint;
+
+ /// No description provided for @customModelLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Custom model name'**
+ String get customModelLabel;
+
+ /// No description provided for @saveAndActivate.
+ ///
+ /// In en, this message translates to:
+ /// **'Save & Activate'**
+ String get saveAndActivate;
+
+ /// No description provided for @removeConfiguration.
+ ///
+ /// In en, this message translates to:
+ /// **'Remove Configuration'**
+ String get removeConfiguration;
+
+ /// No description provided for @apiKeyEmpty.
+ ///
+ /// In en, this message translates to:
+ /// **'API key cannot be empty'**
+ String get apiKeyEmpty;
+
+ /// No description provided for @modelEmpty.
+ ///
+ /// In en, this message translates to:
+ /// **'Model name cannot be empty'**
+ String get modelEmpty;
+
+ /// No description provided for @configuredAndActivated.
+ ///
+ /// In en, this message translates to:
+ /// **'configured and activated'**
+ String get configuredAndActivated;
+
+ /// No description provided for @saveFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to save'**
+ String get saveFailed;
+
+ /// No description provided for @removeFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to remove'**
+ String get removeFailed;
+
+ /// No description provided for @removeProvider.
+ ///
+ /// In en, this message translates to:
+ /// **'Remove'**
+ String get removeProvider;
+
+ /// No description provided for @removeProviderContent.
+ ///
+ /// In en, this message translates to:
+ /// **'This will delete the API key and deactivate the model.'**
+ String get removeProviderContent;
+
+ /// No description provided for @startingTerminal.
+ ///
+ /// In en, this message translates to:
+ /// **'Starting terminal...'**
+ String get startingTerminal;
+
+ /// No description provided for @failedToStartTerminal.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to start terminal'**
+ String get failedToStartTerminal;
+
+ /// No description provided for @openClawOnboarding.
+ ///
+ /// In en, this message translates to:
+ /// **'OpenClaw Onboarding'**
+ String get openClawOnboarding;
+
+ /// No description provided for @startingOnboarding.
+ ///
+ /// In en, this message translates to:
+ /// **'Starting onboarding...'**
+ String get startingOnboarding;
+
+ /// No description provided for @failedToStartOnboarding.
+ ///
+ /// In en, this message translates to:
+ /// **'Failed to start onboarding'**
+ String get failedToStartOnboarding;
+
+ /// No description provided for @goToDashboard.
+ ///
+ /// In en, this message translates to:
+ /// **'Go to Dashboard'**
+ String get goToDashboard;
+
+ /// No description provided for @cliProxyManagement.
+ ///
+ /// In en, this message translates to:
+ /// **'CLIProxy Management'**
+ String get cliProxyManagement;
+
+ /// No description provided for @cliProxyNotRunning.
+ ///
+ /// In en, this message translates to:
+ /// **'CLIProxy service is not running'**
+ String get cliProxyNotRunning;
+
+ /// No description provided for @openInBrowser.
+ ///
+ /// In en, this message translates to:
+ /// **'Open in browser'**
+ String get openInBrowser;
+
+ /// No description provided for @refresh.
+ ///
+ /// In en, this message translates to:
+ /// **'Refresh'**
+ String get refresh;
+
+ /// No description provided for @reconnectProxy.
+ ///
+ /// In en, this message translates to:
+ /// **'Reconnect'**
+ String get reconnectProxy;
+
+ /// No description provided for @installedBadge.
+ ///
+ /// In en, this message translates to:
+ /// **'Installed'**
+ String get installedBadge;
+}
+
+class _AppLocalizationsDelegate
+ extends LocalizationsDelegate {
+ const _AppLocalizationsDelegate();
+
+ @override
+ Future load(Locale locale) {
+ return SynchronousFuture(lookupAppLocalizations(locale));
+ }
+
+ @override
+ bool isSupported(Locale locale) =>
+ ['en', 'zh'].contains(locale.languageCode);
+
+ @override
+ bool shouldReload(_AppLocalizationsDelegate old) => false;
+}
+
+AppLocalizations lookupAppLocalizations(Locale locale) {
+ // Lookup logic when only language code is specified.
+ switch (locale.languageCode) {
+ case 'en':
+ return AppLocalizationsEn();
+ case 'zh':
+ return AppLocalizationsZh();
+ }
+
+ throw FlutterError(
+ 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
+ 'an issue with the localizations generation tool. Please file an issue '
+ 'on GitHub with a reproducible sample app and the gen-l10n configuration '
+ 'that was used.');
+}
diff --git a/flutter_app/lib/l10n/app_localizations_en.dart b/flutter_app/lib/l10n/app_localizations_en.dart
new file mode 100644
index 0000000..76839e9
--- /dev/null
+++ b/flutter_app/lib/l10n/app_localizations_en.dart
@@ -0,0 +1,521 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for English (`en`).
+class AppLocalizationsEn extends AppLocalizations {
+ AppLocalizationsEn([String locale = 'en']) : super(locale);
+
+ @override
+ String get appName => 'OpenClaw';
+
+ @override
+ String get cancel => 'Cancel';
+
+ @override
+ String get confirm => 'Confirm';
+
+ @override
+ String get retry => 'Retry';
+
+ @override
+ String get done => 'Done';
+
+ @override
+ String get save => 'Save';
+
+ @override
+ String get remove => 'Remove';
+
+ @override
+ String get install => 'Install';
+
+ @override
+ String get loading => 'Loading...';
+
+ @override
+ String get error => 'Error';
+
+ @override
+ String get copy => 'Copy';
+
+ @override
+ String get paste => 'Paste';
+
+ @override
+ String get openUrl => 'Open URL';
+
+ @override
+ String get screenshot => 'Screenshot';
+
+ @override
+ String get restart => 'Restart';
+
+ @override
+ String get later => 'Later';
+
+ @override
+ String get download => 'Download';
+
+ @override
+ String get copied => 'Copied to clipboard';
+
+ @override
+ String get noUrlFound => 'No URL found in selection';
+
+ @override
+ String get linkCopied => 'Link copied';
+
+ @override
+ String get openLink => 'Open Link';
+
+ @override
+ String get aiGatewayForAndroid => 'AI Gateway for Android';
+
+ @override
+ String get checkingSetup => 'Checking setup status...';
+
+ @override
+ String get repairingBypass => 'Repairing bionic bypass...';
+
+ @override
+ String get reinstallingNode => 'Reinstalling Node.js...';
+
+ @override
+ String get reinstallingOpenClaw => 'Reinstalling OpenClaw...';
+
+ @override
+ String get quickActions => 'QUICK ACTIONS';
+
+ @override
+ String get terminal => 'Terminal';
+
+ @override
+ String get terminalSubtitle => 'Open Ubuntu shell with OpenClaw';
+
+ @override
+ String get webDashboard => 'Web Dashboard';
+
+ @override
+ String get webDashboardSubtitle => 'Open OpenClaw dashboard in browser';
+
+ @override
+ String get startGatewayFirst => 'Start gateway first';
+
+ @override
+ String get onboarding => 'Onboarding';
+
+ @override
+ String get onboardingSubtitle => 'Configure API keys and binding';
+
+ @override
+ String get configure => 'Configure';
+
+ @override
+ String get configureSubtitle => 'Manage gateway settings';
+
+ @override
+ String get aiProviders => 'AI Providers';
+
+ @override
+ String get aiProvidersSubtitle => 'Configure models and API keys';
+
+ @override
+ String get packages => 'Packages';
+
+ @override
+ String get packagesSubtitle => 'Install optional tools (Go, Homebrew, SSH)';
+
+ @override
+ String get sshAccess => 'SSH Access';
+
+ @override
+ String get sshAccessSubtitle => 'Remote terminal access via SSH';
+
+ @override
+ String get logs => 'Logs';
+
+ @override
+ String get logsSubtitle => 'View gateway output and errors';
+
+ @override
+ String get snapshot => 'Snapshot';
+
+ @override
+ String get snapshotSubtitle => 'Backup or restore your config';
+
+ @override
+ String get node => 'Node';
+
+ @override
+ String get nodeConnected => 'Connected to gateway';
+
+ @override
+ String get nodeCapabilities => 'Device capabilities for AI';
+
+ @override
+ String get dashboardUrlCopied => 'Dashboard URL copied';
+
+ @override
+ String get copyDashboardUrl => 'Copy dashboard URL';
+
+ @override
+ String get cliProxy => 'CLIProxy Manager';
+
+ @override
+ String get cliProxySubtitle => 'Manage free AI account proxy';
+
+ @override
+ String get gateway => 'Gateway';
+
+ @override
+ String get startGateway => 'Start Gateway';
+
+ @override
+ String get stopGateway => 'Stop Gateway';
+
+ @override
+ String get viewLogs => 'View Logs';
+
+ @override
+ String get urlCopied => 'URL copied to clipboard';
+
+ @override
+ String get copyUrl => 'Copy URL';
+
+ @override
+ String get openDashboard => 'Open dashboard';
+
+ @override
+ String get gatewayRunning => 'Running';
+
+ @override
+ String get gatewayStarting => 'Starting';
+
+ @override
+ String get gatewayError => 'Error';
+
+ @override
+ String get gatewayStopped => 'Stopped';
+
+ @override
+ String get enableNode => 'Enable Node';
+
+ @override
+ String get disableNode => 'Disable Node';
+
+ @override
+ String get reconnect => 'Reconnect';
+
+ @override
+ String get nodePaired => 'Paired';
+
+ @override
+ String get nodeConnecting => 'Connecting';
+
+ @override
+ String get nodeDisconnected => 'Disconnected';
+
+ @override
+ String get nodeDisabled => 'Disabled';
+
+ @override
+ String get nodeConfigure => 'Configure';
+
+ @override
+ String get settings => 'Settings';
+
+ @override
+ String get general => 'GENERAL';
+
+ @override
+ String get autoStartGateway => 'Auto-start gateway';
+
+ @override
+ String get autoStartSubtitle => 'Start the gateway when the app opens';
+
+ @override
+ String get batteryOptimization => 'Battery Optimization';
+
+ @override
+ String get batteryOptimized => 'Optimized (may kill background sessions)';
+
+ @override
+ String get batteryUnrestricted => 'Unrestricted (recommended)';
+
+ @override
+ String get setupStorage => 'Setup Storage';
+
+ @override
+ String get storageGranted =>
+ 'Granted — proot can access /sdcard. Revoke if not needed.';
+
+ @override
+ String get storageNotGranted => 'Allow access to shared storage';
+
+ @override
+ String get nodeSection => 'NODE';
+
+ @override
+ String get enableNodeTitle => 'Enable Node';
+
+ @override
+ String get enableNodeSubtitle => 'Provide device capabilities to the gateway';
+
+ @override
+ String get nodeConfiguration => 'Node Configuration';
+
+ @override
+ String get nodeConfigSubtitle => 'Connection, pairing, and capabilities';
+
+ @override
+ String get systemInfo => 'SYSTEM INFO';
+
+ @override
+ String get architecture => 'Architecture';
+
+ @override
+ String get prootPath => 'PRoot path';
+
+ @override
+ String get rootfs => 'Rootfs';
+
+ @override
+ String get installed => 'Installed';
+
+ @override
+ String get notInstalled => 'Not installed';
+
+ @override
+ String get maintenance => 'MAINTENANCE';
+
+ @override
+ String get exportSnapshot => 'Export Snapshot';
+
+ @override
+ String get exportSnapshotSubtitle => 'Backup config to Downloads';
+
+ @override
+ String get importSnapshot => 'Import Snapshot';
+
+ @override
+ String get importSnapshotSubtitle => 'Restore config from backup';
+
+ @override
+ String get rerunSetup => 'Re-run setup';
+
+ @override
+ String get rerunSetupSubtitle => 'Reinstall or repair the environment';
+
+ @override
+ String get about => 'ABOUT';
+
+ @override
+ String get checkForUpdates => 'Check for Updates';
+
+ @override
+ String get checkUpdatesSubtitle => 'Check GitHub for a newer release';
+
+ @override
+ String get developer => 'Developer';
+
+ @override
+ String get github => 'GitHub';
+
+ @override
+ String get contact => 'Contact';
+
+ @override
+ String get license => 'License';
+
+ @override
+ String get updateAvailable => 'Update Available';
+
+ @override
+ String get currentVersion => 'Current';
+
+ @override
+ String get latestVersion => 'Latest';
+
+ @override
+ String get alreadyLatest => 'You\'re on the latest version';
+
+ @override
+ String get checkUpdateFailed => 'Could not check for updates';
+
+ @override
+ String get snapshotSaved => 'Snapshot saved to';
+
+ @override
+ String get exportFailed => 'Export failed';
+
+ @override
+ String get noSnapshotFound => 'No snapshot found at';
+
+ @override
+ String get snapshotRestored =>
+ 'Snapshot restored successfully. Restart the gateway to apply.';
+
+ @override
+ String get importFailed => 'Import failed';
+
+ @override
+ String get setupOpenClaw => 'Setup OpenClaw';
+
+ @override
+ String get setupRunning =>
+ 'Setting up the environment. This may take several minutes.';
+
+ @override
+ String get setupDescription =>
+ 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.';
+
+ @override
+ String get downloadRootfs => 'Download Ubuntu rootfs';
+
+ @override
+ String get extractRootfs => 'Extract rootfs';
+
+ @override
+ String get installNode => 'Install Node.js';
+
+ @override
+ String get installOpenClaw => 'Install OpenClaw';
+
+ @override
+ String get configureBionicBypass => 'Configure Bionic Bypass';
+
+ @override
+ String get setupComplete => 'Setup complete!';
+
+ @override
+ String get configureApiKeys => 'Configure API Keys';
+
+ @override
+ String get beginSetup => 'Begin Setup';
+
+ @override
+ String get retrySetup => 'Retry Setup';
+
+ @override
+ String get storageRequired =>
+ 'Requires ~500MB of storage and an internet connection';
+
+ @override
+ String get optionalPackages => 'OPTIONAL PACKAGES';
+
+ @override
+ String get gatewayLogs => 'Gateway Logs';
+
+ @override
+ String get filterLogs => 'Filter logs...';
+
+ @override
+ String get noLogsYet => 'No logs yet. Start the gateway.';
+
+ @override
+ String get noMatchingLogs => 'No matching logs.';
+
+ @override
+ String get copyAllLogs => 'Copy all logs';
+
+ @override
+ String get autoScrollOn => 'Auto-scroll on';
+
+ @override
+ String get autoScrollOff => 'Auto-scroll off';
+
+ @override
+ String get activeModel => 'Active Model';
+
+ @override
+ String get selectProvider =>
+ 'Select a provider to configure its API key and model.';
+
+ @override
+ String get active => 'Active';
+
+ @override
+ String get configured => 'Configured';
+
+ @override
+ String get apiKey => 'API Key';
+
+ @override
+ String get model => 'Model';
+
+ @override
+ String get customModel => 'Custom...';
+
+ @override
+ String get customModelHint => 'e.g. meta/llama-3.3-70b-instruct';
+
+ @override
+ String get customModelLabel => 'Custom model name';
+
+ @override
+ String get saveAndActivate => 'Save & Activate';
+
+ @override
+ String get removeConfiguration => 'Remove Configuration';
+
+ @override
+ String get apiKeyEmpty => 'API key cannot be empty';
+
+ @override
+ String get modelEmpty => 'Model name cannot be empty';
+
+ @override
+ String get configuredAndActivated => 'configured and activated';
+
+ @override
+ String get saveFailed => 'Failed to save';
+
+ @override
+ String get removeFailed => 'Failed to remove';
+
+ @override
+ String get removeProvider => 'Remove';
+
+ @override
+ String get removeProviderContent =>
+ 'This will delete the API key and deactivate the model.';
+
+ @override
+ String get startingTerminal => 'Starting terminal...';
+
+ @override
+ String get failedToStartTerminal => 'Failed to start terminal';
+
+ @override
+ String get openClawOnboarding => 'OpenClaw Onboarding';
+
+ @override
+ String get startingOnboarding => 'Starting onboarding...';
+
+ @override
+ String get failedToStartOnboarding => 'Failed to start onboarding';
+
+ @override
+ String get goToDashboard => 'Go to Dashboard';
+
+ @override
+ String get cliProxyManagement => 'CLIProxy Management';
+
+ @override
+ String get cliProxyNotRunning => 'CLIProxy service is not running';
+
+ @override
+ String get openInBrowser => 'Open in browser';
+
+ @override
+ String get refresh => 'Refresh';
+
+ @override
+ String get reconnectProxy => 'Reconnect';
+
+ @override
+ String get installedBadge => 'Installed';
+}
diff --git a/flutter_app/lib/l10n/app_localizations_zh.dart b/flutter_app/lib/l10n/app_localizations_zh.dart
new file mode 100644
index 0000000..1cefea7
--- /dev/null
+++ b/flutter_app/lib/l10n/app_localizations_zh.dart
@@ -0,0 +1,514 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Chinese (`zh`).
+class AppLocalizationsZh extends AppLocalizations {
+ AppLocalizationsZh([String locale = 'zh']) : super(locale);
+
+ @override
+ String get appName => 'OpenClaw';
+
+ @override
+ String get cancel => '取消';
+
+ @override
+ String get confirm => '确认';
+
+ @override
+ String get retry => '重试';
+
+ @override
+ String get done => '完成';
+
+ @override
+ String get save => '保存';
+
+ @override
+ String get remove => '移除';
+
+ @override
+ String get install => '安装';
+
+ @override
+ String get loading => '加载中...';
+
+ @override
+ String get error => '错误';
+
+ @override
+ String get copy => '复制';
+
+ @override
+ String get paste => '粘贴';
+
+ @override
+ String get openUrl => '打开链接';
+
+ @override
+ String get screenshot => '截图';
+
+ @override
+ String get restart => '重启';
+
+ @override
+ String get later => '稍后';
+
+ @override
+ String get download => '下载';
+
+ @override
+ String get copied => '已复制到剪贴板';
+
+ @override
+ String get noUrlFound => '未找到链接';
+
+ @override
+ String get linkCopied => '链接已复制';
+
+ @override
+ String get openLink => '打开链接';
+
+ @override
+ String get aiGatewayForAndroid => 'Android AI 网关';
+
+ @override
+ String get checkingSetup => '检查安装状态...';
+
+ @override
+ String get repairingBypass => '修复 Bionic Bypass...';
+
+ @override
+ String get reinstallingNode => '重新安装 Node.js...';
+
+ @override
+ String get reinstallingOpenClaw => '重新安装 OpenClaw...';
+
+ @override
+ String get quickActions => '快捷操作';
+
+ @override
+ String get terminal => '终端';
+
+ @override
+ String get terminalSubtitle => '打开 Ubuntu Shell';
+
+ @override
+ String get webDashboard => 'Web 控制台';
+
+ @override
+ String get webDashboardSubtitle => '在浏览器中打开 OpenClaw 控制台';
+
+ @override
+ String get startGatewayFirst => '请先启动网关';
+
+ @override
+ String get onboarding => '初始配置';
+
+ @override
+ String get onboardingSubtitle => '配置 API 密钥和绑定设置';
+
+ @override
+ String get configure => '网关配置';
+
+ @override
+ String get configureSubtitle => '管理网关设置';
+
+ @override
+ String get aiProviders => 'AI 提供商';
+
+ @override
+ String get aiProvidersSubtitle => '配置模型和 API 密钥';
+
+ @override
+ String get packages => '扩展包';
+
+ @override
+ String get packagesSubtitle => '安装可选工具 (Go, Homebrew, SSH)';
+
+ @override
+ String get sshAccess => 'SSH 访问';
+
+ @override
+ String get sshAccessSubtitle => '通过 SSH 远程访问终端';
+
+ @override
+ String get logs => '日志';
+
+ @override
+ String get logsSubtitle => '查看网关输出和错误';
+
+ @override
+ String get snapshot => '快照';
+
+ @override
+ String get snapshotSubtitle => '备份或恢复配置';
+
+ @override
+ String get node => '节点';
+
+ @override
+ String get nodeConnected => '已连接到网关';
+
+ @override
+ String get nodeCapabilities => '为 AI 提供设备能力';
+
+ @override
+ String get dashboardUrlCopied => '控制台链接已复制';
+
+ @override
+ String get copyDashboardUrl => '复制控制台链接';
+
+ @override
+ String get cliProxy => 'CLIProxy 管理';
+
+ @override
+ String get cliProxySubtitle => '管理免费 AI 账号代理';
+
+ @override
+ String get gateway => '网关';
+
+ @override
+ String get startGateway => '启动网关';
+
+ @override
+ String get stopGateway => '停止网关';
+
+ @override
+ String get viewLogs => '查看日志';
+
+ @override
+ String get urlCopied => '链接已复制';
+
+ @override
+ String get copyUrl => '复制链接';
+
+ @override
+ String get openDashboard => '打开控制台';
+
+ @override
+ String get gatewayRunning => '运行中';
+
+ @override
+ String get gatewayStarting => '启动中';
+
+ @override
+ String get gatewayError => '错误';
+
+ @override
+ String get gatewayStopped => '已停止';
+
+ @override
+ String get enableNode => '启用节点';
+
+ @override
+ String get disableNode => '禁用节点';
+
+ @override
+ String get reconnect => '重新连接';
+
+ @override
+ String get nodePaired => '已配对';
+
+ @override
+ String get nodeConnecting => '连接中';
+
+ @override
+ String get nodeDisconnected => '已断开';
+
+ @override
+ String get nodeDisabled => '已禁用';
+
+ @override
+ String get nodeConfigure => '配置';
+
+ @override
+ String get settings => '设置';
+
+ @override
+ String get general => '通用';
+
+ @override
+ String get autoStartGateway => '自动启动网关';
+
+ @override
+ String get autoStartSubtitle => '应用打开时自动启动网关';
+
+ @override
+ String get batteryOptimization => '电池优化';
+
+ @override
+ String get batteryOptimized => '已优化(可能终止后台会话)';
+
+ @override
+ String get batteryUnrestricted => '无限制(推荐)';
+
+ @override
+ String get setupStorage => '存储权限';
+
+ @override
+ String get storageGranted => '已授权 — proot 可访问 /sdcard,不需要时请撤销';
+
+ @override
+ String get storageNotGranted => '允许访问共享存储';
+
+ @override
+ String get nodeSection => '节点';
+
+ @override
+ String get enableNodeTitle => '启用节点';
+
+ @override
+ String get enableNodeSubtitle => '为网关提供设备能力';
+
+ @override
+ String get nodeConfiguration => '节点配置';
+
+ @override
+ String get nodeConfigSubtitle => '连接、配对和能力设置';
+
+ @override
+ String get systemInfo => '系统信息';
+
+ @override
+ String get architecture => '架构';
+
+ @override
+ String get prootPath => 'PRoot 路径';
+
+ @override
+ String get rootfs => '根文件系统';
+
+ @override
+ String get installed => '已安装';
+
+ @override
+ String get notInstalled => '未安装';
+
+ @override
+ String get maintenance => '维护';
+
+ @override
+ String get exportSnapshot => '导出快照';
+
+ @override
+ String get exportSnapshotSubtitle => '备份配置到下载目录';
+
+ @override
+ String get importSnapshot => '导入快照';
+
+ @override
+ String get importSnapshotSubtitle => '从备份恢复配置';
+
+ @override
+ String get rerunSetup => '重新安装';
+
+ @override
+ String get rerunSetupSubtitle => '重新安装或修复环境';
+
+ @override
+ String get about => '关于';
+
+ @override
+ String get checkForUpdates => '检查更新';
+
+ @override
+ String get checkUpdatesSubtitle => '在 GitHub 检查新版本';
+
+ @override
+ String get developer => '开发者';
+
+ @override
+ String get github => 'GitHub';
+
+ @override
+ String get contact => '联系方式';
+
+ @override
+ String get license => '许可证';
+
+ @override
+ String get updateAvailable => '发现新版本';
+
+ @override
+ String get currentVersion => '当前版本';
+
+ @override
+ String get latestVersion => '最新版本';
+
+ @override
+ String get alreadyLatest => '已是最新版本';
+
+ @override
+ String get checkUpdateFailed => '无法检查更新';
+
+ @override
+ String get snapshotSaved => '快照已保存至';
+
+ @override
+ String get exportFailed => '导出失败';
+
+ @override
+ String get noSnapshotFound => '未找到快照文件';
+
+ @override
+ String get snapshotRestored => '快照已恢复,重启网关以应用更改';
+
+ @override
+ String get importFailed => '导入失败';
+
+ @override
+ String get setupOpenClaw => '安装 OpenClaw';
+
+ @override
+ String get setupRunning => '正在配置环境,可能需要几分钟。';
+
+ @override
+ String get setupDescription => '将下载 Ubuntu、Node.js 和 OpenClaw 到独立环境中。';
+
+ @override
+ String get downloadRootfs => '下载 Ubuntu 根文件系统';
+
+ @override
+ String get extractRootfs => '解压根文件系统';
+
+ @override
+ String get installNode => '安装 Node.js';
+
+ @override
+ String get installOpenClaw => '安装 OpenClaw';
+
+ @override
+ String get configureBionicBypass => '配置 Bionic Bypass';
+
+ @override
+ String get setupComplete => '安装完成!';
+
+ @override
+ String get configureApiKeys => '配置 API 密钥';
+
+ @override
+ String get beginSetup => '开始安装';
+
+ @override
+ String get retrySetup => '重试安装';
+
+ @override
+ String get storageRequired => '需要约 500MB 存储空间和网络连接';
+
+ @override
+ String get optionalPackages => '可选扩展包';
+
+ @override
+ String get gatewayLogs => '网关日志';
+
+ @override
+ String get filterLogs => '过滤日志...';
+
+ @override
+ String get noLogsYet => '暂无日志,请启动网关。';
+
+ @override
+ String get noMatchingLogs => '没有匹配的日志。';
+
+ @override
+ String get copyAllLogs => '复制全部日志';
+
+ @override
+ String get autoScrollOn => '自动滚动已开启';
+
+ @override
+ String get autoScrollOff => '自动滚动已关闭';
+
+ @override
+ String get activeModel => '当前模型';
+
+ @override
+ String get selectProvider => '选择提供商配置 API 密钥和模型。';
+
+ @override
+ String get active => '使用中';
+
+ @override
+ String get configured => '已配置';
+
+ @override
+ String get apiKey => 'API 密钥';
+
+ @override
+ String get model => '模型';
+
+ @override
+ String get customModel => '自定义...';
+
+ @override
+ String get customModelHint => '例如:meta/llama-3.3-70b-instruct';
+
+ @override
+ String get customModelLabel => '自定义模型名称';
+
+ @override
+ String get saveAndActivate => '保存并激活';
+
+ @override
+ String get removeConfiguration => '移除配置';
+
+ @override
+ String get apiKeyEmpty => 'API 密钥不能为空';
+
+ @override
+ String get modelEmpty => '模型名称不能为空';
+
+ @override
+ String get configuredAndActivated => '已配置并激活';
+
+ @override
+ String get saveFailed => '保存失败';
+
+ @override
+ String get removeFailed => '移除失败';
+
+ @override
+ String get removeProvider => '移除提供商';
+
+ @override
+ String get removeProviderContent => '这将删除 API 密钥并停用该模型。';
+
+ @override
+ String get startingTerminal => '正在启动终端...';
+
+ @override
+ String get failedToStartTerminal => '启动终端失败';
+
+ @override
+ String get openClawOnboarding => 'OpenClaw 初始配置';
+
+ @override
+ String get startingOnboarding => '正在启动配置向导...';
+
+ @override
+ String get failedToStartOnboarding => '启动配置向导失败';
+
+ @override
+ String get goToDashboard => '前往控制台';
+
+ @override
+ String get cliProxyManagement => 'CLIProxy 管理中心';
+
+ @override
+ String get cliProxyNotRunning => 'CLIProxy 服务未运行';
+
+ @override
+ String get openInBrowser => '在浏览器中打开';
+
+ @override
+ String get refresh => '刷新';
+
+ @override
+ String get reconnectProxy => '重新连接';
+
+ @override
+ String get installedBadge => '已安装';
+}
diff --git a/flutter_app/lib/l10n/app_strings.dart b/flutter_app/lib/l10n/app_strings.dart
new file mode 100644
index 0000000..4c42dc4
--- /dev/null
+++ b/flutter_app/lib/l10n/app_strings.dart
@@ -0,0 +1,381 @@
+import 'dart:ui';
+
+class AppStrings {
+ AppStrings._();
+
+ static bool get _isChinese =>
+ PlatformDispatcher.instance.locale.languageCode == 'zh';
+
+ static bool get isChinese => _isChinese;
+
+ static String get appName => 'OpenClaw';
+ static String get cancel => _isChinese ? '取消' : 'Cancel';
+ static String get retry => _isChinese ? '重试' : 'Retry';
+ static String get remove => _isChinese ? '移除' : 'Remove';
+ static String get done => _isChinese ? '完成' : 'Done';
+ static String get install => _isChinese ? '安装' : 'Install';
+ static String get loading => _isChinese ? '加载中...' : 'Loading...';
+ static String get copy => _isChinese ? '复制' : 'Copy';
+ static String get paste => _isChinese ? '粘贴' : 'Paste';
+ static String get openUrl => _isChinese ? '打开链接' : 'Open URL';
+ static String get screenshot => _isChinese ? '截图' : 'Screenshot';
+ static String get restart => _isChinese ? '重启' : 'Restart';
+ static String get later => _isChinese ? '稍后' : 'Later';
+ static String get download => _isChinese ? '下载' : 'Download';
+ static String get copied => _isChinese ? '已复制到剪贴板' : 'Copied to clipboard';
+ static String get noUrlFound =>
+ _isChinese ? '未找到链接' : 'No URL found in selection';
+ static String get linkCopied => _isChinese ? '链接已复制' : 'Link copied';
+
+ static String get aiGatewayForAndroid =>
+ _isChinese ? 'Android AI 网关' : 'AI Gateway for Android';
+ static String get checkingSetup =>
+ _isChinese ? '检查安装状态...' : 'Checking setup status...';
+ static String get repairingBypass =>
+ _isChinese ? '修复 Bionic Bypass...' : 'Repairing bionic bypass...';
+ static String get reinstallingNode =>
+ _isChinese ? '重新安装 Node.js...' : 'Reinstalling Node.js...';
+ static String get reinstallingOpenClaw =>
+ _isChinese ? '重新安装 OpenClaw...' : 'Reinstalling OpenClaw...';
+
+ static String get quickActions => _isChinese ? '快捷操作' : 'QUICK ACTIONS';
+ static String get terminal => _isChinese ? '终端' : 'Terminal';
+ static String get terminalSubtitle =>
+ _isChinese ? '打开 Ubuntu Shell' : 'Open Ubuntu shell with OpenClaw';
+ static String get webDashboard => _isChinese ? 'Web 控制台' : 'Web Dashboard';
+ static String get webDashboardSubtitle => _isChinese
+ ? '在浏览器中打开 OpenClaw 控制台'
+ : 'Open OpenClaw dashboard in browser';
+ static String get startGatewayFirst =>
+ _isChinese ? '请先启动网关' : 'Start gateway first';
+ static String get onboarding => _isChinese ? '初始配置' : 'Onboarding';
+ static String get onboardingSubtitle =>
+ _isChinese ? '配置 API 密钥和绑定设置' : 'Configure API keys and binding';
+ static String get configure => _isChinese ? '网关配置' : 'Configure';
+ static String get configureSubtitle =>
+ _isChinese ? '管理网关设置' : 'Manage gateway settings';
+ static String get aiProviders => _isChinese ? 'AI 提供商' : 'AI Providers';
+ static String get aiProvidersSubtitle =>
+ _isChinese ? '配置模型和 API 密钥' : 'Configure models and API keys';
+ static String get packages => _isChinese ? '扩展包' : 'Packages';
+ static String get packagesSubtitle => _isChinese
+ ? '安装可选工具 (Go, Homebrew, SSH)'
+ : 'Install optional tools (Go, Homebrew, SSH)';
+ static String get sshAccess => _isChinese ? 'SSH 访问' : 'SSH Access';
+ static String get sshAccessSubtitle =>
+ _isChinese ? '通过 SSH 远程访问终端' : 'Remote terminal access via SSH';
+ static String get logs => _isChinese ? '日志' : 'Logs';
+ static String get logsSubtitle =>
+ _isChinese ? '查看网关输出和错误' : 'View gateway output and errors';
+ static String get snapshot => _isChinese ? '快照' : 'Snapshot';
+ static String get snapshotSubtitle =>
+ _isChinese ? '备份或恢复配置' : 'Backup or restore your config';
+ static String get node => _isChinese ? '节点' : 'Node';
+ static String get nodeConnected =>
+ _isChinese ? '已连接到网关' : 'Connected to gateway';
+ static String get nodeCapabilities =>
+ _isChinese ? '为 AI 提供设备能力' : 'Device capabilities for AI';
+ static String get dashboardUrlCopied =>
+ _isChinese ? '控制台链接已复制' : 'Dashboard URL copied';
+ static String get copyDashboardUrl =>
+ _isChinese ? '复制控制台链接' : 'Copy dashboard URL';
+ static String get cliProxy => _isChinese ? 'CLIProxy 管理' : 'CLIProxy Manager';
+ static String get cliProxySubtitle =>
+ _isChinese ? '管理免费 AI 账号代理' : 'Manage free AI account proxy';
+
+ static String get gateway => _isChinese ? '网关' : 'Gateway';
+ static String get startGateway => _isChinese ? '启动网关' : 'Start Gateway';
+ static String get stopGateway => _isChinese ? '停止网关' : 'Stop Gateway';
+ static String get viewLogs => _isChinese ? '查看日志' : 'View Logs';
+ static String get urlCopied =>
+ _isChinese ? '链接已复制' : 'URL copied to clipboard';
+ static String get copyUrl => _isChinese ? '复制链接' : 'Copy URL';
+ static String get openDashboard => _isChinese ? '打开控制台' : 'Open dashboard';
+ static String get gatewayRunning => _isChinese ? '运行中' : 'Running';
+ static String get gatewayStarting => _isChinese ? '启动中' : 'Starting';
+ static String get gatewayError => _isChinese ? '错误' : 'Error';
+ static String get gatewayStopped => _isChinese ? '已停止' : 'Stopped';
+
+ static String get enableNode => _isChinese ? '启用节点' : 'Enable Node';
+ static String get disableNode => _isChinese ? '禁用节点' : 'Disable Node';
+ static String get reconnect => _isChinese ? '重新连接' : 'Reconnect';
+ static String get nodePaired => _isChinese ? '已配对' : 'Paired';
+ static String get nodeConnecting => _isChinese ? '连接中' : 'Connecting';
+ static String get nodeDisconnected => _isChinese ? '已断开' : 'Disconnected';
+ static String get nodeDisabled => _isChinese ? '已禁用' : 'Disabled';
+ static String get nodeConfigure => _isChinese ? '配置' : 'Configure';
+
+ static String get settings => _isChinese ? '设置' : 'Settings';
+ static String get general => _isChinese ? '通用' : 'GENERAL';
+ static String get autoStartGateway =>
+ _isChinese ? '自动启动网关' : 'Auto-start gateway';
+ static String get autoStartSubtitle =>
+ _isChinese ? '应用打开时自动启动网关' : 'Start the gateway when the app opens';
+ static String get batteryOptimization =>
+ _isChinese ? '电池优化' : 'Battery Optimization';
+ static String get batteryOptimized =>
+ _isChinese ? '已优化(可能终止后台会话)' : 'Optimized (may kill background sessions)';
+ static String get batteryUnrestricted =>
+ _isChinese ? '无限制(推荐)' : 'Unrestricted (recommended)';
+ static String get setupStorage => _isChinese ? '存储权限' : 'Setup Storage';
+ static String get storageGranted => _isChinese
+ ? '已授权 — proot 可访问 /sdcard,不需要时请撤销'
+ : 'Granted — proot can access /sdcard. Revoke if not needed.';
+ static String get storageNotGranted =>
+ _isChinese ? '允许访问共享存储' : 'Allow access to shared storage';
+ static String get nodeSection => _isChinese ? '节点' : 'NODE';
+ static String get enableNodeTitle => _isChinese ? '启用节点' : 'Enable Node';
+ static String get enableNodeSubtitle =>
+ _isChinese ? '为网关提供设备能力' : 'Provide device capabilities to the gateway';
+ static String get nodeConfiguration =>
+ _isChinese ? '节点配置' : 'Node Configuration';
+ static String get nodeConfigSubtitle =>
+ _isChinese ? '连接、配对和能力设置' : 'Connection, pairing, and capabilities';
+ static String get systemInfo => _isChinese ? '系统信息' : 'SYSTEM INFO';
+ static String get architecture => _isChinese ? '架构' : 'Architecture';
+ static String get prootPath => _isChinese ? 'PRoot 路径' : 'PRoot path';
+ static String get rootfs => _isChinese ? '根文件系统' : 'Rootfs';
+ static String get installed => _isChinese ? '已安装' : 'Installed';
+ static String get notInstalled => _isChinese ? '未安装' : 'Not installed';
+ static String get maintenance => _isChinese ? '维护' : 'MAINTENANCE';
+ static String get exportSnapshot => _isChinese ? '导出快照' : 'Export Snapshot';
+ static String get exportSnapshotSubtitle =>
+ _isChinese ? '备份配置到下载目录' : 'Backup config to Downloads';
+ static String get importSnapshot => _isChinese ? '导入快照' : 'Import Snapshot';
+ static String get importSnapshotSubtitle =>
+ _isChinese ? '从备份恢复配置' : 'Restore config from backup';
+ static String get rerunSetup => _isChinese ? '重新安装' : 'Re-run setup';
+ static String get rerunSetupSubtitle =>
+ _isChinese ? '重新安装或修复环境' : 'Reinstall or repair the environment';
+ static String get about => _isChinese ? '关于' : 'ABOUT';
+ static String get checkForUpdates =>
+ _isChinese ? '检查更新' : 'Check for Updates';
+ static String get checkUpdatesSubtitle =>
+ _isChinese ? '在 GitHub 检查新版本' : 'Check GitHub for a newer release';
+ static String get developer => _isChinese ? '开发者' : 'Developer';
+ static String get github => 'GitHub';
+ static String get contact => _isChinese ? '联系方式' : 'Contact';
+ static String get license => _isChinese ? '许可证' : 'License';
+ static String get updateAvailable =>
+ _isChinese ? '发现新版本' : 'Update Available';
+ static String get currentVersion => _isChinese ? '当前版本' : 'Current';
+ static String get latestVersion => _isChinese ? '最新版本' : 'Latest';
+ static String get alreadyLatest =>
+ _isChinese ? '已是最新版本' : "You're on the latest version";
+ static String get checkUpdateFailed =>
+ _isChinese ? '无法检查更新' : 'Could not check for updates';
+ static String get snapshotSaved =>
+ _isChinese ? '快照已保存至' : 'Snapshot saved to';
+ static String get exportFailed => _isChinese ? '导出失败' : 'Export failed';
+ static String get noSnapshotFound =>
+ _isChinese ? '未找到快照文件' : 'No snapshot found at';
+ static String get snapshotRestored => _isChinese
+ ? '快照已恢复,重启网关以应用更改'
+ : 'Snapshot restored successfully. Restart the gateway to apply.';
+ static String get importFailed => _isChinese ? '导入失败' : 'Import failed';
+
+ static String get setupOpenClaw =>
+ _isChinese ? '安装 OpenClaw' : 'Setup OpenClaw';
+ static String get setupRunning => _isChinese
+ ? '正在配置环境,可能需要几分钟。'
+ : 'Setting up the environment. This may take several minutes.';
+ static String get setupDescription => _isChinese
+ ? '将下载 Ubuntu、Node.js 和 OpenClaw 到独立环境中。'
+ : 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.';
+ static String get downloadRootfs =>
+ _isChinese ? '下载 Ubuntu 根文件系统' : 'Download Ubuntu rootfs';
+ static String get extractRootfs => _isChinese ? '解压根文件系统' : 'Extract rootfs';
+ static String get installNode =>
+ _isChinese ? '安装 Node.js' : 'Install Node.js';
+ static String get installOpenClaw =>
+ _isChinese ? '安装 OpenClaw' : 'Install OpenClaw';
+ static String get configureBionicBypass =>
+ _isChinese ? '配置 Bionic Bypass' : 'Configure Bionic Bypass';
+ static String get setupComplete => _isChinese ? '安装完成!' : 'Setup complete!';
+ static String get configureApiKeys =>
+ _isChinese ? '配置 API 密钥' : 'Configure API Keys';
+ static String get beginSetup => _isChinese ? '开始安装' : 'Begin Setup';
+ static String get retrySetup => _isChinese ? '重试安装' : 'Retry Setup';
+ static String get storageRequired => _isChinese
+ ? '需要约 500MB 存储空间和网络连接'
+ : 'Requires ~500MB of storage and an internet connection';
+ static String get optionalPackages =>
+ _isChinese ? '可选扩展包' : 'OPTIONAL PACKAGES';
+
+ static String get gatewayLogs => _isChinese ? '网关日志' : 'Gateway Logs';
+ static String get filterLogs => _isChinese ? '过滤日志...' : 'Filter logs...';
+ static String get noLogsYet =>
+ _isChinese ? '暂无日志,请启动网关。' : 'No logs yet. Start the gateway.';
+ static String get noMatchingLogs =>
+ _isChinese ? '没有匹配的日志。' : 'No matching logs.';
+ static String get copyAllLogs => _isChinese ? '复制全部日志' : 'Copy all logs';
+ static String get autoScrollOn => _isChinese ? '自动滚动已开启' : 'Auto-scroll on';
+ static String get autoScrollOff => _isChinese ? '自动滚动已关闭' : 'Auto-scroll off';
+
+ static String get activeModel => _isChinese ? '当前模型' : 'Active Model';
+ static String get selectProvider => _isChinese
+ ? '选择提供商配置 API 密钥和模型。'
+ : 'Select a provider to configure its API key and model.';
+ static String get active => _isChinese ? '使用中' : 'Active';
+ static String get configured => _isChinese ? '已配置' : 'Configured';
+ static String get apiKey => _isChinese ? 'API 密钥' : 'API Key';
+ static String get model => _isChinese ? '模型' : 'Model';
+ static String get customModel => _isChinese ? '自定义...' : 'Custom...';
+ static String get customModelHint => _isChinese
+ ? '例如:meta/llama-3.3-70b-instruct'
+ : 'e.g. meta/llama-3.3-70b-instruct';
+ static String get customModelLabel =>
+ _isChinese ? '自定义模型名称' : 'Custom model name';
+ static String get saveAndActivate => _isChinese ? '保存并激活' : 'Save & Activate';
+ static String get removeConfiguration =>
+ _isChinese ? '移除配置' : 'Remove Configuration';
+ static String get apiKeyEmpty =>
+ _isChinese ? 'API 密钥不能为空' : 'API key cannot be empty';
+ static String get modelEmpty =>
+ _isChinese ? '模型名称不能为空' : 'Model name cannot be empty';
+ static String get configuredAndActivated =>
+ _isChinese ? '已配置并激活' : 'configured and activated';
+ static String get saveFailed => _isChinese ? '保存失败' : 'Failed to save';
+ static String get removeFailed => _isChinese ? '移除失败' : 'Failed to remove';
+ static String get removeProvider => _isChinese ? '移除' : 'Remove';
+ static String get removeProviderContent => _isChinese
+ ? '这将删除 API 密钥并停用该模型。'
+ : 'This will delete the API key and deactivate the model.';
+
+ static String get startingTerminal =>
+ _isChinese ? '正在启动终端...' : 'Starting terminal...';
+ static String get failedToStartTerminal =>
+ _isChinese ? '启动终端失败' : 'Failed to start terminal';
+
+ static String get openClawOnboarding =>
+ _isChinese ? 'OpenClaw 初始配置' : 'OpenClaw Onboarding';
+ static String get startingOnboarding =>
+ _isChinese ? '正在启动配置向导...' : 'Starting onboarding...';
+ static String get goToDashboard => _isChinese ? '前往控制台' : 'Go to Dashboard';
+
+ static String get cliProxyManagement =>
+ _isChinese ? 'CLIProxy 管理中心' : 'CLIProxy Management';
+ static String get cliProxyNotRunning =>
+ _isChinese ? 'CLIProxy 服务未运行' : 'CLIProxy service is not running';
+ static String get openInBrowser => _isChinese ? '在浏览器中打开' : 'Open in browser';
+
+ static String translateError(String error) {
+ if (!_isChinese) return error;
+ if (error.contains('Download failed') || error.contains('download')) {
+ return '下载失败:请检查网络连接后重试。\n$error';
+ }
+ if (error.contains('PROOT_ERROR') || error.contains('libproot')) {
+ return '运行环境错误:proot 库缺失或不兼容当前设备架构。\n$error';
+ }
+ if (error.contains('No such file or directory')) {
+ return '文件不存在:安装包可能不完整,请重试安装。\n$error';
+ }
+ if (error.contains('Permission denied')) {
+ return '权限不足:请检查存储权限设置。\n$error';
+ }
+ if (error.contains('timeout') || error.contains('Timeout')) {
+ return '连接超时:请检查网络后重试。\n$error';
+ }
+ if (error.contains('Setup failed')) {
+ return '安装失败:$error';
+ }
+ return error;
+ }
+
+// Node Screen
+ static String get gatewayConnection =>
+ _isChinese ? '网关连接' : 'GATEWAY CONNECTION';
+ static String get localGateway => _isChinese ? '本地网关' : 'Local Gateway';
+ static String get localGatewaySubtitle =>
+ _isChinese ? '自动与本设备上的网关配对' : 'Auto-pair with gateway on this device';
+ static String get remoteGateway => _isChinese ? '远程网关' : 'Remote Gateway';
+ static String get remoteGatewaySubtitle =>
+ _isChinese ? '连接到另一台设备上的网关' : 'Connect to a gateway on another device';
+ static String get gatewayHost => _isChinese ? '网关地址' : 'Gateway Host';
+ static String get gatewayPort => _isChinese ? '网关端口' : 'Gateway Port';
+ static String get gatewayToken => _isChinese ? '网关令牌' : 'Gateway Token';
+ static String get gatewayTokenHint => _isChinese
+ ? '从网关控制台 URL 中粘贴令牌'
+ : 'Paste token from gateway dashboard URL';
+ static String get gatewayTokenHelper => _isChinese
+ ? '在控制台 URL 的 #token= 后面找到'
+ : 'Found in dashboard URL after #token=';
+ static String get connect => _isChinese ? '连接' : 'Connect';
+ static String get pairing => _isChinese ? '配对' : 'PAIRING';
+ static String get pairingPrompt =>
+ _isChinese ? '在网关上批准此配对码:' : 'Approve this code on the gateway:';
+ static String get capabilities => _isChinese ? '设备能力' : 'CAPABILITIES';
+ static String get capCamera => _isChinese ? '摄像头' : 'Camera';
+ static String get capCameraDesc =>
+ _isChinese ? '拍摄照片和视频' : 'Capture photos and video clips';
+ static String get capCanvas => _isChinese ? '画布' : 'Canvas';
+ static String get capCanvasDesc =>
+ _isChinese ? '移动端不可用' : 'Not available on mobile';
+ static String get capLocation => _isChinese ? '位置' : 'Location';
+ static String get capLocationDesc =>
+ _isChinese ? '获取设备 GPS 坐标' : 'Get device GPS coordinates';
+ static String get capScreen => _isChinese ? '屏幕录制' : 'Screen Recording';
+ static String get capScreenDesc => _isChinese
+ ? '录制设备屏幕(每次需要授权)'
+ : 'Record device screen (requires consent each time)';
+ static String get capFlash => _isChinese ? '手电筒' : 'Flashlight';
+ static String get capFlashDesc =>
+ _isChinese ? '开关设备手电筒' : 'Toggle device torch on/off';
+ static String get capVibration => _isChinese ? '振动' : 'Vibration';
+ static String get capVibrationDesc => _isChinese
+ ? '触发触觉反馈和振动'
+ : 'Trigger haptic feedback and vibration patterns';
+ static String get capSensors => _isChinese ? '传感器' : 'Sensors';
+ static String get capSensorsDesc => _isChinese
+ ? '读取加速度计、陀螺仪、磁力计、气压计'
+ : 'Read accelerometer, gyroscope, magnetometer, barometer';
+ static String get capSerial => _isChinese ? '串口' : 'Serial';
+ static String get capSerialDesc =>
+ _isChinese ? '蓝牙和 USB 串口通信' : 'Bluetooth and USB serial communication';
+ static String get deviceInfo => _isChinese ? '设备信息' : 'DEVICE INFO';
+ static String get deviceId => _isChinese ? '设备 ID' : 'Device ID';
+ static String get nodeLogs => _isChinese ? '节点日志' : 'NODE LOGS';
+ static String get noLogsYetNode => _isChinese ? '暂无日志' : 'No logs yet';
+
+ // Provider Detail
+ static String get baseUrl => 'Base URL';
+ static String get baseUrlHelper => _isChinese
+ ? 'CLIProxy 默认: http://127.0.0.1:18790/v1'
+ : 'CLIProxy default: http://127.0.0.1:18790/v1';
+
+ // AI Provider descriptions
+ static String get providerAnthropicDesc => _isChinese
+ ? 'Claude 系列模型,擅长推理和编程'
+ : 'Claude models for advanced reasoning and coding';
+ static String get providerCustomDesc => _isChinese
+ ? '任意 OpenAI 兼容 API(如 CLIProxy 18790 端口)'
+ : 'Any OpenAI-compatible API (e.g. CLIProxy on port 18790)';
+ // CLIProxy Screen
+ static String get cliProxyRunning => _isChinese ? '运行中' : 'Running';
+ static String get cliProxyStopped => _isChinese ? '已停止' : 'Stopped';
+ static String get cliProxyRefresh => _isChinese ? '刷新' : 'Refresh';
+ static String get cliProxyStop =>
+ _isChinese ? '停止 CLIProxy' : 'Stop CLIProxy';
+ static String get cliProxyStart =>
+ _isChinese ? '启动 CLIProxy' : 'Start CLIProxy';
+ static String get cliProxyStarting => _isChinese ? '正在启动...' : 'Starting...';
+ static String get cliProxyInstall =>
+ _isChinese ? '安装 CLIProxy' : 'Install CLIProxy';
+ static String get cliProxyInstallTitle =>
+ _isChinese ? '安装 CLIProxy' : 'Install CLIProxy';
+ static String get cliProxyInstallDone =>
+ _isChinese ? '安装完成,返回' : 'Done, go back';
+ static String get cliProxyInstallStarting =>
+ _isChinese ? '正在启动安装...' : 'Starting install...';
+ static String get cliProxyGuide => _isChinese
+ ? '点击下方按钮启动服务,或先安装 CLIProxy。'
+ : 'Tap the button below to start the service, or install CLIProxy first.';
+ // CLIProxy Screen
+ static String get cliProxyInstallBtn =>
+ _isChinese ? '安装 CLIProxy' : 'Install CLIProxy';
+ // 9Router
+ static String get nineRouterTerminal => _isChinese ? '9Router 终端' : '9Router Terminal';
+ static String get nineRouterTerminalSubtitle => _isChinese ? '启动免费 AI 账号代理服务' : 'Start free AI account proxy';
+ static String get nineRouterConsole => _isChinese ? '9Router 控制台' : '9Router Console';
+ static String get nineRouterConsoleSubtitle => _isChinese ? '打开 9Router Web 管理界面' : 'Open 9Router web dashboard';
+}
\ No newline at end of file
diff --git a/flutter_app/lib/l10n/app_zh.arb b/flutter_app/lib/l10n/app_zh.arb
new file mode 100644
index 0000000..ec3432f
--- /dev/null
+++ b/flutter_app/lib/l10n/app_zh.arb
@@ -0,0 +1,171 @@
+{
+ "@@locale": "zh",
+ "appName": "OpenClaw",
+ "cancel": "取消",
+ "confirm": "确认",
+ "retry": "重试",
+ "done": "完成",
+ "save": "保存",
+ "remove": "移除",
+ "install": "安装",
+ "loading": "加载中...",
+ "error": "错误",
+ "copy": "复制",
+ "paste": "粘贴",
+ "openUrl": "打开链接",
+ "screenshot": "截图",
+ "restart": "重启",
+ "later": "稍后",
+ "download": "下载",
+ "copied": "已复制到剪贴板",
+ "noUrlFound": "未找到链接",
+ "linkCopied": "链接已复制",
+ "openLink": "打开链接",
+ "aiGatewayForAndroid": "Android AI 网关",
+ "checkingSetup": "检查安装状态...",
+ "repairingBypass": "修复 Bionic Bypass...",
+ "reinstallingNode": "重新安装 Node.js...",
+ "reinstallingOpenClaw": "重新安装 OpenClaw...",
+ "quickActions": "快捷操作",
+ "terminal": "终端",
+ "terminalSubtitle": "打开 Ubuntu Shell",
+ "webDashboard": "Web 控制台",
+ "webDashboardSubtitle": "在浏览器中打开 OpenClaw 控制台",
+ "startGatewayFirst": "请先启动网关",
+ "onboarding": "初始配置",
+ "onboardingSubtitle": "配置 API 密钥和绑定设置",
+ "configure": "网关配置",
+ "configureSubtitle": "管理网关设置",
+ "aiProviders": "AI 提供商",
+ "aiProvidersSubtitle": "配置模型和 API 密钥",
+ "packages": "扩展包",
+ "packagesSubtitle": "安装可选工具 (Go, Homebrew, SSH)",
+ "sshAccess": "SSH 访问",
+ "sshAccessSubtitle": "通过 SSH 远程访问终端",
+ "logs": "日志",
+ "logsSubtitle": "查看网关输出和错误",
+ "snapshot": "快照",
+ "snapshotSubtitle": "备份或恢复配置",
+ "node": "节点",
+ "nodeConnected": "已连接到网关",
+ "nodeCapabilities": "为 AI 提供设备能力",
+ "dashboardUrlCopied": "控制台链接已复制",
+ "copyDashboardUrl": "复制控制台链接",
+ "cliProxy": "CLIProxy 管理",
+ "cliProxySubtitle": "管理免费 AI 账号代理",
+ "gateway": "网关",
+ "startGateway": "启动网关",
+ "stopGateway": "停止网关",
+ "viewLogs": "查看日志",
+ "urlCopied": "链接已复制",
+ "copyUrl": "复制链接",
+ "openDashboard": "打开控制台",
+ "gatewayRunning": "运行中",
+ "gatewayStarting": "启动中",
+ "gatewayError": "错误",
+ "gatewayStopped": "已停止",
+ "enableNode": "启用节点",
+ "disableNode": "禁用节点",
+ "reconnect": "重新连接",
+ "nodePaired": "已配对",
+ "nodeConnecting": "连接中",
+ "nodeDisconnected": "已断开",
+ "nodeDisabled": "已禁用",
+ "nodeConfigure": "配置",
+ "settings": "设置",
+ "general": "通用",
+ "autoStartGateway": "自动启动网关",
+ "autoStartSubtitle": "应用打开时自动启动网关",
+ "batteryOptimization": "电池优化",
+ "batteryOptimized": "已优化(可能终止后台会话)",
+ "batteryUnrestricted": "无限制(推荐)",
+ "setupStorage": "存储权限",
+ "storageGranted": "已授权 — proot 可访问 /sdcard,不需要时请撤销",
+ "storageNotGranted": "允许访问共享存储",
+ "nodeSection": "节点",
+ "enableNodeTitle": "启用节点",
+ "enableNodeSubtitle": "为网关提供设备能力",
+ "nodeConfiguration": "节点配置",
+ "nodeConfigSubtitle": "连接、配对和能力设置",
+ "systemInfo": "系统信息",
+ "architecture": "架构",
+ "prootPath": "PRoot 路径",
+ "rootfs": "根文件系统",
+ "installed": "已安装",
+ "notInstalled": "未安装",
+ "maintenance": "维护",
+ "exportSnapshot": "导出快照",
+ "exportSnapshotSubtitle": "备份配置到下载目录",
+ "importSnapshot": "导入快照",
+ "importSnapshotSubtitle": "从备份恢复配置",
+ "rerunSetup": "重新安装",
+ "rerunSetupSubtitle": "重新安装或修复环境",
+ "about": "关于",
+ "checkForUpdates": "检查更新",
+ "checkUpdatesSubtitle": "在 GitHub 检查新版本",
+ "developer": "开发者",
+ "github": "GitHub",
+ "contact": "联系方式",
+ "license": "许可证",
+ "updateAvailable": "发现新版本",
+ "currentVersion": "当前版本",
+ "latestVersion": "最新版本",
+ "alreadyLatest": "已是最新版本",
+ "checkUpdateFailed": "无法检查更新",
+ "snapshotSaved": "快照已保存至",
+ "exportFailed": "导出失败",
+ "noSnapshotFound": "未找到快照文件",
+ "snapshotRestored": "快照已恢复,重启网关以应用更改",
+ "importFailed": "导入失败",
+ "setupOpenClaw": "安装 OpenClaw",
+ "setupRunning": "正在配置环境,可能需要几分钟。",
+ "setupDescription": "将下载 Ubuntu、Node.js 和 OpenClaw 到独立环境中。",
+ "downloadRootfs": "下载 Ubuntu 根文件系统",
+ "extractRootfs": "解压根文件系统",
+ "installNode": "安装 Node.js",
+ "installOpenClaw": "安装 OpenClaw",
+ "configureBionicBypass": "配置 Bionic Bypass",
+ "setupComplete": "安装完成!",
+ "configureApiKeys": "配置 API 密钥",
+ "beginSetup": "开始安装",
+ "retrySetup": "重试安装",
+ "storageRequired": "需要约 500MB 存储空间和网络连接",
+ "optionalPackages": "可选扩展包",
+ "gatewayLogs": "网关日志",
+ "filterLogs": "过滤日志...",
+ "noLogsYet": "暂无日志,请启动网关。",
+ "noMatchingLogs": "没有匹配的日志。",
+ "copyAllLogs": "复制全部日志",
+ "autoScrollOn": "自动滚动已开启",
+ "autoScrollOff": "自动滚动已关闭",
+ "activeModel": "当前模型",
+ "selectProvider": "选择提供商配置 API 密钥和模型。",
+ "active": "使用中",
+ "configured": "已配置",
+ "apiKey": "API 密钥",
+ "model": "模型",
+ "customModel": "自定义...",
+ "customModelHint": "例如:meta/llama-3.3-70b-instruct",
+ "customModelLabel": "自定义模型名称",
+ "saveAndActivate": "保存并激活",
+ "removeConfiguration": "移除配置",
+ "apiKeyEmpty": "API 密钥不能为空",
+ "modelEmpty": "模型名称不能为空",
+ "configuredAndActivated": "已配置并激活",
+ "saveFailed": "保存失败",
+ "removeFailed": "移除失败",
+ "removeProvider": "移除提供商",
+ "removeProviderContent": "这将删除 API 密钥并停用该模型。",
+ "startingTerminal": "正在启动终端...",
+ "failedToStartTerminal": "启动终端失败",
+ "openClawOnboarding": "OpenClaw 初始配置",
+ "startingOnboarding": "正在启动配置向导...",
+ "failedToStartOnboarding": "启动配置向导失败",
+ "goToDashboard": "前往控制台",
+ "cliProxyManagement": "CLIProxy 管理中心",
+ "cliProxyNotRunning": "CLIProxy 服务未运行",
+ "openInBrowser": "在浏览器中打开",
+ "refresh": "刷新",
+ "reconnectProxy": "重新连接",
+ "installedBadge": "已安装"
+}
diff --git a/flutter_app/lib/models/ai_provider.dart b/flutter_app/lib/models/ai_provider.dart
index d24f8a5..22f2ce9 100644
--- a/flutter_app/lib/models/ai_provider.dart
+++ b/flutter_app/lib/models/ai_provider.dart
@@ -26,7 +26,7 @@ class AiProvider {
static const anthropic = AiProvider(
id: 'anthropic',
name: 'Anthropic',
- description: 'Claude models — advanced reasoning and coding',
+ description: 'Claude models �?advanced reasoning and coding',
icon: Icons.psychology,
color: Color(0xFFD97706),
baseUrl: 'https://api.anthropic.com/v1',
@@ -133,6 +133,30 @@ class AiProvider {
apiKeyHint: 'xai-...',
);
+ static const custom = AiProvider(
+ id: 'custom',
+ name: 'Custom / CLIProxy',
+ description: 'Any OpenAI-compatible API (e.g. CLIProxy on port 18790)',
+ icon: Icons.tune,
+ color: Color(0xFF8B5CF6),
+ baseUrl: 'http://127.0.0.1:18790/v1',
+ defaultModels: [
+ 'claude-sonnet-4-20250514',
+ 'gemini-2.5-pro',
+ 'gpt-4o',
+ ],
+ apiKeyHint: 'your-api-key (or any string if not required)',
+ );
+
/// All available AI providers.
- static const all = [anthropic, openai, google, openrouter, nvidia, deepseek, xai];
+ static const all = [
+ anthropic,
+ openai,
+ google,
+ openrouter,
+ nvidia,
+ deepseek,
+ xai,
+ custom
+ ];
}
diff --git a/flutter_app/lib/providers/node_provider.dart b/flutter_app/lib/providers/node_provider.dart
index 2848636..6c240a0 100644
--- a/flutter_app/lib/providers/node_provider.dart
+++ b/flutter_app/lib/providers/node_provider.dart
@@ -63,7 +63,7 @@ class NodeProvider extends ChangeNotifier with WidgetsBindingObserver {
text = 'Node reconnecting...';
break;
case NodeStatus.error:
- text = 'Node error — retrying';
+ text = 'Node error ?retrying';
break;
default:
return;
@@ -84,7 +84,7 @@ class NodeProvider extends ChangeNotifier with WidgetsBindingObserver {
}
}
- /// App returned to foreground — force connection health check.
+ /// App returned to foreground ?force connection health check.
/// Dart timers freeze while backgrounded, so the watchdog and ping
/// timers won't have fired. We must check and reconnect manually.
Future _onAppResumed() async {
@@ -99,7 +99,7 @@ class NodeProvider extends ChangeNotifier with WidgetsBindingObserver {
} catch (_) {}
if (_state.isPaired && _nodeService.isConnectionStale) {
- // WebSocket went stale while in background — force reconnect
+ // WebSocket went stale while in background ?force reconnect
await _nodeService.disconnect();
await _nodeService.connect();
} else if (!_state.isPaired && !_state.isConnecting) {
@@ -111,7 +111,7 @@ class NodeProvider extends ChangeNotifier with WidgetsBindingObserver {
_startWatchdog();
}
- /// App going to background — ensure the foreground service is running
+ /// App going to background ?ensure the foreground service is running
/// so Android keeps our process alive.
Future _onAppPaused() async {
if (_state.isDisabled) return;
@@ -254,10 +254,10 @@ class NodeProvider extends ChangeNotifier with WidgetsBindingObserver {
} catch (_) {}
if (!_state.isPaired && !_state.isConnecting) {
- // Connection dropped — reconnect
+ // Connection dropped ?reconnect
_nodeService.connect();
} else if (_state.isPaired && _nodeService.isConnectionStale) {
- // Connection appears alive but no data received — force reconnect
+ // Connection appears alive but no data received ?force reconnect
_nodeService.disconnect().then((_) => _nodeService.connect());
}
});
diff --git a/flutter_app/lib/screens/cliproxy_install_screen.dart b/flutter_app/lib/screens/cliproxy_install_screen.dart
new file mode 100644
index 0000000..a4e0449
--- /dev/null
+++ b/flutter_app/lib/screens/cliproxy_install_screen.dart
@@ -0,0 +1,238 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:xterm/xterm.dart';
+import 'package:flutter_pty/flutter_pty.dart';
+import '../l10n/app_strings.dart';
+import '../services/native_bridge.dart';
+import '../services/terminal_service.dart';
+import '../widgets/terminal_toolbar.dart';
+
+class CliProxyInstallScreen extends StatefulWidget {
+ const CliProxyInstallScreen({super.key});
+
+ @override
+ State createState() => _CliProxyInstallScreenState();
+}
+
+class _CliProxyInstallScreenState extends State {
+ late final Terminal _terminal;
+ late final TerminalController _controller;
+ Pty? _pty;
+ bool _loading = true;
+ bool _finished = false;
+ String? _error;
+ final _ctrlNotifier = ValueNotifier(false);
+ final _altNotifier = ValueNotifier(false);
+
+ static const _sentinel = 'CLIPROXY_INSTALL_COMPLETE';
+
+ static const _installCommand = 'set -e; '
+ 'echo ">>> Installing 9Router (Node.js AI proxy)..."; '
+ 'node --version; '
+ 'npm --version; '
+ 'echo ">>> Installing 9router globally..."; '
+ 'npm install -g 9router; '
+ 'echo ">>> 9Router installed"; '
+ '9router --version 2>/dev/null || echo "9router ready"; '
+ 'echo ">>> CLIPROXY_INSTALL_COMPLETE"';
+
+ static const _fontFallback = [
+ 'monospace',
+ 'Noto Sans Mono',
+ 'Noto Sans Mono CJK SC',
+ 'sans-serif',
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ _terminal = Terminal(maxLines: 10000);
+ _controller = TerminalController();
+ NativeBridge.startTerminalService();
+ WidgetsBinding.instance.addPostFrameCallback((_) => _startInstall());
+ }
+
+ Future _startInstall() async {
+ _pty?.kill();
+ _pty = null;
+ try {
+ try {
+ await NativeBridge.setupDirs();
+ } catch (_) {}
+ try {
+ await NativeBridge.writeResolv();
+ } catch (_) {}
+ try {
+ final filesDir = await NativeBridge.getFilesDir();
+ const rc = 'nameserver 8.8.8.8\nnameserver 8.8.4.4\n';
+ final rf = File('$filesDir/config/resolv.conf');
+ if (!rf.existsSync()) {
+ Directory('$filesDir/config').createSync(recursive: true);
+ rf.writeAsStringSync(rc);
+ }
+ final rr = File('$filesDir/rootfs/ubuntu/etc/resolv.conf');
+ if (!rr.existsSync()) {
+ rr.parent.createSync(recursive: true);
+ rr.writeAsStringSync(rc);
+ }
+ } catch (_) {}
+
+ final config = await TerminalService.getProotShellConfig();
+ final args = TerminalService.buildProotArgs(
+ config,
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+ final cmdArgs = List.from(args)
+ ..removeLast()
+ ..removeLast()
+ ..addAll(['/bin/bash', '-lc', _installCommand]);
+
+ _pty = Pty.start(
+ config['executable']!,
+ arguments: cmdArgs,
+ environment: TerminalService.buildHostEnv(config),
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+
+ _pty!.output.cast>().listen((data) {
+ final text = utf8.decode(data, allowMalformed: true);
+ _terminal.write(text);
+ if (!_finished && text.contains(_sentinel)) {
+ if (mounted) setState(() => _finished = true);
+ }
+ });
+
+ _pty!.exitCode.then((code) {
+ _terminal.write('\r\n[Process exited with code $code]\r\n');
+ if (mounted && !_finished) setState(() => _finished = true);
+ });
+
+ _terminal.onOutput = (data) {
+ if (_ctrlNotifier.value && data.length == 1) {
+ final code = data.toLowerCase().codeUnitAt(0);
+ if (code >= 97 && code <= 122) {
+ _pty?.write(Uint8List.fromList([code - 96]));
+ _ctrlNotifier.value = false;
+ return;
+ }
+ }
+ if (_altNotifier.value && data.isNotEmpty) {
+ _pty?.write(utf8.encode('\x1b$data'));
+ _altNotifier.value = false;
+ return;
+ }
+ _pty?.write(utf8.encode(data));
+ };
+ _terminal.onResize = (w, h, pw, ph) => _pty?.resize(h, w);
+ setState(() => _loading = false);
+ } catch (e) {
+ setState(() {
+ _loading = false;
+ _error = 'Failed: $e';
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ _ctrlNotifier.dispose();
+ _altNotifier.dispose();
+ _controller.dispose();
+ _pty?.kill();
+ NativeBridge.stopTerminalService();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(AppStrings.cliProxyInstallTitle),
+ automaticallyImplyLeading: false,
+ ),
+ body: Column(
+ children: [
+ if (_loading)
+ Expanded(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const CircularProgressIndicator(),
+ const SizedBox(height: 16),
+ Text(AppStrings.cliProxyInstallStarting),
+ ],
+ ),
+ ),
+ )
+ else if (_error != null)
+ Expanded(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error_outline,
+ size: 48, color: Theme.of(context).colorScheme.error),
+ const SizedBox(height: 16),
+ Text(_error!,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.error)),
+ const SizedBox(height: 16),
+ FilledButton.icon(
+ onPressed: () => setState(() {
+ _loading = true;
+ _error = null;
+ _finished = false;
+ _startInstall();
+ }),
+ icon: const Icon(Icons.refresh),
+ label: Text(AppStrings.retry),
+ ),
+ ],
+ ),
+ ),
+ ),
+ )
+ else ...[
+ Expanded(
+ child: TerminalView(
+ _terminal,
+ controller: _controller,
+ textStyle: const TerminalStyle(
+ fontSize: 11,
+ height: 1.0,
+ fontFamily: 'DejaVuSansMono',
+ fontFamilyFallback: _fontFallback,
+ ),
+ ),
+ ),
+ TerminalToolbar(
+ pty: _pty,
+ ctrlNotifier: _ctrlNotifier,
+ altNotifier: _altNotifier,
+ ),
+ ],
+ if (_finished)
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: () => Navigator.of(context).pop(true),
+ icon: const Icon(Icons.check),
+ label: Text(AppStrings.cliProxyInstallDone),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/flutter_app/lib/screens/cliproxy_screen.dart b/flutter_app/lib/screens/cliproxy_screen.dart
new file mode 100644
index 0000000..ed46871
--- /dev/null
+++ b/flutter_app/lib/screens/cliproxy_screen.dart
@@ -0,0 +1,494 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:xterm/xterm.dart' as xterm;
+import 'package:flutter_pty/flutter_pty.dart' as flutter_pty;
+import '../app.dart';
+import '../l10n/app_strings.dart';
+import '../services/native_bridge.dart';
+import '../services/terminal_service.dart';
+import '../utils/responsive.dart';
+import 'cliproxy_install_screen.dart';
+
+class CliProxyScreen extends StatefulWidget {
+ const CliProxyScreen({super.key});
+
+ @override
+ State createState() => _CliProxyScreenState();
+}
+
+class _CliProxyScreenState extends State {
+ static const String _cliProxyUrl = 'http://127.0.0.1:20128/';
+ static const int _cliProxyPort = 20128;
+
+ late final WebViewController _controller;
+ bool _loading = true;
+ bool _hasError = false;
+ bool _isRunning = false;
+ bool _isStarting = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onPageStarted: (_) => setState(() {
+ _loading = true;
+ _hasError = false;
+ }),
+ onPageFinished: (_) => setState(() {
+ _loading = false;
+ _isRunning = true;
+ }),
+ onWebResourceError: (error) {
+ if (error.isForMainFrame ?? true) {
+ setState(() {
+ _loading = false;
+ _hasError = true;
+ _isRunning = false;
+ });
+ }
+ },
+ ),
+ )
+ ..loadRequest(Uri.parse(_cliProxyUrl));
+ _checkRunning();
+ }
+
+ Future _checkRunning() async {
+ try {
+ final socket = await Socket.connect('127.0.0.1', _cliProxyPort,
+ timeout: const Duration(seconds: 2));
+ socket.destroy();
+ if (mounted) setState(() => _isRunning = true);
+ } catch (_) {
+ if (mounted) setState(() => _isRunning = false);
+ }
+ }
+
+ Future _startCliProxy() async {
+ // 跳转到 OpenClaw 终端,自动输入启动命令
+ // 用户在终端里看到 9router 启动后,返回此页面点重新连接
+ if (!mounted) return;
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: Text(AppStrings.isChinese ? '启动 9Router' : 'Start 9Router'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(AppStrings.isChinese
+ ? '请在终端中运行以下命令启动 9Router:'
+ : 'Run the following command in the terminal:'),
+ const SizedBox(height: 12),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.black87,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const SelectableText(
+ '9router --port 20128 &',
+ style: TextStyle(
+ fontFamily: 'monospace',
+ color: Colors.greenAccent,
+ fontSize: 13,
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ AppStrings.isChinese
+ ? '启动后选择 Hide to Tray,然后返回点重新连接。'
+ : 'Select "Hide to Tray", then come back and tap Reconnect.',
+ style: TextStyle(color: Colors.grey[600], fontSize: 12),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(ctx),
+ child: Text(AppStrings.cancel),
+ ),
+ FilledButton(
+ onPressed: () {
+ Navigator.pop(ctx);
+ // 跳转到终端并自动执行命令
+ Navigator.of(context)
+ .push(
+ MaterialPageRoute(
+ builder: (_) => const _NineRouterTerminalScreen()),
+ )
+ .then((_) {
+ Future.delayed(const Duration(seconds: 3), () {
+ _checkRunning().then((_) {
+ if (_isRunning && mounted) {
+ setState(() => _hasError = false);
+ _controller.reload();
+ }
+ });
+ });
+ });
+ },
+ child: Text(AppStrings.isChinese ? '打开终端' : 'Open Terminal'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future _stopCliProxy() async {
+ try {
+ await NativeBridge.runInProot(
+ 'pkill -f "9router" 2>/dev/null || true',
+ timeout: 10,
+ );
+ await Future.delayed(const Duration(seconds: 1));
+ setState(() {
+ _isRunning = false;
+ _hasError = true;
+ });
+ } catch (_) {}
+ }
+
+ Future _showCliProxyLog() async {
+ String log = '';
+ try {
+ final result = await NativeBridge.runInProot(
+ 'cat /tmp/9router.log 2>/dev/null || echo "No log file found"',
+ timeout: 10,
+ );
+ log = result;
+ } catch (e) {
+ log = 'Failed to read log: $e';
+ }
+ if (!mounted) return;
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title:
+ Text(AppStrings.isChinese ? '9Router 启动日志' : '9Router Startup Log'),
+ content: SizedBox(
+ width: double.maxFinite,
+ height: 300,
+ child: SingleChildScrollView(
+ child: SelectableText(
+ log.isEmpty ? (AppStrings.isChinese ? '暂无日志' : 'No logs') : log,
+ style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
+ ),
+ ),
+ ),
+ actions: [
+ FilledButton(
+ onPressed: () => Navigator.pop(ctx),
+ child: Text(AppStrings.done),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isTablet = Responsive.isTablet(context);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(AppStrings.cliProxyManagement),
+ actions: [
+ if (_isStarting)
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Center(
+ child: SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ ),
+ ),
+ )
+ else if (_isRunning)
+ IconButton(
+ icon: const Icon(Icons.stop_circle_outlined),
+ tooltip: AppStrings.cliProxyStop,
+ onPressed: _stopCliProxy,
+ )
+ else
+ IconButton(
+ icon: const Icon(Icons.play_circle_outlined),
+ tooltip: AppStrings.cliProxyStart,
+ onPressed: _startCliProxy,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(right: 4),
+ child: Center(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: (_isRunning
+ ? AppColors.statusGreen
+ : AppColors.statusGrey)
+ .withAlpha(25),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: (_isRunning
+ ? AppColors.statusGreen
+ : AppColors.statusGrey)
+ .withAlpha(60),
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ _isRunning ? Icons.circle : Icons.circle_outlined,
+ size: 8,
+ color: _isRunning
+ ? AppColors.statusGreen
+ : AppColors.statusGrey,
+ ),
+ const SizedBox(width: 4),
+ Text(
+ _isRunning
+ ? AppStrings.cliProxyRunning
+ : AppStrings.cliProxyStopped,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: _isRunning
+ ? AppColors.statusGreen
+ : AppColors.statusGrey,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.refresh),
+ tooltip: AppStrings.cliProxyRefresh,
+ onPressed: () {
+ setState(() {
+ _loading = true;
+ _hasError = false;
+ });
+ _controller.reload();
+ _checkRunning();
+ },
+ ),
+ IconButton(
+ icon: const Icon(Icons.open_in_browser),
+ tooltip: AppStrings.openInBrowser,
+ onPressed: () => launchUrl(Uri.parse(_cliProxyUrl),
+ mode: LaunchMode.externalApplication),
+ ),
+ ],
+ ),
+ body: _hasError
+ ? _buildErrorView(context, isTablet)
+ : Stack(
+ children: [
+ WebViewWidget(controller: _controller),
+ if (_loading) const Center(child: CircularProgressIndicator()),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildErrorView(BuildContext context, bool isTablet) {
+ final theme = Theme.of(context);
+ return Responsive.constrain(
+ Center(
+ child: Padding(
+ padding: EdgeInsets.all(isTablet ? 48 : 32),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.cloud_off,
+ size: isTablet ? 80 : 64,
+ color: theme.colorScheme.onSurfaceVariant),
+ const SizedBox(height: 24),
+ Text(AppStrings.cliProxyNotRunning,
+ style: theme.textTheme.titleLarge
+ ?.copyWith(fontWeight: FontWeight.w600),
+ textAlign: TextAlign.center),
+ const SizedBox(height: 12),
+ Text(AppStrings.cliProxyGuide,
+ style: theme.textTheme.bodyMedium
+ ?.copyWith(color: theme.colorScheme.onSurfaceVariant),
+ textAlign: TextAlign.center),
+ const SizedBox(height: 32),
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: _isStarting ? null : _startCliProxy,
+ icon: _isStarting
+ ? const SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(
+ strokeWidth: 2, color: Colors.white))
+ : const Icon(Icons.play_arrow),
+ label: Text(_isStarting
+ ? AppStrings.cliProxyStarting
+ : AppStrings.cliProxyStart),
+ ),
+ ),
+ const SizedBox(height: 12),
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton.icon(
+ onPressed: () async {
+ final result = await Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => const CliProxyInstallScreen()),
+ );
+ if (result == true) _startCliProxy();
+ },
+ icon: const Icon(Icons.download),
+ label: Text(AppStrings.cliProxyInstallBtn),
+ ),
+ ),
+ const SizedBox(height: 12),
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ _loading = true;
+ _hasError = false;
+ });
+ _controller.reload();
+ _checkRunning();
+ },
+ icon: const Icon(Icons.refresh, size: 18),
+ label: Text(AppStrings.reconnect),
+ ),
+ const SizedBox(height: 8),
+ TextButton.icon(
+ onPressed: _showCliProxyLog,
+ icon: const Icon(Icons.article_outlined, size: 18),
+ label:
+ Text(AppStrings.isChinese ? '查看启动日志' : 'View startup log'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// 终端界面:在 proot 里前台运行 9router
+class _NineRouterTerminalScreen extends StatefulWidget {
+ const _NineRouterTerminalScreen();
+
+ @override
+ State<_NineRouterTerminalScreen> createState() =>
+ _NineRouterTerminalScreenState();
+}
+
+class _NineRouterTerminalScreenState extends State<_NineRouterTerminalScreen> {
+ static const _fontFallback = ['monospace', 'Noto Sans Mono', 'sans-serif'];
+
+ late final xterm.Terminal _terminal;
+ late final xterm.TerminalController _controller;
+ flutter_pty.Pty? _pty;
+ bool _loading = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _terminal = xterm.Terminal(maxLines: 5000);
+ _controller = xterm.TerminalController();
+ NativeBridge.startTerminalService();
+ WidgetsBinding.instance.addPostFrameCallback((_) => _start());
+ }
+
+ Future _start() async {
+ try {
+ try {
+ await NativeBridge.setupDirs();
+ } catch (_) {}
+ try {
+ await NativeBridge.writeResolv();
+ } catch (_) {}
+ final config = await TerminalService.getProotShellConfig();
+ final args = TerminalService.buildProotArgs(
+ config,
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+ final cmdArgs = List.from(args)
+ ..removeLast()
+ ..removeLast()
+ ..addAll([
+ '/bin/bash',
+ '-lc',
+ 'echo "Starting 9Router on port 20128..."; '
+ 'nohup 9router --port 20128 > /tmp/9router.log 2>&1 & '
+ 'sleep 2 && echo "9Router started" && cat /tmp/9router.log'
+ ]);
+
+ _pty = flutter_pty.Pty.start(
+ config['executable']!,
+ arguments: cmdArgs,
+ environment: TerminalService.buildHostEnv(config),
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+ _pty!.output.cast>().listen((data) {
+ _terminal.write(utf8.decode(data, allowMalformed: true));
+ });
+ _pty!.exitCode.then((code) {
+ _terminal.write('\r\n[Process exited with code $code]\r\n');
+ });
+ _terminal.onOutput = (data) => _pty?.write(utf8.encode(data));
+ _terminal.onResize = (w, h, pw, ph) => _pty?.resize(h, w);
+ setState(() => _loading = false);
+ } catch (e) {
+ setState(() => _loading = false);
+ _terminal.write('Error: $e\r\n');
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ _pty?.kill();
+ NativeBridge.stopTerminalService();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(AppStrings.isChinese ? '9Router 运行终端' : '9Router Terminal'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.arrow_back),
+ tooltip: AppStrings.isChinese ? '返回管理界面' : 'Back',
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ body: _loading
+ ? const Center(child: CircularProgressIndicator())
+ : xterm.TerminalView(
+ _terminal,
+ controller: _controller,
+ textStyle: const xterm.TerminalStyle(
+ fontSize: 11,
+ height: 1.0,
+ fontFamily: 'DejaVuSansMono',
+ fontFamilyFallback: _fontFallback,
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter_app/lib/screens/configure_screen.dart b/flutter_app/lib/screens/configure_screen.dart
index f695700..60fb8da 100644
--- a/flutter_app/lib/screens/configure_screen.dart
+++ b/flutter_app/lib/screens/configure_screen.dart
@@ -1,4 +1,4 @@
-import 'dart:convert';
+import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -297,7 +297,7 @@ class _ConfigureScreenState extends State {
body: Column(
children: [
if (_loading)
- const Expanded(
+ Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
diff --git a/flutter_app/lib/screens/dashboard_screen.dart b/flutter_app/lib/screens/dashboard_screen.dart
index dc8d5c1..35d5787 100644
--- a/flutter_app/lib/screens/dashboard_screen.dart
+++ b/flutter_app/lib/screens/dashboard_screen.dart
@@ -1,9 +1,11 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../constants.dart';
+import '../l10n/app_strings.dart';
import '../providers/gateway_provider.dart';
import '../providers/node_provider.dart';
+import '../utils/responsive.dart';
import '../widgets/gateway_controls.dart';
import '../widgets/status_card.dart';
import 'node_screen.dart';
@@ -16,6 +18,8 @@ import 'packages_screen.dart';
import 'providers_screen.dart';
import 'settings_screen.dart';
import 'ssh_screen.dart';
+import 'nine_router_terminal_screen.dart';
+import 'nine_router_webview_screen.dart';
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@@ -23,10 +27,11 @@ class DashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final isTablet = Responsive.isTablet(context);
return Scaffold(
appBar: AppBar(
- title: const Text('OpenClaw'),
+ title: Text(AppStrings.appName),
actions: [
IconButton(
icon: const Icon(Icons.settings),
@@ -36,181 +41,238 @@ class DashboardScreen extends StatelessWidget {
),
],
),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const GatewayControls(),
- const SizedBox(height: 20),
- Padding(
- padding: const EdgeInsets.only(left: 4, bottom: 8),
- child: Text(
- 'QUICK ACTIONS',
- style: theme.textTheme.labelSmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- fontWeight: FontWeight.w600,
- letterSpacing: 1.2,
- ),
- ),
- ),
- StatusCard(
- title: 'Terminal',
- subtitle: 'Open Ubuntu shell with OpenClaw',
- icon: Icons.terminal,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const TerminalScreen()),
- ),
- ),
- Consumer(
- builder: (context, provider, _) {
- final url = provider.state.dashboardUrl;
- final token = url != null
- ? RegExp(r'#token=([0-9a-f]+)').firstMatch(url)?.group(1)
- : null;
- final subtitle = provider.state.isRunning
- ? (token != null
- ? 'Token: ${token.substring(0, (token.length > 8 ? 8 : token.length))}...'
- : 'Open OpenClaw dashboard in browser')
- : 'Start gateway first';
- return StatusCard(
- title: 'Web Dashboard',
- subtitle: subtitle,
- icon: Icons.dashboard,
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (token != null)
- IconButton(
- icon: const Icon(Icons.copy, size: 18),
- tooltip: 'Copy dashboard URL',
- onPressed: () {
- Clipboard.setData(ClipboardData(text: url!));
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Dashboard URL copied')),
- );
- },
- ),
- const Icon(Icons.chevron_right),
- ],
+ body: Responsive.constrain(
+ SingleChildScrollView(
+ padding: EdgeInsets.all(isTablet ? 24 : 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const GatewayControls(),
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.only(left: 4, bottom: 8),
+ child: Text(
+ AppStrings.quickActions,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ fontWeight: FontWeight.w600,
+ letterSpacing: 1.2,
),
- onTap: provider.state.isRunning
- ? () => Navigator.of(context).push(
- MaterialPageRoute(
- builder: (_) => WebDashboardScreen(
- url: url,
- ),
- ),
- )
- : null,
- );
- },
- ),
- StatusCard(
- title: 'Onboarding',
- subtitle: 'Configure API keys and binding',
- icon: Icons.vpn_key,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const OnboardingScreen()),
- ),
- ),
- StatusCard(
- title: 'Configure',
- subtitle: 'Manage gateway settings',
- icon: Icons.tune,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const ConfigureScreen()),
- ),
- ),
- StatusCard(
- title: 'AI Providers',
- subtitle: 'Configure models and API keys',
- icon: Icons.model_training,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const ProvidersScreen()),
- ),
- ),
- StatusCard(
- title: 'Packages',
- subtitle: 'Install optional tools (Go, Homebrew, SSH)',
- icon: Icons.extension,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const PackagesScreen()),
- ),
- ),
- StatusCard(
- title: 'SSH Access',
- subtitle: 'Remote terminal access via SSH',
- icon: Icons.terminal,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const SshScreen()),
- ),
- ),
- StatusCard(
- title: 'Logs',
- subtitle: 'View gateway output and errors',
- icon: Icons.article_outlined,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const LogsScreen()),
- ),
- ),
- StatusCard(
- title: 'Snapshot',
- subtitle: 'Backup or restore your config',
- icon: Icons.backup,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const SettingsScreen()),
+ ),
),
- ),
- Consumer(
- builder: (context, nodeProvider, _) {
- final nodeState = nodeProvider.state;
- return StatusCard(
- title: 'Node',
- subtitle: nodeState.isPaired
- ? 'Connected to gateway'
- : nodeState.isDisabled
- ? 'Device capabilities for AI'
- : nodeState.statusText,
- icon: Icons.devices,
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const NodeScreen()),
- ),
- );
- },
- ),
- const SizedBox(height: 24),
- Center(
- child: Column(
- children: [
- Text(
- 'OpenClaw v${AppConstants.version}',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ if (isTablet)
+ _buildTabletGrid(context, theme)
+ else
+ _buildPhoneList(context, theme),
+ const SizedBox(height: 24),
+ Center(
+ child: Column(
+ children: [
+ Text(
+ 'OpenClaw v${AppConstants.version}',
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
),
- ),
- const SizedBox(height: 2),
- Text(
- 'by ${AppConstants.authorName} | ${AppConstants.orgName}',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ const SizedBox(height: 2),
+ Text(
+ 'by ${AppConstants.authorName} | ${AppConstants.orgName}',
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
);
}
+
+ // 手机:竖向列�?
+ Widget _buildPhoneList(BuildContext context, ThemeData theme) {
+ return Column(
+ children: _buildCards(context),
+ );
+ }
+
+ // 平板�?列网�?
+ Widget _buildTabletGrid(BuildContext context, ThemeData theme) {
+ final cards = _buildCards(context);
+ final rows = [];
+ for (int i = 0; i < cards.length; i += 2) {
+ rows.add(
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(child: cards[i]),
+ const SizedBox(width: 12),
+ Expanded(
+ child: i + 1 < cards.length ? cards[i + 1] : const SizedBox()),
+ ],
+ ),
+ );
+ }
+ return Column(children: rows);
+ }
+
+ List _buildCards(BuildContext context) {
+ return [
+ StatusCard(
+ title: AppStrings.terminal,
+ subtitle: AppStrings.terminalSubtitle,
+ icon: Icons.terminal,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const TerminalScreen()),
+ ),
+ ),
+ Consumer(
+ builder: (context, provider, _) {
+ final url = provider.state.dashboardUrl;
+ final token = url != null
+ ? RegExp(r'#token=([0-9a-f]+)').firstMatch(url)?.group(1)
+ : null;
+ final subtitle = provider.state.isRunning
+ ? (token != null
+ ? 'Token: ${token.substring(0, (token.length > 8 ? 8 : token.length))}...'
+ : AppStrings.webDashboardSubtitle)
+ : AppStrings.startGatewayFirst;
+ return StatusCard(
+ title: AppStrings.webDashboard,
+ subtitle: subtitle,
+ icon: Icons.dashboard,
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (token != null)
+ IconButton(
+ icon: const Icon(Icons.copy, size: 18),
+ tooltip: AppStrings.copyDashboardUrl,
+ onPressed: () {
+ Clipboard.setData(ClipboardData(text: url!));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(AppStrings.dashboardUrlCopied)),
+ );
+ },
+ ),
+ const Icon(Icons.chevron_right),
+ ],
+ ),
+ onTap: provider.state.isRunning
+ ? () => Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => WebDashboardScreen(url: url),
+ ),
+ )
+ : null,
+ );
+ },
+ ),
+ StatusCard(
+ title: AppStrings.isChinese ? '9Router 终端' : '9Router Terminal',
+ subtitle: AppStrings.isChinese
+ ? '启动免费 AI 账号代理服务'
+ : 'Start free AI account proxy',
+ icon: Icons.swap_horiz,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const NineRouterTerminalScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.isChinese ? '9Router 控制台' : '9Router Console',
+ subtitle:
+ AppStrings.isChinese ? '打开 Web 管理界面' : 'Open web management UI',
+ icon: Icons.dashboard_customize,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const NineRouterWebviewScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.onboarding,
+ subtitle: AppStrings.onboardingSubtitle,
+ icon: Icons.vpn_key,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const OnboardingScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.configure,
+ subtitle: AppStrings.configureSubtitle,
+ icon: Icons.tune,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const ConfigureScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.aiProviders,
+ subtitle: AppStrings.aiProvidersSubtitle,
+ icon: Icons.model_training,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const ProvidersScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.packages,
+ subtitle: AppStrings.packagesSubtitle,
+ icon: Icons.extension,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const PackagesScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.sshAccess,
+ subtitle: AppStrings.sshAccessSubtitle,
+ icon: Icons.terminal,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const SshScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.logs,
+ subtitle: AppStrings.logsSubtitle,
+ icon: Icons.article_outlined,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const LogsScreen()),
+ ),
+ ),
+ StatusCard(
+ title: AppStrings.snapshot,
+ subtitle: AppStrings.snapshotSubtitle,
+ icon: Icons.backup,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const SettingsScreen()),
+ ),
+ ),
+ Consumer(
+ builder: (context, nodeProvider, _) {
+ final nodeState = nodeProvider.state;
+ return StatusCard(
+ title: AppStrings.node,
+ subtitle: nodeState.isPaired
+ ? AppStrings.nodeConnected
+ : nodeState.isDisabled
+ ? AppStrings.nodeCapabilities
+ : nodeState.statusText,
+ icon: Icons.devices,
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const NodeScreen()),
+ ),
+ );
+ },
+ ),
+ ];
+ }
}
diff --git a/flutter_app/lib/screens/logs_screen.dart b/flutter_app/lib/screens/logs_screen.dart
index eca00e2..acf729a 100644
--- a/flutter_app/lib/screens/logs_screen.dart
+++ b/flutter_app/lib/screens/logs_screen.dart
@@ -1,9 +1,12 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../app.dart';
+import '../l10n/app_strings.dart';
import '../providers/gateway_provider.dart';
import '../services/screenshot_service.dart';
+import '../utils/log_parser.dart';
+import '../utils/responsive.dart';
class LogsScreen extends StatefulWidget {
const LogsScreen({super.key});
@@ -17,6 +20,7 @@ class _LogsScreenState extends State {
final _searchController = TextEditingController();
final _screenshotKey = GlobalKey();
bool _autoScroll = true;
+ bool _friendlyMode = true; // 友好模式 vs 原始模式
String _filter = '';
@override
@@ -32,104 +36,215 @@ class _LogsScreenState extends State {
return Scaffold(
appBar: AppBar(
- title: const Text('Gateway Logs'),
+ title: Text(AppStrings.gatewayLogs),
actions: [
+ // 切换友好/原始模式
+ IconButton(
+ icon: Icon(_friendlyMode ? Icons.code : Icons.auto_awesome),
+ tooltip: _friendlyMode
+ ? (AppStrings.isChinese ? '切换到原始日志' : 'Raw logs')
+ : (AppStrings.isChinese ? '切换到友好模式' : 'Friendly mode'),
+ onPressed: () => setState(() => _friendlyMode = !_friendlyMode),
+ ),
IconButton(
icon: const Icon(Icons.camera_alt_outlined),
- tooltip: 'Screenshot',
+ tooltip: AppStrings.screenshot,
onPressed: _takeScreenshot,
),
IconButton(
icon: Icon(
- _autoScroll ? Icons.vertical_align_bottom : Icons.vertical_align_top,
+ _autoScroll
+ ? Icons.vertical_align_bottom
+ : Icons.vertical_align_top,
),
- tooltip: _autoScroll ? 'Auto-scroll on' : 'Auto-scroll off',
+ tooltip: _autoScroll
+ ? AppStrings.autoScrollOn
+ : AppStrings.autoScrollOff,
onPressed: () => setState(() => _autoScroll = !_autoScroll),
),
IconButton(
icon: const Icon(Icons.copy),
- tooltip: 'Copy all logs',
+ tooltip: AppStrings.copyAllLogs,
onPressed: () => _copyLogs(context),
),
],
),
- body: Column(
- children: [
- Padding(
- padding: const EdgeInsets.all(8),
- child: TextField(
- controller: _searchController,
- decoration: InputDecoration(
- hintText: 'Filter logs...',
- prefixIcon: const Icon(Icons.search),
- isDense: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 8,
+ body: Responsive.constrain(
+ Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8),
+ child: TextField(
+ controller: _searchController,
+ decoration: InputDecoration(
+ hintText: AppStrings.filterLogs,
+ prefixIcon: const Icon(Icons.search),
+ isDense: true,
+ contentPadding:
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ suffixIcon: _filter.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.clear),
+ onPressed: () {
+ _searchController.clear();
+ setState(() => _filter = '');
+ },
+ )
+ : null,
),
- suffixIcon: _filter.isNotEmpty
- ? IconButton(
- icon: const Icon(Icons.clear),
- onPressed: () {
- _searchController.clear();
- setState(() => _filter = '');
- },
- )
- : null,
+ onChanged: (value) => setState(() => _filter = value),
),
- onChanged: (value) => setState(() => _filter = value),
),
- ),
- Expanded(
- child: RepaintBoundary(
- key: _screenshotKey,
- child: Consumer(
- builder: (context, provider, _) {
- final logs = provider.state.logs;
- final filtered = _filter.isEmpty
- ? logs
- : logs.where((l) =>
- l.toLowerCase().contains(_filter.toLowerCase())).toList();
-
- if (filtered.isEmpty) {
- return Center(
- child: Text(
- logs.isEmpty ? 'No logs yet. Start the gateway.' : 'No matching logs.',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- );
- }
-
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (_autoScroll && _scrollController.hasClients) {
- _scrollController.jumpTo(
- _scrollController.position.maxScrollExtent,
- );
- }
- });
-
- return ListView.builder(
- controller: _scrollController,
- padding: const EdgeInsets.symmetric(horizontal: 8),
- itemCount: filtered.length,
- itemBuilder: (context, index) {
- final line = filtered[index];
- return Text(
- line,
- style: TextStyle(
- fontFamily: 'monospace',
- fontSize: 12,
- color: _logColor(line, theme),
- ),
+ Expanded(
+ child: RepaintBoundary(
+ key: _screenshotKey,
+ child: Consumer(
+ builder: (context, provider, _) {
+ final logs = provider.state.logs;
+ final filtered = _filter.isEmpty
+ ? logs
+ : logs
+ .where((l) =>
+ l.toLowerCase().contains(_filter.toLowerCase()))
+ .toList();
+
+ if (filtered.isEmpty) {
+ return Center(
+ child: Text(
+ logs.isEmpty
+ ? AppStrings.noLogsYet
+ : AppStrings.noMatchingLogs,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ );
+ }
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (_autoScroll && _scrollController.hasClients) {
+ _scrollController.jumpTo(
+ _scrollController.position.maxScrollExtent,
+ );
+ }
+ });
+
+ return ListView.builder(
+ controller: _scrollController,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8, vertical: 4),
+ itemCount: filtered.length,
+ itemBuilder: (context, index) {
+ final line = filtered[index];
+ if (_friendlyMode) {
+ return _buildFriendlyLogItem(line, theme);
+ }
+ return _buildRawLogItem(line, theme);
+ },
);
},
- );
- },
+ ),
+ ),
),
+ ],
+ ),
+ ),
+ );
+ }
+
+ // 友好模式:卡片样式
+ Widget _buildFriendlyLogItem(String line, ThemeData theme) {
+ final parsed = LogParser.parse(line, theme);
+ final isDark = theme.brightness == Brightness.dark;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2),
+ child: Container(
+ decoration: BoxDecoration(
+ color: parsed.color.withAlpha(isDark ? 15 : 10),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: parsed.color.withAlpha(40)),
+ ),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: parsed.detail != null ? () => _showRawLog(line) : null,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(parsed.icon, size: 16, color: parsed.color),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ parsed.friendlyMessage,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurface,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ if (parsed.time.isNotEmpty) ...[
+ const SizedBox(height: 2),
+ Text(
+ parsed.time,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ fontSize: 10,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ if (parsed.detail != null)
+ Icon(Icons.chevron_right,
+ size: 14, color: theme.colorScheme.onSurfaceVariant),
+ ],
),
),
+ ),
+ ),
+ );
+ }
+
+ // 原始模式:等宽字体文本
+ Widget _buildRawLogItem(String line, ThemeData theme) {
+ return Text(
+ line,
+ style: TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 11,
+ color: _logColor(line, theme),
+ ),
+ );
+ }
+
+ void _showRawLog(String line) {
+ showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: Text(AppStrings.isChinese ? '原始日志' : 'Raw Log'),
+ content: SingleChildScrollView(
+ child: SelectableText(
+ line,
+ style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Clipboard.setData(ClipboardData(text: line));
+ Navigator.pop(ctx);
+ },
+ child: Text(AppStrings.copy),
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pop(ctx),
+ child: Text(AppStrings.done),
+ ),
],
),
);
@@ -142,20 +257,17 @@ class _LogsScreenState extends State {
if (line.contains('[WARN]') || line.contains('WARNING')) {
return AppColors.statusAmber;
}
- if (line.contains('[INFO]')) {
- return AppColors.mutedText;
- }
+ if (line.contains('[INFO]')) return AppColors.mutedText;
return theme.colorScheme.onSurface;
}
Future _takeScreenshot() async {
- final path = await ScreenshotService.capture(_screenshotKey, prefix: 'logs');
+ final path =
+ await ScreenshotService.capture(_screenshotKey, prefix: 'logs');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text(path != null
- ? 'Screenshot saved: ${path.split('/').last}'
- : 'Failed to capture screenshot'),
+ content: Text(path != null ? '截图已保存: ${path.split('/').last}' : '截图失败'),
),
);
}
@@ -165,7 +277,7 @@ class _LogsScreenState extends State {
final text = provider.state.logs.join('\n');
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Logs copied to clipboard')),
+ SnackBar(content: Text(AppStrings.copied)),
);
}
}
diff --git a/flutter_app/lib/screens/nine_router_terminal_screen.dart b/flutter_app/lib/screens/nine_router_terminal_screen.dart
new file mode 100644
index 0000000..46f0747
--- /dev/null
+++ b/flutter_app/lib/screens/nine_router_terminal_screen.dart
@@ -0,0 +1,221 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:xterm/xterm.dart';
+import 'package:flutter_pty/flutter_pty.dart';
+import '../l10n/app_strings.dart';
+import '../services/native_bridge.dart';
+import '../services/terminal_service.dart';
+import '../widgets/terminal_toolbar.dart';
+
+/// 9Router 专属终端,使用独立的 NineRouterService 保活进程
+class NineRouterTerminalScreen extends StatefulWidget {
+ const NineRouterTerminalScreen({super.key});
+
+ @override
+ State createState() =>
+ _NineRouterTerminalScreenState();
+}
+
+class _NineRouterTerminalScreenState extends State {
+ late final Terminal _terminal;
+ late final TerminalController _controller;
+ Pty? _pty;
+ bool _loading = true;
+ String? _error;
+ final _ctrlNotifier = ValueNotifier(false);
+ final _altNotifier = ValueNotifier(false);
+
+ static const _fontFallback = [
+ 'monospace',
+ 'Noto Sans Mono',
+ 'Noto Sans Mono CJK SC',
+ 'Noto Color Emoji',
+ 'sans-serif',
+ ];
+
+ // 自动执行的启动命令
+ static const _autoCommand =
+ 'nohup 9router --port 20128 > /tmp/9router.log 2>&1 & echo "9Router starting..." && sleep 2 && curl -s http://127.0.0.1:20128/ > /dev/null && echo "9Router is running on port 20128!" || echo "Starting in background, check CLIProxy console."\n';
+
+ @override
+ void initState() {
+ super.initState();
+ _terminal = Terminal(maxLines: 10000);
+ _controller = TerminalController();
+ // 使用独立的 NineRouterService
+ NativeBridge.startNineRouterService();
+ WidgetsBinding.instance.addPostFrameCallback((_) => _startPty());
+ }
+
+ Future _startPty() async {
+ _pty?.kill();
+ _pty = null;
+ try {
+ try {
+ await NativeBridge.setupDirs();
+ } catch (_) {}
+ try {
+ await NativeBridge.writeResolv();
+ } catch (_) {}
+ try {
+ final filesDir = await NativeBridge.getFilesDir();
+ const rc = 'nameserver 8.8.8.8\nnameserver 8.8.4.4\n';
+ final rf = File('$filesDir/config/resolv.conf');
+ if (!rf.existsSync()) {
+ Directory('$filesDir/config').createSync(recursive: true);
+ rf.writeAsStringSync(rc);
+ }
+ final rr = File('$filesDir/rootfs/ubuntu/etc/resolv.conf');
+ if (!rr.existsSync()) {
+ rr.parent.createSync(recursive: true);
+ rr.writeAsStringSync(rc);
+ }
+ } catch (_) {}
+
+ final config = await TerminalService.getProotShellConfig();
+ final args = TerminalService.buildProotArgs(
+ config,
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+
+ _pty = Pty.start(
+ config['executable']!,
+ arguments: args,
+ environment: TerminalService.buildHostEnv(config),
+ columns: _terminal.viewWidth,
+ rows: _terminal.viewHeight,
+ );
+
+ _pty!.output.cast>().listen((data) {
+ _terminal.write(utf8.decode(data, allowMalformed: true));
+ });
+
+ _pty!.exitCode.then((code) {
+ _terminal.write('\r\n[Process exited with code $code]\r\n');
+ });
+
+ _terminal.onOutput = (data) {
+ if (_ctrlNotifier.value && data.length == 1) {
+ final code = data.toLowerCase().codeUnitAt(0);
+ if (code >= 97 && code <= 122) {
+ _pty?.write(Uint8List.fromList([code - 96]));
+ _ctrlNotifier.value = false;
+ return;
+ }
+ }
+ if (_altNotifier.value && data.isNotEmpty) {
+ _pty?.write(utf8.encode('\x1b$data'));
+ _altNotifier.value = false;
+ return;
+ }
+ _pty?.write(utf8.encode(data));
+ };
+
+ _terminal.onResize = (w, h, pw, ph) => _pty?.resize(h, w);
+
+ setState(() => _loading = false);
+
+ // 等 shell 就绪后自动执行启动命令
+ await Future.delayed(const Duration(milliseconds: 1500));
+ _pty?.write(utf8.encode(_autoCommand));
+ } catch (e) {
+ setState(() {
+ _loading = false;
+ _error = 'Failed to start: $e';
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ _ctrlNotifier.dispose();
+ _altNotifier.dispose();
+ _controller.dispose();
+ _pty?.kill();
+ // 不停止 NineRouterService,让 9router 继续在后台运行
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(AppStrings.isChinese ? '9Router 终端' : '9Router Terminal'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.refresh),
+ tooltip: AppStrings.restart,
+ onPressed: () {
+ _pty?.kill();
+ setState(() {
+ _loading = true;
+ _error = null;
+ });
+ _startPty();
+ },
+ ),
+ ],
+ ),
+ body: _loading
+ ? const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('正在启动 9Router...'),
+ ],
+ ))
+ : _error != null
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error_outline,
+ size: 48, color: Theme.of(context).colorScheme.error),
+ const SizedBox(height: 16),
+ Text(_error!, textAlign: TextAlign.center),
+ const SizedBox(height: 16),
+ FilledButton.icon(
+ onPressed: () {
+ setState(() {
+ _loading = true;
+ _error = null;
+ });
+ _startPty();
+ },
+ icon: const Icon(Icons.refresh),
+ label: Text(AppStrings.retry),
+ ),
+ ],
+ ),
+ ))
+ : Column(
+ children: [
+ Expanded(
+ child: TerminalView(
+ _terminal,
+ controller: _controller,
+ textStyle: const TerminalStyle(
+ fontSize: 11,
+ height: 1.0,
+ fontFamily: 'DejaVuSansMono',
+ fontFamilyFallback: _fontFallback,
+ ),
+ ),
+ ),
+ TerminalToolbar(
+ pty: _pty,
+ ctrlNotifier: _ctrlNotifier,
+ altNotifier: _altNotifier,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/flutter_app/lib/screens/nine_router_webview_screen.dart b/flutter_app/lib/screens/nine_router_webview_screen.dart
new file mode 100644
index 0000000..4ca4ee3
--- /dev/null
+++ b/flutter_app/lib/screens/nine_router_webview_screen.dart
@@ -0,0 +1,125 @@
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:url_launcher/url_launcher.dart';
+import '../l10n/app_strings.dart';
+import '../utils/responsive.dart';
+
+class NineRouterWebviewScreen extends StatefulWidget {
+ const NineRouterWebviewScreen({super.key});
+
+ @override
+ State createState() =>
+ _NineRouterWebviewScreenState();
+}
+
+class _NineRouterWebviewScreenState extends State {
+ static const String _url = 'http://127.0.0.1:20128/';
+
+ late final WebViewController _controller;
+ bool _loading = true;
+ bool _hasError = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(NavigationDelegate(
+ onPageStarted: (_) => setState(() {
+ _loading = true;
+ _hasError = false;
+ }),
+ onPageFinished: (_) => setState(() => _loading = false),
+ onWebResourceError: (error) {
+ if (error.isForMainFrame ?? true) {
+ setState(() {
+ _loading = false;
+ _hasError = true;
+ });
+ }
+ },
+ ))
+ ..loadRequest(Uri.parse(_url));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isTablet = Responsive.isTablet(context);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(AppStrings.isChinese ? '9Router 控制台' : '9Router Console'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.refresh),
+ tooltip: AppStrings.isChinese ? '刷新' : 'Refresh',
+ onPressed: () {
+ setState(() {
+ _loading = true;
+ _hasError = false;
+ });
+ _controller.reload();
+ },
+ ),
+ IconButton(
+ icon: const Icon(Icons.open_in_browser),
+ tooltip: AppStrings.openInBrowser,
+ onPressed: () => launchUrl(Uri.parse(_url),
+ mode: LaunchMode.externalApplication),
+ ),
+ ],
+ ),
+ body: _hasError
+ ? Responsive.constrain(Center(
+ child: Padding(
+ padding: EdgeInsets.all(isTablet ? 48 : 32),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.cloud_off,
+ size: isTablet ? 80 : 64,
+ color: theme.colorScheme.onSurfaceVariant),
+ const SizedBox(height: 24),
+ Text(
+ AppStrings.isChinese
+ ? '9Router 未运行'
+ : '9Router is not running',
+ style: theme.textTheme.titleLarge
+ ?.copyWith(fontWeight: FontWeight.w600),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ AppStrings.isChinese
+ ? '请先在"9Router 终端"中启动服务,然后返回此页面。'
+ : 'Please start the service in "9Router Terminal" first.',
+ style: theme.textTheme.bodyMedium
+ ?.copyWith(color: theme.colorScheme.onSurfaceVariant),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 32),
+ FilledButton.icon(
+ onPressed: () {
+ setState(() {
+ _loading = true;
+ _hasError = false;
+ });
+ _controller.reload();
+ },
+ icon: const Icon(Icons.refresh),
+ label: Text(AppStrings.reconnect),
+ ),
+ ],
+ ),
+ ),
+ ))
+ : Stack(
+ children: [
+ WebViewWidget(controller: _controller),
+ if (_loading) const Center(child: CircularProgressIndicator()),
+ ],
+ ),
+ );
+ }
+}
diff --git a/flutter_app/lib/screens/node_screen.dart b/flutter_app/lib/screens/node_screen.dart
index f9b012d..133dde0 100644
--- a/flutter_app/lib/screens/node_screen.dart
+++ b/flutter_app/lib/screens/node_screen.dart
@@ -1,8 +1,10 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app.dart';
+import '../l10n/app_strings.dart';
import '../providers/node_provider.dart';
import '../services/preferences_service.dart';
+import '../utils/responsive.dart';
import '../widgets/node_controls.dart';
class NodeScreen extends StatefulWidget {
@@ -53,226 +55,183 @@ class _NodeScreenState extends State {
final theme = Theme.of(context);
return Scaffold(
- appBar: AppBar(title: const Text('Node Configuration')),
+ appBar: AppBar(title: Text(AppStrings.nodeConfiguration)),
body: _loading
? const Center(child: CircularProgressIndicator())
- : Consumer(
- builder: (context, provider, _) {
- final state = provider.state;
-
- return ListView(
- padding: const EdgeInsets.all(16),
- children: [
- const NodeControls(),
- const SizedBox(height: 16),
-
- // Gateway Connection
- _sectionHeader(theme, 'GATEWAY CONNECTION'),
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- RadioListTile(
- title: const Text('Local Gateway'),
- subtitle: const Text('Auto-pair with gateway on this device'),
- value: true,
- groupValue: _isLocal,
- onChanged: (value) {
- setState(() => _isLocal = value!);
- },
- ),
- RadioListTile(
- title: const Text('Remote Gateway'),
- subtitle: const Text('Connect to a gateway on another device'),
- value: false,
- groupValue: _isLocal,
- onChanged: (value) {
- setState(() => _isLocal = value!);
- },
- ),
- if (!_isLocal) ...[
- const SizedBox(height: 12),
- TextField(
- controller: _hostController,
- decoration: const InputDecoration(
- labelText: 'Gateway Host',
- hintText: '192.168.1.100',
- ),
- ),
- const SizedBox(height: 12),
- TextField(
- controller: _portController,
- decoration: const InputDecoration(
- labelText: 'Gateway Port',
- hintText: '18789',
- ),
- keyboardType: TextInputType.number,
- ),
- const SizedBox(height: 12),
- TextField(
- controller: _tokenController,
- decoration: const InputDecoration(
- labelText: 'Gateway Token',
- hintText: 'Paste token from gateway dashboard URL',
- helperText: 'Found in dashboard URL after #token=',
- prefixIcon: Icon(Icons.key),
- ),
- obscureText: true,
- ),
- const SizedBox(height: 12),
- FilledButton.icon(
- onPressed: () {
- final host = _hostController.text.trim();
- final port = int.tryParse(_portController.text.trim()) ?? 18789;
- final token = _tokenController.text.trim();
- if (host.isNotEmpty) {
- provider.connectRemote(host, port,
- token: token.isNotEmpty ? token : null);
- }
- },
- icon: const Icon(Icons.link),
- label: const Text('Connect'),
- ),
- ],
- ],
- ),
- ),
- ),
- const SizedBox(height: 16),
-
- // Pairing Status
- if (state.pairingCode != null) ...[
- _sectionHeader(theme, 'PAIRING'),
+ : Responsive.constrain(
+ Consumer(
+ builder: (context, provider, _) {
+ final state = provider.state;
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ const NodeControls(),
+ const SizedBox(height: 16),
+ _sectionHeader(theme, AppStrings.gatewayConnection),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Icon(Icons.qr_code, size: 48),
- const SizedBox(height: 8),
- Text(
- 'Approve this code on the gateway:',
- style: theme.textTheme.bodyMedium,
+ RadioListTile(
+ title: Text(AppStrings.localGateway),
+ subtitle: Text(AppStrings.localGatewaySubtitle),
+ value: true,
+ groupValue: _isLocal,
+ onChanged: (v) => setState(() => _isLocal = v!),
),
- const SizedBox(height: 8),
- SelectableText(
- state.pairingCode!,
- style: theme.textTheme.headlineMedium?.copyWith(
- fontFamily: 'monospace',
- fontWeight: FontWeight.bold,
- color: theme.colorScheme.primary,
- ),
+ RadioListTile(
+ title: Text(AppStrings.remoteGateway),
+ subtitle:
+ Text(AppStrings.remoteGatewaySubtitle),
+ value: false,
+ groupValue: _isLocal,
+ onChanged: (v) => setState(() => _isLocal = v!),
),
+ if (!_isLocal) ...[
+ const SizedBox(height: 12),
+ TextField(
+ controller: _hostController,
+ decoration: InputDecoration(
+ labelText: AppStrings.gatewayHost,
+ hintText: '192.168.1.100',
+ ),
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _portController,
+ decoration: InputDecoration(
+ labelText: AppStrings.gatewayPort,
+ hintText: '18789',
+ ),
+ keyboardType: TextInputType.number,
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: _tokenController,
+ decoration: InputDecoration(
+ labelText: AppStrings.gatewayToken,
+ hintText: AppStrings.gatewayTokenHint,
+ helperText: AppStrings.gatewayTokenHelper,
+ prefixIcon: const Icon(Icons.key),
+ ),
+ obscureText: true,
+ ),
+ const SizedBox(height: 12),
+ FilledButton.icon(
+ onPressed: () {
+ final host = _hostController.text.trim();
+ final port = int.tryParse(
+ _portController.text.trim()) ??
+ 18789;
+ final token = _tokenController.text.trim();
+ if (host.isNotEmpty) {
+ provider.connectRemote(host, port,
+ token:
+ token.isNotEmpty ? token : null);
+ }
+ },
+ icon: const Icon(Icons.link),
+ label: Text(AppStrings.connect),
+ ),
+ ],
],
),
),
),
const SizedBox(height: 16),
- ],
-
- // Capabilities
- _sectionHeader(theme, 'CAPABILITIES'),
- _capabilityTile(
- theme,
- 'Camera',
- 'Capture photos and video clips',
- Icons.camera_alt,
- ),
- _capabilityTile(
- theme,
- 'Canvas',
- 'Not available on mobile',
- Icons.web,
- available: false,
- ),
- _capabilityTile(
- theme,
- 'Location',
- 'Get device GPS coordinates',
- Icons.location_on,
- ),
- _capabilityTile(
- theme,
- 'Screen Recording',
- 'Record device screen (requires consent each time)',
- Icons.screen_share,
- ),
- _capabilityTile(
- theme,
- 'Flashlight',
- 'Toggle device torch on/off',
- Icons.flashlight_on,
- ),
- _capabilityTile(
- theme,
- 'Vibration',
- 'Trigger haptic feedback and vibration patterns',
- Icons.vibration,
- ),
- _capabilityTile(
- theme,
- 'Sensors',
- 'Read accelerometer, gyroscope, magnetometer, barometer',
- Icons.sensors,
- ),
- _capabilityTile(
- theme,
- 'Serial',
- 'Bluetooth and USB serial communication',
- Icons.usb,
- ),
- const SizedBox(height: 16),
-
- // Device Info
- if (state.deviceId != null) ...[
- _sectionHeader(theme, 'DEVICE INFO'),
- ListTile(
- title: const Text('Device ID'),
- subtitle: SelectableText(
- state.deviceId!,
- style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
- ),
- leading: const Icon(Icons.fingerprint),
- ),
- ],
- const SizedBox(height: 16),
-
- // Logs
- _sectionHeader(theme, 'NODE LOGS'),
- Card(
- child: Container(
- height: 200,
- padding: const EdgeInsets.all(12),
- child: state.logs.isEmpty
- ? Center(
- child: Text(
- 'No logs yet',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ if (state.pairingCode != null) ...[
+ _sectionHeader(theme, AppStrings.pairing),
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ children: [
+ const Icon(Icons.qr_code, size: 48),
+ const SizedBox(height: 8),
+ Text(AppStrings.pairingPrompt,
+ style: theme.textTheme.bodyMedium),
+ const SizedBox(height: 8),
+ SelectableText(
+ state.pairingCode!,
+ style:
+ theme.textTheme.headlineMedium?.copyWith(
+ fontFamily: 'monospace',
+ fontWeight: FontWeight.bold,
+ color: theme.colorScheme.primary,
),
),
- )
- : ListView.builder(
- reverse: true,
- itemCount: state.logs.length,
- itemBuilder: (context, index) {
- final log = state.logs[state.logs.length - 1 - index];
- return Text(
- log,
- style: const TextStyle(
- fontFamily: 'monospace',
- fontSize: 11,
+ ],
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ ],
+ _sectionHeader(theme, AppStrings.capabilities),
+ _capabilityTile(theme, AppStrings.capCamera,
+ AppStrings.capCameraDesc, Icons.camera_alt),
+ _capabilityTile(theme, AppStrings.capCanvas,
+ AppStrings.capCanvasDesc, Icons.web,
+ available: false),
+ _capabilityTile(theme, AppStrings.capLocation,
+ AppStrings.capLocationDesc, Icons.location_on),
+ _capabilityTile(theme, AppStrings.capScreen,
+ AppStrings.capScreenDesc, Icons.screen_share),
+ _capabilityTile(theme, AppStrings.capFlash,
+ AppStrings.capFlashDesc, Icons.flashlight_on),
+ _capabilityTile(theme, AppStrings.capVibration,
+ AppStrings.capVibrationDesc, Icons.vibration),
+ _capabilityTile(theme, AppStrings.capSensors,
+ AppStrings.capSensorsDesc, Icons.sensors),
+ _capabilityTile(theme, AppStrings.capSerial,
+ AppStrings.capSerialDesc, Icons.usb),
+ const SizedBox(height: 16),
+ if (state.deviceId != null) ...[
+ _sectionHeader(theme, AppStrings.deviceInfo),
+ ListTile(
+ title: Text(AppStrings.deviceId),
+ subtitle: SelectableText(
+ state.deviceId!,
+ style: const TextStyle(
+ fontFamily: 'monospace', fontSize: 12),
+ ),
+ leading: const Icon(Icons.fingerprint),
+ ),
+ ],
+ const SizedBox(height: 16),
+ _sectionHeader(theme, AppStrings.nodeLogs),
+ Card(
+ child: Container(
+ height: 200,
+ padding: const EdgeInsets.all(12),
+ child: state.logs.isEmpty
+ ? Center(
+ child: Text(
+ AppStrings.noLogsYetNode,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
),
- );
- },
- ),
+ ),
+ )
+ : ListView.builder(
+ reverse: true,
+ itemCount: state.logs.length,
+ itemBuilder: (context, index) {
+ final log = state
+ .logs[state.logs.length - 1 - index];
+ return Text(log,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 11));
+ },
+ ),
+ ),
),
- ),
- ],
- );
- },
+ ],
+ );
+ },
+ ),
),
);
}
@@ -300,16 +259,9 @@ class _NodeScreenState extends State {
title: Text(title),
subtitle: Text(subtitle),
trailing: available
- ? const Icon(
- Icons.check_circle,
- color: AppColors.statusGreen,
- size: 20,
- )
- : const Icon(
- Icons.block,
- color: AppColors.statusAmber,
- size: 20,
- ),
+ ? const Icon(Icons.check_circle,
+ color: AppColors.statusGreen, size: 20)
+ : const Icon(Icons.block, color: AppColors.statusAmber, size: 20),
),
);
}
diff --git a/flutter_app/lib/screens/onboarding_screen.dart b/flutter_app/lib/screens/onboarding_screen.dart
index b2e409e..685cd98 100644
--- a/flutter_app/lib/screens/onboarding_screen.dart
+++ b/flutter_app/lib/screens/onboarding_screen.dart
@@ -1,4 +1,4 @@
-import 'dart:convert';
+import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -6,6 +6,7 @@ import 'package:xterm/xterm.dart';
import 'package:flutter_pty/flutter_pty.dart';
import 'package:url_launcher/url_launcher.dart';
import '../constants.dart';
+import '../l10n/app_strings.dart';
import '../services/native_bridge.dart';
import '../services/screenshot_service.dart';
import '../services/terminal_service.dart';
@@ -69,7 +70,7 @@ class _OnboardingScreenState extends State {
// Defer PTY start until after the first frame so TerminalView has been
// laid out and _terminal.viewWidth/viewHeight reflect real screen
// dimensions instead of the 80×24 default. This is critical for QR
- // codes — the shell must know the actual column count to avoid wrapping.
+ // codes �?the shell must know the actual column count to avoid wrapping.
WidgetsBinding.instance.addPostFrameCallback((_) {
_startOnboarding();
});
@@ -415,9 +416,9 @@ class _OnboardingScreenState extends State {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
- title: const Text('OpenClaw Onboarding'),
+ title: Text(AppStrings.openClawOnboarding),
leading: widget.isFirstRun
- ? null // no back button during first-run
+ ? null
: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
@@ -426,22 +427,22 @@ class _OnboardingScreenState extends State {
actions: [
IconButton(
icon: const Icon(Icons.camera_alt_outlined),
- tooltip: 'Screenshot',
+ tooltip: AppStrings.screenshot,
onPressed: _takeScreenshot,
),
IconButton(
icon: const Icon(Icons.copy),
- tooltip: 'Copy',
+ tooltip: AppStrings.copy,
onPressed: _copySelection,
),
IconButton(
icon: const Icon(Icons.open_in_browser),
- tooltip: 'Open URL',
+ tooltip: AppStrings.openUrl,
onPressed: _openSelection,
),
IconButton(
icon: const Icon(Icons.paste),
- tooltip: 'Paste',
+ tooltip: AppStrings.paste,
onPressed: _paste,
),
],
@@ -449,14 +450,14 @@ class _OnboardingScreenState extends State {
body: Column(
children: [
if (_loading)
- const Expanded(
+ Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
- Text('Starting onboarding...'),
+ Text(AppStrings.startingOnboarding),
],
),
),
@@ -534,8 +535,8 @@ class _OnboardingScreenState extends State {
? Icons.arrow_forward
: Icons.check),
label: Text(widget.isFirstRun
- ? 'Go to Dashboard'
- : 'Done'),
+ ? AppStrings.goToDashboard
+ : AppStrings.done),
),
),
),
diff --git a/flutter_app/lib/screens/package_install_screen.dart b/flutter_app/lib/screens/package_install_screen.dart
index 9cb543d..1edbfe9 100644
--- a/flutter_app/lib/screens/package_install_screen.dart
+++ b/flutter_app/lib/screens/package_install_screen.dart
@@ -1,4 +1,4 @@
-import 'dart:convert';
+import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -210,7 +210,7 @@ class _PackageInstallScreenState extends State {
body: Column(
children: [
if (_loading)
- const Expanded(
+ Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
diff --git a/flutter_app/lib/screens/packages_screen.dart b/flutter_app/lib/screens/packages_screen.dart
index 9324309..3d97ebb 100644
--- a/flutter_app/lib/screens/packages_screen.dart
+++ b/flutter_app/lib/screens/packages_screen.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import '../app.dart';
import '../models/optional_package.dart';
import '../services/package_service.dart';
@@ -82,7 +82,7 @@ class _PackagesScreenState extends State {
return Scaffold(
appBar: AppBar(title: const Text('Optional Packages')),
body: _loading
- ? const Center(child: CircularProgressIndicator())
+ ? Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(16),
children: [
diff --git a/flutter_app/lib/screens/provider_detail_screen.dart b/flutter_app/lib/screens/provider_detail_screen.dart
index 8601b88..09633cd 100644
--- a/flutter_app/lib/screens/provider_detail_screen.dart
+++ b/flutter_app/lib/screens/provider_detail_screen.dart
@@ -1,9 +1,10 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import '../app.dart';
+import '../l10n/app_strings.dart';
import '../models/ai_provider.dart';
import '../services/provider_config_service.dart';
+import '../utils/responsive.dart';
-/// Form screen to configure API key and model for a single AI provider.
class ProviderDetailScreen extends StatefulWidget {
final AiProvider provider;
final String? existingApiKey;
@@ -25,29 +26,38 @@ class _ProviderDetailScreenState extends State {
late final TextEditingController _apiKeyController;
late final TextEditingController _customModelController;
+ late final TextEditingController _baseUrlController;
late String _selectedModel;
bool _isCustomModel = false;
bool _obscureKey = true;
bool _saving = false;
bool _removing = false;
- bool get _isConfigured => widget.existingApiKey != null && widget.existingApiKey!.isNotEmpty;
+ bool get _isCustomProvider => widget.provider.id == 'custom';
+
+ bool get _isConfigured =>
+ widget.existingApiKey != null && widget.existingApiKey!.isNotEmpty;
- /// Returns the effective model name to save.
String get _effectiveModel =>
_isCustomModel ? _customModelController.text.trim() : _selectedModel;
+ String get _effectiveBaseUrl => _isCustomProvider
+ ? _baseUrlController.text.trim()
+ : widget.provider.baseUrl;
+
@override
void initState() {
super.initState();
- _apiKeyController = TextEditingController(text: widget.existingApiKey ?? '');
+ _apiKeyController =
+ TextEditingController(text: widget.existingApiKey ?? '');
_customModelController = TextEditingController();
+ _baseUrlController = TextEditingController(text: widget.provider.baseUrl);
- final existing = widget.existingModel ?? widget.provider.defaultModels.first;
+ final existing =
+ widget.existingModel ?? widget.provider.defaultModels.first;
if (widget.provider.defaultModels.contains(existing)) {
_selectedModel = existing;
} else {
- // Existing model is not in the predefined list — treat as custom
_selectedModel = _customModelSentinel;
_isCustomModel = true;
_customModelController.text = existing;
@@ -56,6 +66,7 @@ class _ProviderDetailScreenState extends State {
@override
void dispose() {
+ _baseUrlController.dispose();
_apiKeyController.dispose();
_customModelController.dispose();
super.dispose();
@@ -65,14 +76,14 @@ class _ProviderDetailScreenState extends State {
final apiKey = _apiKeyController.text.trim();
if (apiKey.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('API key cannot be empty')),
+ SnackBar(content: Text(AppStrings.apiKeyEmpty)),
);
return;
}
final model = _effectiveModel;
if (model.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Model name cannot be empty')),
+ SnackBar(content: Text(AppStrings.modelEmpty)),
);
return;
}
@@ -83,17 +94,20 @@ class _ProviderDetailScreenState extends State {
provider: widget.provider,
apiKey: apiKey,
model: model,
+ baseUrl: _effectiveBaseUrl,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('${widget.provider.name} configured and activated')),
+ SnackBar(
+ content: Text(
+ '${widget.provider.name} ${AppStrings.configuredAndActivated}')),
);
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Failed to save: $e')),
+ SnackBar(content: Text('${AppStrings.saveFailed}: $e')),
);
}
} finally {
@@ -105,16 +119,16 @@ class _ProviderDetailScreenState extends State {
final confirmed = await showDialog(
context: context,
builder: (ctx) => AlertDialog(
- title: Text('Remove ${widget.provider.name}?'),
- content: const Text('This will delete the API key and deactivate the model.'),
+ title: Text('${AppStrings.removeProvider} ${widget.provider.name}?'),
+ content: Text(AppStrings.removeProviderContent),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
- child: const Text('Cancel'),
+ child: Text(AppStrings.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
- child: const Text('Remove'),
+ child: Text(AppStrings.remove),
),
],
),
@@ -124,17 +138,19 @@ class _ProviderDetailScreenState extends State {
setState(() => _removing = true);
try {
- await ProviderConfigService.removeProviderConfig(provider: widget.provider);
+ await ProviderConfigService.removeProviderConfig(
+ provider: widget.provider);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('${widget.provider.name} removed')),
+ SnackBar(
+ content: Text('${widget.provider.name} ${AppStrings.remove}')),
);
Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Failed to remove: $e')),
+ SnackBar(content: Text('${AppStrings.removeFailed}: $e')),
);
}
} finally {
@@ -150,134 +166,150 @@ class _ProviderDetailScreenState extends State {
return Scaffold(
appBar: AppBar(title: Text(widget.provider.name)),
- body: ListView(
- padding: const EdgeInsets.all(16),
- children: [
- // Provider header
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: iconBg,
- borderRadius: BorderRadius.circular(12),
+ body: Responsive.constrain(
+ ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ children: [
+ Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: iconBg,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(widget.provider.icon,
+ color: widget.provider.color),
),
- child: Icon(widget.provider.icon, color: widget.provider.color),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- widget.provider.name,
- style: theme.textTheme.titleMedium?.copyWith(
- fontWeight: FontWeight.w600,
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.provider.name,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
),
- ),
- const SizedBox(height: 4),
- Text(
- widget.provider.description,
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ const SizedBox(height: 4),
+ Text(
+ widget.provider.description,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
- ),
- const SizedBox(height: 24),
-
- // API Key
- Text(
- 'API Key',
- style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
- ),
- const SizedBox(height: 8),
- TextField(
- controller: _apiKeyController,
- obscureText: _obscureKey,
- decoration: InputDecoration(
- hintText: widget.provider.apiKeyHint,
- suffixIcon: IconButton(
- icon: Icon(_obscureKey ? Icons.visibility_off : Icons.visibility),
- onPressed: () => setState(() => _obscureKey = !_obscureKey),
- ),
+ const SizedBox(height: 24),
+ // Base URL - 仅 custom 提供商可编辑,其他只读显示
+ Text(
+ 'Base URL',
+ style: theme.textTheme.titleSmall
+ ?.copyWith(fontWeight: FontWeight.w600),
),
- ),
- const SizedBox(height: 24),
-
- // Model selection
- Text(
- 'Model',
- style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
- ),
- const SizedBox(height: 8),
- DropdownButtonFormField(
- value: _selectedModel,
- isExpanded: true,
- decoration: const InputDecoration(),
- items: [
- ...widget.provider.defaultModels
- .map((m) => DropdownMenuItem(value: m, child: Text(m))),
- const DropdownMenuItem(
- value: _customModelSentinel,
- child: Text('Custom...'),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _baseUrlController,
+ readOnly: !_isCustomProvider,
+ decoration: InputDecoration(
+ hintText: 'http://127.0.0.1:18790/v1',
+ helperText: _isCustomProvider ? AppStrings.baseUrlHelper : null,
),
- ],
- onChanged: (value) {
- if (value != null) {
- setState(() {
- _selectedModel = value;
- _isCustomModel = value == _customModelSentinel;
- });
- }
- },
- ),
- if (_isCustomModel) ...[
- const SizedBox(height: 12),
+ ),
+ const SizedBox(height: 24),
+ Text(
+ AppStrings.apiKey,
+ style: theme.textTheme.titleSmall
+ ?.copyWith(fontWeight: FontWeight.w600),
+ ),
+ const SizedBox(height: 8),
TextField(
- controller: _customModelController,
- decoration: const InputDecoration(
- hintText: 'e.g. meta/llama-3.3-70b-instruct',
- labelText: 'Custom model name',
+ controller: _apiKeyController,
+ obscureText: _obscureKey,
+ decoration: InputDecoration(
+ hintText: widget.provider.apiKeyHint,
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscureKey ? Icons.visibility_off : Icons.visibility),
+ onPressed: () => setState(() => _obscureKey = !_obscureKey),
+ ),
),
),
- ],
- const SizedBox(height: 32),
-
- // Actions
- FilledButton(
- onPressed: _saving ? null : _save,
- child: _saving
- ? const SizedBox(
- height: 20,
- width: 20,
- child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
- )
- : const Text('Save & Activate'),
- ),
- if (_isConfigured) ...[
- const SizedBox(height: 12),
- OutlinedButton(
- onPressed: _removing ? null : _remove,
- child: _removing
+ const SizedBox(height: 24),
+ Text(
+ AppStrings.model,
+ style: theme.textTheme.titleSmall
+ ?.copyWith(fontWeight: FontWeight.w600),
+ ),
+ const SizedBox(height: 8),
+ DropdownButtonFormField(
+ value: _selectedModel,
+ isExpanded: true,
+ decoration: const InputDecoration(),
+ items: [
+ ...widget.provider.defaultModels
+ .map((m) => DropdownMenuItem(value: m, child: Text(m))),
+ DropdownMenuItem(
+ value: _customModelSentinel,
+ child: Text(AppStrings.customModel),
+ ),
+ ],
+ onChanged: (value) {
+ if (value != null) {
+ setState(() {
+ _selectedModel = value;
+ _isCustomModel = value == _customModelSentinel;
+ });
+ }
+ },
+ ),
+ if (_isCustomModel) ...[
+ const SizedBox(height: 12),
+ TextField(
+ controller: _customModelController,
+ decoration: InputDecoration(
+ hintText: AppStrings.customModelHint,
+ labelText: AppStrings.customModelLabel,
+ ),
+ ),
+ ],
+ const SizedBox(height: 32),
+ FilledButton(
+ onPressed: _saving ? null : _save,
+ child: _saving
? const SizedBox(
height: 20,
width: 20,
- child: CircularProgressIndicator(strokeWidth: 2),
+ child: CircularProgressIndicator(
+ strokeWidth: 2, color: Colors.white),
)
- : const Text('Remove Configuration'),
+ : Text(AppStrings.saveAndActivate),
),
+ if (_isConfigured) ...[
+ const SizedBox(height: 12),
+ OutlinedButton(
+ onPressed: _removing ? null : _remove,
+ child: _removing
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : Text(AppStrings.removeConfiguration),
+ ),
+ ],
],
- ],
+ ),
),
);
}
diff --git a/flutter_app/lib/screens/providers_screen.dart b/flutter_app/lib/screens/providers_screen.dart
index 3bbb535..baa8f82 100644
--- a/flutter_app/lib/screens/providers_screen.dart
+++ b/flutter_app/lib/screens/providers_screen.dart
@@ -1,7 +1,9 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import '../app.dart';
+import '../l10n/app_strings.dart';
import '../models/ai_provider.dart';
import '../services/provider_config_service.dart';
+import '../utils/responsive.dart';
import 'provider_detail_screen.dart';
/// Lists all AI providers with their configuration status.
@@ -53,13 +55,12 @@ class _ProvidersScreenState extends State {
String _statusLabel(AiProvider provider) {
final isConfigured = _providers.containsKey(provider.id);
if (!isConfigured) return '';
- // Check if the active model belongs to this provider
if (_activeModel != null) {
final isActive = provider.defaultModels.any((m) => _activeModel!.contains(m)) ||
_activeModel!.contains(provider.id);
- if (isActive) return 'Active';
+ if (isActive) return AppStrings.active;
}
- return 'Configured';
+ return AppStrings.configured;
}
@override
@@ -68,68 +69,69 @@ class _ProvidersScreenState extends State {
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
- appBar: AppBar(title: const Text('AI Providers')),
+ appBar: AppBar(title: Text(AppStrings.aiProviders)),
body: _loading
- ? const Center(child: CircularProgressIndicator())
- : ListView(
- padding: const EdgeInsets.all(16),
- children: [
- // Active model card
- if (_activeModel != null && _activeModel!.isNotEmpty) ...[
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Row(
- children: [
- Container(
- padding: const EdgeInsets.all(10),
- decoration: BoxDecoration(
- color: AppColors.statusGreen.withAlpha(25),
- borderRadius: BorderRadius.circular(12),
- ),
- child: const Icon(
- Icons.check_circle,
- color: AppColors.statusGreen,
+ ? Center(child: CircularProgressIndicator())
+ : Responsive.constrain(
+ ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ if (_activeModel != null && _activeModel!.isNotEmpty) ...[
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: AppColors.statusGreen.withAlpha(25),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Icon(
+ Icons.check_circle,
+ color: AppColors.statusGreen,
+ ),
),
- ),
- const SizedBox(width: 16),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Active Model',
- style: theme.textTheme.labelSmall?.copyWith(
- color: AppColors.statusGreen,
- fontWeight: FontWeight.w600,
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ AppStrings.activeModel,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: AppColors.statusGreen,
+ fontWeight: FontWeight.w600,
+ ),
),
- ),
- const SizedBox(height: 2),
- Text(
- _activeModel!,
- style: theme.textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.w600,
+ const SizedBox(height: 2),
+ Text(
+ _activeModel!,
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
+ const SizedBox(height: 16),
+ ],
+ Text(
+ AppStrings.selectProvider,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
),
const SizedBox(height: 16),
+ for (final provider in AiProvider.all)
+ _buildProviderCard(theme, provider, isDark),
],
- Text(
- 'Select a provider to configure its API key and model.',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- const SizedBox(height: 16),
- for (final provider in AiProvider.all)
- _buildProviderCard(theme, provider, isDark),
- ],
+ ),
),
);
}
@@ -177,7 +179,7 @@ class _ProvidersScreenState extends State {
vertical: 2,
),
decoration: BoxDecoration(
- color: (status == 'Active'
+ color: (status == AppStrings.active
? AppColors.statusGreen
: AppColors.statusAmber)
.withAlpha(25),
@@ -186,7 +188,7 @@ class _ProvidersScreenState extends State {
child: Text(
status,
style: theme.textTheme.labelSmall?.copyWith(
- color: status == 'Active'
+ color: status == AppStrings.active
? AppColors.statusGreen
: AppColors.statusAmber,
fontWeight: FontWeight.w600,
diff --git a/flutter_app/lib/screens/settings_screen.dart b/flutter_app/lib/screens/settings_screen.dart
index 3aad664..dffeb49 100644
--- a/flutter_app/lib/screens/settings_screen.dart
+++ b/flutter_app/lib/screens/settings_screen.dart
@@ -1,4 +1,4 @@
-import 'dart:convert';
+import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
@@ -6,10 +6,12 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../app.dart';
import '../constants.dart';
+import '../l10n/app_strings.dart';
import '../providers/node_provider.dart';
import '../services/native_bridge.dart';
import '../services/preferences_service.dart';
import '../services/update_service.dart';
+import '../utils/responsive.dart';
import 'node_screen.dart';
import 'setup_wizard_screen.dart';
@@ -85,253 +87,206 @@ class _SettingsScreenState extends State {
final theme = Theme.of(context);
return Scaffold(
- appBar: AppBar(title: const Text('Settings')),
+ appBar: AppBar(title: Text(AppStrings.settings)),
body: _loading
- ? const Center(child: CircularProgressIndicator())
- : ListView(
- children: [
- _sectionHeader(theme, 'GENERAL'),
- SwitchListTile(
- title: const Text('Auto-start gateway'),
- subtitle: const Text('Start the gateway when the app opens'),
- value: _autoStart,
- onChanged: (value) {
- setState(() => _autoStart = value);
- _prefs.autoStartGateway = value;
- },
- ),
- ListTile(
- title: const Text('Battery Optimization'),
- subtitle: Text(_batteryOptimized
- ? 'Optimized (may kill background sessions)'
- : 'Unrestricted (recommended)'),
- leading: const Icon(Icons.battery_alert),
- trailing: _batteryOptimized
- ? const Icon(Icons.warning, color: AppColors.statusAmber)
- : const Icon(Icons.check_circle, color: AppColors.statusGreen),
- onTap: () async {
- await NativeBridge.requestBatteryOptimization();
- // Refresh status after returning from settings
- final optimized = await NativeBridge.isBatteryOptimized();
- setState(() => _batteryOptimized = optimized);
- },
- ),
- ListTile(
- title: const Text('Setup Storage'),
- subtitle: Text(_storageGranted
- ? 'Granted — proot can access /sdcard. Revoke if not needed.'
- : 'Allow access to shared storage'),
- leading: const Icon(Icons.sd_storage),
- trailing: _storageGranted
- ? const Icon(Icons.warning_amber, color: AppColors.statusAmber)
- : const Icon(Icons.warning, color: AppColors.statusAmber),
- onTap: () async {
- await NativeBridge.requestStoragePermission();
- // Refresh after returning from permission screen
- final granted = await NativeBridge.hasStoragePermission();
- setState(() => _storageGranted = granted);
- },
- ),
- const Divider(),
- _sectionHeader(theme, 'NODE'),
- SwitchListTile(
- title: const Text('Enable Node'),
- subtitle: const Text('Provide device capabilities to the gateway'),
- value: _nodeEnabled,
- onChanged: (value) {
- setState(() => _nodeEnabled = value);
- _prefs.nodeEnabled = value;
- final nodeProvider = context.read();
- if (value) {
- nodeProvider.enable();
- } else {
- nodeProvider.disable();
- }
- },
- ),
- ListTile(
- title: const Text('Node Configuration'),
- subtitle: const Text('Connection, pairing, and capabilities'),
- leading: const Icon(Icons.devices),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).push(
- MaterialPageRoute(builder: (_) => const NodeScreen()),
+ ? Center(child: CircularProgressIndicator())
+ : Responsive.constrain(
+ ListView(
+ children: [
+ _sectionHeader(theme, AppStrings.general.toUpperCase()),
+ SwitchListTile(
+ title: Text(AppStrings.autoStartGateway),
+ subtitle: Text(AppStrings.autoStartSubtitle),
+ value: _autoStart,
+ onChanged: (value) {
+ setState(() => _autoStart = value);
+ _prefs.autoStartGateway = value;
+ },
),
- ),
- const Divider(),
- _sectionHeader(theme, 'SYSTEM INFO'),
- ListTile(
- title: const Text('Architecture'),
- subtitle: Text(_arch),
- leading: const Icon(Icons.memory),
- ),
- ListTile(
- title: const Text('PRoot path'),
- subtitle: Text(_prootPath),
- leading: const Icon(Icons.folder),
- ),
- ListTile(
- title: const Text('Rootfs'),
- subtitle: Text(_status['rootfsExists'] == true
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.storage),
- ),
- ListTile(
- title: const Text('Node.js'),
- subtitle: Text(_status['nodeInstalled'] == true
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.code),
- ),
- ListTile(
- title: const Text('OpenClaw'),
- subtitle: Text(_status['openclawInstalled'] == true
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.cloud),
- ),
- ListTile(
- title: const Text('Go (Golang)'),
- subtitle: Text(_goInstalled
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.integration_instructions),
- ),
- ListTile(
- title: const Text('Homebrew'),
- subtitle: Text(_brewInstalled
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.science),
- ),
- ListTile(
- title: const Text('OpenSSH'),
- subtitle: Text(_sshInstalled
- ? 'Installed'
- : 'Not installed'),
- leading: const Icon(Icons.vpn_key),
- ),
- const Divider(),
- _sectionHeader(theme, 'MAINTENANCE'),
- ListTile(
- title: const Text('Export Snapshot'),
- subtitle: const Text('Backup config to Downloads'),
- leading: const Icon(Icons.upload_file),
- trailing: const Icon(Icons.chevron_right),
- onTap: _exportSnapshot,
- ),
- ListTile(
- title: const Text('Import Snapshot'),
- subtitle: const Text('Restore config from backup'),
- leading: const Icon(Icons.download),
- trailing: const Icon(Icons.chevron_right),
- onTap: _importSnapshot,
- ),
- ListTile(
- title: const Text('Re-run setup'),
- subtitle: const Text('Reinstall or repair the environment'),
- leading: const Icon(Icons.build),
- trailing: const Icon(Icons.chevron_right),
- onTap: () => Navigator.of(context).pushReplacement(
- MaterialPageRoute(
- builder: (_) => const SetupWizardScreen(),
+ ListTile(
+ title: Text(AppStrings.batteryOptimization),
+ subtitle: Text(_batteryOptimized
+ ? AppStrings.batteryOptimized
+ : AppStrings.batteryUnrestricted),
+ leading: const Icon(Icons.battery_alert),
+ trailing: _batteryOptimized
+ ? const Icon(Icons.warning, color: AppColors.statusAmber)
+ : const Icon(Icons.check_circle, color: AppColors.statusGreen),
+ onTap: () async {
+ await NativeBridge.requestBatteryOptimization();
+ final optimized = await NativeBridge.isBatteryOptimized();
+ setState(() => _batteryOptimized = optimized);
+ },
+ ),
+ ListTile(
+ title: Text(AppStrings.setupStorage),
+ subtitle: Text(_storageGranted
+ ? AppStrings.storageGranted
+ : AppStrings.storageNotGranted),
+ leading: const Icon(Icons.sd_storage),
+ trailing: _storageGranted
+ ? const Icon(Icons.warning_amber, color: AppColors.statusAmber)
+ : const Icon(Icons.warning, color: AppColors.statusAmber),
+ onTap: () async {
+ await NativeBridge.requestStoragePermission();
+ final granted = await NativeBridge.hasStoragePermission();
+ setState(() => _storageGranted = granted);
+ },
+ ),
+ const Divider(),
+ _sectionHeader(theme, AppStrings.nodeSection.toUpperCase()),
+ SwitchListTile(
+ title: Text(AppStrings.enableNodeTitle),
+ subtitle: Text(AppStrings.enableNodeSubtitle),
+ value: _nodeEnabled,
+ onChanged: (value) {
+ setState(() => _nodeEnabled = value);
+ _prefs.nodeEnabled = value;
+ final nodeProvider = context.read();
+ if (value) {
+ nodeProvider.enable();
+ } else {
+ nodeProvider.disable();
+ }
+ },
+ ),
+ ListTile(
+ title: Text(AppStrings.nodeConfiguration),
+ subtitle: Text(AppStrings.nodeConfigSubtitle),
+ leading: const Icon(Icons.devices),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => const NodeScreen()),
),
),
- ),
- const Divider(),
- _sectionHeader(theme, 'ABOUT'),
- const ListTile(
- title: Text('OpenClaw'),
- subtitle: Text(
- 'AI Gateway for Android\nVersion ${AppConstants.version}',
+ const Divider(),
+ _sectionHeader(theme, AppStrings.systemInfo.toUpperCase()),
+ ListTile(
+ title: Text(AppStrings.architecture),
+ subtitle: Text(_arch),
+ leading: const Icon(Icons.memory),
),
- leading: Icon(Icons.info_outline),
- isThreeLine: true,
- ),
- ListTile(
- title: const Text('Check for Updates'),
- subtitle: const Text('Check GitHub for a newer release'),
- leading: _checkingUpdate
- ? const SizedBox(
- width: 24,
- height: 24,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- : const Icon(Icons.system_update),
- onTap: _checkingUpdate ? null : _checkForUpdates,
- ),
- const ListTile(
- title: Text('Developer'),
- subtitle: Text(AppConstants.authorName),
- leading: Icon(Icons.person),
- ),
- ListTile(
- title: const Text('GitHub'),
- subtitle: const Text('mithun50/openclaw-termux'),
- leading: const Icon(Icons.code),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse(AppConstants.githubUrl),
- mode: LaunchMode.externalApplication,
+ ListTile(
+ title: Text(AppStrings.prootPath),
+ subtitle: Text(_prootPath),
+ leading: const Icon(Icons.folder),
),
- ),
- ListTile(
- title: const Text('Contact'),
- subtitle: const Text(AppConstants.authorEmail),
- leading: const Icon(Icons.email),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse('mailto:${AppConstants.authorEmail}'),
+ ListTile(
+ title: Text(AppStrings.rootfs),
+ subtitle: Text(_status['rootfsExists'] == true
+ ? AppStrings.installed
+ : AppStrings.notInstalled),
+ leading: const Icon(Icons.storage),
),
- ),
- const ListTile(
- title: Text('License'),
- subtitle: Text(AppConstants.license),
- leading: Icon(Icons.description),
- ),
- const Divider(),
- _sectionHeader(theme, AppConstants.orgName.toUpperCase()),
- ListTile(
- title: const Text('Instagram'),
- subtitle: const Text('@nexgenxplorer_nxg'),
- leading: const Icon(Icons.camera_alt),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse(AppConstants.instagramUrl),
- mode: LaunchMode.externalApplication,
+ ListTile(
+ title: const Text('Node.js'),
+ subtitle: Text(_status['nodeInstalled'] == true
+ ? AppStrings.installed
+ : AppStrings.notInstalled),
+ leading: const Icon(Icons.code),
),
- ),
- ListTile(
- title: const Text('YouTube'),
- subtitle: const Text('@nexgenxplorer'),
- leading: const Icon(Icons.play_circle_fill),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse(AppConstants.youtubeUrl),
- mode: LaunchMode.externalApplication,
+ ListTile(
+ title: const Text('OpenClaw'),
+ subtitle: Text(_status['openclawInstalled'] == true
+ ? AppStrings.installed
+ : AppStrings.notInstalled),
+ leading: const Icon(Icons.cloud),
),
- ),
- ListTile(
- title: const Text('Play Store'),
- subtitle: const Text('NextGenX Apps'),
- leading: const Icon(Icons.shop),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse(AppConstants.playStoreUrl),
- mode: LaunchMode.externalApplication,
+ ListTile(
+ title: const Text('Go (Golang)'),
+ subtitle: Text(_goInstalled ? AppStrings.installed : AppStrings.notInstalled),
+ leading: const Icon(Icons.integration_instructions),
+ ),
+ ListTile(
+ title: const Text('Homebrew'),
+ subtitle: Text(_brewInstalled ? AppStrings.installed : AppStrings.notInstalled),
+ leading: const Icon(Icons.science),
+ ),
+ ListTile(
+ title: const Text('OpenSSH'),
+ subtitle: Text(_sshInstalled ? AppStrings.installed : AppStrings.notInstalled),
+ leading: const Icon(Icons.vpn_key),
+ ),
+ const Divider(),
+ _sectionHeader(theme, AppStrings.maintenance.toUpperCase()),
+ ListTile(
+ title: Text(AppStrings.exportSnapshot),
+ subtitle: Text(AppStrings.exportSnapshotSubtitle),
+ leading: const Icon(Icons.upload_file),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: _exportSnapshot,
+ ),
+ ListTile(
+ title: Text(AppStrings.importSnapshot),
+ subtitle: Text(AppStrings.importSnapshotSubtitle),
+ leading: const Icon(Icons.download),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: _importSnapshot,
),
- ),
- ListTile(
- title: const Text('Email'),
- subtitle: const Text(AppConstants.orgEmail),
- leading: const Icon(Icons.email_outlined),
- trailing: const Icon(Icons.open_in_new, size: 18),
- onTap: () => launchUrl(
- Uri.parse('mailto:${AppConstants.orgEmail}'),
+ ListTile(
+ title: Text(AppStrings.rerunSetup),
+ subtitle: Text(AppStrings.rerunSetupSubtitle),
+ leading: const Icon(Icons.build),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (_) => const SetupWizardScreen(),
+ ),
+ ),
+ ),
+ const Divider(),
+ _sectionHeader(theme, AppStrings.about.toUpperCase()),
+ const ListTile(
+ title: Text('OpenClaw'),
+ subtitle: Text(
+ 'AI Gateway for Android\nVersion ${AppConstants.version}',
+ ),
+ leading: Icon(Icons.info_outline),
+ isThreeLine: true,
),
- ),
- ],
+ ListTile(
+ title: Text(AppStrings.checkForUpdates),
+ subtitle: Text(AppStrings.checkUpdatesSubtitle),
+ leading: _checkingUpdate
+ ? const SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.system_update),
+ onTap: _checkingUpdate ? null : _checkForUpdates,
+ ),
+ ListTile(
+ title: Text(AppStrings.developer),
+ subtitle: const Text(AppConstants.authorName),
+ leading: const Icon(Icons.person),
+ ),
+ ListTile(
+ title: Text(AppStrings.github),
+ subtitle: const Text('mithun50/openclaw-termux'),
+ leading: const Icon(Icons.code),
+ trailing: const Icon(Icons.open_in_new, size: 18),
+ onTap: () => launchUrl(
+ Uri.parse(AppConstants.githubUrl),
+ mode: LaunchMode.externalApplication,
+ ),
+ ),
+ ListTile(
+ title: Text(AppStrings.contact),
+ subtitle: const Text(AppConstants.authorEmail),
+ leading: const Icon(Icons.email),
+ trailing: const Icon(Icons.open_in_new, size: 18),
+ onTap: () => launchUrl(
+ Uri.parse('mailto:${AppConstants.authorEmail}'),
+ ),
+ ),
+ ListTile(
+ title: Text(AppStrings.license),
+ subtitle: const Text(AppConstants.license),
+ leading: const Icon(Icons.description),
+ ),
+ ],
+ ),
),
);
}
@@ -373,12 +328,12 @@ class _SettingsScreenState extends State {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Snapshot saved to $path')),
+ SnackBar(content: Text('${AppStrings.snapshotSaved}: $path')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Export failed: $e')),
+ SnackBar(content: Text('${AppStrings.exportFailed}: $e')),
);
}
}
@@ -391,7 +346,7 @@ class _SettingsScreenState extends State {
if (!await file.exists()) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('No snapshot found at $path')),
+ SnackBar(content: Text('${AppStrings.noSnapshotFound}: $path')),
);
return;
}
@@ -433,12 +388,12 @@ class _SettingsScreenState extends State {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Snapshot restored successfully. Restart the gateway to apply.')),
+ SnackBar(content: Text(AppStrings.snapshotRestored)),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Import failed: $e')),
+ SnackBar(content: Text('${AppStrings.importFailed}: $e')),
);
}
}
@@ -452,16 +407,15 @@ class _SettingsScreenState extends State {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
- title: const Text('Update Available'),
+ title: Text(AppStrings.updateAvailable),
content: Text(
- 'A new version is available.\n\n'
- 'Current: ${AppConstants.version}\n'
- 'Latest: ${result.latest}',
+ '${AppStrings.currentVersion}: ${AppConstants.version}\n'
+ '${AppStrings.latestVersion}: ${result.latest}',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
- child: const Text('Later'),
+ child: Text(AppStrings.later),
),
FilledButton(
onPressed: () {
@@ -471,20 +425,20 @@ class _SettingsScreenState extends State {
mode: LaunchMode.externalApplication,
);
},
- child: const Text('Download'),
+ child: Text(AppStrings.download),
),
],
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("You're on the latest version")),
+ SnackBar(content: Text(AppStrings.alreadyLatest)),
);
}
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Could not check for updates')),
+ SnackBar(content: Text(AppStrings.checkUpdateFailed)),
);
} finally {
if (mounted) setState(() => _checkingUpdate = false);
diff --git a/flutter_app/lib/screens/setup_wizard_screen.dart b/flutter_app/lib/screens/setup_wizard_screen.dart
index dc18c5d..a79ed76 100644
--- a/flutter_app/lib/screens/setup_wizard_screen.dart
+++ b/flutter_app/lib/screens/setup_wizard_screen.dart
@@ -1,11 +1,13 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app.dart';
import '../constants.dart';
+import '../l10n/app_strings.dart';
import '../models/setup_state.dart';
import '../models/optional_package.dart';
import '../providers/setup_provider.dart';
import '../services/package_service.dart';
+import '../utils/responsive.dart';
import '../widgets/progress_step.dart';
import 'onboarding_screen.dart';
import 'package_install_screen.dart';
@@ -46,114 +48,116 @@ class _SetupWizardScreenState extends State {
builder: (context, provider, _) {
final state = provider.state;
- // Load package statuses once setup completes
if (state.isComplete && _pkgStatuses.isEmpty) {
_refreshPkgStatuses();
}
return Padding(
padding: const EdgeInsets.all(24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 32),
- Image.asset(
- 'assets/ic_launcher.png',
- width: 64,
- height: 64,
- ),
- const SizedBox(height: 16),
- Text(
- 'Setup OpenClaw',
- style: theme.textTheme.headlineSmall?.copyWith(
- fontWeight: FontWeight.bold,
+ child: Responsive.constrain(
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 32),
+ Image.asset('assets/ic_launcher.png',
+ width: 64, height: 64),
+ const SizedBox(height: 16),
+ Text(
+ AppStrings.setupOpenClaw,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
),
- ),
- const SizedBox(height: 8),
- Text(
- _started
- ? 'Setting up the environment. This may take several minutes.'
- : 'This will download Ubuntu, Node.js, and OpenClaw into a self-contained environment.',
- style: theme.textTheme.bodyMedium?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
+ const SizedBox(height: 8),
+ Text(
+ _started
+ ? AppStrings.setupRunning
+ : AppStrings.setupDescription,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
),
- ),
- const SizedBox(height: 32),
- Expanded(
- child: _buildSteps(state, theme, isDark),
- ),
- if (state.hasError) ...[
- ConstrainedBox(
- constraints: const BoxConstraints(maxHeight: 160),
- child: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: theme.colorScheme.errorContainer,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(Icons.error_outline, color: theme.colorScheme.error),
- const SizedBox(width: 8),
- Expanded(
- child: SingleChildScrollView(
- child: Text(
- state.error ?? 'Unknown error',
- style: TextStyle(color: theme.colorScheme.onErrorContainer),
+ const SizedBox(height: 32),
+ Expanded(child: _buildSteps(state, theme, isDark)),
+ if (state.hasError) ...[
+ ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 160),
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.errorContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(Icons.error_outline,
+ color: theme.colorScheme.error),
+ const SizedBox(width: 8),
+ Expanded(
+ child: SingleChildScrollView(
+ child: Text(
+ AppStrings.translateError(
+ state.error ?? 'Unknown error'),
+ style: TextStyle(
+ color:
+ theme.colorScheme.onErrorContainer),
+ ),
),
),
- ),
- ],
+ ],
+ ),
),
),
- ),
- const SizedBox(height: 16),
- ],
- if (state.isComplete)
- SizedBox(
- width: double.infinity,
- child: FilledButton.icon(
- onPressed: () => _goToOnboarding(context),
- icon: const Icon(Icons.arrow_forward),
- label: const Text('Configure API Keys'),
+ const SizedBox(height: 16),
+ ],
+ if (state.isComplete)
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: () => _goToOnboarding(context),
+ icon: const Icon(Icons.arrow_forward),
+ label: Text(AppStrings.configureApiKeys),
+ ),
+ )
+ else if (!_started || state.hasError)
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: provider.isRunning
+ ? null
+ : () {
+ setState(() => _started = true);
+ provider.runSetup();
+ },
+ icon: const Icon(Icons.download),
+ label: Text(_started
+ ? AppStrings.retrySetup
+ : AppStrings.beginSetup),
+ ),
),
- )
- else if (!_started || state.hasError)
- SizedBox(
- width: double.infinity,
- child: FilledButton.icon(
- onPressed: provider.isRunning
- ? null
- : () {
- setState(() => _started = true);
- provider.runSetup();
- },
- icon: const Icon(Icons.download),
- label: Text(_started ? 'Retry Setup' : 'Begin Setup'),
+ if (!_started) ...[
+ const SizedBox(height: 8),
+ Center(
+ child: Text(
+ AppStrings.storageRequired,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
),
- ),
- if (!_started) ...[
- const SizedBox(height: 8),
+ ],
+ const SizedBox(height: 16),
Center(
child: Text(
- 'Requires ~500MB of storage and an internet connection',
+ 'by ${AppConstants.authorName} | ${AppConstants.orgName}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
- const SizedBox(height: 16),
- Center(
- child: Text(
- 'by ${AppConstants.authorName} | ${AppConstants.orgName}',
- style: theme.textTheme.bodySmall?.copyWith(
- color: theme.colorScheme.onSurfaceVariant,
- ),
- ),
- ),
- ],
+ ),
),
);
},
@@ -164,11 +168,11 @@ class _SetupWizardScreenState extends State {
Widget _buildSteps(SetupState state, ThemeData theme, bool isDark) {
final steps = [
- (1, 'Download Ubuntu rootfs', SetupStep.downloadingRootfs),
- (2, 'Extract rootfs', SetupStep.extractingRootfs),
- (3, 'Install Node.js', SetupStep.installingNode),
- (4, 'Install OpenClaw', SetupStep.installingOpenClaw),
- (5, 'Configure Bionic Bypass', SetupStep.configuringBypass),
+ (1, AppStrings.downloadRootfs, SetupStep.downloadingRootfs),
+ (2, AppStrings.extractRootfs, SetupStep.extractingRootfs),
+ (3, AppStrings.installNode, SetupStep.installingNode),
+ (4, AppStrings.installOpenClaw, SetupStep.installingOpenClaw),
+ (5, AppStrings.configureBionicBypass, SetupStep.configuringBypass),
];
return ListView(
@@ -183,16 +187,16 @@ class _SetupWizardScreenState extends State {
progress: state.step == step ? state.progress : null,
),
if (state.isComplete) ...[
- const ProgressStep(
+ ProgressStep(
stepNumber: 6,
- label: 'Setup complete!',
+ label: AppStrings.setupComplete,
isComplete: true,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
- 'OPTIONAL PACKAGES',
+ AppStrings.optionalPackages,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
@@ -208,7 +212,8 @@ class _SetupWizardScreenState extends State {
);
}
- Widget _buildPackageTile(ThemeData theme, OptionalPackage package, bool isDark) {
+ Widget _buildPackageTile(
+ ThemeData theme, OptionalPackage package, bool isDark) {
final installed = _pkgStatuses[package.id] ?? false;
final iconBg = isDark ? AppColors.darkSurfaceAlt : const Color(0xFFF3F4F6);
@@ -222,7 +227,8 @@ class _SetupWizardScreenState extends State {
color: iconBg,
borderRadius: BorderRadius.circular(10),
),
- child: Icon(package.icon, color: theme.colorScheme.onSurfaceVariant, size: 22),
+ child: Icon(package.icon,
+ color: theme.colorScheme.onSurfaceVariant, size: 22),
),
title: Row(
children: [
@@ -231,17 +237,18 @@ class _SetupWizardScreenState extends State {
if (installed) ...[
const SizedBox(width: 8),
Container(
- padding:
- const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: AppColors.statusGreen.withAlpha(25),
borderRadius: BorderRadius.circular(8),
),
- child: Text('Installed',
- style: theme.textTheme.labelSmall?.copyWith(
- color: AppColors.statusGreen,
- fontWeight: FontWeight.w600,
- )),
+ child: Text(
+ AppStrings.installed,
+ style: theme.textTheme.labelSmall?.copyWith(
+ color: AppColors.statusGreen,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
),
],
],
@@ -251,7 +258,7 @@ class _SetupWizardScreenState extends State {
? const Icon(Icons.check_circle, color: AppColors.statusGreen)
: OutlinedButton(
onPressed: () => _installPackage(package),
- child: const Text('Install'),
+ child: Text(AppStrings.install),
),
),
);
diff --git a/flutter_app/lib/screens/splash_screen.dart b/flutter_app/lib/screens/splash_screen.dart
index c6b5387..5639e84 100644
--- a/flutter_app/lib/screens/splash_screen.dart
+++ b/flutter_app/lib/screens/splash_screen.dart
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../constants.dart';
+import '../l10n/app_strings.dart';
import '../services/native_bridge.dart';
import '../services/preferences_service.dart';
import 'setup_wizard_screen.dart';
@@ -18,7 +19,7 @@ class SplashScreen extends StatefulWidget {
class _SplashScreenState extends State
with SingleTickerProviderStateMixin {
- String _status = 'Loading...';
+ String _status = AppStrings.loading;
late final AnimationController _fadeController;
late final Animation _fadeAnimation;
@@ -47,7 +48,7 @@ class _SplashScreenState extends State
await Future.delayed(const Duration(milliseconds: 500));
try {
- setState(() => _status = 'Checking setup status...');
+ setState(() => _status = AppStrings.checkingSetup);
// Ensure directories and resolv.conf exist on every app open.
// Android may clear the files directory during update or reinstall (#40).
@@ -126,17 +127,17 @@ class _SplashScreenState extends State
final openclawOk = status['openclawInstalled'] == true;
final bypassOk = status['bypassInstalled'] == true;
- // Core rootfs must exist — can't repair without it
+ // Core rootfs must exist ?can't repair without it
if (rootfsOk && bashOk) {
// Regenerate bionic bypass if missing
if (!bypassOk) {
- setState(() => _status = 'Repairing bionic bypass...');
+ setState(() => _status = AppStrings.repairingBypass);
await NativeBridge.installBionicBypass();
}
// Reinstall node if binary is missing (#97)
if (!nodeOk) {
- setState(() => _status = 'Reinstalling Node.js...');
+ setState(() => _status = AppStrings.reinstallingNode);
try {
final arch = await NativeBridge.getArch();
final nodeTarUrl = AppConstants.getNodeTarballUrl(arch);
@@ -150,7 +151,7 @@ class _SplashScreenState extends State
// Reinstall openclaw if package.json is missing (#97)
if (!openclawOk && nodeOk) {
- setState(() => _status = 'Reinstalling OpenClaw...');
+ setState(() => _status = AppStrings.reinstallingOpenClaw);
try {
const wrapper = '/root/.openclaw/node-wrapper.js';
const nodeRun = 'node $wrapper';
@@ -213,7 +214,7 @@ class _SplashScreenState extends State
),
const SizedBox(height: 8),
Text(
- 'AI Gateway for Android',
+ AppStrings.aiGatewayForAndroid,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
diff --git a/flutter_app/lib/screens/ssh_screen.dart b/flutter_app/lib/screens/ssh_screen.dart
index d14698d..ce95815 100644
--- a/flutter_app/lib/screens/ssh_screen.dart
+++ b/flutter_app/lib/screens/ssh_screen.dart
@@ -1,10 +1,10 @@
-import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../app.dart';
import '../services/ssh_service.dart';
import 'packages_screen.dart';
-/// SSH server management screen — start/stop sshd, set password, show connection info.
+/// SSH server management screen �?start/stop sshd, set password, show connection info.
class SshScreen extends StatefulWidget {
const SshScreen({super.key});
@@ -127,7 +127,7 @@ class _SshScreenState extends State {
return Scaffold(
appBar: AppBar(title: const Text('SSH Access')),
body: _loading
- ? const Center(child: CircularProgressIndicator())
+ ? Center(child: CircularProgressIndicator())
: _installed
? _buildInstalledView(theme, isDark)
: _buildNotInstalledView(theme),
diff --git a/flutter_app/lib/screens/terminal_screen.dart b/flutter_app/lib/screens/terminal_screen.dart
index 25bdeb9..5ca89b0 100644
--- a/flutter_app/lib/screens/terminal_screen.dart
+++ b/flutter_app/lib/screens/terminal_screen.dart
@@ -1,10 +1,11 @@
-import 'dart:convert';
+import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:xterm/xterm.dart';
import 'package:flutter_pty/flutter_pty.dart';
import 'package:url_launcher/url_launcher.dart';
+import '../l10n/app_strings.dart';
import '../services/native_bridge.dart';
import '../services/screenshot_service.dart';
import '../services/terminal_service.dart';
@@ -107,14 +108,14 @@ class _TerminalScreenState extends State {
if (_ctrlNotifier.value && data.length == 1) {
final code = data.toLowerCase().codeUnitAt(0);
if (code >= 97 && code <= 122) {
- // Ctrl+a-z → bytes 1-26
+ // Ctrl+a-z �?bytes 1-26
_pty?.write(Uint8List.fromList([code - 96]));
_ctrlNotifier.value = false;
return;
}
}
if (_altNotifier.value && data.isNotEmpty) {
- // Alt+key → ESC + key
+ // Alt+key �?ESC + key
_pty?.write(utf8.encode('\x1b$data'));
_altNotifier.value = false;
return;
@@ -195,10 +196,10 @@ class _TerminalScreenState extends State {
if (url != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: const Text('Copied to clipboard'),
+ content: Text(AppStrings.copied),
duration: const Duration(seconds: 3),
action: SnackBarAction(
- label: 'Open',
+ label: '打开',
onPressed: () {
final uri = Uri.tryParse(url);
if (uri != null) {
@@ -210,9 +211,9 @@ class _TerminalScreenState extends State {
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Copied to clipboard'),
- duration: Duration(seconds: 1),
+ SnackBar(
+ content: Text(AppStrings.copied),
+ duration: const Duration(seconds: 1),
),
);
}
@@ -231,9 +232,9 @@ class _TerminalScreenState extends State {
}
}
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('No URL found in selection'),
- duration: Duration(seconds: 1),
+ SnackBar(
+ content: Text(AppStrings.noUrlFound),
+ duration: const Duration(seconds: 1),
),
);
}
@@ -251,8 +252,8 @@ class _TerminalScreenState extends State {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(path != null
- ? 'Screenshot saved: ${path.split('/').last}'
- : 'Failed to capture screenshot'),
+ ? '截图已保�? ${path.split('/').last}'
+ : '截图失败'),
),
);
}
@@ -302,24 +303,24 @@ class _TerminalScreenState extends State {
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
- child: const Text('Cancel'),
+ child: Text(AppStrings.cancel),
),
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: url));
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Link copied'),
- duration: Duration(seconds: 1),
+ SnackBar(
+ content: Text(AppStrings.linkCopied),
+ duration: const Duration(seconds: 1),
),
);
Navigator.pop(ctx, false);
},
- child: const Text('Copy'),
+ child: Text(AppStrings.copy),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
- child: const Text('Open'),
+ child: const Text('打开'),
),
],
),
@@ -334,31 +335,31 @@ class _TerminalScreenState extends State {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
- title: const Text('Terminal'),
+ title: Text(AppStrings.terminal),
actions: [
IconButton(
icon: const Icon(Icons.camera_alt_outlined),
- tooltip: 'Screenshot',
+ tooltip: AppStrings.screenshot,
onPressed: _takeScreenshot,
),
IconButton(
icon: const Icon(Icons.copy),
- tooltip: 'Copy',
+ tooltip: AppStrings.copy,
onPressed: _copySelection,
),
IconButton(
icon: const Icon(Icons.open_in_browser),
- tooltip: 'Open URL',
+ tooltip: AppStrings.openUrl,
onPressed: _openSelection,
),
IconButton(
icon: const Icon(Icons.paste),
- tooltip: 'Paste',
+ tooltip: AppStrings.paste,
onPressed: _paste,
),
IconButton(
icon: const Icon(Icons.refresh),
- tooltip: 'Restart',
+ tooltip: AppStrings.restart,
onPressed: () {
_pty?.kill();
setState(() {
@@ -376,13 +377,13 @@ class _TerminalScreenState extends State {
Widget _buildBody() {
if (_loading) {
- return const Center(
+ return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
- Text('Starting terminal...'),
+ Text(AppStrings.startingTerminal),
],
),
);
@@ -416,7 +417,7 @@ class _TerminalScreenState extends State {
_startPty();
},
icon: const Icon(Icons.refresh),
- label: const Text('Retry'),
+ label: Text(AppStrings.retry),
),
],
),
diff --git a/flutter_app/lib/services/bootstrap_service.dart b/flutter_app/lib/services/bootstrap_service.dart
index 9399cbb..b934bfe 100644
--- a/flutter_app/lib/services/bootstrap_service.dart
+++ b/flutter_app/lib/services/bootstrap_service.dart
@@ -139,7 +139,7 @@ class BootstrapService {
));
// Blanket recursive chmod on all bin/lib directories.
// Java tar extraction loses execute bits; dpkg needs tar, xz,
- // gzip, rm, mv, etc. — easier to fix everything than enumerate.
+ // gzip, rm, mv, etc. ?easier to fix everything than enumerate.
await NativeBridge.runInProot(
'chmod -R 755 /usr/bin /usr/sbin /bin /sbin '
'/usr/local/bin /usr/local/sbin 2>/dev/null; '
@@ -153,7 +153,7 @@ class BootstrapService {
// --- Install base packages via apt-get (like Termux proot-distro) ---
// Now that our proot matches Termux exactly (env -i, clean host env,
// proper flags), dpkg works normally. No need for Java-side deb
- // extraction — let dpkg+tar handle it inside proot like Termux does.
+ // extraction ?let dpkg+tar handle it inside proot like Termux does.
_updateSetupNotification('Updating package lists...', progress: 48);
onProgress(const SetupState(
step: SetupStep.installingNode,
@@ -173,7 +173,7 @@ class BootstrapService {
// python3, make, g++: node-gyp needs these to compile native addons
// (npm's bundled node-gyp runs as a JS module, not a spawned process,
// so proot-compat.js spawn mock can't intercept it)
- // dpkg extracts via tar inside proot — permissions are correct.
+ // dpkg extracts via tar inside proot ?permissions are correct.
// Post-install scripts (update-ca-certificates) run automatically.
// Pre-configure tzdata to avoid interactive continent/timezone prompt
// (tzdata is a dependency of python3 and ignores DEBIAN_FRONTEND on
@@ -188,7 +188,7 @@ class BootstrapService {
);
// Git config (.gitconfig) is written by installBionicBypass() on the
- // Java side — directly to $rootfsDir/root/.gitconfig — rewrites
+ // Java side ?directly to $rootfsDir/root/.gitconfig ?rewrites
// SSH→HTTPS for npm git deps (no SSH keys in proot).
// --- Install Node.js via binary tarball ---
@@ -259,7 +259,7 @@ class BootstrapService {
progress: 0.0,
message: 'Installing OpenClaw (this may take a few minutes)...',
));
- // Install openclaw — fork/exec works now with our Termux-matching proot.
+ // Install openclaw ?fork/exec works now with our Termux-matching proot.
await NativeBridge.runInProot(
'$nodeRun $npmCli install -g openclaw',
timeout: 1800,
@@ -273,7 +273,7 @@ class BootstrapService {
));
// npm global install creates symlinks for bin entries, but symlinks
// can fail silently in proot. Create shell wrappers from Java side
- // (reads package.json directly from rootfs filesystem — no escaping).
+ // (reads package.json directly from rootfs filesystem ?no escaping).
await NativeBridge.createBinWrappers('openclaw');
_updateSetupNotification('Verifying OpenClaw...', progress: 96);
diff --git a/flutter_app/lib/services/capabilities/flash_capability.dart b/flutter_app/lib/services/capabilities/flash_capability.dart
index 217f0b4..6f66dc6 100644
--- a/flutter_app/lib/services/capabilities/flash_capability.dart
+++ b/flutter_app/lib/services/capabilities/flash_capability.dart
@@ -34,7 +34,7 @@ class FlashCapability extends CapabilityHandler {
if (_controller!.value.isInitialized && !_controller!.value.hasError) {
return _controller!;
}
- // Controller is stale/errored — dispose and recreate
+ // Controller is stale/errored ?dispose and recreate
try { _controller!.dispose(); } catch (_) {}
_controller = null;
}
diff --git a/flutter_app/lib/services/gateway_service.dart b/flutter_app/lib/services/gateway_service.dart
index a51097f..d97c1fe 100644
--- a/flutter_app/lib/services/gateway_service.dart
+++ b/flutter_app/lib/services/gateway_service.dart
@@ -196,7 +196,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
}
}
- /// Repair openclaw.json on disk — fixes corrupted model entries and ensures
+ /// Repair openclaw.json on disk ?fixes corrupted model entries and ensures
/// gateway.mode=local is set. Called on init() before any gateway start (#88).
Future _repairConfigFile() async {
try {
@@ -210,7 +210,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
try {
config = Map.from(jsonDecode(content) as Map);
} catch (_) {
- return; // Unparseable — _writeNodeAllowConfig will recreate it
+ return; // Unparseable ?_writeNodeAllowConfig will recreate it
}
bool modified = false;
@@ -223,7 +223,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
modified = true;
}
- // Fix model entries: strings → objects (#83, #88)
+ // Fix model entries: strings ?objects (#83, #88)
final models = config['models'] as Map?;
if (models != null) {
final providers = models['providers'] as Map?;
@@ -272,7 +272,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
}
/// Read the actual gateway auth token from openclaw.json config file (#74, #82).
- /// This is the source of truth — more reliable than regex-scraping stdout.
+ /// This is the source of truth ?more reliable than regex-scraping stdout.
Future _readTokenFromConfig() async {
try {
final raw = await NativeBridge.readRootfsFile('root/.openclaw/openclaw.json');
@@ -308,7 +308,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
));
try {
- // Ensure directories exist — Android may have cleared them (#40).
+ // Ensure directories exist ?Android may have cleared them (#40).
// Non-fatal: the GatewayService foreground service also creates them.
try { await NativeBridge.setupDirs(); } catch (_) {}
try { await NativeBridge.writeResolv(); } catch (_) {}
@@ -373,7 +373,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
void _startHealthCheck() {
_cancelAllTimers();
- // Delay the first health check by 30s — Node.js inside proot needs time to start.
+ // Delay the first health check by 30s ?Node.js inside proot needs time to start.
// Use a Timer (not Future.delayed) so it can be cancelled on stop().
_initialDelayTimer = Timer(const Duration(seconds: 30), () {
_initialDelayTimer = null;
@@ -393,7 +393,7 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2));
.timeout(const Duration(seconds: 3));
if (response.statusCode < 500 && _state.status != GatewayStatus.running) {
- // Read the actual token from openclaw.json — source of truth (#74, #82).
+ // Read the actual token from openclaw.json ?source of truth (#74, #82).
// This ensures the displayed token always matches the gateway's config,
// even if the stdout regex didn't capture it.
String? configUrl = _state.dashboardUrl;
diff --git a/flutter_app/lib/services/native_bridge.dart b/flutter_app/lib/services/native_bridge.dart
index 70b0cbe..e9cf76c 100644
--- a/flutter_app/lib/services/native_bridge.dart
+++ b/flutter_app/lib/services/native_bridge.dart
@@ -35,7 +35,8 @@ class NativeBridge {
}
static Future runInProot(String command, {int timeout = 900}) async {
- return await _channel.invokeMethod('runInProot', {'command': command, 'timeout': timeout});
+ return await _channel
+ .invokeMethod('runInProot', {'command': command, 'timeout': timeout});
}
static Future startGateway() async {
@@ -67,11 +68,13 @@ class NativeBridge {
}
static Future extractNodeTarball(String tarPath) async {
- return await _channel.invokeMethod('extractNodeTarball', {'tarPath': tarPath});
+ return await _channel
+ .invokeMethod('extractNodeTarball', {'tarPath': tarPath});
}
static Future createBinWrappers(String packageName) async {
- return await _channel.invokeMethod('createBinWrappers', {'packageName': packageName});
+ return await _channel
+ .invokeMethod('createBinWrappers', {'packageName': packageName});
}
static Future startTerminalService() async {
@@ -86,6 +89,18 @@ class NativeBridge {
return await _channel.invokeMethod('isTerminalServiceRunning');
}
+ static Future startNineRouterService() async {
+ return await _channel.invokeMethod('startNineRouterService');
+ }
+
+ static Future stopNineRouterService() async {
+ return await _channel.invokeMethod('stopNineRouterService');
+ }
+
+ static Future isNineRouterServiceRunning() async {
+ return await _channel.invokeMethod('isNineRouterServiceRunning');
+ }
+
static Future startNodeService() async {
return await _channel.invokeMethod('startNodeService');
}
@@ -99,7 +114,8 @@ class NativeBridge {
}
static Future updateNodeNotification(String text) async {
- return await _channel.invokeMethod('updateNodeNotification', {'text': text});
+ return await _channel
+ .invokeMethod('updateNodeNotification', {'text': text});
}
static Future requestBatteryOptimization() async {
@@ -114,24 +130,31 @@ class NativeBridge {
return await _channel.invokeMethod('startSetupService');
}
- static Future updateSetupNotification(String text, {int progress = -1}) async {
- return await _channel.invokeMethod('updateSetupNotification', {'text': text, 'progress': progress});
+ static Future updateSetupNotification(String text,
+ {int progress = -1}) async {
+ return await _channel.invokeMethod(
+ 'updateSetupNotification', {'text': text, 'progress': progress});
}
static Future stopSetupService() async {
return await _channel.invokeMethod('stopSetupService');
}
- static Future showUrlNotification(String url, {String title = 'URL Detected'}) async {
- return await _channel.invokeMethod('showUrlNotification', {'url': url, 'title': title});
+ static Future showUrlNotification(String url,
+ {String title = 'URL Detected'}) async {
+ return await _channel
+ .invokeMethod('showUrlNotification', {'url': url, 'title': title});
}
static Stream get gatewayLogStream {
- return _eventChannel.receiveBroadcastStream().map((event) => event.toString());
+ return _eventChannel
+ .receiveBroadcastStream()
+ .map((event) => event.toString());
}
static Future requestScreenCapture(int durationMs) async {
- return await _channel.invokeMethod('requestScreenCapture', {'durationMs': durationMs});
+ return await _channel
+ .invokeMethod('requestScreenCapture', {'durationMs': durationMs});
}
static Future stopScreenCapture() async {
@@ -155,7 +178,8 @@ class NativeBridge {
}
static Future writeRootfsFile(String path, String content) async {
- return await _channel.invokeMethod('writeRootfsFile', {'path': path, 'content': content});
+ return await _channel
+ .invokeMethod('writeRootfsFile', {'path': path, 'content': content});
}
// SSH Service
@@ -185,6 +209,7 @@ class NativeBridge {
}
static Future setRootPassword(String password) async {
- return await _channel.invokeMethod('setRootPassword', {'password': password});
+ return await _channel
+ .invokeMethod('setRootPassword', {'password': password});
}
}
diff --git a/flutter_app/lib/services/node_service.dart b/flutter_app/lib/services/node_service.dart
index 95c9256..465784c 100644
--- a/flutter_app/lib/services/node_service.dart
+++ b/flutter_app/lib/services/node_service.dart
@@ -263,7 +263,7 @@ class NodeService {
));
_log('[NODE] Paired and connected');
- // Send capabilities advertisement — include both 'capabilities' (legacy)
+ // Send capabilities advertisement ?include both 'capabilities' (legacy)
// and 'commands' (matching the connect frame format) so the gateway can
// discover node commands regardless of which field it checks (#56).
final capabilities = _capabilityHandlers.keys.toList();
diff --git a/flutter_app/lib/services/package_service.dart b/flutter_app/lib/services/package_service.dart
index 50bd5e5..906e023 100644
--- a/flutter_app/lib/services/package_service.dart
+++ b/flutter_app/lib/services/package_service.dart
@@ -21,7 +21,7 @@ class PackageService {
}
/// Check installation status for all optional packages.
- /// Returns a map of package id → installed boolean.
+ /// Returns a map of package id ?installed boolean.
static Future