lilu 4 місяців тому
коміт
5554116fd3
100 змінених файлів з 3566 додано та 0 видалено
  1. 45 0
      .gitignore
  2. 45 0
      .metadata
  3. 25 0
      .vscode/launch.json
  4. 8 0
      README.md
  5. 28 0
      analysis_options.yaml
  6. 14 0
      android/.gitignore
  7. 68 0
      android/app/build.gradle.kts
  8. BIN
      android/app/libs/arm64-v8a/libhev-socks5-tunnel.so
  9. BIN
      android/app/libs/armeabi-v7a/libhev-socks5-tunnel.so
  10. BIN
      android/app/libs/libxray.aar
  11. 7 0
      android/app/src/debug/AndroidManifest.xml
  12. 90 0
      android/app/src/main/AndroidManifest.xml
  13. 126 0
      android/app/src/main/assets/connect.json
  14. BIN
      android/app/src/main/assets/geoip.dat
  15. 295 0
      android/app/src/main/assets/geosite.dat
  16. 105 0
      android/app/src/main/kotlin/app/xixi/nomo/App.kt
  17. 284 0
      android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt
  18. 319 0
      android/app/src/main/kotlin/app/xixi/nomo/CoreApiImpl.kt
  19. 109 0
      android/app/src/main/kotlin/app/xixi/nomo/MainActivity.kt
  20. 14 0
      android/app/src/main/kotlin/app/xixi/nomo/ProxyNode.kt
  21. 20 0
      android/app/src/main/kotlin/app/xixi/nomo/ProxyStartOptions.kt
  22. 11 0
      android/app/src/main/kotlin/app/xixi/nomo/StatusDetectOptions.kt
  23. 59 0
      android/app/src/main/kotlin/app/xixi/nomo/TProxyService.kt
  24. 168 0
      android/app/src/main/kotlin/app/xixi/nomo/VLog.kt
  25. 311 0
      android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt
  26. 255 0
      android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt
  27. 21 0
      android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt
  28. BIN
      android/app/src/main/res/drawable-hdpi/android12splash.png
  29. BIN
      android/app/src/main/res/drawable-hdpi/ic_launcher_background.png
  30. BIN
      android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
  31. BIN
      android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png
  32. BIN
      android/app/src/main/res/drawable-hdpi/splash.png
  33. BIN
      android/app/src/main/res/drawable-mdpi/android12splash.png
  34. BIN
      android/app/src/main/res/drawable-mdpi/ic_launcher_background.png
  35. BIN
      android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
  36. BIN
      android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png
  37. BIN
      android/app/src/main/res/drawable-mdpi/splash.png
  38. BIN
      android/app/src/main/res/drawable-night-hdpi/android12splash.png
  39. BIN
      android/app/src/main/res/drawable-night-mdpi/android12splash.png
  40. BIN
      android/app/src/main/res/drawable-night-xhdpi/android12splash.png
  41. BIN
      android/app/src/main/res/drawable-night-xxhdpi/android12splash.png
  42. BIN
      android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png
  43. BIN
      android/app/src/main/res/drawable-v21/background.png
  44. 9 0
      android/app/src/main/res/drawable-v21/launch_background.xml
  45. BIN
      android/app/src/main/res/drawable-xhdpi/android12splash.png
  46. BIN
      android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png
  47. BIN
      android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
  48. BIN
      android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png
  49. BIN
      android/app/src/main/res/drawable-xhdpi/splash.png
  50. BIN
      android/app/src/main/res/drawable-xxhdpi/android12splash.png
  51. BIN
      android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
  52. BIN
      android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
  53. BIN
      android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png
  54. BIN
      android/app/src/main/res/drawable-xxhdpi/splash.png
  55. BIN
      android/app/src/main/res/drawable-xxxhdpi/android12splash.png
  56. BIN
      android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
  57. BIN
      android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
  58. BIN
      android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png
  59. BIN
      android/app/src/main/res/drawable-xxxhdpi/splash.png
  60. BIN
      android/app/src/main/res/drawable/background.png
  61. 34 0
      android/app/src/main/res/drawable/ic_xvpn.xml
  62. 9 0
      android/app/src/main/res/drawable/launch_background.xml
  63. 14 0
      android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml
  64. BIN
      android/app/src/main/res/mipmap-hdpi/launcher_icon.png
  65. BIN
      android/app/src/main/res/mipmap-mdpi/launcher_icon.png
  66. BIN
      android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
  67. BIN
      android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
  68. BIN
      android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
  69. 22 0
      android/app/src/main/res/values-night-v31/styles.xml
  70. 22 0
      android/app/src/main/res/values-night/styles.xml
  71. 22 0
      android/app/src/main/res/values-v31/styles.xml
  72. 26 0
      android/app/src/main/res/values/styles.xml
  73. 7 0
      android/app/src/profile/AndroidManifest.xml
  74. 24 0
      android/build.gradle.kts
  75. 337 0
      android/build/reports/problems/problems-report.html
  76. 3 0
      android/gradle.properties
  77. 66 0
      android/gradle/libs.versions.toml
  78. 5 0
      android/gradle/wrapper/gradle-wrapper.properties
  79. 26 0
      android/settings.gradle.kts
  80. 4 0
      assets/flags/ad.svg
  81. 6 0
      assets/flags/ae.svg
  82. 81 0
      assets/flags/af.svg
  83. 14 0
      assets/flags/ag.svg
  84. 29 0
      assets/flags/ai.svg
  85. 2 0
      assets/flags/al.svg
  86. 5 0
      assets/flags/am.svg
  87. 13 0
      assets/flags/ao.svg
  88. 2 0
      assets/flags/aq.svg
  89. 32 0
      assets/flags/ar.svg
  90. 62 0
      assets/flags/arab.svg
  91. 4 0
      assets/flags/as.svg
  92. 13 0
      assets/flags/asean.svg
  93. 4 0
      assets/flags/at.svg
  94. 8 0
      assets/flags/au.svg
  95. 186 0
      assets/flags/aw.svg
  96. 18 0
      assets/flags/ax.svg
  97. 8 0
      assets/flags/az.svg
  98. 12 0
      assets/flags/ba.svg
  99. 6 0
      assets/flags/bb.svg
  100. 4 0
      assets/flags/bd.svg

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release

+ 45 - 0
.metadata

@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
+  channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: android
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: ios
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: linux
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: macos
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: web
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+    - platform: windows
+      create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+      base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 25 - 0
.vscode/launch.json

@@ -0,0 +1,25 @@
+{
+    // 使用 IntelliSense 了解相关属性。 
+    // 悬停以查看现有属性的描述。
+    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "nomo",
+            "request": "launch",
+            "type": "dart"
+        },
+        {
+            "name": "nomo (profile mode)",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "profile"
+        },
+        {
+            "name": "nomo (release mode)",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "release"
+        }
+    ]
+}

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# ixVPN - Flutter VPN 应用
+
+ixVPN 是一个基于 Flutter 开发的跨平台 VPN 应用,支持 Android、iOS、macOS、Windows 和 Linux 平台。
+
+## License
+
+[MIT](LICENSE)
+

+ 28 - 0
analysis_options.yaml

@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at https://dart.dev/lints.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 14 - 0
android/.gitignore

@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks

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

@@ -0,0 +1,68 @@
+plugins {
+    id("com.android.application")
+    id("kotlin-android")
+    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+    id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+    namespace = "app.xixi.nomo"
+    compileSdk = flutter.compileSdkVersion
+    ndkVersion = flutter.ndkVersion
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
+    }
+
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_11.toString()
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId = "app.xixi.nomo"
+        // You can update the following values to match your application needs.
+        // For more information, see: https://flutter.dev/to/review-gradle-config.
+        minSdk = flutter.minSdkVersion
+        targetSdk = flutter.targetSdkVersion
+        versionCode = flutter.versionCode
+        versionName = flutter.versionName
+    }
+
+    sourceSets {
+        getByName("main") {
+            jniLibs.srcDirs("libs")
+        }
+    }
+
+    packaging {
+        jniLibs {
+            useLegacyPackaging = true
+            keepDebugSymbols += "**/*.so"
+        }
+        resources {
+            excludes += ":META-INF/LICENSE"
+            excludes += ":META-INF/LICENSE.md"
+            excludes += ":META-INF/NOTICE"
+        }
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig = signingConfigs.getByName("debug")
+        }
+    }
+}
+
+flutter {
+    source = "../.."
+}
+
+dependencies {
+    // Core Libraries
+    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
+    implementation("com.google.code.gson:gson:2.13.2")
+}

BIN
android/app/libs/arm64-v8a/libhev-socks5-tunnel.so


BIN
android/app/libs/armeabi-v7a/libhev-socks5-tunnel.so


BIN
android/app/libs/libxray.aar


+ 7 - 0
android/app/src/debug/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 90 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,90 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <uses-permission
+        android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission
+        android:name="android.permission.CHANGE_NETWORK_STATE"/>
+    <uses-permission
+        android:name="android.permission.INTERNET"/>
+    <uses-permission
+        android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission
+        android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission
+        android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>
+    <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
+    <uses-permission
+        android:name="android.permission.POST_NOTIFICATIONS"/>
+    <uses-permission
+        android:name="android:activate_vpn"/>
+    <uses-permission
+        android:name="android.permission.PACKAGE_USAGE_STATS"
+        tools:ignore="ProtectedPermissions"/>
+    <application
+        android:name=".App"
+        android:allowBackup="false"
+        android:enableOnBackInvokedCallback="false"
+        android:fullBackupContent="false"
+        android:hardwareAccelerated="true"
+        android:icon="@mipmap/launcher_icon"
+        android:label="NoMo"
+        android:requestLegacyExternalStorage="true"
+        android:usesCleartextTraffic="true"
+        tools:targetApi="tiramisu">
+        <activity
+            android:name=".MainActivity"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:exported="true"
+            android:hardwareAccelerated="true"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+                android:name="io.flutter.embedding.android.NormalTheme"
+                android:resource="@style/NormalTheme"/>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <service
+            android:name=".XRayService"
+            android:directBootAware="true"
+            android:exported="false"
+            android:foregroundServiceType="systemExempted"
+            android:label="NoMo"
+            android:permission="android.permission.BIND_VPN_SERVICE"
+            android:process=":nomo_vpn_service"
+            tools:ignore="ForegroundServicePermission">
+            <intent-filter>
+                <action
+                    android:name="android.net.VpnService"/>
+            </intent-filter>
+            <meta-data
+                android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
+                android:value="false"/>
+        </service>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2"/>
+    </application>
+    <!-- Required to query activities that can process text, see:
+         https://developer.android.com/training/package-visibility and
+         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
+
+         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
+    <queries>
+        <intent>
+            <action
+                android:name="android.intent.action.PROCESS_TEXT"/>
+            <data
+                android:mimeType="text/plain"/>
+        </intent>
+    </queries>
+</manifest>

+ 126 - 0
android/app/src/main/assets/connect.json

@@ -0,0 +1,126 @@
+{
+  "log": {
+    "loglevel": "warning"
+  },
+  "stats": {},
+  "inbounds": [
+    {
+      "tag": "socks-in",
+      "port": 10808,
+      "listen": "127.0.0.1",
+      "protocol": "socks",
+      "settings": {
+        "auth": "noauth",
+        "udp": true,
+        "userLevel": 8
+      },
+      "sniffing": {
+        "enabled": true,
+        "destOverride": ["http", "tls"]
+      }
+    },
+    {
+      "tag": "http-in",
+      "port": 8080,
+      "listen": "127.0.0.1",
+      "protocol": "http",
+      "settings": {
+        "userLevel": 8
+      }
+    }
+  ],
+  "outbounds": [
+    {
+      "tag": "proxy",
+      "mux": {
+        "concurrency": -1,
+        "enabled": false
+      },
+      "protocol": "vless",
+      "settings": {
+        "vnext": [
+          {
+            "address": "38.75.137.27",
+            "port": 8089,
+            "users": [
+              {
+                "id": "c91112b9-0790-49a7-869f-805ff2bf5caa",
+                "encryption": "none",
+                "flow": ""
+              }
+            ]
+          }
+        ]
+      },
+      "streamSettings": {
+        "network": "xhttp",
+        "security": "none",
+        "xhttpSettings": {
+          "path": "/xhttp",
+          "host": "38.75.137.27"
+        }
+      }
+    },
+    {
+      "tag": "direct",
+      "protocol": "freedom",
+      "settings": {
+        "domainStrategy": "UseIPv4"
+      }
+    },
+    {
+      "tag": "block",
+      "protocol": "blackhole",
+      "settings": {
+        "response": {
+          "type": "http"
+        }
+      }
+    }
+  ],
+  "routing": {
+    "domainStrategy": "AsIs",
+    "rules": [
+      {
+        "type": "field",
+        "domain": [
+          "geosite:private"
+        ],
+        "outboundTag": "direct"
+      },
+      {
+        "type": "field",
+        "ip": [
+          "geoip:private",
+          "geoip:cn"
+        ],
+        "outboundTag": "direct"
+      },
+      {
+        "type": "field",
+        "domain": [
+          "geosite:cn"
+        ],
+        "outboundTag": "direct"
+      },
+      {
+        "type": "field",
+        "network": "tcp,udp",
+        "outboundTag": "proxy"
+      }
+    ]
+  },
+  "dns": {
+    "servers": [
+      "1.1.1.1",
+      "8.8.8.8",
+      {
+        "address": "223.5.5.5",
+        "port": 53,
+        "domains": [
+          "geosite:cn"
+        ]
+      }
+    ]
+  }
+}

BIN
android/app/src/main/assets/geoip.dat


Різницю між файлами не показано, бо вона завелика
+ 295 - 0
android/app/src/main/assets/geosite.dat


+ 105 - 0
android/app/src/main/kotlin/app/xixi/nomo/App.kt

@@ -0,0 +1,105 @@
+package app.xixi.nomo
+
+import android.app.Application
+import android.content.Context
+import android.os.Process
+import io.flutter.FlutterInjector
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.FlutterEngineCache
+import android.util.Log
+class App : Application() {
+    
+    companion object {
+        private const val TAG = "App"
+        private const val ENGINE_ID = "main_engine"
+        
+        // 获取缓存的Flutter引擎
+        fun getFlutterEngine(): FlutterEngine? {
+            return FlutterEngineCache.getInstance().get(ENGINE_ID)
+        }
+        
+        // 检查是否为主进程
+        private fun isMainProcess(context: Context): Boolean {
+            val processName = getProcessName(context)
+            val packageName = context.packageName
+            return processName == packageName
+        }
+        
+        // 获取当前进程名
+        private fun getProcessName(context: Context): String {
+            val pid = Process.myPid()
+            val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
+            val runningApps = activityManager.runningAppProcesses
+            runningApps?.forEach { processInfo ->
+                if (processInfo.pid == pid) {
+                    return processInfo.processName
+                }
+            }
+            return context.packageName
+        }
+    }
+    
+    override fun onCreate() {
+        super.onCreate()
+        
+        // 检查是否为主进程
+        if (isMainProcess(this)) {
+            Log.d(TAG, "主进程启动,开始初始化...")
+            
+            // 在主进程中初始化Flutter引擎
+            initializeFlutterEngine()
+            
+            // 其他应用初始化逻辑
+            initializeApp()
+        } else {
+            Log.d(TAG, "非主进程启动,跳过Flutter引擎初始化")
+        }
+    }
+    
+    private fun initializeFlutterEngine() {
+        try {
+            Log.d(TAG, "开始在主进程中初始化Flutter引擎...")
+            
+            // 确保Flutter注入器已初始化
+            FlutterInjector.instance()
+            
+            // 创建Flutter引擎
+            val flutterEngine = FlutterEngine(this)
+            // .apply {
+            //     // 设置Dart入口点
+            //     dartExecutor.executeDartEntrypoint(
+            //         DartExecutor.DartEntrypoint.createDefault()
+            //     )
+                
+            //     // 设置生命周期通道
+            //     lifecycleChannel.appIsResumed()
+            // }
+            
+            // 缓存引擎以便后续使用
+//            FlutterEngineCache.getInstance().put(ENGINE_ID, flutterEngine)
+            
+            Log.d(TAG, "Flutter引擎在主进程中初始化完成")
+            
+        } catch (e: Exception) {
+            Log.e(TAG, "Flutter引擎初始化失败: ${e.message}", e)
+        }
+    }
+
+    private fun initializeApp() {
+        try {
+            Log.d(TAG, "开始初始化应用...")
+            
+            // 在这里添加其他应用级别的初始化逻辑
+            // 例如:初始化第三方SDK、配置日志等
+
+        } catch (e: Exception) {
+            Log.e(TAG, "应用初始化失败: ${e.message}", e)
+        }
+    }
+    
+    override fun attachBaseContext(base: Context?) {
+        super.attachBaseContext(base)
+        // 确保Flutter注入器已初始化(在所有进程中)
+        FlutterInjector.instance()
+    }
+}

+ 284 - 0
android/app/src/main/kotlin/app/xixi/nomo/CoreApi.g.kt

