diff --git a/.github/workflows/flutter_web_build.yml b/.github/workflows/flutter_web_build.yml
index 8c64d2d..42cc147 100644
--- a/.github/workflows/flutter_web_build.yml
+++ b/.github/workflows/flutter_web_build.yml
@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.24.3'
+ flutter-version: '3.32.8'
channel: 'stable'
- name: Build web
diff --git a/.gitignore b/.gitignore
index 29a3a50..ff69a8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
+.build/
.buildlog/
.history
.svn/
+.swiftpm/
migrate_working_dir/
# IntelliJ related
@@ -41,3 +43,8 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
+/android/app/.cxx/
+
+/macos/DerivedData/
+
+/dist
\ No newline at end of file
diff --git a/README.md b/README.md
index 1f787ae..099328d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Hosts Editor 是一个使用 Flutter 开发的应用程序,旨在简化 Linux
## 特性
-- **跨平台支持**:虽然目前只支持 Linux、Windows、Web 系统,但未来计划扩展到其他操作系统。
+- **跨平台支持**:虽然目前只支持 Linux、Windows、MacOS、Web 系统,但未来计划扩展到其他操作系统。
- **直观的用户界面**:使用 Flutter 构建,提供流畅的用户体验。
- **实时预览**:在编辑 hosts 文件时,实时查看更改效果。
- **安全性**:确保对 hosts 文件的修改是安全的,避免不必要的错误。
@@ -35,4 +35,8 @@ hosts /etc/hosts
### 测试是否配置成功
-
\ No newline at end of file
+
+
+### 导入导出
+
+
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 53ed703..b1fe309 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -5,10 +5,18 @@ plugins {
id "dev.flutter.flutter-gradle-plugin"
}
+def keystoreProperties = new Properties()
+def keystorePropertiesFile = rootProject.file('key.properties')
+if (keystorePropertiesFile.exists()) {
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+}
+
android {
namespace = "top.webb_l.hosts"
compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+// ndkVersion = flutter.ndkVersion
+
+ ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -19,6 +27,15 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
+ signingConfigs {
+ release {
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+ storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
+ storePassword keystoreProperties['storePassword']
+ }
+ }
+
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "top.webb_l.hosts"
@@ -32,9 +49,7 @@ android {
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.debug
+ signingConfig = signingConfigs.release
}
}
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3d19912..b5cc9c6 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,45 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:label="hosts">
+
+
+
+
+
+
-
+ to determine the Window background behind the Flutter UI.
+ -->
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
+
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/android/app/src/main/java/top/webb_l/hosts/HostsVpnService.kt b/android/app/src/main/java/top/webb_l/hosts/HostsVpnService.kt
new file mode 100644
index 0000000..cb60a37
--- /dev/null
+++ b/android/app/src/main/java/top/webb_l/hosts/HostsVpnService.kt
@@ -0,0 +1,57 @@
+package top.webb_l.hosts
+
+import android.content.Intent
+import android.net.VpnService
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import kotlin.concurrent.thread
+
+class HostsVpnService : VpnService() {
+
+ private var vpnInterface: ParcelFileDescriptor? = null
+
+ override fun onDestroy() {
+ super.onDestroy()
+ // 清理资源
+ vpnInterface?.close()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // 启动 VPN
+ startVpn()
+ return START_STICKY
+ }
+
+ private fun startVpn() {
+ // 配置 VPN
+ val builder = Builder()
+ builder.setSession("MyVPN")
+ .addAddress("10.0.0.2", 24) // VPN 地址
+ .addRoute("0.0.0.0", 0) // 路由
+
+ vpnInterface = builder.establish()
+
+ // 这里是处理网络流量的逻辑
+ // 你需要实现读取数据包、解析 DNS 请求、根据 hosts 文件进行重定向等
+ Log.e("TAG", "run: ${vpnInterface}")
+ val inputStream = FileInputStream(vpnInterface!!.fileDescriptor)
+ val outputStream = FileOutputStream(vpnInterface!!.fileDescriptor)
+
+ val buffer = ByteArray(32767)
+ thread {
+ while (true) {
+ // 读取数据包
+ val length = inputStream.read(buffer)
+ if (length > 0) {
+ // 处理数据包
+ // 这里可以添加 DNS 解析和 hosts 文件的处理逻辑
+ Log.e("TAG", "startVpn: ${String(buffer, 0, length)}", )
+ // 将数据包写回输出流
+ outputStream.write(buffer, 0, length)
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/top/webb_l/hosts/MainActivity.kt b/android/app/src/main/kotlin/top/webb_l/hosts/MainActivity.kt
index f262e6f..f505603 100644
--- a/android/app/src/main/kotlin/top/webb_l/hosts/MainActivity.kt
+++ b/android/app/src/main/kotlin/top/webb_l/hosts/MainActivity.kt
@@ -1,5 +1,29 @@
package top.webb_l.hosts
+import android.content.Intent
+import android.net.VpnService
+import android.util.Log
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
-class MainActivity: FlutterActivity()
+class MainActivity : FlutterActivity() {
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ val intent = VpnService.prepare(this)
+ startService(Intent(this, HostsVpnService::class.java))
+ Log.e("TAG", "configureFlutterEngine: ", )
+// if (intent != null) {
+// startActivityForResult(intent, 0)
+// } else {
+// onActivityResult(0, RESULT_OK, null)
+// }
+ super.configureFlutterEngine(flutterEngine)
+ }
+
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode == RESULT_OK) {
+ startService(Intent(this, HostsVpnService::class.java))
+ }
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+}
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..c442385 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..295851a 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..c203237 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..3369c4e 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..a8ff1eb 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/build.gradle b/android/build.gradle
index d2ffbff..69776d8 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,3 +1,12 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
+ }
+}
allprojects {
repositories {
google()
diff --git a/android/gradle.properties b/android/gradle.properties
index 2597170..db24e8b 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,3 +1,6 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index e1ca574..09523c0 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
index 536165d..a4fea4d 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "7.3.0" apply false
- id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+ id "com.android.application" version '8.7.2' apply false
+ id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}
include ":app"
diff --git a/image/1.png b/image/1.png
index b56dc2b..0a15fc6 100644
Binary files a/image/1.png and b/image/1.png differ
diff --git a/image/2.png b/image/2.png
index a588f10..bac2ecc 100644
Binary files a/image/2.png and b/image/2.png differ
diff --git a/image/3.png b/image/3.png
index e8edb65..c674531 100644
Binary files a/image/3.png and b/image/3.png differ
diff --git a/image/4.png b/image/4.png
index 22f2129..d04e496 100644
Binary files a/image/4.png and b/image/4.png differ
diff --git a/image/7.png b/image/7.png
new file mode 100644
index 0000000..ace01ea
Binary files /dev/null and b/image/7.png differ
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 84e7b66..bfc2bd9 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..d0d98aa 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
+{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc9ada4..62f508e 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 7353c41..79e5e96 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 797d452..b5d85b8 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6ed2d93..a956c53 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 4cd7b00..02dd760 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index fe73094..ff52e06 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 321773c..3cb987f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 797d452..b5d85b8 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 502f463..8b4535f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 0ec3034..25417e9 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..44bd318
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..25547c3
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..0aea464
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..ed12be9
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 0ec3034..25417e9 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e9f5fea..aceff94 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..62bb10d
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..f613590
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 84ac32a..7e86d99 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 8953cba..b0ac857 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0467bf1..d4010ba 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/l10n.yaml b/l10n.yaml
index 4e6692e..9fcf641 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -1,3 +1,4 @@
arb-dir: lib/l10n
-template-arb-file: app_en.arb
-output-localization-file: app_localizations.dart
\ No newline at end of file
+template-arb-file: app_zh.arb
+output-localization-file: app_localizations.dart
+output-class: AppLocalizations
diff --git a/lib/app.dart b/lib/app.dart
new file mode 100644
index 0000000..ab3fe9c
--- /dev/null
+++ b/lib/app.dart
@@ -0,0 +1,112 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:hosts/home/view/home_page.dart';
+import 'package:hosts/l10n/app_localizations.dart';
+import 'package:hosts/model/global_settings.dart';
+import 'package:hosts/model/simple_host_file.dart';
+import 'package:hosts/server/server_manager.dart';
+import 'package:hosts/theme.dart';
+import 'package:hosts/util/file_manager.dart';
+import 'package:hosts/util/settings_manager.dart';
+
+class HostsApp extends MaterialApp {
+ final String filePath;
+
+ HostsApp(this.filePath, {super.key})
+ : super(
+ onGenerateTitle: (context) => AppLocalizations.of(context)!.app_name,
+ // locale: Locale("en"),
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ theme: ThemeData(
+ brightness: Brightness.light,
+ colorScheme: MaterialTheme.lightScheme(),
+ useMaterial3: true,
+ ),
+ darkTheme: ThemeData(
+ brightness: Brightness.dark,
+ colorScheme: MaterialTheme.darkScheme(),
+ useMaterial3: true,
+ ),
+ themeMode: ThemeMode.system,
+ home: _platformSpecificWidget(filePath),
+ );
+}
+
+Widget _platformSpecificWidget(String filePath) {
+ if (filePath.isNotEmpty) {
+ GlobalSettings().filePath = filePath;
+ }
+ if (GlobalSettings().filePath != null) {
+ return SimpleHomePage();
+ } else {
+ return FutureBuilder(
+ future: _initializeApp(),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(child: CircularProgressIndicator());
+ } else {
+ return const HomePage();
+ }
+ },
+ );
+ }
+}
+
+Future _initializeApp() async {
+ SettingsManager settingsManager = SettingsManager();
+ FileManager fileManager = FileManager();
+
+ bool firstOpenApp = await settingsManager.getBool(settingKeyFirstOpenApp);
+ if (!firstOpenApp) {
+ const String fileName = "system";
+ await fileManager.createHosts(fileName);
+ await settingsManager.setList(settingKeyHostConfigs,
+ [SimpleHostFile(fileName: fileName, remark: "")]);
+ await settingsManager.setString(settingKeyUseHostFile, fileName);
+ File(FileManager.systemHostFilePath)
+ .copy(await fileManager.getHostsFilePath(fileName));
+ settingsManager.setBool(settingKeyFirstOpenApp, true);
+ }
+
+ // 异步启动服务器,不阻塞应用初始化
+ _startServerInBackground(settingsManager);
+}
+
+/// 在后台异步启动服务器,不阻塞应用初始化
+void _startServerInBackground(SettingsManager settingsManager) {
+ ServerManager serverManager = ServerManager();
+
+ Future.microtask(() async {
+ try {
+ // 检查是否启用了自动启动服务器
+ bool isAutoStartEnabled =
+ await settingsManager.getBool(settingKeyAutoStartEnabled);
+ if (isAutoStartEnabled) {
+ // 获取保存的hosts文件列表
+ List savedHostsList =
+ await settingsManager.getList(settingKeyAutoStartHosts);
+
+ if (savedHostsList.isNotEmpty) {
+ // 将JSON数据转换为SimpleHostFile对象
+ List autoStartHosts = savedHostsList
+ .map((json) => SimpleHostFile.fromJson(json))
+ .toList();
+
+ // 启动服务器
+ await serverManager.startServer(
+ allowedHostFiles:
+ autoStartHosts.map((host) => host.fileName).toList(),
+ );
+
+ print('自动启动服务器成功,共享${autoStartHosts.length}个hosts文件');
+ } else {
+ print('自动启动已启用,但没有找到保存的hosts文件列表');
+ }
+ }
+ } catch (e) {
+ print('自动启动服务器失败: $e');
+ }
+ });
+}
diff --git a/lib/enums.dart b/lib/enums.dart
index 00feeb2..6d3182d 100644
--- a/lib/enums.dart
+++ b/lib/enums.dart
@@ -1,3 +1,12 @@
enum EditMode { Text, Table }
enum AdvancedSettingsEnum { Open, Close }
+
+/// 排序方向枚举
+enum SortDirection {
+ /// 升序
+ ascending,
+
+ /// 降序
+ descending,
+}
\ No newline at end of file
diff --git a/lib/home/cubit/home_cubit.dart b/lib/home/cubit/home_cubit.dart
new file mode 100755
index 0000000..ab09c78
--- /dev/null
+++ b/lib/home/cubit/home_cubit.dart
@@ -0,0 +1,304 @@
+import 'package:bloc/bloc.dart';
+import 'package:flutter/material.dart';
+import 'package:hosts/enums.dart';
+import 'package:hosts/l10n/app_localizations.dart' as gen;
+import 'package:hosts/model/simple_host_file.dart';
+import 'package:hosts/util/file_manager.dart';
+import 'package:hosts/util/settings_manager.dart';
+import 'package:hosts/util/string_util.dart';
+
+part 'home_state.dart';
+
+/// 首页业务逻辑处理类
+/// 管理首页的所有状态变更
+class HomeCubit extends Cubit {
+ /// 构造函数
+ /// 初始化状态为HomeInitial
+ HomeCubit() : super(const HomeInitial(HomeStateData()));
+
+ /// 设置管理器
+ final SettingsManager _settingsManager = SettingsManager();
+
+ /// 文件管理器
+ final FileManager _fileManager = FileManager();
+
+ /// 加载host文件列表
+ /// [context] 用于本地化
+ /// [isInit] 是否为初始化加载
+ Future loadHostFiles(BuildContext context,
+ [bool isInit = false]) async {
+ List tempHostFiles = [];
+ List tempSelectHostFiles = [];
+ List hostConfigs =
+ await _settingsManager.getList(settingKeyHostConfigs);
+
+ if (isInit) {
+ // TODO 支持多个文件选择
+ tempSelectHostFiles = [
+ (await _settingsManager.getString(settingKeyUseHostFile) ?? "")
+ ];
+ }
+
+ for (Map config in hostConfigs) {
+ SimpleHostFile hostFile = SimpleHostFile.fromJson(config);
+ tempHostFiles.add(hostFile);
+
+ if (hostFile.fileName == "system") {
+ hostFile.remark = gen.AppLocalizations.of(context)!.default_hosts_text;
+ }
+ }
+
+ final fileId =
+ tempSelectHostFiles.isNotEmpty ? tempSelectHostFiles.first : "system";
+
+ emit(
+ HomeInitial(
+ HomeStateData(
+ hostFiles: tempHostFiles,
+ useHostFiles: tempSelectHostFiles,
+ editMode: EditMode.Table,
+ ),
+ ),
+ );
+
+ selectHost(fileId);
+ }
+
+ /// 添加新的host文件
+ /// [remark] 文件备注信息
+ Future addHostFile(String remark) async {
+ if (remark.isEmpty) return;
+
+ // 获取当前 hostFiles 列表
+ List currentHostFiles = List.from(state.data.hostFiles);
+ List hostConfigs =
+ await _settingsManager.getList(settingKeyHostConfigs);
+
+ // 生成随机文件名
+ final String fileName = generateRandomString(18);
+
+ // 创建新的 hostFile
+ SimpleHostFile newHostFile =
+ SimpleHostFile(fileName: fileName, remark: remark);
+
+ // 添加到 hostFiles 列表
+ currentHostFiles.add(newHostFile);
+ hostConfigs.add(newHostFile.toJson());
+
+ // 创建实际的文件
+ await _fileManager.createHosts(fileName);
+
+ // 保存到
+ await _settingsManager.setList(settingKeyHostConfigs, hostConfigs);
+
+ // 更新状态
+ emit(
+ HomeInitial(
+ state.data.copyWith(
+ hostFiles: currentHostFiles,
+ ),
+ ),
+ );
+ }
+
+ /// 更新host文件备注
+ /// [fileName] 要更新的文件名
+ /// [newRemark] 新的备注信息
+ Future updateHostFileRemark(String fileName, String newRemark) async {
+ if (fileName.isEmpty || newRemark.isEmpty) return;
+
+ List updatedHostFiles = [];
+ List hostConfigs =
+ await _settingsManager.getList(settingKeyHostConfigs);
+
+ bool updated = false;
+
+ // 更新 hostFiles
+ for (SimpleHostFile hostFile in state.data.hostFiles) {
+ if (hostFile.fileName == fileName) {
+ updatedHostFiles
+ .add(SimpleHostFile(fileName: fileName, remark: newRemark));
+ updated = true;
+ } else {
+ updatedHostFiles.add(hostFile);
+ }
+ }
+
+ if (!updated) return;
+
+ // 更新 hostConfigs
+ List