@@ -0,0 +1,284 @@
+// Autogenerated from Pigeon (v26.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
+
+
+import android.util.Log
+import io.flutter.plugin.common.BasicMessageChannel
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.MessageCodec
+import io.flutter.plugin.common.StandardMethodCodec
+import io.flutter.plugin.common.StandardMessageCodec
+import java.io.ByteArrayOutputStream
+import java.nio.ByteBuffer
+private object CoreApiPigeonUtils {
+
+  fun wrapResult(result: Any?): List<Any?> {
+    return listOf(result)
+  }
+
+  fun wrapError(exception: Throwable): List<Any?> {
+    return if (exception is FlutterError) {
+      listOf(
+        exception.code,
+        exception.message,
+        exception.details
+      )
+    } else {
+      listOf(
+        exception.javaClass.simpleName,
+        exception.toString(),
+        "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
+      )
+    }
+  }
+}
+
+/**
+ * Error class for passing custom error details to Flutter via a thrown PlatformException.
+ * @property code The error code.
+ * @property message The error message.
+ * @property details The error details. Must be a datatype supported by the api codec.
+ */
+class FlutterError (
+  val code: String,
+  override val message: String? = null,
+  val details: Any? = null
+) : Throwable()
+private open class CoreApiPigeonCodec : StandardMessageCodec() {
+  override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
+    return     super.readValueOfType(type, buffer)
+  }
+  override fun writeValue(stream: ByteArrayOutputStream, value: Any?)   {
+    super.writeValue(stream, value)
+  }
+}
+
+val CoreApiPigeonMethodCodec = StandardMethodCodec(CoreApiPigeonCodec())
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface CoreApi {
+  fun getApps(): String?
+  fun getSystemLocale(): String?
+  fun connect(): Boolean?
+  fun disconnect(): Boolean?
+  fun getRemoteIp(): String?
+  fun getAdvertisingId(): String?
+  fun moveTaskToBack(): Boolean?
+  fun isConnected(): Boolean?
+  fun getSimInfo(): String?
+  fun reconnect(): Boolean?
+
+  companion object {
+    /** The codec used by CoreApi. */
+    val codec: MessageCodec<Any?> by lazy {
+      CoreApiPigeonCodec()
+    }
+    /** Sets up an instance of `CoreApi` to handle messages through the `binaryMessenger`. */
+    @JvmOverloads
+    fun setUp(binaryMessenger: BinaryMessenger, api: CoreApi?, messageChannelSuffix: String = "") {
+      val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getApps$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.getApps())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getSystemLocale$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.getSystemLocale())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.connect$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.connect())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.disconnect$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.disconnect())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getRemoteIp$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.getRemoteIp())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getAdvertisingId$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.getAdvertisingId())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.moveTaskToBack$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.moveTaskToBack())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.isConnected$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.isConnected())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.getSimInfo$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.getSimInfo())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+      run {
+        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app.xixi.nomo.CoreApi.reconnect$separatedMessageChannelSuffix", codec)
+        if (api != null) {
+          channel.setMessageHandler { _, reply ->
+            val wrapped: List<Any?> = try {
+              listOf(api.reconnect())
+            } catch (exception: Throwable) {
+              CoreApiPigeonUtils.wrapError(exception)
+            }
+            reply.reply(wrapped)
+          }
+        } else {
+          channel.setMessageHandler(null)
+        }
+      }
+    }
+  }
+}
+
+private class CoreApiPigeonStreamHandler<T>(
+    val wrapper: CoreApiPigeonEventChannelWrapper<T>
+) : EventChannel.StreamHandler {
+  var pigeonSink: PigeonEventSink<T>? = null
+
+  override fun onListen(p0: Any?, sink: EventChannel.EventSink) {
+    pigeonSink = PigeonEventSink<T>(sink)
+    wrapper.onListen(p0, pigeonSink!!)
+  }
+
+  override fun onCancel(p0: Any?) {
+    pigeonSink = null
+    wrapper.onCancel(p0)
+  }
+}
+
+interface CoreApiPigeonEventChannelWrapper<T> {
+  open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
+
+  open fun onCancel(p0: Any?) {}
+}
+
+class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
+  fun success(value: T) {
+    sink.success(value)
+  }
+
+  fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
+    sink.error(errorCode, errorMessage, errorDetails)
+  }
+
+  fun endOfStream() {
+    sink.endOfStream()
+  }
+}
+      
+abstract class OnEventChangeStreamHandler : CoreApiPigeonEventChannelWrapper<String> {
+  companion object {
+    fun register(messenger: BinaryMessenger, streamHandler: OnEventChangeStreamHandler, instanceName: String = "") {
+      var channelName: String = "dev.flutter.pigeon.app.xixi.nomo.CoreChangeEventApi.onEventChange"
+      if (instanceName.isNotEmpty()) {
+        channelName += ".$instanceName"
+      }
+      val internalStreamHandler = CoreApiPigeonStreamHandler<String>(streamHandler)
+      EventChannel(messenger, channelName, CoreApiPigeonMethodCodec).setStreamHandler(internalStreamHandler)
+    }
+  }
+}
+      

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

@@ -0,0 +1,319 @@
+package app.xixi.nomo
+
+import CoreApi
+import FlutterError
+import OnEventChangeStreamHandler
+import PigeonEventSink
+import android.app.Activity
+import android.content.Context.TELEPHONY_SERVICE
+import android.content.Intent
+import android.net.VpnService
+import android.telephony.TelephonyManager
+import android.util.Log
+import com.google.gson.Gson
+import io.flutter.plugin.common.BinaryMessenger
+import java.io.InputStream
+import java.util.UUID
+
+class CoreApiImpl(private val activity: Activity) : CoreApi {
+    companion object {
+        private const val TAG = "CoreApiImpl"
+        private const val REQUEST_VPN_PERMISSION = 1001
+    }
+    
+    // 事件流处理器
+    private var eventSink: PigeonEventSink<String>? = null
+
+    // xray服务
+    private var xrayApi: XRayApi? = null
+    
+    // 事件流处理器实现
+    private val eventStreamHandler = object : OnEventChangeStreamHandler() {
+        override fun onListen(p0: Any?, sink: PigeonEventSink<String>) {
+            Log.d(TAG, "Flutter 开始监听事件变化")
+            eventSink = sink
+        }
+        
+        override fun onCancel(p0: Any?) {
+            Log.d(TAG, "Flutter 取消监听事件变化")
+            eventSink = null
+        }
+    }
+    
+    // 注册事件流处理器
+    fun registerEventStreamHandler(messenger: BinaryMessenger) {
+        OnEventChangeStreamHandler.register(messenger, eventStreamHandler)
+        Log.d(TAG, "事件流处理器已注册")
+    }
+
+    // 通知 Flutter 连接状态变化
+    private fun notifyConnectionStateChange(state: String) {
+        eventSink?.let { sink ->
+            try {
+                sink.success(state)
+                Log.d(TAG, "已通知 Flutter 连接状态变化: $state")
+            } catch (e: Exception) {
+                Log.e(TAG, "通知 Flutter 失败", e)
+            }
+        } ?: Log.w(TAG, "事件流未连接,无法通知 Flutter")
+    }
+
+    /**
+     * 初始化xray服务
+     */
+    fun initXrayApi() {
+        VLog.i(TAG, "CoreApiImpl.initXrayApi() 创建 XRayApi 实例")
+        xrayApi = XRayApi()
+    }
+
+    /**
+     * 设置XRayApi状态监听
+     */
+    fun setXRayApiStatusListener() {
+        xrayApi?.setVpnStatusEventListener { status, message ->
+            VLog.i(TAG, "接收到VPN状态变化: status=$status, message=$message")
+            // 根据状态码转换连接状态
+            val connectionState = when (status) {
+                0L -> "disconnected"
+                1L -> "connecting"
+                2L -> "connected"
+                3L -> "error"
+                else -> "unknown"
+            }
+            // 通知Flutter连接状态变化
+            notifyConnectionStateChange(connectionState)
+        }
+    }
+
+    fun xrayInit() {
+        VLog.i(TAG, "CoreApiImpl.xrayInit() 被调用")
+        xrayApi?.init(activity)
+        VLog.i(TAG, "CoreApiImpl.xrayInit() 调用完成")
+    }
+
+    fun xrayUnInit() {
+        xrayApi?.uninit()
+    }
+
+
+    override fun getApps(): String? {
+        TODO("Not yet implemented")
+    }
+
+    override fun getSystemLocale(): String? {
+        TODO("Not yet implemented")
+    }
+
+    override fun connect(): Boolean? {
+        Log.i(TAG, "Starting V2Ray with permission check")
+        // 检查VPN权限
+        val intent = VpnService.prepare(activity)
+        if (intent == null) {
+            // 已有VPN权限,直接启动
+            Log.i(TAG, "VPN permission already granted")
+            startV2RayService()
+        } else {
+            // 需要请求VPN权限
+            Log.i(TAG, "Requesting VPN permission")
+            activity.startActivityForResult(intent, REQUEST_VPN_PERMISSION)
+        }
+        return true
+    }
+
+    override fun disconnect(): Boolean? {
+        VLog.i(TAG, "============ 开始停止 V2Ray 服务 ============")
+        
+        // 通知 Flutter 开始断开连接
+        notifyConnectionStateChange("disconnecting")
+
+        // 停止V2Ray服务
+        VLog.i(TAG, "调用 xrayApi.stopXray()")
+        xrayApi?.stopXray()
+        xrayUnInit()
+        VLog.i(TAG, "V2Ray 服务停止完成")
+        notifyConnectionStateChange("disconnected")
+        return true
+    }
+
+    override fun getRemoteIp(): String? {
+        TODO("Not yet implemented")
+    }
+
+    override fun getAdvertisingId(): String? {
+        TODO("Not yet implemented")
+    }
+
+    override fun moveTaskToBack(): Boolean? {
+        try {
+            activity.moveTaskToBack(true)
+            return true
+        } catch (e: Exception) {
+            throw FlutterError("-1", e.message)
+        }
+    }
+
+    override fun isConnected(): Boolean? {
+        val isRunning = XRayApi.isServiceRunning(activity)
+        VLog.i(TAG, "XRayService is running = $isRunning")
+        return isRunning
+    }
+
+    override fun getSimInfo(): String? {
+        try {
+            val telephonyManager = activity.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
+            val simState = telephonyManager.simState
+            val simReady = simState == TelephonyManager.SIM_STATE_READY
+
+            val carrierName = telephonyManager.simOperatorName ?: "Unknown"
+            val simOperator = telephonyManager.simOperator ?: ""
+            val simCountryIso = telephonyManager.simCountryIso ?: ""
+            val mcc = if (simOperator.length >= 3) simOperator.substring(0, 3) else ""
+            val mnc = if (simOperator.length > 3) simOperator.substring(3) else ""
+
+            val networkCarrierName = telephonyManager.networkOperatorName ?: "Unknown"
+            val networkOperator = telephonyManager.networkOperator ?: ""
+            val networkCountryIso = telephonyManager.networkCountryIso ?: ""
+            val networkMcc =
+                if (networkOperator.length >= 3) networkOperator.substring(0, 3) else ""
+            val networkMnc = if (networkOperator.length > 3) networkOperator.substring(3) else ""
+
+            val data = mapOf(
+                "simReady" to simReady,
+                "carrierName" to carrierName,
+                "mcc" to mcc,
+                "mnc" to mnc,
+                "countryIso" to simCountryIso,
+                "networkCarrierName" to networkCarrierName,
+                "networkMcc" to networkMcc,
+                "networkMnc" to networkMnc,
+                "networkCountryIso" to networkCountryIso
+            )
+            return Gson().toJson(data)
+        } catch (e: Exception) {
+            throw FlutterError("-1", e.message)
+        }
+    }
+
+    override fun reconnect(): Boolean? {
+        xrayInit()
+        return true
+    }
+
+    fun startV2RayService() {
+        VLog.i(TAG, "============ 开始启动 V2Ray 服务 ============")
+
+        // 通知 Flutter 开始连接
+        notifyConnectionStateChange("connecting")
+        
+        // 1. 初始化 XRay (启动和绑定服务)
+        VLog.i(TAG, "步骤 1: 调用 xrayInit() 初始化服务")
+        xrayInit()
+        
+        // 2. 启动 XRay (发送启动消息)
+        // 注意:startXray() 内部已经处理了等待服务连接的逻辑
+        // 如果服务还在连接中 (XRAY_SVR_CONNECT_SERVICE),会注册回调等待
+        // 如果服务已连接 (XRAY_SVR_SERVICE_WORKING),会直接发送启动消息
+        VLog.i(TAG, "步骤 2: 调用 startXray() 启动代理")
+        startXray()
+        
+        VLog.i(TAG, "startV2RayService 调用完成,等待服务连接和启动")
+        // 注意:实际的连接成功应该在服务启动完成后通知
+        // 这里先暂时保持原有逻辑
+        notifyConnectionStateChange("connected")
+    }
+
+    /**
+     * 查询统计信息
+     */
+    fun queryStats() {
+        Log.i(TAG, "Querying V2Ray stats...")
+    }
+    
+    /**
+     * 通知 Flutter 统计信息更新
+     */
+    fun notifyStatsUpdate(uplink: Long, downlink: Long) {
+        val statsJson = "{\"uplink\":$uplink,\"downlink\":$downlink}"
+        notifyConnectionStateChange("stats_update:$statsJson")
+    }
+    
+    /**
+     * 通知 Flutter 错误信息
+     */
+    fun notifyError(errorMessage: String) {
+        notifyConnectionStateChange("error:$errorMessage")
+    }
+    
+    /**
+     * 通知 Flutter 服务状态变化
+     */
+    fun notifyServiceStatus(isRunning: Boolean) {
+        val status = if (isRunning) "running" else "stopped"
+        notifyConnectionStateChange("service_status:$status")
+    }
+
+    // 启动V2Ray服务
+    private fun startXray() {
+        VLog.i(TAG, "CoreApiImpl.startXray() 被调用,开始准备配置")
+        
+        val config = XrayConfig()
+        config.sessionId = UUID.randomUUID().toString()
+        config.socksPort = 10808
+        
+        VLog.i(TAG, "生成的 sessionId: ${config.sessionId}")
+        VLog.i(TAG, "socksPort: ${config.socksPort}")
+        
+        val detectOptions = StatusDetectOptions()
+        detectOptions.statusDetectInterval = 60
+        val detectUrls = ArrayList<String>()
+        detectUrls.add("https://www.baidu.com")
+        detectOptions.statusDetectUrls = detectUrls
+        
+        val proxyNodes= ArrayList<ProxyNode>()
+        val proxyNode = ProxyNode()
+        proxyNode.nodeId = "10001"
+        proxyNode.coreConfig = readAssets("connect.json")
+        proxyNodes.add(proxyNode)
+        
+        VLog.i(TAG, "读取配置文件: connect.json")
+        
+        val startOptions = ProxyStartOptions()
+        startOptions.socksPort = config.socksPort
+        startOptions.nodes = proxyNodes
+        startOptions.statusDetectOptions = detectOptions
+        config.startOptions = startOptions
+        
+        VLog.i(TAG, "配置准备完成,调用 xrayApi.startXray()")
+        val result = xrayApi?.startXray(config)
+        VLog.i(TAG, "xrayApi.startXray() 返回结果: $result")
+    }
+
+
+    private fun readAssets(fileName: String): String {
+        try {
+            val `in`: InputStream = activity.resources.assets.open(fileName)
+            val lenght = `in`.available()
+            val buffer = ByteArray(lenght)
+            `in`.read(buffer)
+            return String(buffer, charset("utf-8"))
+        } catch (e: java.lang.Exception) {
+            e.printStackTrace()
+            return ""
+        }
+    }
+
+    /**
+     * 处理Activity结果(需要在Activity的onActivityResult中调用)
+     */
+    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_VPN_PERMISSION) {
+            if (resultCode == Activity.RESULT_OK) {
+                Log.i(TAG, "VPN permission granted, starting V2Ray service")
+                startV2RayService()
+            } else {
+                Log.w(TAG, "VPN permission denied")
+                notifyConnectionStateChange("permission_denied")
+            }
+        }
+    }
+}

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

@@ -0,0 +1,109 @@
+package app.xixi.nomo
+
+import android.app.ComponentCaller
+import android.content.Intent
+import android.graphics.Color
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import android.util.Log
+import androidx.core.view.WindowCompat
+
+class MainActivity: FlutterActivity() {
+
+    private lateinit var coreApiImpl: CoreApiImpl
+    companion object {
+        private const val TAG = "MainActivity"
+    }
+    
+    override fun provideFlutterEngine(context: android.content.Context): FlutterEngine? {
+        // 使用预初始化的Flutter引擎
+        val engine = App.getFlutterEngine()
+        if (engine != null) {
+            // 确保引擎状态正确
+            engine.lifecycleChannel.appIsResumed()
+            Log.d(TAG, "使用预初始化的Flutter引擎")
+        } else {
+            Log.w(TAG, "预初始化的Flutter引擎未找到,将创建新引擎")
+        }
+        return engine
+    }
+    
+    override fun onCreate(savedInstanceState: android.os.Bundle?) {
+        super.onCreate(savedInstanceState)
+        VLog.init(this, "client")
+        // 1. Edge-to-Edge 布局(内容延伸到状态栏和导航栏)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+
+        // 2. 针对Android 14+的系统导航栏透明处理
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            // Android 14+ 使用新的API
+            window.isNavigationBarContrastEnforced = false
+            window.navigationBarColor = Color.TRANSPARENT
+            window.statusBarColor = Color.TRANSPARENT
+        } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
+            // Android 11+ 使用WindowInsetsController
+            window.insetsController?.let { controller ->
+                controller.systemBarsBehavior = 
+                    android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+                controller.setSystemBarsAppearance(
+                    android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
+                    android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+                )
+            }
+            window.navigationBarColor = Color.TRANSPARENT
+            window.statusBarColor = Color.TRANSPARENT
+        } else {
+            // Android 10及以下使用传统方法
+            window.decorView.systemUiVisibility = (
+                android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+                android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+                android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+            )
+            window.navigationBarColor = Color.TRANSPARENT
+            window.statusBarColor = Color.TRANSPARENT
+        }
+
+        Log.d(TAG, "MainActivity onCreate - 进程ID: ${android.os.Process.myPid()}")
+    }
+
+    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+        super.configureFlutterEngine(flutterEngine)
+        val messenger = flutterEngine.dartExecutor.binaryMessenger
+        
+        // 创建 CoreApiImpl 实例
+        coreApiImpl = CoreApiImpl(this)
+
+        // 初始化 XrayApi
+        coreApiImpl.initXrayApi();
+        
+        // 设置 XRayApi 状态监听
+        coreApiImpl.setXRayApiStatusListener()
+        
+        // 设置 CoreApi
+        CoreApi.setUp(messenger, coreApiImpl)
+        
+        // 注册事件流处理器
+        coreApiImpl.registerEventStreamHandler(messenger)
+        
+        Log.d(TAG, "CoreApi 和事件流处理器已配置完成")
+    }
+
+//    override fun onResume() {
+//        super.onResume()
+//        coreApiImpl.xrayInit()
+//    }
+//
+//    override fun onPause() {
+//        super.onPause()
+//        coreApiImpl.xrayUnInit()
+//    }
+
+    override fun onActivityResult(
+        requestCode: Int,
+        resultCode: Int,
+        data: Intent?
+    ) {
+        super.onActivityResult(requestCode, resultCode, data)
+        coreApiImpl.onActivityResult(requestCode, resultCode, data)
+    }
+}

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

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

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

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

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

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

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

@@ -0,0 +1,59 @@
+package app.xixi.nomo
+
+import android.content.Context
+import android.util.Log
+import java.io.File
+import java.io.FileOutputStream
+
+internal class TProxyService {
+    external fun TProxyStartService(configStr: String, tunFd: Int)
+    external fun TProxyStopService()
+    external fun TProxyGetStats(): LongArray
+
+    companion object {
+        private const val TAG = "ixvpn"
+
+        init {
+            try {
+                System.loadLibrary("hev-socks5-tunnel")
+            } catch (e: Exception) {
+                Log.e(TAG, "load hev-socks5-tunnel err!")
+            }
+        }
+    }
+
+    private fun buildConfig(socksPort: Int): String {
+        return buildString {
+            append("tunnel:\n")
+            append("  mtu: ${1500}\n")
+            append("  ipv4: \n")
+            append("socks5:\n")
+            append("  port: $socksPort\n")
+            append("  address: 127.0.0.1\n")
+            append("  udp: 'udp'\n")
+        }
+    }
+
+    fun startTunnel(context: Context, socksPort: Int, tunFd: Int) {
+        val configStr = buildConfig(socksPort)
+        val configFileDir = context.cacheDir.absolutePath
+        val configFileName = "hev-socks5-tunnel.yaml"
+        val cfgFile = File(configFileDir, configFileName)
+        try {
+            FileOutputStream(cfgFile, true).use { fos ->
+                fos.write(configStr.toByteArray())
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+        TProxyStartService("$configFileDir/$configFileName", tunFd)
+    }
+
+    fun stopTunnel() {
+        try {
+            TProxyStopService()
+        } catch (e: Exception) {
+            // Ignore exception
+        }
+    }
+}

+ 168 - 0
android/app/src/main/kotlin/app/xixi/nomo/VLog.kt

@@ -0,0 +1,168 @@
+package app.xixi.nomo
+
+import android.content.Context
+import android.util.Log
+import java.io.File
+import java.io.FileWriter
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.Executors
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * 文件日志工具类
+ * 将日志同时输出到 logcat 和文件,确保日志不丢失
+ */
+object VLog {
+    private var logFile: File? = null
+    private var writer: PrintWriter? = null
+    private val lock = ReentrantLock()
+    private val executor = Executors.newSingleThreadExecutor()
+    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+    
+    private const val MAX_LOG_SIZE = 10 * 1024 * 1024 // 10MB
+    private const val MAX_LOG_FILES = 1 // 每种类型就保留一个日志文件
+    
+    /**
+     * 初始化日志系统
+     */
+    fun init(context: Context, name: String = "service") {
+        executor.execute {
+            try {
+                val logDir = File(context.filesDir, "logs")
+                if (!logDir.exists()) {
+                    logDir.mkdirs()
+                }
+                
+                // 清理旧日志文件
+                cleanOldLogs(logDir, name)
+                
+                // 创建新的日志文件
+                val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+                logFile = File(logDir, "${name}_$timestamp.log")
+                writer = PrintWriter(FileWriter(logFile, true), true)
+                
+                i("FileLogger", "日志系统初始化成功: ${logFile?.absolutePath}")
+            } catch (e: Exception) {
+                Log.e("FileLogger", "初始化日志系统失败", e)
+            }
+        }
+    }
+    
+    /**
+     * 清理旧的日志文件,只保留最近的几个
+     */
+    private fun cleanOldLogs(logDir: File, name: String) {
+        try {
+            val logFiles = logDir.listFiles { file ->
+                file.name.startsWith("${name}_") && file.name.endsWith(".log")
+            }?.sortedByDescending { it.lastModified() } ?: return
+            
+            // 删除超出数量限制的旧文件
+            if (logFiles.size >= MAX_LOG_FILES) {
+                logFiles.drop(MAX_LOG_FILES - 1).forEach { file ->
+                    file.delete()
+                    Log.i("FileLogger", "删除旧日志文件: ${file.name}")
+                }
+            }
+        } catch (e: Exception) {
+            Log.e("FileLogger", "清理旧日志失败", e)
+        }
+    }
+    
+    /**
+     * 检查并轮转日志文件
+     */
+    private fun checkAndRotateLog(context: Context) {
+        try {
+            val currentFile = logFile ?: return
+            if (currentFile.length() > MAX_LOG_SIZE) {
+                writer?.close()
+                init(context)
+            }
+        } catch (e: Exception) {
+            Log.e("FileLogger", "轮转日志文件失败", e)
+        }
+    }
+    
+    /**
+     * 写入日志
+     */
+    private fun writeLog(level: String, tag: String, message: String, throwable: Throwable? = null) {
+        executor.execute {
+            lock.withLock {
+                try {
+                    val timestamp = dateFormat.format(Date())
+                    val logMessage = "[$timestamp] [$level] [$tag] $message"
+                    
+                    writer?.println(logMessage)
+                    throwable?.let {
+                        writer?.println(it.stackTraceToString())
+                    }
+                    writer?.flush()
+                } catch (e: Exception) {
+                    Log.e("FileLogger", "写入日志失败", e)
+                }
+            }
+        }
+    }
+    
+    /**
+     * Info 级别日志
+     */
+    fun i(tag: String, message: String) {
+        Log.i(tag, message)
+        writeLog("INFO", tag, message)
+    }
+    
+    /**
+     * Debug 级别日志
+     */
+    fun d(tag: String, message: String) {
+        Log.d(tag, message)
+        writeLog("DEBUG", tag, message)
+    }
+    
+    /**
+     * Warning 级别日志
+     */
+    fun w(tag: String, message: String, throwable: Throwable? = null) {
+        Log.w(tag, message, throwable)
+        writeLog("WARN", tag, message, throwable)
+    }
+    
+    /**
+     * Error 级别日志
+     */
+    fun e(tag: String, message: String, throwable: Throwable? = null) {
+        Log.e(tag, message, throwable)
+        writeLog("ERROR", tag, message, throwable)
+    }
+    
+    /**
+     * 获取当前日志文件路径
+     */
+    fun getLogFilePath(): String? {
+        return logFile?.absolutePath
+    }
+    
+    /**
+     * 关闭日志系统
+     */
+    fun close() {
+        executor.execute {
+            lock.withLock {
+                try {
+                    writer?.close()
+                    writer = null
+                } catch (e: Exception) {
+                    Log.e("FileLogger", "关闭日志系统失败", e)
+                }
+            }
+        }
+    }
+}
+

+ 311 - 0
android/app/src/main/kotlin/app/xixi/nomo/XRayApi.kt

@@ -0,0 +1,311 @@
+package app.xixi.nomo
+
+import android.app.ActivityManager
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.Bundle
+import android.os.DeadObjectException
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import androidx.core.content.ContextCompat
+import java.util.concurrent.locks.ReentrantLock
+
+class XRayApi {
+    private val lock = ReentrantLock()
+    private val mReplyMsgHandler = Messenger(ReplyMsgHandler(Looper.getMainLooper()))
+    private var context: Context? = null
+    private var mService: Messenger? = null
+    private var vpnState = VPN_STATE_IDLE
+    private var xraySvrState = XRAY_SVR_IDLE
+    private var isInited = false
+    private var firstInit = true
+    private var serviceEvent: OnXrayServiceEvent? = null
+    private var vpnStatusEvent: OnVpnStatusEvent? = null
+
+    private inner class ReplyMsgHandler(looper: Looper) : Handler(looper) {
+        override fun handleMessage(msg: Message) {
+            // Handle message
+        }
+    }
+
+    private fun interface OnXrayServiceEvent {
+        fun onXrayServiceStarted()
+    }
+
+    fun interface OnVpnStatusEvent {
+        fun onVpnStatusChange(status: Long, message: String)
+    }
+
+    private val mConnection = object : ServiceConnection {
+        override fun onServiceConnected(className: ComponentName, service: IBinder) {
+            VLog.i(TAG, "xray service connected 服务连接成功")
+            mService = Messenger(service)
+            lock.lock()
+            try {
+                xraySvrState = XRAY_SVR_SERVICE_WORKING
+                VLog.i(TAG, "服务状态更新为 XRAY_SVR_SERVICE_WORKING")
+            } finally {
+                lock.unlock()
+            }
+            // 如果有等待执行的事件,现在执行
+            serviceEvent?.let {
+                VLog.i(TAG, "执行等待的服务事件")
+                it.onXrayServiceStarted()
+                serviceEvent = null
+            }
+        }
+
+        override fun onServiceDisconnected(className: ComponentName) {
+            VLog.i(TAG, "xray service disconnected 服务断开连接")
+            lock.lock()
+            try {
+                mService = null
+                vpnState = VPN_STATE_IDLE
+                xraySvrState = XRAY_SVR_IDLE
+            } finally {
+                lock.unlock()
+            }
+        }
+    }
+
+    private val messageReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (intent.action == XRAY_STAT_MESSAGE_ID) {
+                val status = intent.getLongExtra("status", 0L)
+                val message = intent.getStringExtra("message") ?: ""
+                VLog.i(TAG, "接收到VPN状态消息: status=$status, message=$message")
+                vpnStatusEvent?.onVpnStatusChange(status, message)
+            }
+        }
+    }
+
+    fun init(context: Context) {
+        VLog.i(TAG, "XRayApi init 开始")
+        if (isInited) {
+            VLog.i(TAG, "XRayApi 已经初始化,跳过")
+            return
+        }
+        this.context = context
+        isInited = true
+
+        lock.lock()
+        try {
+            if (xraySvrState != XRAY_SVR_IDLE) {
+                VLog.i(TAG, "XRay 服务状态不是 IDLE,当前状态: $xraySvrState")
+                return
+            }
+            val intentFilter = IntentFilter().apply {
+                addAction(XRAY_STAT_MESSAGE_ID)
+            }
+            ContextCompat.registerReceiver(
+                this.context!!,
+                messageReceiver,
+                intentFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED
+            )
+            VLog.i(TAG, "BroadcastReceiver 已注册")
+            doStartVpnService(null)
+            VLog.i(TAG, "XRayApi init 完成,开始启动和绑定服务")
+        } finally {
+            lock.unlock()
+        }
+    }
+
+    fun uninit() {
+        if (!isInited) return
+        VLog.i(TAG, "uninit 开始清理服务")
+        
+        try {
+            context?.unregisterReceiver(messageReceiver)
+            VLog.i(TAG, "BroadcastReceiver 已注销")
+        } catch (e: Exception) {
+            VLog.e(TAG, "注销 BroadcastReceiver 失败", e)
+        }
+        
+        try {
+            context?.unbindService(mConnection)
+            VLog.i(TAG, "Service 已解绑")
+        } catch (e: Exception) {
+            VLog.e(TAG, "解绑 Service 失败", e)
+        }
+        
+        // 重要:停止 Service(因为启动时用了 startService)
+        try {
+            val intent = Intent(context, XRayService::class.java)
+            context?.stopService(intent)
+            VLog.i(TAG, "stopService 已调用")
+        } catch (e: Exception) {
+            VLog.e(TAG, "停止 Service 失败", e)
+        }
+        
+        lock.lock()
+        try {
+            xraySvrState = XRAY_SVR_IDLE
+        } finally {
+            lock.unlock()
+        }
+        isInited = false
+        firstInit = true
+        VLog.i(TAG, "uninit 完成")
+    }
+
+    private fun doStartVpnService(callback: OnXrayServiceEvent?) {
+        context?.let { ctx ->
+            VLog.i(TAG, "doStartVpnService 开始")
+            doStartService(ctx)
+            val intent = Intent(ctx, XRayService::class.java)
+            VLog.i(TAG, "准备绑定 xray service")
+            if (!ctx.bindService(intent, mConnection, 0)) {
+                VLog.e(TAG, "绑定 xray service 失败")
+                return
+            }
+            xraySvrState = XRAY_SVR_CONNECT_SERVICE
+            serviceEvent = callback
+            VLog.i(TAG, "xray service 绑定请求已发送,状态更新为 XRAY_SVR_CONNECT_SERVICE")
+        }
+    }
+
+    private fun doStartService(context: Context) {
+        if (isServiceRunning(context)) {
+            VLog.i(TAG, "XRayService 已经在运行")
+            return
+        }
+        val intent = Intent(context, XRayService::class.java)
+        VLog.i(TAG, "启动 XRayService")
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            context.startForegroundService(intent)
+        } else {
+            context.startService(intent)
+        }
+//        context.startService(intent)
+        VLog.i(TAG, "XRayService 启动命令已发送")
+    }
+
+    fun startXray(config: XrayConfig): Boolean {
+        lock.lock()
+        return try {
+            VLog.i(TAG, "startXray 开始,当前 xraySvrState = $xraySvrState")
+            when (xraySvrState) {
+                XRAY_SVR_IDLE -> {
+                    VLog.w(TAG, "服务状态为 IDLE,需要先启动服务")
+                    doStartVpnService(OnXrayServiceEvent {
+                        VLog.i(TAG, "服务启动完成,开始 startXray")
+                        doStartXray(config)
+                    })
+                    lock.unlock()
+                    true
+                }
+                XRAY_SVR_CONNECT_SERVICE, XRAY_SVR_QUERY_STATE -> {
+                    VLog.i(TAG, "服务正在连接中,注册回调等待连接完成")
+                    serviceEvent = OnXrayServiceEvent {
+                        VLog.i(TAG, "服务连接完成,现在执行 startXray")
+                        doStartXray(config)
+                    }
+                    lock.unlock()
+                    true
+                }
+                XRAY_SVR_SERVICE_WORKING -> {
+                    lock.unlock()
+                    VLog.i(TAG, "服务已就绪,直接启动 xray")
+                    doStartXray(config)
+                }
+                else -> {
+                    VLog.e(TAG, "未知的服务状态: $xraySvrState")
+                    lock.unlock()
+                    false
+                }
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "startXray 异常", e)
+            lock.unlock()
+            false
+        }
+    }
+
+    fun doStartXray(config: XrayConfig): Boolean {
+        VLog.i(TAG, "doStartXray 开始执行")
+        VLog.i(TAG, "配置信息 - socksPort: ${config.socksPort}, sessionId: ${config.sessionId}")
+        
+        val msg = Message.obtain(null, XRayService.XRAY_MSG_START, 0, 0)
+        val bundle = Bundle().apply {
+            putInt("socksPort", config.socksPort)
+            putString("sessionId", config.sessionId)
+            putString("startOptions", config.getProxyStartOptions())
+            putStringArrayList("allowVpnApps", config.allowVpnApps)
+            putStringArrayList("disallowVpnApps", config.disallowVpnApps)
+        }
+        msg.data = bundle
+        return try {
+            msg.replyTo = mReplyMsgHandler
+            mService?.send(msg)
+            VLog.i(TAG, "启动消息已发送到 XRayService")
+            true
+        } catch (e: DeadObjectException) {
+            VLog.e(TAG, "发送启动消息失败:DeadObjectException", e)
+            false
+        } catch (e: Exception) {
+            VLog.e(TAG, "发送启动消息失败", e)
+            false
+        }
+    }
+
+    fun stopXray() {
+        VLog.i(TAG, "stopXray 被调用")
+        lock.lock()
+        try {
+            if (xraySvrState != XRAY_SVR_SERVICE_WORKING) {
+                VLog.w(TAG, "xray 服务未在运行,当前状态: $xraySvrState")
+                return
+            }
+        } finally {
+            lock.unlock()
+        }
+        val msg = Message.obtain(null, XRayService.XRAY_MSG_STOP, 0, 0)
+        try {
+            mService?.send(msg)
+            VLog.i(TAG, "停止消息已发送到 XRayService")
+        } catch (e: Exception) {
+            VLog.e(TAG, "发送停止消息失败", e)
+        }
+    }
+
+    fun setVpnStatusEventListener(listener: OnVpnStatusEvent?) {
+        vpnStatusEvent = listener
+    }
+
+    companion object {
+        private const val TAG = "ixvpn"
+        const val XRAY_SVR_IDLE = 0
+        const val XRAY_SVR_CONNECT_SERVICE = 1
+        const val XRAY_SVR_QUERY_STATE = 2
+        const val XRAY_SVR_SERVICE_WORKING = 7
+
+        const val VPN_STATE_IDLE = 0
+        const val VPN_STATE_CONNECTING = 1
+        const val VPN_STATE_CONNECTED = 2
+        const val VPN_STATE_ERROR = 3
+        const val VPN_STATE_STARTING = 4 // internal using
+        const val VPN_STATE_STOPPING = 5 // internal using
+
+        const val XRAY_STAT_MESSAGE_ID = "xray_service_stat_message"
+
+        fun isServiceRunning(mContext: Context): Boolean {
+            val activityManager = mContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+            val serviceList = activityManager.getRunningServices(300)
+
+            if (serviceList.isEmpty()) {
+                return false
+            }
+
+            return serviceList.any { it.service.className == "app.xixi.nomo.XRayService" }
+        }
+    }
+}

+ 255 - 0
android/app/src/main/kotlin/app/xixi/nomo/XRayService.kt

@@ -0,0 +1,255 @@
+package app.xixi.nomo
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Intent
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+import android.net.VpnService
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import android.os.Message
+import android.os.Messenger
+import android.os.ParcelFileDescriptor
+import androidx.core.app.NotificationCompat
+import go.Seq
+import ixvpn_mobile.Ixvpn_mobile
+import ixvpn_mobile.ProxyConnectorHandler
+
+
+class XRayService : VpnService() {
+
+    private val mMessenger = Messenger(IncomingHandler())
+    private val tunnelService = TProxyService()
+    private var tunFileDescriptor: ParcelFileDescriptor? = null
+
+    private val vpnHandler = object : ProxyConnectorHandler {
+        override fun proxyStatusChange(l: Long, msg: String) {
+            VLog.i(TAG, "status:$l msg:$msg")
+            // 发送状态消息到XRayApi
+            sendStatusMessage(l, msg)
+        }
+    }
+
+    private inner class IncomingHandler : Handler() {
+        override fun handleMessage(msg: Message) {
+            handleRemoteMessage(msg)
+        }
+    }
+
+    override fun onBind(intent: Intent): IBinder? {
+        // 首先检查是否是 VPN 服务的绑定请求
+        if (SERVICE_INTERFACE == intent.action) {
+            return super.onBind(intent)
+        }
+        // 其他绑定请求返回 Messenger
+        return mMessenger.binder
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        // 初始化文件日志系统
+        VLog.init(applicationContext)
+        VLog.i(TAG, "XRayService onCreate")
+        
+        Seq.setContext(applicationContext)
+        Ixvpn_mobile.initProxyConnector()
+        createNotificationChannel()
+    }
+
+    override fun onRevoke() {
+        super.onRevoke()
+        VLog.i(TAG, "onRevoke")
+    }
+
+    override fun onTaskRemoved(rootIntent: Intent?) {
+        super.onTaskRemoved(rootIntent)
+        VLog.i(TAG, "onTaskRemoved")
+    }
+
+    override fun onDestroy() {
+        VLog.i(TAG, "onDestroy")
+        // 先停止前台服务
+        try {
+            stopForeground(STOP_FOREGROUND_REMOVE)
+        } catch (e: Exception) {
+            VLog.e(TAG, "stopForeground 失败", e)
+        }
+        
+        // 清理资源
+        try {
+            Ixvpn_mobile.freeProxyConnector()
+        } catch (e: Exception) {
+            VLog.e(TAG, "freeProxyConnector 失败", e)
+        }
+        
+        // 关闭日志系统
+        VLog.close()
+        
+        super.onDestroy()
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        createNotification()
+        return START_NOT_STICKY
+    }
+
+    private fun handleRemoteMessage(msg: Message) {
+        when (msg.what) {
+            XRAY_MSG_START -> dealStartMsg(msg)
+            XRAY_MSG_STOP -> dealStopMsg()
+        }
+    }
+
+    private fun dealStartMsg(msg: Message) {
+        VLog.i(TAG, "deal start xray msg")
+        val bundle = msg.data
+        val socksPort = bundle.getInt("socksPort")
+        val startOptions = bundle.getString("startOptions")
+        val sessionId = bundle.getString("sessionId")
+        val allowVpnApps = bundle.getStringArrayList("allowVpnApps") ?: arrayListOf()
+        val disallowVpnApps = bundle.getStringArrayList("disallowVpnApps") ?: arrayListOf()
+        
+        VLog.i(TAG, "socksPort: $socksPort, sessionId: $sessionId")
+        VLog.i(TAG, "allowVpnApps count: ${allowVpnApps.size}, disallowVpnApps count: ${disallowVpnApps.size}")
+        
+        try {
+            val builder = Builder()
+            val ipAddress = "192.168.34.2"
+            builder.setSession("ixvpn")
+                .setMtu(1500)
+                .addAddress(ipAddress, 24)
+                .addRoute("0.0.0.0", 0)
+                .addDnsServer("8.8.8.8")
+
+            if (allowVpnApps.isNotEmpty()) {
+                allowVpnApps.forEach { app ->
+                    builder.addAllowedApplication(app)
+                }
+                VLog.i(TAG, "允许的应用: ${allowVpnApps.joinToString(", ")}")
+            } else {
+                disallowVpnApps.forEach { app ->
+                    builder.addDisallowedApplication(app)
+                }
+                builder.addDisallowedApplication(packageName)
+                VLog.i(TAG, "禁止的应用: ${disallowVpnApps.joinToString(", ")}")
+            }
+            
+            tunFileDescriptor = builder.establish()
+            tunFileDescriptor?.let { tfd ->
+                VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
+                tunnelService.startTunnel(this, socksPort, tfd.fd)
+                Ixvpn_mobile.proxyConnectorStart(sessionId, startOptions, vpnHandler)
+                VLog.i(TAG, "XRay proxy started successfully")
+            } ?: run {
+                VLog.e(TAG, "Failed to establish VPN tunnel")
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "启动 XRay 失败", e)
+        }
+    }
+
+    private fun dealStopMsg() {
+        VLog.i(TAG, "deal stop xray msg")
+        doStop()
+    }
+
+    private fun doStop() {
+        VLog.i(TAG, "开始停止 XRay 服务")
+        try {
+            tunnelService.stopTunnel()
+            VLog.i(TAG, "Tunnel stopped")
+        } catch (e: Exception) {
+            VLog.e(TAG, "停止 tunnel 失败", e)
+        }
+        
+        try {
+            Ixvpn_mobile.proxyConnectorStop()
+            VLog.i(TAG, "Proxy connector stopped")
+        } catch (e: Exception) {
+            VLog.e(TAG, "停止 proxy connector 失败", e)
+        }
+        
+        tunFileDescriptor?.let { tfd ->
+            try {
+                tfd.close()
+                VLog.i(TAG, "TUN file descriptor closed")
+            } catch (e: Exception) {
+                VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
+            }
+            tunFileDescriptor = null
+        }
+        VLog.i(TAG, "xray stopped")
+    }
+
+    private fun sendStatusMessage(status: Long, message: String) {
+        try {
+            val intent = Intent("xray_service_stat_message").apply {
+                putExtra("status", status)
+                putExtra("message", message)
+            }
+            sendBroadcast(intent)
+            VLog.i(TAG, "状态消息已发送: status=$status, message=$message")
+        } catch (e: Exception) {
+            VLog.e(TAG, "发送状态消息失败", e)
+        }
+    }
+
+    private fun createNotificationChannel() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            val channelName = "NoMo Service Notifications"
+            val channelDescription = "NoMo Service is running in the background"
+            val importance = NotificationManager.IMPORTANCE_LOW
+
+            val channel = NotificationChannel(CHANNEL_ID, channelName, importance)
+            channel.setShowBadge(false)
+            channel.description = channelDescription
+
+            val notificationManager =
+                getSystemService(NotificationManager::class.java)
+            notificationManager?.createNotificationChannel(channel)
+        }
+    }
+
+    private fun createNotification() {
+        VLog.i(TAG, "开始创建通知")
+        try {
+            val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
+                .setContentTitle("NoMo Service Running")
+                //.setContentText("NoMo connection is active.")
+                .setSmallIcon(R.drawable.ic_xvpn)
+                .setPriority(NotificationCompat.PRIORITY_MIN)
+                .setOngoing(true)
+                .setShowWhen(false)
+                .setOnlyAlertOnce(true)
+                .build()
+
+            VLog.i(TAG, "通知创建成功,准备启动前台服务")
+            
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                try {
+                    startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED)
+                    VLog.i(TAG, "前台服务已启动 (Android 14+)")
+                } catch (e: SecurityException) {
+                    VLog.w(TAG, "使用 SYSTEM_EXEMPTED 类型失败,降级为普通前台服务", e)
+                    startForeground(1, notification)
+                    VLog.i(TAG, "前台服务已启动 (降级模式)")
+                }
+            } else {
+                startForeground(1, notification)
+                VLog.i(TAG, "前台服务已启动")
+            }
+        } catch (e: Exception) {
+            VLog.e(TAG, "创建通知失败", e)
+        }
+    }
+
+    companion object {
+        const val XRAY_MSG_START = 1
+        const val XRAY_MSG_STOP = 2
+        private const val TAG = "ixvpn"
+
+        private const val CHANNEL_ID: String = "nomo_channel_id"
+    }
+}

+ 21 - 0
android/app/src/main/kotlin/app/xixi/nomo/XrayConfig.kt

@@ -0,0 +1,21 @@
+package app.xixi.nomo
+
+import com.google.gson.Gson
+
+data class XrayConfig(
+    var sessionId: String = "",
+    var socksPort: Int = 0,
+    var startOptions: ProxyStartOptions? = null,
+    var allowVpnApps: ArrayList<String> = ArrayList(),
+    var disallowVpnApps: ArrayList<String> = ArrayList()
+) {
+    fun getProxyStartOptions(): String {
+        return try {
+            val gson = Gson()
+            gson.toJson(startOptions)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            ""
+        }
+    }
+}

BIN
android/app/src/main/res/drawable-hdpi/android12splash.png


BIN
android/app/src/main/res/drawable-hdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png


BIN
android/app/src/main/res/drawable-hdpi/splash.png


BIN
android/app/src/main/res/drawable-mdpi/android12splash.png


BIN
android/app/src/main/res/drawable-mdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png


BIN
android/app/src/main/res/drawable-mdpi/splash.png


BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png


BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png


BIN
android/app/src/main/res/drawable-night-xhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-night-xxhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-v21/background.png


+ 9 - 0
android/app/src/main/res/drawable-v21/launch_background.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <bitmap android:gravity="fill" android:src="@drawable/background"/>
+    </item>
+    <item>
+        <bitmap android:gravity="center" android:src="@drawable/splash"/>
+    </item>
+</layer-list>

BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png


BIN
android/app/src/main/res/drawable-xhdpi/splash.png


BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png


BIN
android/app/src/main/res/drawable-xxhdpi/splash.png


BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png


BIN
android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png


BIN
android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png


BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png


BIN
android/app/src/main/res/drawable/background.png


+ 34 - 0
android/app/src/main/res/drawable/ic_xvpn.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M19.35,10.04 C18.67,6.59,15.64,4,12,4 C9.11,4,6.6,5.64,5.35,8.04
+C2.34,8.36,0,10.91,0,14 C0,17.31,2.69,20,6,20 L19,20 C21.76,20,24,17.76,24,15
+C24,12.36,21.95,10.22,19.35,10.04 Z M19,18 L6,18 C3.79,18,2,16.21,2,14
+S3.79,10,6,10 L6.71,10 C7.37,7.69,9.48,6,12,6 C15.04,6,17.5,8.46,17.5,11.5
+L17.5,12 L19,12 C20.66,12,22,13.34,22,15 S20.66,18,19,18 Z" />
+    <path
+        android:strokeColor="#000000"
+        android:strokeWidth="2"
+        android:pathData="M6.58994,13.1803 C6.58994,13.1803,8.59173,15.8724,12.011,15.8726
+C15.2788,15.8728,17.3696,13.2502,17.3696,13.2502" />
+</vector>

+ 9 - 0
android/app/src/main/res/drawable/launch_background.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <bitmap android:gravity="fill" android:src="@drawable/background"/>
+    </item>
+    <item>
+        <bitmap android:gravity="center" android:src="@drawable/splash"/>
+    </item>
+</layer-list>

+ 14 - 0
android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background"/>
+  <foreground>
+      <inset
+          android:drawable="@drawable/ic_launcher_foreground"
+          android:inset="16%" />
+  </foreground>
+  <monochrome>
+      <inset
+          android:drawable="@drawable/ic_launcher_monochrome"
+          android:inset="16%" />
+  </monochrome>
+</adaptive-icon>

BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png


+ 22 - 0
android/app/src/main/res/values-night-v31/styles.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+        <item name="android:windowSplashScreenBackground">#000000</item>
+        <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
+        <item name="android:windowSplashScreenIconBackgroundColor">#000000</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 22 - 0
android/app/src/main/res/values-night/styles.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 22 - 0
android/app/src/main/res/values-v31/styles.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+        <item name="android:windowSplashScreenBackground">#000000</item>
+        <item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
+        <item name="android:windowSplashScreenIconBackgroundColor">#000000</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 26 - 0
android/app/src/main/res/values/styles.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+        <item name="android:forceDarkAllowed">false</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+</resources>

+ 7 - 0
android/app/src/profile/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 24 - 0
android/build.gradle.kts

@@ -0,0 +1,24 @@
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+val newBuildDir: Directory =
+    rootProject.layout.buildDirectory
+        .dir("../../build")
+        .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+    project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+    project.evaluationDependsOn(":app")
+}
+
+tasks.register<Delete>("clean") {
+    delete(rootProject.layout.buildDirectory)
+}

Різницю між файлами не показано, бо вона завелика
+ 337 - 0
android/build/reports/problems/problems-report.html


+ 3 - 0
android/gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true

+ 66 - 0
android/gradle/libs.versions.toml

@@ -0,0 +1,66 @@
+[versions]
+agp = "8.12.2"
+desugarJdkLibs = "2.1.5"
+gradleLicensePlugin = "0.9.8"
+kotlin = "2.2.10"
+coreKtx = "1.16.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.12.0"
+activity = "1.10.1"
+constraintlayout = "2.2.1"
+mmkvStatic = "1.3.14"
+gson = "2.12.1"
+quickieFoss = "1.14.0"
+kotlinxCoroutinesAndroid = "1.10.2"
+kotlinxCoroutinesCore = "1.10.2"
+swiperefreshlayout = "1.1.0"
+toasty = "1.5.2"
+editorkit = "2.9.0"
+core = "3.5.3"
+workRuntimeKtx = "2.10.3"
+lifecycleViewmodelKtx = "2.9.2"
+multidex = "2.0.1"
+mockitoMockitoInline = "5.2.0"
+flexbox = "3.0.0"
+preferenceKtx = "1.2.1"
+recyclerview = "1.4.0"
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
+desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
+gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradleLicensePlugin" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
+toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" }
+editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
+language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
+language-json = { module = "com.blacksquircle.ui:language-json", version.ref = "editorkit" }
+core = { module = "com.google.zxing:core", version.ref = "core" }
+work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
+work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workRuntimeKtx" }
+lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleViewmodelKtx" }
+lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtx" }
+multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
+org-mockito-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoMockitoInline" }
+mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoMockitoInline" }
+flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
+recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }

+ 5 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

+ 26 - 0
android/settings.gradle.kts

@@ -0,0 +1,26 @@
+pluginManagement {
+    val flutterSdkPath =
+        run {
+            val properties = java.util.Properties()
+            file("local.properties").inputStream().use { properties.load(it) }
+            val flutterSdkPath = properties.getProperty("flutter.sdk")
+            require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+            flutterSdkPath
+        }
+
+    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+
+plugins {
+    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+    id("com.android.application") version "8.9.1" apply false
+    id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")

Різницю між файлами не показано, бо вона завелика
+ 4 - 0
assets/flags/ad.svg


+ 6 - 0
assets/flags/ae.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ae" viewBox="0 0 640 480">
+  <path fill="#00732f" d="M0 0h640v160H0z"/>
+  <path fill="#fff" d="M0 160h640v160H0z"/>
+  <path fill="#000001" d="M0 320h640v160H0z"/>
+  <path fill="red" d="M0 0h220v480H0z"/>
+</svg>

+ 81 - 0
assets/flags/af.svg

@@ -0,0 +1,81 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-af" viewBox="0 0 640 480">
+  <g fill-rule="evenodd" stroke-width="1pt">
+    <path fill="#000001" d="M0 0h640v480H0z"/>
+    <path fill="#090" d="M426.7 0H640v480H426.7z"/>
+    <path fill="#bf0000" d="M213.3 0h213.4v480H213.3z"/>
+  </g>
+  <g fill="#fff" fill-rule="evenodd" stroke="#bd6b00" stroke-width=".5" transform="translate(1 27.3)scale(1.06346)">
+    <path d="M319.5 225.8h8.3c0 3.2 2 6.6 4.5 8.5h-16c2.5-2.2 3.2-5 3.2-8.5z"/>
+    <path stroke="none" d="m266.7 178.5 4.6 5 57 .2 4.6-5-14.6-.3-7-5h-23l-6.6 5.1z"/>
+    <path d="M290 172.7h19.7c2.6-1.4 3.5-5.9 3.5-8.4 0-7.4-5.3-11-10.5-11.2q-1.4-.1-1.9-1.3c-.5-1.6-.4-2.7-1-2.6-.4 0-.3 1-.7 2.4q-.6 1.3-2 1.6c-6.4.3-10.6 5-10.5 11.1.1 4 .6 6.4 3.4 8.4z"/>
+    <path stroke="none" d="M257.7 242.8H342l-7.5-6.1h-69.4z"/>
+    <path d="m296.4 219.7 1.5 4.6h3.5l-2.8-4.6zm-2 4.6 1 4.6h4l-1.5-4.6zm7 0 2.8 4.6h5.9l-4.6-4.6zm-34.5 10.4c3.1-2.9 5.1-5.3 5.1-8.8h7.6q0 3 1.8 3h7.7v-4.5h-5.6v-24.7c-.2-8.8 10.6-13.8 15-13.8h-26.3v-.8h55.3v.8H301c7.9 0 15.5 7.5 15.6 13.8v7h-1l-.1-6.9c0-6.9-8.7-13.3-15.7-13.1-6 .1-15.4 5.9-15.3 13v2.2l14.3.1-.1 2.5 2.2 1.4 4.5 1.4v3.8l3.2.9v3.7l3.8 1.7v3.8l2.5 1.5-.1 3.9 3.3 2.3h-7.8l4.9 5.5h-7.3l-3.6-5.5h-4.7l2.1 5.4h-5l-1.3-5.4h-6.2v5.8H267zm22.2-15v4.6h5.3l-1-4.6H289z"/>
+    <path fill="none" d="M289.4 211.7h3.3v7.6h-3.3z"/>
+    <path fill="none" d="M284.7 219.8h3.2v-5.6c0-2.4 2.2-4.9 3.2-5 1.2 0 2.9 2.3 3 4.8v5.8h3.4v-14.4h-12.8zm25.6 3.3h4v3.2h-4zm-2.4-5.3h4v3.1h-4zm-3.9-5.4h4v3.1h-4zm-3.3-4.5h4v3.1h-4z"/>
+    <path fill="none" d="m298 219.8 4.2.2 7.3 6.4v-3.8l-2.5-1.8v-3l-3.6-2v-3.3l-3.5-1.2V207l-1.7-1.5z"/>
+    <path d="M315.4 210.3h1v7.1h-1z"/>
+    <g id="af-a">
+      <path d="M257.3 186.5c-1.2-2-2.7 2.8-7.8 6.3-2.3 1.6-4 5.9-4 8.7v5.8c-.1 1.1-1.4 3.8-.5 4.5 2.2 1.6 5.1 5.4 6.4 6.7 1.2 1 2.2-5.3 3-8 1-3 .6-6.7 3.2-9.4 1.8-2 6.4-3.8 6-4.6z"/>
+      <path fill="#bf0000" d="M257 201.9a10 10 0 0 0-1.6-2.6 6 6 0 0 0-2.4-1.8 5 5 0 0 1-2.4-1.5l-.8-1.5v-2l-.3.3c-2.3 1.6-4 5.9-4 8.7v2.3q.2.8.6 1.3l1.1.8 2.7.7a7 7 0 0 1 2.6 2 11 11 0 0 1 1.8 2.6l.2-.8c.8-2.7.7-5.9 2.6-8.5z"/>
+      <path fill="none" d="M249.8 192.4c-.5 3.3 1.4 4.5 3.2 5.1 1.8.7 3.3 2.6 4 4.4m-11.7 1.5c.8 3 2.8 2.6 4.6 3.2s3.7 3 4.5 4.8"/>
+      <path d="m255.6 184.5 1-.6 17.7 29.9-1 .6z"/>
+      <path d="M257.5 183.3a2 2 0 1 1-4 0 2 2 0 1 1 4 0zm15.2-24h7.2v1.6h-7.2zm0 3.1h7.2v13.8h-7.2zm-.4-5h8c.2-2.7-2.5-5.6-4-5.6-1.6.1-4.1 3-4 5.6z"/>
+      <path fill="#bd6b00" stroke="none" d="M292.6 155.8c-1.5.6-2.7 2.3-3.4 4.3s-1 4.3-.6 6.1q.1 1 .5 1.5.3.5.6.5.5 0 .7-.3l.2-.8q-.2-3 .3-5.4a8 8 0 0 1 3-4.4q.4-.3.5-.7l-.3-.7q-.7-.5-1.5-.1m.2.4q.6-.2 1 .1l.1.2-.3.4a8 8 0 0 0-3.1 4.6 17 17 0 0 0-.3 5.6l-.2.6s0 .1-.2 0l-.4-.3-.4-1.2q-.4-2.9.7-6 1.1-2.9 3-4z"/>
+      <path fill="#bd6b00" stroke="none" d="M295.2 157.7q-2.3 1.2-3 4.2a14 14 0 0 0-.3 5.9q.5 2 1.6 2 .5.2.8-.3t.2-1q-.6-2.5-.3-5.1.4-2.6 2.2-4.1.5-.4.5-.8l-.2-.6q-.7-.5-1.5-.2m.2.5q.6-.2 1 0l.1.3-.3.4a7 7 0 0 0-2.4 4.4q-.4 2.8.2 5.2v.8l-.5.1c-.3 0-1-.5-1.2-1.7-.3-1.7-.2-3.9.3-5.7q.8-2.9 2.8-3.8"/>
+      <path d="M272.3 187.4h8v11h-8zm.5 17.4h7.7v2.4h-7.7zm-.2 4.1h8v8.7h-8zm-.6 10.5h8.7v4.9H272zm1.1-16.6h7l1.4-2.4h-9.6zm9.4-8.6.1-6h4.8a17 17 0 0 0-4.9 6z"/>
+      <path fill="none" d="M273.6 196.7c0 1.3 1.5.8 1.5.1v-5.6c0-1 2.4-.8 2.4-.1v6c0 1 1.7.9 1.6 0v-7c0-2.2-5.5-2.1-5.5-.1zm0 13.3h5.7v7h-5.7z"/>
+      <path d="M277.2 213h2v1h-2zm-3.5 0h2v1h-2zm2-3h1.5v3h-1.5zm0 4h1.5v3.1h-1.5zM244 139c.4 5.5-1.4 8.6-4.3 8.1-.8-3 1-5.1 4.3-8.1zm-6.5 12.3c-2.6-1.3-.7-11.5.3-15.8.7 5.5 2 13.3-.3 15.8z"/>
+      <path d="M238.4 151.8c4.4 1.5 8-3.2 9.1-8.7-3.6 5-9.5 5-9 8.7zm-3.3 5.1c-3.4-.9-1.4-11.7-.7-16 .7 4.5 3.1 14.5.7 16zm1.2-.3c.2-3.7 3.9-2.7 6.5-4.7-.5 2-2 5.2-6.5 4.7zm-4.2 5c-3.4-1-1.4-12.6-1.6-17.4 1 4.2 4.2 16.3 1.6 17.4zm1.6-.5c2.8.9 6.5-1 6.8-4.3-2.5 1.7-6.3.4-6.8 4.3z"/>
+      <path d="M229.5 166.7c-3.2.3-1.8-9.6-1.8-18.8 1.2 8.6 4.5 16.5 1.8 18.8z"/>
+      <path d="M230.7 166.3c2.2 1 6.1-.7 7.2-4.4-4 1.7-6.6 0-7.2 4.4zm25.6-22.2c-.6 4.9-2.6 7.7-5.5 7.2-.8-3 1.6-5 5.5-7.2zm-7.8 12.4c4.9.7 6.6-3 10-7.9-4.7 3.4-10.2 4-10 8z"/>
+      <path d="M247 156c-2.6-3.2 0-7.3 2-10.7-.4 5.1 1.3 8-2 10.7zm-1 5.3c-.4-3.2 5-3.9 7.4-5.6-.9 1.8-2 6.7-7.5 5.6z"/>
+      <path d="M244.8 161.3c-3.7-.4-2.2-6.7.5-10.1-1.1 4.8 2 8.1-.5 10.1z"/>
+      <path d="M242 166.6c-4.2-2-1.5-7.2 0-10.3-.6 4.1 2.8 7.2 0 10.2z"/>
+      <path d="M242.8 166c2.2 3 6.5-.8 7.4-5.2-3.7 3.1-6.5 2.6-7.4 5.3zm-9.6 20.3c-.4-4.3 2.8-12 .5-16.2-.3-.6.7-2.1 1.4-1.2 1 1.5 2 5.7 2.5 4.1s.5-4.6 2-5.2c1-.3 2.3-.6 1.9 1-.4 1.4-1.2 3.4-.3 3.5.5 0 2-2 3.3-3 1-.8 2.6.6 1 1.8-4.8 4-9.5 5.9-12.3 15.2zm-8.7 64.5c-.6 0-1.3-.3-.6.6 5.7 7 7.3 9 15.6 8 8.3-1.1 10.3-3.4 16.2-6.7a15 15 0 0 1 11.2-1c1.6.5 2.6.5 1.4-.7s-2.5-2.7-4-3.8a18 18 0 0 0-12.7-2.7c-6 1-11.1 4.9-17.2 6.4a25 25 0 0 1-9.9 0zm47.8 12.5c1 .2 1.7 2.2 2.3.9.8-2.3.2-4-.8-3.9-1.2.3-3.1 3-1.5 3z"/>
+      <path stroke="none" d="M220.6 183q-1.8-2 1-1.9c1.4 0 4.2 1 5.3.1 1-.7.5-3.7 1-5 .2-.9.7-2 2-.2 3.6 5.8 8 12.8 10 19.6 1 3.8 0 9.8-3.4 13.8 0-3.4-1.2-5.7-2.7-8.6-2-3.7-9.1-14-13.2-17.9z"/>
+      <path d="M235.5 213.4c4 0 4.7-5.3 4.7-6.8-2 .4-5.4 3.7-4.7 6.8zm34.5 51.9c2.8.6 2.7-6.2-.2-9.1 1.3 4.4-2 8.4.1 9zm-1.2-.1c.2 3.2-8-.4-10-3 4.8 2.1 9.8.4 10 3zm-3.5-4.6c.3 3.1-7 .3-9.3-2.1 4.9 1.6 9-.5 9.3 2zm1.3.4c2.9.7 2.4-6.4-.4-8.8 1.4 4.7-1.8 8.1.4 8.8zm-3-4.3c2.9.7 1.2-5.4-.9-7.8.4 4.4-1 7.5 1 7.8zm-1.5 0c.3 3.2-5.4.8-7.6-2.3 4.8 1.5 7.3-.3 7.6 2.3zm-1.5-2.5c1.8-1.3-.1-4.8-3.7-4.6.4 2.1 1.6 5.9 3.7 4.6zm14 14.7c.1 3.2-8 1.6-10.6-1.8 5.2 1 10.3-.8 10.5 1.8zm-32.4-5.8c.3 3.2-8.6-.4-10.8-3.4 4.7 1.6 10.5.8 10.8 3.4zm5.4 1.3c1.9-1.3-1.9-4.7-5-5.5.4 2.1 3 6.8 5 5.6zm.6 2.3c.2 2.9-9.5 1.3-12-1.4 8.3 1.5 11.7-1.1 12 1.4z"/>
+      <path d="M252.8 268.6c1 2.7-8.3 2-11.6.5 5.3 0 10.8-2.4 11.6-.5z"/>
+      <path d="M257.1 270.6c1 2.4-7.6 2.4-11.8 1 5.6 0 10.8-3.4 11.8-1zm6.3 1.3c1.6 2.9-7.6 3.1-10.5 1.7 5.2-.7 9.2-4 10.5-1.7zm-10.7-4.9c-2.9 1.8-2.7-3.6-5-7.3 3.6 3.3 7 5.6 5 7.3z"/>
+      <path d="M257.9 269c-2.4 2.1-4.4-5.3-6.6-9.5 3.6 4 8.8 7.7 6.6 9.4zm6.8 2c-2 2.4-8-7-10.2-12 3.3 3.9 11.8 10 10.2 12zm-5.8 7.2c-1 3.6-16.2-3.4-18-7.1 8.8 4.6 18.2 3.6 18 7zm-48.7-73.8c-.4-.5-1.4 0-1.2 1.1.3 1.5 2.5 9.2 6.3 11.8 2.7 2 17 5.1 23.4 6.5q5.3 1 8.9 5.3a94 94 0 0 0-3-9.8c-1.2-3-4.4-6.2-7.8-6.3-6.1-.3-14.1-.8-20-3.3a16 16 0 0 1-6.7-5.3z"/>
+      <path d="M245.5 234.9c2 1.4 4.1-3.7 1.7-8.6-.1 4.7-3.8 6.3-1.7 8.6z"/>
+      <path d="M247.4 239.6c2.7.8 3.5-4 1.8-7.8.3 4.1-4.3 6.6-1.8 7.8z"/>
+      <path d="M249.5 243.4c2.6 1.3 3.5-3.6 1.7-7.1.2 4.5-3.7 5.9-1.7 7z"/>
+      <path d="M248.4 243.7c-1 3-7-2.7-8-5.8 3.7 3.7 8.7 3.2 8 5.7z"/>
+      <path d="M245.7 239c-1.2 3-8.7-5-10.4-8.7 3.7 3.7 11.2 6.5 10.4 8.6z"/>
+      <path d="M244.2 234.3c-1.2 3.5-9.3-5.8-11.7-9.1 4 3.6 12.6 6.6 11.7 9.1zm-.3-3.4c3-.6-.1-3-3.7-6.9-.1 4.1.5 7 3.7 6.9z"/>
+      <path d="M239 228.5c1.3-1.3-1.1-1.9-4.1-5.3-.5 2.3 2.8 6.5 4.2 5.3zm14 15.2c1.6 1 2.6-2.3.7-5.2-.5 3.2-2.1 4-.7 5.2zm-34.2-20.3c-3.3 2-8.6-6-10-9.3 2.9 3.8 10.6 7.2 10 9.3z"/>
+      <path d="M221.7 228c-1.9 2-7.7-3.5-9.7-6.3 3 2.7 10.5 3 9.7 6.3z"/>
+      <path d="M224.8 232.2c-.6 2.8-9-3.5-11-6.5 3.6 3.5 11.6 3.2 11 6.5z"/>
+      <path d="M223.5 235.3c-1.3 2.5-8.2-3.8-9.9-7 4.3 3.6 11 4.5 10 7zM220 223c2.1-2.3 1.2-3.4-.4-7-.8 3.7-2.1 5.2.4 7zm2.9 4.3c4 .2 0-4.6-1-8.7.4 4.6-1 8.3 1 8.7z"/>
+      <path d="M225.4 231.1c2.7-.6 2-4.5-.2-9.2.5 5.1-2.3 8 .2 9.2zm-1 7.7c-1 3-8.8-4-10-6.8 4 3.4 10.7 4.5 10 6.8z"/>
+      <path d="M229.1 243.6c-1.1 3-9.3-3.2-11.8-6.6 4.9 4 12.4 3.6 11.8 6.6z"/>
+      <path d="M233.9 248.5c-1.3 4.3-9.9-2.6-12.4-6 5.4 4.2 13 3 12.4 6zm-8-11c2.3 1.1 3.2-5.4 1.9-10.1 0 5-4.7 8.8-2 10z"/>
+      <path d="M229.8 242.7c2.8.8 2-6.3-.5-11-.3 4.7-2.3 9 .5 11zm5 4.9c3 .1 1-6.1-1.6-9.6.4 4.5-1 9 1.6 9.6zm-5.5 2.6c-1 1.6-3.2-1.3-7-3.5 3.4 1 7.4 2 7 3.5zm-1.8-52.7c3-2.2.7-6.2 0-10-1 3.6-3.4 8.4 0 10zm0 5.3c-4.5-.5-3.8-6.1-4-9.7 1.4 4.9 5 5.7 4 9.8zm.6-.7c3.7-.2 3.5-4.4 3.7-8.6-1.9 3.9-4 4.5-3.7 8.6z"/>
+      <path d="M228 207.3c-3 .3-4.4-2.6-5-7 2.7 4.1 5.1 2.8 5 7zm1-.3c3.7.5 3-3.8 3-7-1.2 3-4.2 4-3 7z"/>
+      <path d="M223.2 205.2c.3 2.8 2.1 7.6 5 6.5 1.1-3.4-2.6-4.1-5-6.5z"/>
+      <path d="M229 212c-1.2-2.4 3-3.7 3.8-6.9.5 4.6.1 7.6-3.8 7zm-11.9-29.2c2.3-2.4.3-6.4-.4-10.2-1 3.6-2.5 8.4.4 10.2zm0 4.6c-4 .5-5-7.7-5.5-11.3 1.4 4.9 6 7 5.5 11.4zm.8 0c2.8-1.5 2.2-4.7 3-7-1.8 2.9-3.6 3.3-3 7z"/>
+      <path d="M217 192.8c-4.1.3-6.6-8.8-6.8-12.4 1.3 4.9 7.4 7.5 6.9 12.4zm.9-.2c4-.9 3.5-3.5 2.9-7.6-1.3 4.2-3.5 3.3-2.9 7.6z"/>
+      <path d="M217 198c-4.6.8-4.3-6.6-8-11.9 3.2 4 9 9 8 11.9zm1-.3c3.6.2 4-5.1 3.8-7.3-.9 2.2-5 4.2-3.7 7.4z"/>
+      <path d="M209.8 192.3c1.7 5.7 4.2 11.4 7.2 11 1.5-3.3-2.9-3.7-7.2-11z"/>
+      <path d="M218.1 202.4c-1.2-2.5 3-3.7 3.8-6.9.5 4.6.1 7.6-3.8 6.9zm-7.1-3.6c2.5 5.1 3.6 11 7 10.1 1.3-4-3.8-4.8-7-10.1z"/>
+      <path d="M218.7 208c-1.5-2.8 2.7-3.7 3.8-7.4.5 4.8 0 8.3-3.8 7.3zm7.2-34.5c2.4.6 5-2.1 4.1-6.2-2.8.6-4 3.2-4.1 6.2zm-7.9-2.1c.2 1.2 1.7 1.3 1.2-.4a5 5 0 0 1 0-3.4 8 8 0 0 0 0-4.6c-.4-1-1.8-.4-1.2.4s.7 2.8.2 3.7q-.7 2.2-.2 4.3zm22.9 16c-1 1.3-2.9.4-1.4-1.5 1.2-1.5 3-2.8 3-4.4.2-2 1.3-5 2.4-6.1s2.4.4 1.2 1.2c-1.3.8-2.2 4.4-2.1 5.8-.1 2-2 3.5-3.1 5zm-3-2.3c-1 1.4-2.4.5-1.6-1.7.7-1.5.8-3.5 1.6-4.6 1.2-1.7 3-3.1 4.1-4.2 1.2-1 2 0 1 1a27 27 0 0 0-3.3 4c-1.4 2.2-.8 4-1.8 5.5zm-15.7-7.2c-.1 2 1.5 2.4 1.4-.4 0-3-2.2-5.8-1-10.3.8-2.2.8-6.3.4-8.4s-2-.8-1.3.9c.6 2-.1 5.6-.6 7.5-1.5 5.4 1.2 8 1 10.7zm4.3-11c-.2 1.9-1.8 2-1.3-.5q.6-2.9 0-5.3c-.6-2.1-.4-5.7 0-7.2.5-1.6 2-.7 1.4.5a10 10 0 0 0-.3 5.9c.6 2 .5 4.8.2 6.7zM210.9 204c.8.9 2 .3 1-1-1-1-.7-1.2-1.3-2.4-.6-1.4-.5-2.1-1.2-3-.7-1-1.6 0-1 .7.8 1 .6 1.6 1 2.5 1 1.5.7 2.3 1.5 3.2zm20.4 24.6a9 9 0 0 1 4.4 6.7 16 16 0 0 0 2 7.1c-2-.5-3-3.7-3.3-6.8-.3-3.2-2-4.5-3-7zm5.1 5.9c1.7 3.1 4 4.3 4.2 6.6.2 2.7.4 2.8 1.1 5.4-2-.5-2.5-.7-3-4.7-.3-2.8-2.6-4.7-2.3-7.3z"/>
+      <path stroke="none" d="M289 263.3c1 1.8 2 4.5 4 4 0-1.3-2.1-2.3-4-4m3 .6c3.7 1.6 7 1.2 7.5 3.6-3.6.4-5-1-7.6-3.6zm-16.1-12.7a14 14 0 0 1 5 7.7 29 29 0 0 0 3.6 7.8 13 13 0 0 1-5.3-7.4c-.7-3-1.6-5.3-3.3-8zm3.1 0c2.8 2.2 5.4 4.8 6.2 7.9.8 2.9 1.3 5.1 3.2 8-3-1.9-4.1-4.7-5-7.8-.7-3-2.5-5.2-4.4-8zm9.2 7.3a1 1 0 0 1 .7-1.2l2.6-.8c1-.3 1.6.4 1.6.9v2q0 .9-.7.9-1.2 0-2.4.7-1 .5-1.5-.5zm10.6 0q0-1-.6-1.2a5 5 0 0 0-2.4-.4q-1.3 0-1.1.6v2.1c0 .8 0 .8.4 1q1.3-.1 2.5.6.9.4 1.1-.6z"/>
+    </g>
+    <use xlink:href="#af-a" width="100%" height="100%" x="-600" transform="scale(-1 1)"/>
+    <g stroke="none">
+      <path d="M328.5 286.6q-.1 1.8 1 3.1a19 19 0 0 0-13.8 1.1c-1.8.8-4-1-1.9-2.7 3-2.3 9.7-1 14.7-1.5m-57.5 0a7 7 0 0 1-.4 3c4.4-1.7 9.1-.2 13.6 1.6 3 1.3 3.3-1 2.8-1.7a7 7 0 0 0-5-2.9zm3.8-21.7q-2-.7-4 1.4c-4.3 4.2-9.4 8.3-13.5 11.6-1.5 1.3-3 3.7 3.4 6 .3.2 5 2 8 2 1.3 0 1.3 1.8 1 2.3-.5 1-.1 1.4-1.1 2.3-1.1 1 0 2.1 1 1.3 3.6-3.2 9.6-1.1 15.3.7 1.4.4 3.8.3 3.8-1.6s1.5-3.4 2.4-3.5c2.4.4 14 .5 17.5.1 2-.3 2.2 2.9 3.3 4 .8.9 3.7 1.1 5.8.2 4-1.8 10-1.8 12.5 0 1 .7 1.9 0 1.3-.7-.8-1-.7-1.6-1.1-2.4-1-2-.2-2.4.8-2.5 11-1.5 14.6-5.2 11.2-8.3-4.4-3.8-9.2-7.7-13.4-12.2-1.2-1.2-2-1.7-4.3-.7a67 67 0 0 1-25.3 5.9 76 76 0 0 1-24.6-5.8z"/>
+      <path fill="#bd6b00" d="m326.6 265.5-1.6.4c-9 3.2-17.2 5.4-25.7 5.4-8.3 0-17-2.4-24.9-5.6a2 2 0 0 0-1.5 0q-.8.2-1.3.7a116 116 0 0 1-11.8 10.3c-.7.5-.6 1.8.5 2.2 8.3 3 16.4 8.5 39.6 8.3 23.5-.2 31.8-5.6 39.2-8.1q.8-.3 1.3-1l.1-.8-.6-.8c-4.3-3.5-8.8-6.3-11.8-10.4q-.5-.7-1.5-.5zm0 .5q.9 0 1.1.3c3 4.3 7.7 7 11.9 10.5l.4.7v.4q-.3.5-1 .7c-7.6 2.6-15.7 8-39 8.2-23.2.2-31.2-5.3-39.5-8.3-.8-.4-.7-1.2-.4-1.4q6.4-4.9 11.8-10.4l1.1-.6h1.2a68 68 0 0 0 25 5.6c8.7 0 17-2.2 26-5.3l1.5-.4z"/>
+      <path d="M269.7 114.6c0-1.4 2-1.5 1.8.4-.3 2.3 4.5 8.3 4.9 12 .3 2.5-1.5 4.6-3.2 6a7 7 0 0 1-6.8.5c-.9-.8-1.7-3.3-1-4.3.2-.3 1.3 3.7 3.7 3.7 3.3 0 6-2.5 6-4.7.2-3.8-5.3-9.8-5.4-13.6m9.5 9.4c.6-.4 1.4 1.3.8 1.7s-1.5-1.3-.8-1.8zm1.5-3.5c-.3.2-.8 0-.7-.2a12 12 0 0 1 3.6-3.3c.4-.2 1 .4.8.7a11 11 0 0 1-3.7 2.8m12.6-10c.3-.6 2.1-1.3 2.6-1.7.4-.5.6.4.4.7-.3.7-1.9 1.7-2.6 1.8q-.6-.1-.4-.7zm4.3.3a8 8 0 0 1 2.5-3.4c.5-.3 1.3 0 1.1.4a9 9 0 0 1-2.9 3.3c-.3.3-.8 0-.7-.3m-3.7 2.7q-.3.5.1.8 1 .3 2 0c.6-.4.3-2.9-.5-1.6-.6.8-1 .6-1.6.8m-7.3 5.6c-1.3-1 .4-2.4 1.7-1.4 2.7 2-4 9.8-7.6 13.4-.7.7-1.3-1-.4-1.9a34 34 0 0 0 6.7-7.6c.4-.5.7-1.6-.4-2.5m15.3-6.6c.1-1-1.6 0-1.6-1.3 0-.7 1.9-1.2 2.7-.4 1.3 1.4.3 3.7-2 3.9-1.8 0-5 2.7-4.5 3.2.5.7 5.4 1.1 8.3.7 1.8-.3 1.4 1.3-.4 1.5s-3.2 0-4.8.6c-2 .5-2.8 3-3.9 4-.2.2-.8-.8-.6-1.2.8-1.2 2-3 3.4-3.6.8-.3-2.4-.4-3.4-.7-.8-.2-.6-1.3-.3-1.9.4-.8 3.4-3.9 4.7-3.8 1.1 0 2.3-.3 2.4-1m5 .2 1.5-1.8c.3-.3.9 0 .8.8-.1.7-1 1.2-1.5 1.7-.5.3-1-.4-.7-.7zm6.5-2.3c.9 0 1 1.6.2 1.8-.6.2-1-1.7-.2-1.8m-2.1 5c0 1.5.7 1.4 2 1.3s2.4 0 2.4-1.2c0-1.3-.7-2.5-1-1.6-.1.8-.3 2.2-.8 1.6-.4-.5-.2-.6-1 .2-.5.5-.5-.2-.8-.6-.2-.3-.8.2-.8.4zm-9.2 7.2c-.3 1.9 0 4.5.9 4.5 1.2 0 3.6-4 4.8-6.2.7-1.2 1.8-1.4 1.3-.1-.7 1.9-.6 6 0 7.2.4.6 3-.6 3.4-1.5.8-1.7.1-4.8.4-6.7.1-1.2 1.3-1.5 1.2-.3l-.1 7.5c0 1 2.9 2.4 3.3-.6.2-1.8 1.2-3.7 0-5.7-.8-1.3 1.1-1.2 2.1.6.7 1.2-.6 3.2-.5 4.7 0 2.4-1.8 3.8-3.1 3.8-1.2 0-2-1.5-3-1.5s-2.2 1.7-3 1.6c-3.6-.2-1.7-5.3-2.8-5.4-1.2 0-2.5 5-4 4.9-1.4-.2-3-4.2-2.3-5.8.5-1.6 1.5-2 1.4-1m16.9-8c-1.7-1 0-3.7.9-2.8 1.6 2 3.2 6.5 4.4 6.9.7.2.6-3.4 1.1-5 .4-1.3 1.8-.9 1.6.7-.1.5-2 6.4-1.8 6.6a47 47 0 0 1 3.3 7.8c.3 1.2-1.1.4-1.3.2-.9-1.4-2.4-6.5-2.4-6.2l-1.7 7.7c-.2 1-1.7.8-1.3-1 .3-1.4 2.3-8.3 2.2-8.6a17 17 0 0 0-5-6.3"/>
+      <path d="M322 131.2c-.4 0-1.2 1 1.2 1.5 3.1.6 6.6-.5 7.6-3.6 1.3-3.7 2-7.2 2.7-8.5.8-1.5 1.8-1.4 1-3.6-.5-1.7-1.5-1.2-1.7-.3-.5 2.3-2.6 10-3.3 11.3q-1.8 3.8-7.5 3.2"/>
+      <path d="M328.4 119c-.4-.7-1.2 0-1 .7a1 1 0 0 0 1.2 1c.7 0 2.2.1 2.2-1 0-.8-.7-1.5-1.1-.6q-.8 1.1-1.3 0zm.7-3c-.2.2 0 1.1.3 1a7 7 0 0 0 3.3-.8c.2-.2.1-.7-.2-.7-1 0-2.6 0-3.4.5m8.8 2.3c.8-1.2 2.8-1.3 2 .4l-6.3 12.3c-.8 1.4-1.4.7-.8-.4.7-1.4 4.9-12 5.1-12.3"/>
+      <path d="M330.2 133c-.2-.8-1.5-2-1.3.2.2 3.8 5.5 2.6 7 1.3s.3 4.3 2.2 4.9c1 .3 3-1.1 4-2.4 2.7-3.5 4.5-8.6 7-12 1-1.4-.5-2.4-1-1.3-2.4 3.8-5.2 11.6-8.3 13.6-2.5 1.6-1.7-2-1.8-3.2-.1-.8-1.1-2-2.4-.9a6 6 0 0 1-3.7 1.2c-.7 0-1.4 0-1.7-1.4"/>
+      <path d="M339.6 126c0-.3-1.1-.4-1 .7 0 .8 1 1 1.1 1 1.5-1.2-.3-.6-.1-1.8zm-2.3 4.4c-.3 0-.6 1 .2 1.1l3.9-.2c.4 0 .6-.9-.4-.8-1.2 0-2.7-.3-3.7 0zm-62-16.6c.5 0 1.6 1.4 1.5 1.9 0 .2-1.2 0-1.5-.3s-.2-1.6 0-1.6m-5.3 10.4c-1 .6.2 1.7 1 1.2 2.8-1.9 7-3.8 8-7.5.3-1.2 1.4-3.1 2.5-3.5 1-.5 2.6 1.9 3.6 0 .6-1 2.7.7 3.2-.4.6-1.3.3-2 .3-3.4 0-.8-.7-1-1.2.3l-.1 1.6q-.4.4-1 .2c-.2-.2 0-.7-.6-1q-.4-.1-.8.2c-.7 1.3-1 2.5-2.1 1-.9-1-1.4-3.1-2-.3-.2 1-1.7 2.4-2.6 2.4-1.1 0-.8-3-3.2-2.5-1.3.3-1.2 2.7-1 3.5.3 1.3 4 .4 3.7 1.2-.6 2.7-4.4 5.4-7.7 7m-22.7 13.2c-.1.5.5 1.7 1.1 1.8.6 0 1-1.3.8-1.8-.2-.3-1.8-.3-1.9 0m3.3 4.9c-.4-.4-1.6.7-.6 1.5.5.5 2.5 1.1 3 .2.8-1.2-.7-5.5 0-6 .5-.5 2.8 2.8 4 3 2.7.4 2-4.6 5-4.2 1.9.2 2.1-2.2 1.8-3.8-.2-1.5-2.6-3.6-3.7-4.6-1.4-1.2-2.1 1-1.2 1.6 1.2 1 3.3 2.9 3.6 4.1.1.6-1.4 1.8-2 1.5-1.4-.8-2.6-4-3.8-4.7-.4-.2-1.4.3-1 1.3.6 1.1 3 2.7 3.1 3.9.1 1-1 3.2-1.8 3.2s-3-2.7-3.7-4c-.4-.5-1.5-.5-1.7.4a22 22 0 0 0 .5 5.5c.2 1.6-.9 1.7-1.5 1.1m-4-8.6c-.4.4.8 1.2 1 1 .4-.4 2.1-2.3 1.8-3-.3-.6-2.6-2-3-1.3-.7 1.1 2.2 1.7 1.7 2zm4.1-8.4s.8 2.5 1.4 1.4c.4-.7-1.4-1.4-1.4-1.4m1.2 4c-.2 0-1 .7-.5 1 .8.4 2.9.8 2.4-.7-.3-.9 3.2 0 2.3-2.4a4 4 0 0 0-1.7-1.7c-.4 0-1.5.5-.8.9.5.2 2 1.1 1.5 1.7-.7.6-1.1-.3-1.9-.1-.4 0-.1 1.2-.4 1.5 0 .2-.7-.4-.9-.3zm5.5-9.5a4 4 0 0 0-1.2 2q.1.5.5.5a3 3 0 0 0 1.2-1.9c0-.3-.2-.8-.5-.6m2.8-.3c-.8-1 1-2.6 1.7-.5.5 1.3 5.5 7.9 6.5 10.1.8 1.5 0 2.1-.9 1-2.5-3.2-4.6-7.2-7.3-10.6m5.2.1c.9-1 2.7-3 2.2-4s-1.5-1-1.7-.7c-1 1.3.8 1 .5 1.4q-.8 1.3-1.3 2.6c-.1.3.1.9.3.7m77.8 3.2c-.7-.5.6-3 1.5-2 2.3 2.7 3.4 11.6 4.1 18.3 0 0-1 .9-1 .7 0-3.5-1.5-14.4-4.6-17m-53.1-8.6c-.8-1.8 1.1-2.4 1.4-1.2 1.3 5.8 4.5 10.2 7 14.1.7 1.2 0 2-1.7.8-1.2-.8-2.5-3.9-3-4-1.2-.2-3.8 5-9.1 3.5-1.4-.4-1.3-4.5-1.4-6.3 0-.9 1-1 1 0 0 1.7 0 5.2 2.1 5.4 1.8 0 5.6-2.4 6.4-4.4s-1.9-5.9-2.7-8z"/>
+      <path d="M344.6 138.4c.4-1.2 6.1-10.8 6.9-12.9.4-1 2 1.8.4 3.3-1.4 1.2-5.5 8-6.3 10.4-.4 1-1.4.5-1-.8"/>
+      <path d="M354.3 129.3c1-4 3.6.6 1.3 2.8-3.4 3.4-4.5 9.9-10 10.9-1.4.3-4-.7-4.8-1.3-.3-.2.2-1.6 1.1-.9 1.3 1 4.1 1.3 5.6.1a25 25 0 0 0 6.8-11.6m-57 12.7c-.3.3-1 .3-1.1.7-.3 1.4 0 2.2-.3 3.6s-1.3 1.4-1.2.3c0-1.4 1.3-3.5.4-3.6-.6-.1-1-.9-.4-1.3q1.5-.9 2.4-.4.5.3.2.7"/>
+      <path d="M296.5 140c-1.4 1.4-2.8 1.9-4.1 3.5-.6.6-.5 1.5-.9 2.4-.3.9-1.4 1-1.7.9-.5-.4-.4-2-1-1.2s-.9 2-1.7 2-2-1.5-1.3-1.5c2.3-.3 2.2-2 3-2.2 1-.1 1 1.5 1.7 1.2.4-.2.7-2.1 1.2-2.6 1.5-1.6 2.7-2.4 4.3-3.6.7-.6 1.3.5.5 1.2zm5.3 5c-1.2.2-1 1.7-.6 1.8.5.3 1.4.4 1.7-1.3.2-.7.3 3.5 1.8 1.9 1-1 3.1.2 4-1 .7-.9 1-1.5.4-2.7-.2-.3-1-.2-1 .7s-.5 1.7-1.3 1.6c-.4-.1.2-1.9-.2-2.4h-.7c-.3.4.3 2.2-.6 2.4-1.2.2-.6-1.2-1-1.4-1.7-.8-1.8.2-2.5.3zm9-3c.9-.2.6-.2 2-1.3.5-.4.6.8.5 1.3 0 .7-1 .2-1.3.9-.4.9-.2 3-.4 3.8 0 .4-.8.4-.8 0-.2-1 .1-2 0-3.3 0-.4-.5-1.1 0-1.3zm-5-2.5-.2 2.3c0 .5 1 .2 1 .1 0-.8.2-2 0-2.3q-.5-.3-.8-.1"/>
+      <path d="m299.5 130.2-1.4 5.6-2-3.8v3.9l-4.4-5.2 1.5 5.6-4-3.4 2.2 3.8-7-4.5 4.4 5.2-5.6-2.8 4 3.4-9-3.4 8.7 4.3a29 29 0 0 1 12.6-2.6q7.5.1 12.5 2.6l8.8-4.3-9 3.4 4-3.4-5.5 2.8 4.3-5.2-7 4.5 2.2-3.8-4 3.3 1.5-5.5-4.3 5.2V132l-2 3.8z"/>
+    </g>
+  </g>
+  <path fill="#fff" d="m311.3 295-.3 2.6h-.4l-.1-1.8-.5-1.6-.5-1.3-1-1.4.8-2.2a7 7 0 0 1 1.5 2.4 9 9 0 0 1 .5 3.2m7-4.2q0 1-.5 1.5-.3.5-1.3.7l.4 1.5v2l-.1 1.3h-.4l-.1-1.3-.2-1-.4-1-.7-1.4-1-1.7.6-2 1 1q.4.3 1 .3 1.2 0 1.2-1.3h.4v1.4m6.4 4.8-.5 2.1q-.6 0-.8-.7l-.4-1.3-.1-1.7-1 .2a2 2 0 0 1-1.3-.4 1 1 0 0 1-.5-1q0-1.4.7-2.3.8-1 1.5-1.1.7 0 1 .4l.3.9v2q0 1.3.3 1.9 0 .4.8 1m-2-3.5q0-.9-.8-.8l-.6.1q-.2.1-.2.3 0 .5 1 .5zm8.7 3-.3 2.6q-.8-.5-1.4-2l-1.3-4.1-1.8 5.5-.8.7v-2.5q.9-1 1-1.5l.8-1.7.5-2.7h.4l.9 2.7q.3 1 .9 1.6l1 1.4"/>
+  <path fill="#bf0000" d="M350.8 319.4q.6.6.7 1.2l.4 1.6-.8.1-1-1.5-1.1-1.2-1.7-1.5-2-1.7q-.6-.3-.6-.5l-.3-.8-.2-1.6 2.7 2.2 2.5 2.2zm-9.5-5.8-.2 2H338l.3-2zm8.4 8.9-7.6 2.3-1.3-2 6.5-2-.7-.8-.9-.6a1 1 0 0 1-.4 1l-1 .6a3 3 0 0 1-1.8 0 2 2 0 0 1-1.3-.7 4 4 0 0 1-.7-2.2q0-1.5.9-1.8 1.1-.3 3 .7a8 8 0 0 1 3 2.4zm-5.8-4-.8-.3h-.6l-.5.3v.6l.5.2h.6l.4-.3zm-8-1.6-.5 2-3.2-.3.5-2zm7.5 7.7-1.7.4H340l-1.5-.4q-.5.8-1.5 1.2l-1.6.6-1.2.3-1-2 1.1-.3 1.3-.4.9-.5-1-.5h-.9l-.2.3h-.5q-.8-1.2-.3-2c.5-.8.9-.8 2-1a7 7 0 0 1 2.6-.2q1.2.1 1.5.9.2.3.2.7l-.4 1.2h1.1l1.7-.3zm-8 1.8-1.6.3a3 3 0 0 1-2.2-.4 6 6 0 0 1-1.7-2.6l-.8-2.2a2 2 0 0 0-.8-1l-.9-.5.6-2.1q.9.4 1.4 1l1 1.7.5 1.5 1.1 2.2q.5.4 1 .3l1.7-.2zm-7-7.5-1 1.9-3-.7 1-1.9zm1.8 8.4-7.5.7-.4-2 6.2-.7-.6-.8-1-.6.5-2q1 .6 1.6 1.3.5.8.8 2.1zm-6 1-2.2-.2-1.7-.5-1.3.4h-3.7l-1.2-.3q-.4-.3-.8-1a4 4 0 0 1-1.5 1l-1.7.1h-1.7l.2-2.1h1.7q1.2.1 2.1-.4a2 2 0 0 0 1.3-1.8l.7.1-.1 1.3q0 .4.3.7.4.3 1 .3h1.5q1.5 0 2-.2.9-.2 1-1.1l.1-.4s.3 0 .5-.2l.5-.2v.7l-.3 1.1 2 .5q.1-.3-.1-.7l-.3-.6.1-.3.3-.2 1-.9.5 1v1zm-11.3-8.7-2 1.3-1.3-.9-1.4 1-1.9-1 1.8-1.3 1.5.8 1.5-1 1.8 1m-3 8.2-7.3-1.2.8-2 6.2 1q0-.6-.2-1l-.5-.8 1.6-1.7q.6.8.7 1.6t-.5 2.1zm-6.1-1-1.6-.3q-1.3-.3-1.5-1.2-.3-.9.8-2.8l1.2-2q.4-.7.3-1.2l-.3-.7 2.2-1.6q.4.8.3 1.4 0 .8-.7 1.8l-.8 1.4a6 6 0 0 0-.9 2.2q0 .6.5.7l1.6.4zm-3.8-8-2.5 1.1-1.8-1.7 2.6-1zm-1 6.6-1.6 1.4-1.7.6-2.4-.1-2.8-.7a8 8 0 0 1-3.4-2q-.9-1.2 0-2.2a7 7 0 0 1 2-1.6q1.1-.7 3.8-1.6l.4.5-2.8 1.2q-.8.4-1.3 1t.2 1.6a11 11 0 0 0 6.3 2.2q1.8 0 2.3-.7.4-.4.5-1l.2-1.6 2.5-1.5-.1 1.5a4 4 0 0 1-1 1.6z"/>
+</svg>

+ 14 - 0
assets/flags/ag.svg

@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ag" viewBox="0 0 640 480">
+  <defs>
+    <clipPath id="ag-a">
+      <path fill-opacity=".7" d="M-79.7 0H603v512H-79.7z"/>
+    </clipPath>
+  </defs>
+  <g fill-rule="evenodd" clip-path="url(#ag-a)" transform="translate(74.7)scale(.9375)">
+    <path fill="#fff" d="M-79.7 0H603v512H-79.7z"/>
+    <path fill="#000001" d="M-79.6 0H603v204.8H-79.7z"/>
+    <path fill="#0072c6" d="M21.3 203.2h480v112h-480z"/>
+    <path fill="#ce1126" d="M603 .1V512H261.6L603 0zM-79.7.1V512h341.3L-79.7 0z"/>
+    <path fill="#fcd116" d="M440.4 203.3 364 184l64.9-49-79.7 11.4 41-69.5-70.7 41L332.3 37l-47.9 63.8-19.3-74-21.7 76.3-47.8-65 13.7 83.2L138.5 78l41 69.5-77.4-12.5 63.8 47.8L86 203.3z"/>
+  </g>
+</svg>

+ 29 - 0
assets/flags/ai.svg

@@ -0,0 +1,29 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-ai" viewBox="0 0 640 480">
+  <defs>
+    <path id="ai-b" fill="#f90" d="M271 87c1.5 3.6 6.5 7.6 7.8 9.6-1.7 2-2 1.8-1.8 5.4 3-3.1 3-3.5 5-3 4.2 4.2.8 13.3-2.8 15.3-3.4 2.1-2.8 0-8 2.6 2.3 2 5.1-.3 7.4.3 1.2 1.5-.6 4.1.4 6.7 2-.2 1.8-4.3 2.2-5.8 1.5-5.4 10.4-9.1 10.8-14.1 1.9-.9 3.7-.3 6 1-1.1-4.6-4.9-4.6-5.9-6-2.4-3.7-4.5-7.8-9.6-9-3.8-.7-3.5.3-6-1.4-1.6-1.2-6.3-3.4-5.5-1.6"/>
+  </defs>
+  <clipPath id="ai-a">
+    <path d="M0 0v120h373.3v120H320zm320 0H160v280H0v-40z"/>
+  </clipPath>
+  <path fill="#012169" d="M0 0h640v480H0z"/>
+  <path stroke="#fff" stroke-width="50" d="m0 0 320 240m0-240L0 240"/>
+  <path stroke="#c8102e" stroke-width="30" d="m0 0 320 240m0-240L0 240" clip-path="url(#ai-a)"/>
+  <path stroke="#fff" stroke-width="75" d="M160 0v280M0 120h373.3"/>
+  <path stroke="#c8102e" stroke-width="50" d="M160 0v280M0 120h373.3"/>
+  <path fill="#012169" d="M0 240h320V0h106.7v320H0z"/>
+  <path fill="#fff" d="M424 191.8c0 90.4 9.7 121.5 29.3 142.5a179 179 0 0 0 35 30 180 180 0 0 0 35-30c19.5-21 29.3-52.1 29.3-142.5-14.2 6.5-22.3 9.7-34 9.5a78 78 0 0 1-30.3-9.5 78 78 0 0 1-30.3 9.5c-11.7.2-19.8-3-34-9.5"/>
+  <g transform="matrix(1.96 0 0 2.002 -40.8 62.9)">
+    <use xlink:href="#ai-b"/>
+    <circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
+  </g>
+  <g transform="matrix(-.916 -1.77 1.733 -.935 563.4 829)">
+    <use xlink:href="#ai-b"/>
+    <circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
+  </g>
+  <g transform="matrix(-1.01 1.716 -1.68 -1.031 925.4 -103.2)">
+    <use xlink:href="#ai-b"/>
+    <circle cx="281.3" cy="91.1" r=".8" fill="#fff" fill-rule="evenodd"/>
+  </g>
+  <path fill="#9cf" d="M440 315.1a78 78 0 0 0 13.3 19.2 179 179 0 0 0 35 30 180 180 0 0 0 35-30 78 78 0 0 0 13.2-19.2z"/>
+  <path fill="#fdc301" d="M421.2 188.2c0 94.2 10.2 126.6 30.6 148.5a187 187 0 0 0 36.5 31.1 186 186 0 0 0 36.4-31.1c20.4-21.9 30.6-54.3 30.6-148.5-14.8 6.8-23.3 10.1-35.5 10-11-.3-22.6-5.7-31.5-10-9 4.3-20.6 9.7-31.5 10-12.3.1-20.7-3.2-35.6-10m4 5c14 6.5 22 9.6 33.5 9.4a76 76 0 0 0 29.6-9.4c8.4 4 19.3 9.2 29.6 9.4 11.5.2 19.4-3 33.4-9.4 0 89-9.6 119.6-28.8 140.2a176 176 0 0 1-34.2 29.4 176 176 0 0 1-34.3-29.4c-19.2-20.6-28.7-51.3-28.7-140.2z"/>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 2 - 0
assets/flags/al.svg


+ 5 - 0
assets/flags/am.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-am" viewBox="0 0 640 480">
+  <path fill="#d90012" d="M0 0h640v160H0z"/>
+  <path fill="#0033a0" d="M0 160h640v160H0z"/>
+  <path fill="#f2a800" d="M0 320h640v160H0z"/>
+</svg>

+ 13 - 0
assets/flags/ao.svg

@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ao" viewBox="0 0 640 480">
+  <g fill-rule="evenodd" stroke-width="1pt">
+    <path fill="red" d="M0 0h640v243.6H0z"/>
+    <path fill="#000001" d="M0 236.4h640V480H0z"/>
+  </g>
+  <path fill="#ffec00" fill-rule="evenodd" d="M228.7 148.2c165.2 43.3 59 255.6-71.3 167.2l-8.8 13.6c76.7 54.6 152.6 10.6 174-46.4 22.2-58.8-7.6-141.5-92.6-150z"/>
+  <path fill="#ffec00" fill-rule="evenodd" d="m170 330.8 21.7 10.1-10.2 21.8-21.7-10.2zm149-99.5h24v24h-24zm-11.7-38.9 22.3-8.6 8.7 22.3-22.3 8.7zm-26-29.1 17.1-16.9 16.9 17-17 16.9zm-26.2-39.8 22.4 8.4-8.5 22.4-22.4-8.4zM316 270l22.3 8.9-9 22.2-22.2-8.9zm-69.9 70 22-9.3 9.5 22-22 9.4zm-39.5 2.8h24v24h-24zm41.3-116-20.3-15-20.3 14.6 8-23-20.3-15h24.5l8.5-22.6 7.8 22.7 24.7-.3-19.6 15.3z"/>
+  <path fill="#fe0" fill-rule="evenodd" d="M336 346.4c-1.2.4-6.2 12.4-9.7 18.2l3.7 1c13.6 4.8 20.4 9.2 26.2 17.5a8 8 0 0 0 10.2.7s2.8-1 6.4-5c3-4.5 2.2-8-1.4-11.1-11-8-22.9-14-35.4-21.3"/>
+  <path fill="#000001" fill-rule="evenodd" d="M365.3 372.8a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.6 0zm-21.4-13.6a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.7 0m10.9 7a4.3 4.3 0 1 1-8.7 0 4.3 4.3 0 0 1 8.7 0"/>
+  <path fill="#fe0" fill-rule="evenodd" d="M324.5 363.7c-42.6-24.3-87.3-50.5-130-74.8-18.7-11.7-19.6-33.4-7-49.9 1.2-2.3 2.8-1.8 3.4-.5 1.5 8 6 16.3 11.4 21.5A5288 5288 0 0 1 334 345.6c-3.4 5.8-6 12.3-9.5 18z"/>
+  <path fill="#ffec00" fill-rule="evenodd" d="m297.2 305.5 17.8 16-16 17.8-17.8-16z"/>
+  <path fill="none" stroke="#000" stroke-width="3" d="m331.5 348.8-125-75.5m109.6 58.1L274 304.1m18.2 42.7L249.3 322"/>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 2 - 0
assets/flags/aq.svg


+ 32 - 0
assets/flags/ar.svg

@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-ar" viewBox="0 0 640 480">
+  <path fill="#74acdf" d="M0 0h640v480H0z"/>
+  <path fill="#fff" d="M0 160h640v160H0z"/>
+  <g id="ar-c" transform="translate(-64)scale(.96)">
+    <path id="ar-a" fill="#f6b40e" stroke="#85340a" stroke-width="1.1" d="m396.8 251.3 28.5 62s.5 1.2 1.3.9c.8-.4.3-1.6.3-1.6l-23.7-64m-.7 24.2c-.4 9.4 5.4 14.6 4.7 23s3.8 13.2 5 16.5c1 3.3-1.2 5.2-.3 5.7 1 .5 3-2.1 2.4-6.8s-4.2-6-3.4-16.3-4.2-12.7-3-22"/>
+    <use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(22.5 400 250)"/>
+    <use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(45 400 250)"/>
+    <use xlink:href="#ar-a" width="100%" height="100%" transform="rotate(67.5 400 250)"/>
+    <path id="ar-b" fill="#85340a" d="M404.3 274.4c.5 9 5.6 13 4.6 21.3 2.2-6.5-3.1-11.6-2.8-21.2m-7.7-23.8 19.5 42.6-16.3-43.9"/>
+    <use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(22.5 400 250)"/>
+    <use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(45 400 250)"/>
+    <use xlink:href="#ar-b" width="100%" height="100%" transform="rotate(67.5 400 250)"/>
+  </g>
+  <use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(90 320 240)"/>
+  <use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(180 320 240)"/>
+  <use xlink:href="#ar-c" width="100%" height="100%" transform="rotate(-90 320 240)"/>
+  <circle cx="320" cy="240" r="26.7" fill="#f6b40e" stroke="#85340a" stroke-width="1.4"/>
+  <path id="ar-h" fill="#843511" stroke-width="1" d="M329 234.3c-1.7 0-3.5.8-4.5 2.4 2 1.9 6.6 2 9.7-.2a7 7 0 0 0-5.1-2.2zm0 .4c1.8 0 3.5.8 3.7 1.6-2 2.3-5.3 2-7.4.4q1.6-2 3.8-2z"/>
+  <use xlink:href="#ar-d" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
+  <use xlink:href="#ar-e" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
+  <use xlink:href="#ar-f" width="100%" height="100%" transform="translate(18.1)"/>
+  <use xlink:href="#ar-g" width="100%" height="100%" transform="matrix(-1 0 0 1 640.2 0)"/>
+  <path fill="#85340a" d="M316 243.7a1.8 1.8 0 1 0 1.8 2.9 4 4 0 0 0 2.2.6h.2q1 0 2.3-.6.5.7 1.5.7a1.8 1.8 0 0 0 .3-3.6q.8.3.8 1.2a1.2 1.2 0 0 1-2.4 0 3 3 0 0 1-2.6 1.7 3 3 0 0 1-2.5-1.7q-.1 1.1-1.3 1.2-1-.1-1.2-1.2c-.2-1.1.3-1 .8-1.2zm2 5.4c-2.1 0-3 2-4.8 3.1 1-.4 1.8-1.2 3.3-2s2.6.2 3.5.2 2-1 3.5-.2l3.3 2c-1.9-1.2-2.7-3-4.8-3q-.7 0-2 .6z"/>
+  <path fill="#85340a" d="M317.2 251.6q-1.1 0-3.4.6c3.7-.8 4.5.5 6.2.5 1.6 0 2.5-1.3 6.1-.5-4-1.2-4.9-.4-6.1-.4-.8 0-1.4-.3-2.8-.2"/>
+  <path fill="#85340a" d="M314 252.2h-.8c4.3.5 2.3 3 6.8 3s2.5-2.5 6.8-3c-4.5-.4-3.1 2.3-6.8 2.3-3.5 0-2.4-2.3-6-2.3"/>
+  <path fill="#85340a" d="M323.7 258.9a3.7 3.7 0 0 0-7.4 0 3.8 3.8 0 0 1 7.4 0"/>
+  <path id="ar-e" fill="#85340a" stroke-width="1" d="M303.4 234.3c4.7-4.1 10.7-4.8 14-1.7a8 8 0 0 1 1.5 3.4q.6 3.6-2.1 7.5l.8.4q2.4-4.7 1.6-9.4l-.6-2.3c-4.5-3.7-10.7-4-15.2 2z"/>
+  <path id="ar-d" fill="#85340a" stroke-width="1" d="M310.8 233c2.7 0 3.3.6 4.5 1.7 1.2 1 1.9.8 2 1 .3.2 0 .8-.3.6q-.7-.2-2.5-1.6c-1.8-1.4-2.5-1-3.7-1-3.7 0-5.7 3-6.1 2.8-.5-.2 2-3.5 6.1-3.5"/>
+  <use xlink:href="#ar-h" width="100%" height="100%" transform="translate(-18.4)"/>
+  <circle id="ar-f" cx="310.9" cy="236.3" r="1.8" fill="#85340a" stroke-width="1"/>
+  <path id="ar-g" fill="#85340a" stroke-width="1" d="M305.9 237.5c3.5 2.7 7 2.5 9 1.3 2-1.3 2-1.7 1.6-1.7s-.8.4-2.4 1.3c-1.7.8-4.1.8-8.2-.9"/>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 62 - 0
assets/flags/arab.svg


Різницю між файлами не показано, бо вона завелика
+ 4 - 0
assets/flags/as.svg


+ 13 - 0
assets/flags/asean.svg

@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" id="flag-icons-asean" viewBox="0 0 640 480">
+  <path fill="#0039a6" d="M0 0h640v480H0z"/>
+  <circle cx="320" cy="240" r="144" fill="#fff"/>
+  <circle cx="320" cy="240" r="137.3" fill="#ed2939"/>
+  <use xlink:href="#asean-a" transform="matrix(-1 0 0 1 640 0)"/>
+  <g id="asean-a" fill="#f9e300">
+    <path d="M357 240c24-14.4 35-43.2 35-72h-11v1c0 9.6-1.5 44.6-27.9 71a106 106 0 0 1 27.9 71v1h11c0-28.8-11.5-57.6-35-72"/>
+    <path d="M377.6 169v-1h-11.5v1.4c0 9.6-2 43.2-20.7 70.6 19.2 27.4 20.7 61 20.7 70.6v1.4h11.5v-1c0-9.6-2.4-44.6-27.8-71a106 106 0 0 0 27.8-71"/>
+    <path d="m341.1 240 1-1a130 130 0 0 0 20.1-69.6V168h-10.5v2c0 10-1.5 42.2-14.4 70a182 182 0 0 1 14.4 70v2h10.5v-1.4c0-9.6-1-39.9-20.1-69.6"/>
+    <path d="M333.4 240a178 178 0 0 0 14.4-72h-11v3.4c0 12-1 41.2-7.2 68.6a336 336 0 0 1 7.2 68.6v3.4h10.6v-2c0-10-1-43.1-13.5-69.5"/>
+    <path d="M325.8 240a331 331 0 0 0 6.7-68.6V168h-10.6v144h10.6v-3.4c0-11.5 0-41.2-6.7-68.1"/>
+  </g>
+</svg>

+ 4 - 0
assets/flags/at.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-at" viewBox="0 0 640 480">
+  <path fill="#fff" d="M0 160h640v160H0z"/>
+  <path fill="#c8102e" d="M0 0h640v160H0zm0 320h640v160H0z"/>
+</svg>

+ 8 - 0
assets/flags/au.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-au" viewBox="0 0 640 480">
+  <path fill="#00008B" d="M0 0h640v480H0z"/>
+  <path fill="#fff" d="m37.5 0 122 90.5L281 0h39v31l-120 89.5 120 89V240h-40l-120-89.5L40.5 240H0v-30l119.5-89L0 32V0z"/>
+  <path fill="red" d="M212 140.5 320 220v20l-135.5-99.5zm-92 10 3 17.5-96 72H0zM320 0v1.5l-124.5 94 1-22L295 0zM0 0l119.5 88h-30L0 21z"/>
+  <path fill="#fff" d="M120.5 0v240h80V0zM0 80v80h320V80z"/>
+  <path fill="red" d="M0 96.5v48h320v-48zM136.5 0v240h48V0z"/>
+  <path fill="#fff" d="m527 396.7-20.5 2.6 2.2 20.5-14.8-14.4-14.7 14.5 2-20.5-20.5-2.4 17.3-11.2-10.9-17.5 19.6 6.5 6.9-19.5 7.1 19.4 19.5-6.7-10.7 17.6zm-3.7-117.2 2.7-13-9.8-9 13.2-1.5 5.5-12.1 5.5 12.1 13.2 1.5-9.8 9 2.7 13-11.6-6.6zm-104.1-60-20.3 2.2 1.8 20.3-14.4-14.5-14.8 14.1 2.4-20.3-20.2-2.7 17.3-10.8-10.5-17.5 19.3 6.8L387 178l6.7 19.3 19.4-6.3-10.9 17.3 17.1 11.2ZM623 186.7l-20.9 2.7 2.3 20.9-15.1-14.7-15 14.8 2.1-21-20.9-2.4 17.7-11.5-11.1-17.9 20 6.7 7-19.8 7.2 19.8 19.9-6.9-11 18zm-96.1-83.5-20.7 2.3 1.9 20.8-14.7-14.8-15.1 14.4 2.4-20.7-20.7-2.8 17.7-11L467 73.5l19.7 6.9 7.3-19.5 6.8 19.7 19.8-6.5-11.1 17.6zM234 385.7l-45.8 5.4 4.6 45.9-32.8-32.4-33 32.2 4.9-45.9-45.8-5.8 38.9-24.8-24-39.4 43.6 15 15.8-43.4 15.5 43.5 43.7-14.7-24.3 39.2 38.8 25.1Z"/>
+</svg>

+ 186 - 0
assets/flags/aw.svg

@@ -0,0 +1,186 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-aw" viewBox="0 0 640 480">
+  <defs>
+    <clipPath id="aw-a">
+      <path fill-opacity=".7" d="M0 0h288v216H0z"/>
+    </clipPath>
+  </defs>
+  <g clip-path="url(#aw-a)" transform="scale(2.2222)">
+    <path fill="#39c" d="M0 0v216h324V0z"/>
+    <path fill="#ff0" d="M0 144v12h324v-12zm0 24v12h324v-12z"/>
+  </g>
+  <path fill="#9cc" d="m142.7 28 2.9 3zm-3 6 3 3zm5.9 0 3 3z"/>
+  <path fill="#ccf" d="m139.7 37 3 2.9-3-3m5.9 0 3 3z"/>
+  <path fill="#6cc" d="m136.7 42.8 3 3z"/>
+  <path fill="#c66" d="m142.7 42.8 2.9 3z"/>
+  <path fill="#6cc" d="m148.6 42.8 2.9 3z"/>
+  <path fill="#ccf" d="m136.7 45.8 3 3zm11.9 0 2.9 3z"/>
+  <path fill="#fcc" d="m139.7 48.7 3 3zm5.9 0 3 3z"/>
+  <path fill="#6cc" d="m133.8 51.7 3 3z"/>
+  <path fill="#c00" stroke="#fff" stroke-width="3.7" d="m142.2 34-20.7 78.5L42.8 134l78.4 20.5 21 78.4 20.9-78.4 78.4-21-78.4-20.9-21-78.4z"/>
+  <path fill="#6cc" d="m151.5 51.7 3 3z"/>
+  <path fill="#9cf" d="m133.8 54.6 3 3zm17.7 0 3 3z"/>
+  <path fill="#fcc" d="m136.7 57.6 3 3zm11.9 0 2.9 3z"/>
+  <path fill="#69c" d="m130.8 60.5 3 3z"/>
+  <path fill="#c33" d="m137.7 62.5 1 2zm11.8 0 1 2z"/>
+  <path fill="#69c" d="m154.5 60.5 3 3z"/>
+  <path fill="#9cf" d="m130.8 63.5 3 3zm23.7 0 3 3z"/>
+  <path fill="#fcc" d="m133.8 66.4 3 3zm17.7 0 3 3z"/>
+  <path fill="#69c" d="m127.9 69.4 3 3zm29.5 0 3 3z"/>
+  <path fill="#9cc" d="m127.9 72.3 3 3zm29.5 0 3 3z"/>
+  <path fill="#cff" d="m127.9 75.3 3 3zm29.5 0 3 3z"/>
+  <path fill="#69c" d="m125 78.3 2.9 2.9z"/>
+  <path fill="#fcc" d="m130.8 78.3 3 2.9zm23.7 0 3 3z"/>
+  <path fill="#69c" d="m160.4 78.3 3 2.9z"/>
+  <path fill="#9cc" d="m125 81.2 2.9 3z"/>
+  <path fill="#c33" d="m131.8 83.2 1 2zm23.6 0 1 2z"/>
+  <path fill="#9cc" d="m160.4 81.2 3 3z"/>
+  <path fill="#cff" d="m125 84.2 2.9 3zm35.5 0 3 3z"/>
+  <path fill="#fcc" d="m127.9 87.1 3 3zm29.5 0 3 3z"/>
+  <path fill="#9cc" d="m122 90 3 3z"/>
+  <path fill="#c33" d="m128.9 92 1 2zm29.5 0 1 2z"/>
+  <path fill="#9cc" d="m163.3 90 3 3z"/>
+  <path fill="#ccf" d="m122 93 3 3zm41.3 0 3 3z"/>
+  <path fill="#fcc" d="m125 96 2.9 3zm35.5 0 3 3z"/>
+  <path fill="#9cc" d="m119 99 3 2.9z"/>
+  <path fill="#c33" d="m126 100.9.9 2zm35.4 0 1 2z"/>
+  <path fill="#9cc" d="m166.3 99 3 2.9z"/>
+  <path fill="#ccf" d="m119 101.9 3 3zm47.3 0 3 3z"/>
+  <path fill="#fcc" d="m122 104.8 3 3zm41.3 0 3 3z"/>
+  <path fill="#9cc" d="m116 107.8 3 3z"/>
+  <path fill="#c33" d="m122 107.8 3 3zm41.3 0 3 3z"/>
+  <path fill="#9cc" d="m169.2 107.8 3 3zm-62 3 3 2.9z"/>
+  <path fill="#ccf" d="m110.2 110.7 3 3zm65 0 2.9 3z"/>
+  <path fill="#9cc" d="m178 110.7 3 3zm-79.6 3 3 3z"/>
+  <path fill="#ccf" d="m101.3 113.7 3 3z"/>
+  <path fill="#fcc" d="m113.1 113.7 3 3z"/>
+  <path fill="#c33" d="m116 113.7 3 3zm53.2 0 3 3z"/>
+  <path fill="#fcc" d="m172.2 113.7 3 3z"/>
+  <path fill="#ccf" d="m184 113.7 3 3z"/>
+  <path fill="#9cc" d="m187 113.7 2.9 3z"/>
+  <path fill="#69c" d="m86.6 116.6 3 3z"/>
+  <path fill="#9cc" d="m89.5 116.6 3 3z"/>
+  <path fill="#cff" d="m92.5 116.6 3 3z"/>
+  <path fill="#fcc" d="m104.3 116.6 3 3z"/>
+  <path fill="#c33" d="m109.2 117.6 2 1zm67.9 0 2 1z"/>
+  <path fill="#fcc" d="m181 116.6 3 3z"/>
+  <path fill="#cff" d="m192.8 116.6 3 3z"/>
+  <path fill="#9cc" d="m195.8 116.6 3 3z"/>
+  <path fill="#69c" d="m198.7 116.6 3 3zm-121 3 3 3z"/>
+  <path fill="#9cc" d="m80.7 119.6 3 3z"/>
+  <path fill="#cff" d="m83.6 119.6 3 3z"/>
+  <path fill="#fcc" d="m95.4 119.6 3 3z"/>
+  <path fill="#c33" d="m100.3 120.6 2 1zm85.6 0 2 1z"/>
+  <path fill="#fcc" d="m189.9 119.6 3 3z"/>
+  <path fill="#cff" d="m201.7 119.6 3 3z"/>
+  <path fill="#9cc" d="m204.6 119.6 3 3z"/>
+  <path fill="#69c" d="m207.6 119.6 3 3zm-138.8 3 3 2.9z"/>
+  <path fill="#9cf" d="m71.8 122.5 3 3z"/>
+  <path fill="#fcc" d="m86.6 122.5 3 3z"/>
+  <path fill="#c33" d="m91.5 123.5 2 1zm103.3 0 2 1z"/>
+  <path fill="#fcc" d="m198.7 122.5 3 3z"/>
+  <path fill="#9cf" d="m213.5 122.5 3 3z"/>
+  <path fill="#69c" d="m216.4 122.5 3 3z"/>
+  <path fill="#6cc" d="m60 125.5 3 3z"/>
+  <path fill="#9cf" d="m63 125.5 2.9 3z"/>
+  <path fill="#fcc" d="m74.8 125.5 2.9 3zm135.8 0 2.9 3z"/>
+  <path fill="#9cf" d="m222.3 125.5 3 3z"/>
+  <path fill="#6cc" d="m225.3 125.5 3 3zm-174.2 3 3 2.9z"/>
+  <path fill="#ccf" d="m54 128.4 3 3z"/>
+  <path fill="#fcc" d="m65.9 128.4 3 3z"/>
+  <path fill="#c33" d="m70.8 129.4 2 1zm144.7 0 2 1z"/>
+  <path fill="#fcc" d="m219.4 128.4 3 3z"/>
+  <path fill="#ccf" d="m231.2 128.4 3 3z"/>
+  <path fill="#6cc" d="m234.2 128.4 3 3z"/>
+  <path fill="#9cc" d="m42.3 131.4 3 3z"/>
+  <path fill="#ccf" d="m45.2 131.4 3 3z"/>
+  <path fill="#fcc" d="m57 131.4 3 3zm171.3 0 3 3z"/>
+  <path fill="#ccf" d="m240 131.4 3 3z"/>
+  <path fill="#9cc" d="m243 131.4 3 3zm-206.6 3 3 2.9z"/>
+  <path fill="#c66" d="m51.1 134.3 3 3zm183 0 3 3z"/>
+  <path fill="#9cc" d="m249 134.3 2.9 3zm-206.6 3 3 3z"/>
+  <path fill="#ccf" d="m45.2 137.3 3 3z"/>
+  <path fill="#fcc" d="m57 137.3 3 3zm171.3 0 3 3z"/>
+  <path fill="#ccf" d="m240 137.3 3 3z"/>
+  <path fill="#9cc" d="m243 137.3 3 3z"/>
+  <path fill="#6cc" d="m51.1 140.3 3 2.9z"/>
+  <path fill="#ccf" d="m54 140.3 3 2.9z"/>
+  <path fill="#fcc" d="m65.9 140.3 3 2.9z"/>
+  <path fill="#c33" d="m70.8 141.2 2 1zm144.7 0 2 1z"/>
+  <path fill="#fcc" d="m219.4 140.3 3 2.9z"/>
+  <path fill="#ccf" d="m231.2 140.3 3 2.9z"/>
+  <path fill="#6cc" d="m234.2 140.3 3 2.9zm-174.2 3 3 3z"/>
+  <path fill="#9cf" d="m63 143.2 2.9 3z"/>
+  <path fill="#fcc" d="m74.8 143.2 2.9 3zm135.8 0 2.9 3z"/>
+  <path fill="#9cf" d="m222.3 143.2 3 3z"/>
+  <path fill="#6cc" d="m225.3 143.2 3 3z"/>
+  <path fill="#69c" d="m68.8 146.2 3 2.9z"/>
+  <path fill="#9cf" d="m71.8 146.2 3 2.9z"/>
+  <path fill="#fcc" d="m86.6 146.2 3 2.9z"/>
+  <path fill="#c33" d="m91.5 147.1 2 1zm103.3 0 2 1z"/>
+  <path fill="#fcc" d="m198.7 146.2 3 2.9z"/>
+  <path fill="#9cf" d="m213.5 146.2 3 2.9z"/>
+  <path fill="#69c" d="m216.4 146.2 3 2.9zm-138.7 3 3 3z"/>
+  <path fill="#9cc" d="m80.7 149.1 3 3z"/>
+  <path fill="#cff" d="m83.6 149.1 3 3z"/>
+  <path fill="#fcc" d="m95.4 149.1 3 3z"/>
+  <path fill="#c33" d="m100.3 150 2 1zm85.6 0 2 1z"/>
+  <path fill="#fcc" d="m189.9 149.1 3 3z"/>
+  <path fill="#cff" d="m201.7 149.1 3 3z"/>
+  <path fill="#9cc" d="m204.6 149.1 3 3z"/>
+  <path fill="#69c" d="m207.6 149.1 3 3zm-121 3 2.9 2.9z"/>
+  <path fill="#9cc" d="m89.5 152 3 3z"/>
+  <path fill="#cff" d="m92.5 152 3 3z"/>
+  <path fill="#fcc" d="m104.3 152 3 3z"/>
+  <path fill="#c33" d="m109.2 153 2 1zm67.9 0 2 1z"/>
+  <path fill="#fcc" d="m181 152 3 3z"/>
+  <path fill="#cff" d="m192.8 152 3 3z"/>
+  <path fill="#9cc" d="m195.8 152 3 3z"/>
+  <path fill="#69c" d="m198.7 152 3 3z"/>
+  <path fill="#9cc" d="m98.4 155 3 3z"/>
+  <path fill="#ccf" d="m101.3 155 3 3z"/>
+  <path fill="#fcc" d="m113.1 155 3 3z"/>
+  <path fill="#c33" d="m116 155 3 3zm53.2 0 3 3z"/>
+  <path fill="#fcc" d="m172.2 155 3 3z"/>
+  <path fill="#ccf" d="m184 155 3 3z"/>
+  <path fill="#9cc" d="m187 155 2.9 3zm-79.7 3 3 3z"/>
+  <path fill="#ccf" d="m110.2 158 3 3zm65 0 2.9 3z"/>
+  <path fill="#9cc" d="m178 158 3 3zm-62 3 3 2.9z"/>
+  <path fill="#c33" d="m122 161 3 2.9zm41.3 0 3 3z"/>
+  <path fill="#9cc" d="m169.2 161 3 2.9z"/>
+  <path fill="#fcc" d="m122 163.9 3 3zm41.3 0 3 3z"/>
+  <path fill="#ccf" d="m119 166.8 3 3z"/>
+  <path fill="#c33" d="m126 168.8.9 2zm35.4 0 1 2z"/>
+  <path fill="#ccf" d="m166.3 166.8 3 3z"/>
+  <path fill="#9cc" d="m119 169.8 3 3zm47.3 0 3 3z"/>
+  <path fill="#fcc" d="m125 172.7 2.9 3zm35.5 0 3 3z"/>
+  <path fill="#ccf" d="m122 175.7 3 3z"/>
+  <path fill="#c33" d="m128.9 177.6 1 2zm29.5 0 1 2z"/>
+  <path fill="#ccf" d="m163.3 175.7 3 3z"/>
+  <path fill="#9cc" d="m122 178.6 3 3zm41.3 0 3 3z"/>
+  <path fill="#fcc" d="m127.9 181.6 3 3zm29.5 0 3 3z"/>
+  <path fill="#cff" d="m125 184.5 2.9 3z"/>
+  <path fill="#c33" d="m131.8 186.5 1 2zm23.6 0 1 2z"/>
+  <path fill="#cff" d="m160.4 184.5 3 3z"/>
+  <path fill="#9cc" d="m125 187.5 2.9 3zm35.5 0 3 3z"/>
+  <path fill="#69c" d="m125 190.4 2.9 3z"/>
+  <path fill="#fcc" d="m130.8 190.4 3 3zm23.7 0 3 3z"/>
+  <path fill="#69c" d="m160.4 190.4 3 3z"/>
+  <path fill="#cff" d="m127.9 193.4 3 3zm29.5 0 3 3z"/>
+  <path fill="#9cc" d="m127.9 196.3 3 3zm29.5 0 3 3z"/>
+  <path fill="#69c" d="m127.9 199.3 3 3zm29.5 0 3 3z"/>
+  <path fill="#fcc" d="m133.8 202.2 3 3zm17.7 0 3 3z"/>
+  <path fill="#9cf" d="m130.8 205.2 3 3z"/>
+  <path fill="#c33" d="m137.7 207.2 1 2zm11.8 0 1 2z"/>
+  <path fill="#9cf" d="m154.5 205.2 3 3z"/>
+  <path fill="#69c" d="m130.8 208.2 3 2.9zm23.7 0 3 3z"/>
+  <path fill="#fcc" d="m136.7 211.1 3 3zm11.9 0 2.9 3z"/>
+  <path fill="#9cf" d="m133.8 214 3 3zm17.7 0 3 3z"/>
+  <path fill="#6cc" d="m133.8 217 3 3zm17.7 0 3 3z"/>
+  <path fill="#fcc" d="m139.7 220 3 3zm5.9 0 3 3z"/>
+  <path fill="#ccf" d="m136.7 222.9 3 3zm11.9 0 2.9 3z"/>
+  <path fill="#6cc" d="m136.7 225.9 3 3z"/>
+  <path fill="#c66" d="m142.7 225.9 2.9 3z"/>
+  <path fill="#6cc" d="m148.6 225.9 2.9 3z"/>
+  <path fill="#ccf" d="m139.7 231.8 3 3zm5.9 0 3 3z"/>
+  <path fill="#9cc" d="m139.7 234.7 3 3zm5.9 0 3 3zm-3 6 3 2.9z"/>
+</svg>

+ 18 - 0
assets/flags/ax.svg

@@ -0,0 +1,18 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ax" viewBox="0 0 640 480">
+  <defs>
+    <clipPath id="ax-a">
+      <path fill-opacity=".7" d="M106.3 0h1133.3v850H106.3z"/>
+    </clipPath>
+  </defs>
+  <g clip-path="url(#ax-a)" transform="matrix(.56472 0 0 .56482 -60 -.1)">
+    <path fill="#0053a5" d="M0 0h1300v850H0z"/>
+    <g fill="#ffce00">
+      <path d="M400 0h250v850H400z"/>
+      <path d="M0 300h1300v250H0z"/>
+    </g>
+    <g fill="#d21034">
+      <path d="M475 0h100v850H475z"/>
+      <path d="M0 375h1300v100H0z"/>
+    </g>
+  </g>
+</svg>

+ 8 - 0
assets/flags/az.svg

@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-az" viewBox="0 0 640 480">
+  <path fill="#3f9c35" d="M.1 0h640v480H.1z"/>
+  <path fill="#ed2939" d="M.1 0h640v320H.1z"/>
+  <path fill="#00b9e4" d="M.1 0h640v160H.1z"/>
+  <circle cx="304" cy="240" r="72" fill="#fff"/>
+  <circle cx="320" cy="240" r="60" fill="#ed2939"/>
+  <path fill="#fff" d="m384 200 7.7 21.5 20.6-9.8-9.8 20.7L424 240l-21.5 7.7 9.8 20.6-20.6-9.8L384 280l-7.7-21.5-20.6 9.8 9.8-20.6L344 240l21.5-7.7-9.8-20.6 20.6 9.8z"/>
+</svg>

+ 12 - 0
assets/flags/ba.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ba" viewBox="0 0 640 480">
+  <defs>
+    <clipPath id="ba-a">
+      <path fill-opacity=".7" d="M-85.3 0h682.6v512H-85.3z"/>
+    </clipPath>
+  </defs>
+  <g fill-rule="evenodd" clip-path="url(#ba-a)" transform="translate(80)scale(.9375)">
+    <path fill="#009" d="M-85.3 0h682.6v512H-85.3z"/>
+    <path fill="#FC0" d="m56.5 0 511 512.3V.3z"/>
+    <path fill="#FFF" d="M439.9 481.5 412 461.2l-28.6 20.2 10.8-33.2-28.2-20.5h35l10.8-33.2 10.7 33.3h35l-28 20.7zm81.3 10.4-35-.1-10.7-33.3-10.8 33.2h-35l28.2 20.5-10.8 33.2 28.6-20.2 28 20.3-10.5-33zM365.6 384.7l28-20.7-35-.1-10.7-33.2-10.8 33.2-35-.1 28.2 20.5-10.8 33.3 28.6-20.3 28 20.4zm-64.3-64.5 28-20.6-35-.1-10.7-33.3-10.9 33.2h-34.9l28.2 20.5-10.8 33.2 28.6-20.2 27.9 20.3zm-63.7-63.6 28-20.7h-35L220 202.5l-10.8 33.2h-35l28.2 20.4-10.8 33.3 28.6-20.3 28 20.4-10.5-33zm-64.4-64.3 28-20.6-35-.1-10.7-33.3-10.9 33.2h-34.9L138 192l-10.8 33.2 28.6-20.2 27.9 20.3-10.4-33zm-63.6-63.9 27.9-20.7h-35L91.9 74.3 81 107.6H46L74.4 128l-10.9 33.2L92.1 141l27.8 20.4zm-64-64 27.9-20.7h-35L27.9 10.3 17 43.6h-35L10.4 64l-11 33.3L28.1 77l27.8 20.4zm-64-64L9.4-20.3h-35l-10.7-33.3L-47-20.4h-35L-53.7 0l-10.8 33.2L-35.9 13l27.8 20.4z"/>
+  </g>
+</svg>

+ 6 - 0
assets/flags/bb.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-bb" viewBox="0 0 640 480">
+  <path fill="#00267f" d="M0 0h640v480H0z"/>
+  <path fill="#ffc726" d="M213.3 0h213.4v480H213.3z"/>
+  <path id="bb-a" fill="#000001" d="M319.8 135.5c-7 19-14 38.6-29.2 53.7 4.7-1.6 13-3 18.2-2.8v79.5l-22.4 3.3c-.8 0-1-1.3-1-3-2.2-24.7-8-45.5-14.8-67-.5-2.9-9-14-2.4-12 .8 0 9.5 3.6 8.2 1.9a85 85 0 0 0-46.4-24c-1.5-.3-2.4.5-1 2.2 22.4 34.6 41.3 75.5 41.1 124 8.8 0 30-5.2 38.7-5.2v56.1H320l2.5-156.7z"/>
+  <use xlink:href="#bb-a" width="100%" height="100%" transform="matrix(-1 0 0 1 639.5 0)"/>
+</svg>

+ 4 - 0
assets/flags/bd.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-bd" viewBox="0 0 640 480">
+  <path fill="#006a4e" d="M0 0h640v480H0z"/>
+  <circle cx="280" cy="240" r="160" fill="#f42a41"/>
+</svg>

Деякі файли не було показано, через те що забагато файлів було змінено