From 3a43dc2fe99adc35f59b7879eabb17371d0e1178 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 27 Oct 2024 16:59:23 +0800 Subject: [PATCH] Add android shortcuts Fix init params issues Fix dynamic color issues Optimize navigator animate Optimize window init Optimize fab Optimize save --- .github/workflows/build.yaml | 2 +- android/app/src/main/AndroidManifest.xml | 9 +- .../kotlin/com/follow/clash/GlobalState.kt | 27 ++++- .../kotlin/com/follow/clash/MainActivity.kt | 2 +- .../kotlin/com/follow/clash/TempActivity.kt | 9 +- .../kotlin/com/follow/clash/extensions/Ext.kt | 58 +++++++++ .../com/follow/clash/plugins/AppPlugin.kt | 54 ++++++--- .../com/follow/clash/plugins/ServicePlugin.kt | 2 - .../follow/clash/services/FlClashService.kt | 7 ++ .../clash/services/FlClashTileService.kt | 15 +-- .../clash/services/FlClashVpnService.kt | 61 ++++------ core/Clash.Meta | 2 +- core/tun.go | 4 +- lib/application.dart | 19 ++- lib/common/common.dart | 3 +- lib/common/navigator.dart | 11 ++ lib/common/other.dart | 10 +- lib/common/window.dart | 11 +- lib/controller.dart | 45 +++---- lib/fragments/dashboard/dashboard.dart | 21 +++- lib/fragments/dashboard/start_button.dart | 93 ++++++++------- lib/fragments/profiles/add_profile.dart | 27 +++-- lib/fragments/profiles/profiles.dart | 112 +++++++++--------- lib/fragments/proxies/setting.dart | 2 +- lib/fragments/proxies/tab.dart | 74 ++++++------ lib/fragments/theme.dart | 2 +- lib/l10n/arb/intl_en.arb | 3 +- lib/l10n/arb/intl_zh_CN.arb | 3 +- lib/l10n/intl/messages_en.dart | 1 + lib/l10n/intl/messages_zh_CN.dart | 1 + lib/l10n/l10n.dart | 10 ++ lib/main.dart | 13 +- lib/manager/app_state_manager.dart | 27 ++++- lib/manager/tile_manager.dart | 3 +- lib/manager/tray_manager.dart | 5 - lib/manager/window_manager.dart | 5 +- lib/models/clash_config.dart | 5 + lib/models/common.dart | 3 +- lib/models/config.dart | 42 ++++--- lib/models/generated/config.freezed.dart | 5 +- lib/models/generated/config.g.dart | 4 +- lib/plugins/app.dart | 15 +++ lib/router/fade_page.dart | 61 ---------- lib/state.dart | 7 +- lib/widgets/builder.dart | 29 +++++ lib/widgets/list.dart | 15 +-- lib/widgets/scaffold.dart | 107 ++++++++++------- lib/widgets/sheet.dart | 12 +- macos/Runner.xcodeproj/project.pbxproj | 6 +- macos/Runner/AppDelegate.swift | 2 +- macos/Runner/Info.plist | 4 +- pubspec.yaml | 2 +- 52 files changed, 612 insertions(+), 460 deletions(-) create mode 100644 lib/common/navigator.dart delete mode 100644 lib/router/fade_page.dart diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5dff1fc..a3868d4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -79,7 +79,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version-file: 'core/go.mod' + go-version: 'stable' cache-dependency-path: | core/go.sum diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8c3b5d3..909c0d8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ - @@ -23,8 +24,8 @@ - + - + diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt index 0ee3115..2dc28b6 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.lifecycle.MutableLiveData import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.ServicePlugin -import com.follow.clash.plugins.VpnPlugin import com.follow.clash.plugins.TilePlugin +import com.follow.clash.plugins.VpnPlugin import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor @@ -33,6 +33,10 @@ object GlobalState { return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? } + fun getText(text: String): String { + return getCurrentAppPlugin()?.getText(text) ?: "" + } + fun getCurrentTilePlugin(): TilePlugin? { val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? @@ -42,6 +46,27 @@ object GlobalState { return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin? } + fun handleToggle(context: Context) { + if (runState.value == RunState.STOP) { + runState.value = RunState.PENDING + val tilePlugin = getCurrentTilePlugin() + if (tilePlugin != null) { + tilePlugin.handleStart() + } else { + initServiceEngine(context) + } + } else { + handleStop() + } + } + + fun handleStop() { + if (runState.value == RunState.START) { + runState.value = RunState.PENDING + getCurrentTilePlugin()?.handleStop() + } + } + fun destroyServiceEngine() { serviceEngine?.destroy() serviceEngine = null diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt index 388379b..90ec24f 100644 --- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -3,8 +3,8 @@ package com.follow.clash import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.ServicePlugin -import com.follow.clash.plugins.VpnPlugin import com.follow.clash.plugins.TilePlugin +import com.follow.clash.plugins.VpnPlugin import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt index 9024901..913e874 100644 --- a/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/TempActivity.kt @@ -2,17 +2,18 @@ package com.follow.clash import android.app.Activity import android.os.Bundle +import com.follow.clash.extensions.wrapAction class TempActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) when (intent.action) { - "com.follow.clash.action.START" -> { - GlobalState.getCurrentTilePlugin()?.handleStart() + wrapAction("STOP") -> { + GlobalState.handleStop() } - "com.follow.clash.action.STOP" -> { - GlobalState.getCurrentTilePlugin()?.handleStop() + wrapAction("CHANGE") -> { + GlobalState.handleToggle(applicationContext) } } finishAndRemoveTask() diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt index 4e30de2..ffc7ae5 100644 --- a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt +++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt @@ -1,21 +1,29 @@ package com.follow.clash.extensions +import android.app.PendingIntent +import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network +import android.os.Build import android.system.OsConstants.IPPROTO_TCP import android.system.OsConstants.IPPROTO_UDP import android.util.Base64 import androidx.core.graphics.drawable.toBitmap +import com.follow.clash.TempActivity import com.follow.clash.models.CIDR import com.follow.clash.models.Metadata +import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine suspend fun Drawable.getBase64(): String { @@ -71,6 +79,34 @@ fun InetAddress.asSocketAddressText(port: Int): String { } } +fun Context.wrapAction(action: String):String{ + return "${this.packageName}.action.$action" +} + +fun Context.getActionIntent(action: String): Intent { + val actionIntent = Intent(this, TempActivity::class.java) + actionIntent.action = wrapAction(action) + return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) +} + +fun Context.getActionPendingIntent(action: String): PendingIntent { + return if (Build.VERSION.SDK_INT >= 31) { + PendingIntent.getActivity( + this, + 0, + getActionIntent(action), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } else { + PendingIntent.getActivity( + this, + 0, + getActionIntent(action), + PendingIntent.FLAG_UPDATE_CURRENT + ) + } +} + private fun numericToTextFormat(src: ByteArray): String { val sb = StringBuilder(39) @@ -87,3 +123,25 @@ private fun numericToTextFormat(src: ByteArray): String { } return sb.toString() } + +suspend fun MethodChannel.awaitResult( + method: String, + arguments: Any? = null +): T? = withContext(Dispatchers.Main) { // 切换到主线程 + suspendCoroutine { continuation -> + invokeMethod(method, arguments, object : MethodChannel.Result { + override fun success(result: Any?) { + @Suppress("UNCHECKED_CAST") + continuation.resume(result as T) + } + + override fun error(code: String, message: String?, details: Any?) { + continuation.resume(null) + } + + override fun notImplemented() { + continuation.resume(null) + } + }) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index b866c84..0cc7831 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -14,9 +14,15 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.getSystemService -import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import androidx.core.content.FileProvider +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import com.follow.clash.GlobalState +import com.follow.clash.R +import com.follow.clash.extensions.awaitResult +import com.follow.clash.extensions.getActionIntent import com.follow.clash.extensions.getBase64 import com.follow.clash.models.Package import com.google.gson.Gson @@ -31,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File import java.util.zip.ZipFile @@ -116,11 +123,21 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { scope = CoroutineScope(Dispatchers.Default) - context = flutterPluginBinding.applicationContext; + context = flutterPluginBinding.applicationContext channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel.setMethodCallHandler(this) } + private fun initShortcuts(label: String) { + val shortcut = ShortcutInfoCompat.Builder(context, "toggle") + .setShortLabel(label) + .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round)) + .setIntent(context.getActionIntent("CHANGE")) + .build() + ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut)) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) scope.cancel() @@ -128,11 +145,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware private fun tip(message: String?) { if (GlobalState.flutterEngine == null) { - if (toast != null) { - toast!!.cancel() - } - toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) - toast!!.show() + Toast.makeText(context, message, Toast.LENGTH_LONG).show() } } @@ -140,13 +153,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware when (call.method) { "moveTaskToBack" -> { activity?.moveTaskToBack(true) - result.success(true); + result.success(true) } "updateExcludeFromRecents" -> { val value = call.argument("value") updateExcludeFromRecents(value) - result.success(true); + result.success(true) + } + + "initShortcuts" -> { + initShortcuts(call.arguments as String) + result.success(true) } "getPackages" -> { @@ -197,7 +215,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } else -> { - result.notImplemented(); + result.notImplemented() } } } @@ -270,7 +288,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware private fun getPackages(): List { val packageManager = context.packageManager - if (packages.isNotEmpty()) return packages; + if (packages.isNotEmpty()) return packages packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { it.packageName != context.packageName || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true @@ -284,7 +302,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware firstInstallTime = it.firstInstallTime ) }?.let { packages.addAll(it) } - return packages; + return packages } private suspend fun getPackagesToJson(): String { @@ -306,7 +324,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware val intent = VpnService.prepare(context) if (intent != null) { activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) - return; + return } vpnCallBack?.invoke() } @@ -330,6 +348,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } } + fun getText(text: String): String? { + return runBlocking { + channel.awaitResult("getText", text) + } + } + private fun isChinaPackage(packageName: String): Boolean { val packageManager = context.packageManager ?: return false skipPrefixList.forEach { @@ -398,7 +422,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity; + activity = binding.activity binding.addActivityResultListener(::onActivityResult) binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) } @@ -408,7 +432,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity; + activity = binding.activity } override fun onDetachedFromActivity() { diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt index fc2c9be..2e0c8eb 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt @@ -1,8 +1,6 @@ package com.follow.clash.plugins import android.content.Context -import android.net.ConnectivityManager -import androidx.core.content.getSystemService import com.follow.clash.GlobalState import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt index 67e4bd5..74aa226 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt @@ -13,7 +13,9 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import com.follow.clash.BaseServiceInterface +import com.follow.clash.GlobalState import com.follow.clash.MainActivity +import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.models.VpnOptions @@ -64,6 +66,11 @@ class FlClashService : Service(), BaseServiceInterface { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE } + addAction( + 0, + GlobalState.getText("stop"), + getActionPendingIntent("CHANGE") + ) setOngoing(true) setShowWhen(false) setOnlyAlertOnce(true) diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt index 1edbb03..2fe0423 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.os.Build -import android.os.IBinder import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi @@ -67,19 +66,7 @@ class FlClashTileService : TileService() { override fun onClick() { super.onClick() activityTransfer() - if (GlobalState.runState.value == RunState.STOP) { - GlobalState.runState.value = RunState.PENDING - val tilePlugin = GlobalState.getCurrentTilePlugin() - if (tilePlugin != null) { - tilePlugin.handleStart() - } else { - GlobalState.initServiceEngine(applicationContext) - } - } else if (GlobalState.runState.value == RunState.START) { - GlobalState.runState.value = RunState.PENDING - GlobalState.getCurrentTilePlugin()?.handleStop() - } - + GlobalState.handleToggle(applicationContext) } override fun onDestroy() { diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt index 79f6900..e5d1caa 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -21,12 +21,14 @@ import com.follow.clash.GlobalState import com.follow.clash.MainActivity import com.follow.clash.R import com.follow.clash.TempActivity +import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.extensions.toCIDR import com.follow.clash.models.AccessControlMode import com.follow.clash.models.VpnOptions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class FlClashVpnService : VpnService(), BaseServiceInterface { @@ -122,26 +124,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { ) } - val stopIntent = Intent(this, TempActivity::class.java) - stopIntent.action = "com.follow.clash.action.STOP" - stopIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - - - val stopPendingIntent = if (Build.VERSION.SDK_INT >= 31) { - PendingIntent.getActivity( - this, - 0, - stopIntent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) - } else { - PendingIntent.getActivity( - this, - 0, - stopIntent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - } with(NotificationCompat.Builder(this, CHANNEL)) { setSmallIcon(R.drawable.ic_stat_name) setContentTitle("FlClash") @@ -152,30 +134,39 @@ class FlClashVpnService : VpnService(), BaseServiceInterface { foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE } setOngoing(true) + addAction( + 0, + GlobalState.getText("stop"), + getActionPendingIntent("STOP") + ) setShowWhen(false) setOnlyAlertOnce(true) setAutoCancel(true) - addAction(0, "Stop", stopPendingIntent); } } @SuppressLint("ForegroundServiceType", "WrongConstant") override fun startForeground(title: String, content: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = getSystemService(NotificationManager::class.java) - var channel = manager?.getNotificationChannel(CHANNEL) - if (channel == null) { - channel = - NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) - manager?.createNotificationChannel(channel) + CoroutineScope(Dispatchers.Default).launch { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = getSystemService(NotificationManager::class.java) + var channel = manager?.getNotificationChannel(CHANNEL) + if (channel == null) { + channel = + NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) + manager?.createNotificationChannel(channel) + } + } + val notification = + notificationBuilder + .setContentTitle(title) + .setContentText(content) + .build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } else { + startForeground(notificationId, notification) } - } - val notification = - notificationBuilder.setContentTitle(title).setContentText(content).build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) - } else { - startForeground(notificationId, notification) } } diff --git a/core/Clash.Meta b/core/Clash.Meta index 8472840..317fd5e 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 8472840f4719e59f9f41cfab2643bf73bb9d5799 +Subproject commit 317fd5ece01f788e75673763761cd06f6be1d4b2 diff --git a/core/tun.go b/core/tun.go index c2e73ac..bae958c 100644 --- a/core/tun.go +++ b/core/tun.go @@ -128,7 +128,7 @@ func initSocketHook() { } return conn.Control(func(fd uintptr) { fdInt := int64(fd) - timeout := time.After(100 * time.Millisecond) + timeout := time.After(500 * time.Millisecond) id := atomic.AddInt64(&fdCounter, 1) markSocket(Fd{ @@ -145,7 +145,7 @@ func initSocketHook() { if exists { return } - time.Sleep(10 * time.Millisecond) + time.Sleep(20 * time.Millisecond) } } }) diff --git a/lib/application.dart b/lib/application.dart index eb6d524..9cbc48f 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:animations/animations.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/manager.dart'; +import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -58,10 +60,18 @@ class ApplicationState extends State { final _pageTransitionsTheme = const PageTransitionsTheme( builders: { - TargetPlatform.android: CupertinoPageTransitionsBuilder(), - TargetPlatform.windows: CupertinoPageTransitionsBuilder(), - TargetPlatform.linux: CupertinoPageTransitionsBuilder(), - TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.android: SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.horizontal, + ), + TargetPlatform.windows: SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.horizontal, + ), + TargetPlatform.linux: SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.horizontal, + ), + TargetPlatform.macOS: SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.horizontal, + ), }, ); @@ -93,6 +103,7 @@ class ApplicationState extends State { } await globalState.appController.init(); globalState.appController.initLink(); + app?.initShortcuts(); }); } diff --git a/lib/common/common.dart b/lib/common/common.dart index fcea849..e47463d 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -29,4 +29,5 @@ export 'scroll.dart'; export 'icons.dart'; export 'http.dart'; export 'keyboard.dart'; -export 'network.dart'; \ No newline at end of file +export 'network.dart'; +export 'navigator.dart'; \ No newline at end of file diff --git a/lib/common/navigator.dart b/lib/common/navigator.dart new file mode 100644 index 0000000..c3059f3 --- /dev/null +++ b/lib/common/navigator.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class BaseNavigator { + static Future push(BuildContext context, Widget child) async { + return await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => child, + ), + ); + } +} diff --git a/lib/common/other.dart b/lib/common/other.dart index 0036837..8b5b435 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; @@ -101,14 +102,13 @@ class Other { } String getTrayIconPath({ - required bool isStart, required Brightness brightness, }) { if(Platform.isMacOS){ return "assets/images/icon_white.png"; } final suffix = Platform.isWindows ? "ico" : "png"; - if (isStart && Platform.isWindows) { + if (Platform.isWindows) { return "assets/images/icon.$suffix"; } return switch (brightness) { @@ -188,10 +188,8 @@ class Other { return parameters[fileNameKey]; } - double getViewWidth() { - final view = WidgetsBinding.instance.platformDispatcher.views.first; - final size = view.physicalSize / view.devicePixelRatio; - return size.width; + FlutterView getScreen() { + return WidgetsBinding.instance.platformDispatcher.views.first; } List parseReleaseBody(String? body) { diff --git a/lib/common/window.dart b/lib/common/window.dart index c233807..a031e05 100755 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'dart:math'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/config.dart'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; @@ -21,14 +23,7 @@ class Window { size: Size(props.width, props.height), minimumSize: const Size(380, 500), ); - if (props.left != null || props.top != null) { - await windowManager.setPosition( - Offset(props.left ?? 0, props.top ?? 0), - ); - } else { - await windowManager.setAlignment(Alignment.center); - } - if(!Platform.isMacOS || version > 10){ + if (!Platform.isMacOS || version > 10) { await windowManager.setTitleBarStyle(TitleBarStyle.hidden); } await windowManager.waitUntilReadyToShow(windowOptions, () async { diff --git a/lib/controller.dart b/lib/controller.dart index 9170137..0c64488 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -29,6 +29,7 @@ class AppController { late Function updateGroupDebounce; late Function addCheckIpNumDebounce; late Function applyProfileDebounce; + late Function savePreferencesDebounce; AppController(this.context) { appState = context.read(); @@ -38,6 +39,9 @@ class AppController { updateClashConfigDebounce = debounce(() async { await updateClashConfig(); }); + savePreferencesDebounce = debounce(() async { + await savePreferences(); + }); applyProfileDebounce = debounce(() async { await applyProfile(isPrue: true); }); @@ -51,10 +55,7 @@ class AppController { updateStatus(bool isStart) async { if (isStart) { - await globalState.handleStart( - config: config, - clashConfig: clashConfig, - ); + await globalState.handleStart(); updateRunTime(); updateTraffic(); globalState.updateFunctionLists = [ @@ -202,17 +203,8 @@ class AppController { } savePreferences() async { - await saveConfigPreferences(); - await saveClashConfigPreferences(); - } - - saveConfigPreferences() async { - debugPrint("saveConfigPreferences"); + debugPrint("[APP] savePreferences"); await preferences.saveConfig(config); - } - - saveClashConfigPreferences() async { - debugPrint("saveClashConfigPreferences"); await preferences.saveClashConfig(clashConfig); } @@ -231,7 +223,7 @@ class AppController { handleBackOrExit() async { if (config.appSetting.minimizeOnExit) { if (system.isDesktop) { - await savePreferences(); + await savePreferencesDebounce(); } await system.back(); } else { @@ -608,7 +600,6 @@ class AppController { } Future _updateSystemTray({ - required bool isStart, required Brightness? brightness, bool force = false, }) async { @@ -617,7 +608,6 @@ class AppController { } await trayManager.setIcon( other.getTrayIconPath( - isStart: isStart, brightness: brightness ?? WidgetsBinding.instance.platformDispatcher.platformBrightness, ), @@ -633,7 +623,6 @@ class AppController { updateTray([bool focus = false]) async { if (!Platform.isLinux) { await _updateSystemTray( - isStart: appFlowingState.isStart, brightness: appState.brightness, force: focus, ); @@ -697,15 +686,18 @@ class AppController { }, checked: config.appSetting.autoLaunch, ); - final adminAutoStartMenuItem = MenuItem.checkbox( - label: appLocalizations.adminAutoLaunch, - onClick: (_) async { - globalState.appController.updateAdminAutoLaunch(); - }, - checked: config.appSetting.adminAutoLaunch, - ); menuItems.add(autoStartMenuItem); - menuItems.add(adminAutoStartMenuItem); + + if(Platform.isWindows){ + final adminAutoStartMenuItem = MenuItem.checkbox( + label: appLocalizations.adminAutoLaunch, + onClick: (_) async { + globalState.appController.updateAdminAutoLaunch(); + }, + checked: config.appSetting.adminAutoLaunch, + ); + menuItems.add(adminAutoStartMenuItem); + } menuItems.add(MenuItem.separator()); final exitMenuItem = MenuItem( label: appLocalizations.exit, @@ -718,7 +710,6 @@ class AppController { await trayManager.setContextMenu(menu); if (Platform.isLinux) { await _updateSystemTray( - isStart: appFlowingState.isStart, brightness: appState.brightness, force: focus, ); diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index 6165796..2e99ad1 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -21,12 +21,25 @@ class DashboardFragment extends StatefulWidget { } class _DashboardFragmentState extends State { + _initFab(bool isCurrent) { + if(!isCurrent){ + return; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.floatingActionButton = const StartButton(); + }); + } + @override Widget build(BuildContext context) { - return FloatLayout( - floatingWidget: const FloatWrapper( - child: StartButton(), - ), + return ActiveBuilder( + label: "dashboard", + builder: (isCurrent, child) { + _initFab(isCurrent); + return child!; + }, child: Align( alignment: Alignment.topCenter, child: SingleChildScrollView( diff --git a/lib/fragments/dashboard/start_button.dart b/lib/fragments/dashboard/start_button.dart index a872ca4..1046de3 100644 --- a/lib/fragments/dashboard/start_button.dart +++ b/lib/fragments/dashboard/start_button.dart @@ -19,9 +19,10 @@ class _StartButtonState extends State @override void initState() { super.initState(); + isStart = globalState.appController.appFlowingState.isStart; _controller = AnimationController( vsync: this, - value: 0, + value: isStart ? 1 : 0, duration: const Duration(milliseconds: 200), ); } @@ -85,58 +86,58 @@ class _StartButtonState extends State ) .width + 16; - return AnimatedBuilder( - animation: _controller.view, - builder: (_, child) { - return SizedBox( - width: 56 + textWidth * _controller.value, - height: 56, - child: FloatingActionButton( - heroTag: null, - onPressed: () { - handleSwitchStart(); - }, - child: Row( - children: [ - Container( - width: 56, - height: 56, - alignment: Alignment.center, - child: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: _controller, + return _updateControllerContainer( + AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return SizedBox( + width: 56 + textWidth * _controller.value, + height: 56, + child: FloatingActionButton( + heroTag: null, + onPressed: () { + handleSwitchStart(); + }, + child: Row( + children: [ + Container( + width: 56, + height: 56, + alignment: Alignment.center, + child: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: _controller, + ), ), - ), - Expanded( - child: ClipRect( - child: OverflowBox( - maxWidth: textWidth, - child: Container( - alignment: Alignment.centerLeft, - child: child!, + Expanded( + child: ClipRect( + child: OverflowBox( + maxWidth: textWidth, + child: Container( + alignment: Alignment.centerLeft, + child: child!, + ), ), ), ), - ), - ], + ], + ), ), - ), - ); - }, - child: child, + ); + }, + child: child, + ), ); }, - child: _updateControllerContainer( - Selector( - selector: (_, appFlowingState) => appFlowingState.runTime, - builder: (_, int? value, __) { - final text = other.getTimeText(value); - return Text( - text, - style: Theme.of(context).textTheme.titleMedium?.toSoftBold, - ); - }, - ), + child: Selector( + selector: (_, appFlowingState) => appFlowingState.runTime, + builder: (_, int? value, __) { + final text = other.getTimeText(value); + return Text( + text, + style: Theme.of(context).textTheme.titleMedium?.toSoftBold, + ); + }, ), ); } diff --git a/lib/fragments/profiles/add_profile.dart b/lib/fragments/profiles/add_profile.dart index 4e93e9f..96e76b7 100644 --- a/lib/fragments/profiles/add_profile.dart +++ b/lib/fragments/profiles/add_profile.dart @@ -7,7 +7,10 @@ import 'package:flutter/material.dart'; class AddProfile extends StatelessWidget { final BuildContext context; - const AddProfile({super.key, required this.context,}); + const AddProfile({ + super.key, + required this.context, + }); _handleAddProfileFormFile() async { globalState.appController.addProfileFormFile(); @@ -18,14 +21,16 @@ class AddProfile extends StatelessWidget { } _toScan() async { - if(system.isDesktop){ + if (system.isDesktop) { globalState.appController.addProfileFormQrCode(); return; } - final url = await Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => const ScanPage())); + final url = await BaseNavigator.push( + context, + const ScanPage(), + ); if (url != null) { - WidgetsBinding.instance.addPostFrameCallback((_){ + WidgetsBinding.instance.addPostFrameCallback((_) { _handleAddProfileFormURL(url); }); } @@ -44,12 +49,12 @@ class AddProfile extends StatelessWidget { Widget build(context) { return ListView( children: [ - ListItem( - leading: const Icon(Icons.qr_code), - title: Text(appLocalizations.qrcode), - subtitle: Text(appLocalizations.qrcodeDesc), - onTap: _toScan, - ), + ListItem( + leading: const Icon(Icons.qr_code), + title: Text(appLocalizations.qrcode), + subtitle: Text(appLocalizations.qrcodeDesc), + onTap: _toScan, + ), ListItem( leading: const Icon(Icons.upload_file), title: Text(appLocalizations.file), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 8624897..cdd9f9b 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -80,7 +80,7 @@ class _ProfilesFragmentState extends State { } } - _initScaffoldState() { + _initScaffold() { WidgetsBinding.instance.addPostFrameCallback( (_) { if (!mounted) return; @@ -112,71 +112,67 @@ class _ProfilesFragmentState extends State { iconSize: 26, ), ]; + commonScaffoldState?.floatingActionButton = FloatingActionButton( + heroTag: null, + onPressed: _handleShowAddExtendPage, + child: const Icon( + Icons.add, + ), + ); }, ); } @override Widget build(BuildContext context) { - return FloatLayout( - floatingWidget: FloatWrapper( - child: FloatingActionButton( - heroTag: null, - onPressed: _handleShowAddExtendPage, - child: const Icon( - Icons.add, - ), + return ActiveBuilder( + label: "profiles", + builder: (isCurrent,child){ + if(isCurrent){ + _initScaffold(); + } + return child!; + }, + child: Selector2( + selector: (_, appState, config) => ProfilesSelectorState( + profiles: config.profiles, + currentProfileId: config.currentProfileId, + columns: other.getProfilesColumns(appState.viewWidth), ), - ), - child: Selector( - selector: (_, appState) => appState.currentLabel == 'profiles', - builder: (_, isCurrent, child) { - if (isCurrent) { - _initScaffoldState(); - } - return child!; - }, - child: Selector2( - selector: (_, appState, config) => ProfilesSelectorState( - profiles: config.profiles, - currentProfileId: config.currentProfileId, - columns: other.getProfilesColumns(appState.viewWidth), - ), - builder: (context, state, child) { - if (state.profiles.isEmpty) { - return NullStatus( - label: appLocalizations.nullProfileDesc, - ); - } - return Align( - alignment: Alignment.topCenter, - child: SingleChildScrollView( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: 88, - ), - child: Grid( - mainAxisSpacing: 16, - crossAxisSpacing: 16, - crossAxisCount: state.columns, - children: [ - for (int i = 0; i < state.profiles.length; i++) - GridItem( - child: ProfileItem( - key: Key(state.profiles[i].id), - profile: state.profiles[i], - groupValue: state.currentProfileId, - onChanged: globalState.appController.changeProfile, - ), - ), - ], - ), - ), + builder: (context, state, child) { + if (state.profiles.isEmpty) { + return NullStatus( + label: appLocalizations.nullProfileDesc, ); - }, - ), + } + return Align( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 88, + ), + child: Grid( + mainAxisSpacing: 16, + crossAxisSpacing: 16, + crossAxisCount: state.columns, + children: [ + for (int i = 0; i < state.profiles.length; i++) + GridItem( + child: ProfileItem( + key: Key(state.profiles[i].id), + profile: state.profiles[i], + groupValue: state.currentProfileId, + onChanged: globalState.appController.changeProfile, + ), + ), + ], + ), + ), + ); + }, ), ); } diff --git a/lib/fragments/proxies/setting.dart b/lib/fragments/proxies/setting.dart index 9ed8741..fc83d9e 100644 --- a/lib/fragments/proxies/setting.dart +++ b/lib/fragments/proxies/setting.dart @@ -191,7 +191,7 @@ class ProxiesSetting extends StatelessWidget { _buildGroupStyleSetting() { return generateSection( - title: "图标样式", + title: appLocalizations.iconStyle, items: [ SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index 8c59d59..38a22e3 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -278,6 +278,23 @@ class ProxyGroupViewState extends State { ); } + initFab(bool isCurrent, List proxies) { + if (!isCurrent) { + return; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.floatingActionButton = DelayTestButton( + onClick: () async { + await _delayTest( + proxies, + ); + }, + ); + }); + } + @override Widget build(BuildContext context) { return Selector2( @@ -303,11 +320,11 @@ class ProxyGroupViewState extends State { proxies, ); _lastProxies = sortedProxies; - return DelayTestButtonContainer( - onClick: () async { - await _delayTest( - proxies, - ); + return ActiveBuilder( + label: "proxies", + builder: (isCurrent, child) { + initFab(isCurrent, proxies); + return child!; }, child: Align( alignment: Alignment.topCenter, @@ -344,22 +361,19 @@ class ProxyGroupViewState extends State { } } -class DelayTestButtonContainer extends StatefulWidget { - final Widget child; +class DelayTestButton extends StatefulWidget { final Future Function() onClick; - const DelayTestButtonContainer({ + const DelayTestButton({ super.key, - required this.child, required this.onClick, }); @override - State createState() => - _DelayTestButtonContainerState(); + State createState() => _DelayTestButtonState(); } -class _DelayTestButtonContainerState extends State +class _DelayTestButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scale; @@ -401,29 +415,23 @@ class _DelayTestButtonContainerState extends State @override Widget build(BuildContext context) { - _controller.reverse(); - return FloatLayout( - floatingWidget: FloatWrapper( - child: AnimatedBuilder( - animation: _controller.view, - builder: (_, child) { - return SizedBox( - width: 56, - height: 56, - child: Transform.scale( - scale: _scale.value, - child: child, - ), - ); - }, - child: FloatingActionButton( - heroTag: null, - onPressed: _healthcheck, - child: const Icon(Icons.network_ping), + return AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return SizedBox( + width: 56, + height: 56, + child: Transform.scale( + scale: _scale.value, + child: child, ), - ), + ); + }, + child: FloatingActionButton( + heroTag: null, + onPressed: _healthcheck, + child: const Icon(Icons.network_ping), ), - child: widget.child, ); } } diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index 99867f7..bb85a71 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -163,7 +163,7 @@ class _ThemeColorsBoxState extends State { ); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 8f8b2fb..c300083 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -322,5 +322,6 @@ "adminAutoLaunch": "Admin auto launch", "adminAutoLaunchDesc": "Boot up by using admin mode", "fontFamily": "FontFamily", - "systemFont": "System font" + "systemFont": "System font", + "toggle": "Toggle" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 789f050..e14bbf4 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -322,5 +322,6 @@ "adminAutoLaunch": "管理员自启动", "adminAutoLaunchDesc": "使用管理员模式开机自启动", "fontFamily": "字体", - "systemFont": "系统字体" + "systemFont": "系统字体", + "toggle": "切换" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index dc9015f..8bbae6f 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -447,6 +447,7 @@ class MessageLookup extends MessageLookupByLibrary { "tight": MessageLookupByLibrary.simpleMessage("Tight"), "time": MessageLookupByLibrary.simpleMessage("Time"), "tip": MessageLookupByLibrary.simpleMessage("tip"), + "toggle": MessageLookupByLibrary.simpleMessage("Toggle"), "tools": MessageLookupByLibrary.simpleMessage("Tools"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index f038d9b..60e270c 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -359,6 +359,7 @@ class MessageLookup extends MessageLookupByLibrary { "tight": MessageLookupByLibrary.simpleMessage("紧凑"), "time": MessageLookupByLibrary.simpleMessage("时间"), "tip": MessageLookupByLibrary.simpleMessage("提示"), + "toggle": MessageLookupByLibrary.simpleMessage("切换"), "tools": MessageLookupByLibrary.simpleMessage("工具"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 7b1da52..4d5ae9f 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -3289,6 +3289,16 @@ class AppLocalizations { args: [], ); } + + /// `Toggle` + String get toggle { + return Intl.message( + 'Toggle', + name: 'toggle', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 173a8fb..41b0a18 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,6 +53,10 @@ Future vpnService() async { final version = await system.version; final config = await preferences.getConfig() ?? Config(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); + await AppLocalizations.load( + other.getLocaleForString(config.appSetting.locale) ?? + WidgetsBinding.instance.platformDispatcher.locale, + ); final appState = AppState( mode: clashConfig.mode, selectedMap: config.currentSelectedMap, @@ -98,15 +102,8 @@ Future vpnService() async { }, ), ); - final appLocalizations = await AppLocalizations.load( - other.getLocaleForString(config.appSetting.locale) ?? - WidgetsBinding.instance.platformDispatcher.locale, - ); await app?.tip(appLocalizations.startVpn); - await globalState.handleStart( - config: config, - clashConfig: clashConfig, - ); + await globalState.handleStart(); tile?.addListener( TileListenerWithVpn( diff --git a/lib/manager/app_state_manager.dart b/lib/manager/app_state_manager.dart index 761b213..fe23823 100644 --- a/lib/manager/app_state_manager.dart +++ b/lib/manager/app_state_manager.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -18,7 +20,6 @@ class AppStateManager extends StatefulWidget { class _AppStateManagerState extends State with WidgetsBindingObserver { - _updateNavigationsContainer(Widget child) { return Selector2( selector: (_, appState, config) { @@ -45,6 +46,22 @@ class _AppStateManagerState extends State ); } + _cacheStateChange(Widget child) { + return Selector2( + selector: (_, config, clashConfig) => "$clashConfig $config", + shouldRebuild: (prev, next) { + if (prev != next) { + globalState.appController.savePreferencesDebounce(); + } + return prev != next; + }, + builder: (context, state, child) { + return child!; + }, + child: child, + ); + } + @override void initState() { super.initState(); @@ -61,7 +78,7 @@ class _AppStateManagerState extends State Future didChangeAppLifecycleState(AppLifecycleState state) async { final isPaused = state == AppLifecycleState.paused; if (isPaused) { - await globalState.appController.savePreferences(); + globalState.appController.savePreferencesDebounce(); } } @@ -73,8 +90,10 @@ class _AppStateManagerState extends State @override Widget build(BuildContext context) { - return _updateNavigationsContainer( - widget.child, + return _cacheStateChange( + _updateNavigationsContainer( + widget.child, + ), ); } } diff --git a/lib/manager/tile_manager.dart b/lib/manager/tile_manager.dart index d2b79ca..32801a4 100644 --- a/lib/manager/tile_manager.dart +++ b/lib/manager/tile_manager.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -29,7 +30,7 @@ class _TileContainerState extends State with TileListener { } @override - void onStop() { + Future onStop() async { globalState.appController.updateStatus(false); super.onStop(); } diff --git a/lib/manager/tray_manager.dart b/lib/manager/tray_manager.dart index b0b88d1..84d83c7 100755 --- a/lib/manager/tray_manager.dart +++ b/lib/manager/tray_manager.dart @@ -1,14 +1,9 @@ -import 'dart:io'; - import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; -import 'package:window_ext/window_ext.dart'; class TrayManager extends StatefulWidget { final Widget child; diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index b427f90..efcb7e2 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -20,7 +20,8 @@ class WindowManager extends StatefulWidget { State createState() => _WindowContainerState(); } -class _WindowContainerState extends State with WindowListener, WindowExtListener { +class _WindowContainerState extends State + with WindowListener, WindowExtListener { Function? updateLaunchDebounce; _autoLaunchContainer(Widget child) { @@ -82,7 +83,7 @@ class _WindowContainerState extends State with WindowListener, Wi @override void onWindowMinimize() async { - await globalState.appController.savePreferences(); + globalState.appController.savePreferencesDebounce(); super.onWindowMinimize(); } diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 51e5b30..11c22a6 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -371,4 +371,9 @@ class ClashConfig extends ChangeNotifier { factory ClashConfig.fromJson(Map json) { return _$ClashConfigFromJson(json); } + + @override + String toString() { + return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _ipv6: $_ipv6, _geodataLoader: $_geodataLoader, _logLevel: $_logLevel, _externalController: $_externalController, _mode: $_mode, _findProcessMode: $_findProcessMode, _keepAliveInterval: $_keepAliveInterval, _unifiedDelay: $_unifiedDelay, _tcpConcurrent: $_tcpConcurrent, _tun: $_tun, _dns: $_dns, _geoXUrl: $_geoXUrl, _rules: $_rules, _globalRealUa: $_globalRealUa, _hosts: $_hosts}'; + } } diff --git a/lib/models/common.dart b/lib/models/common.dart index d77c33c..4f95f79 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -431,7 +431,6 @@ class HotKeyAction with _$HotKeyAction { _$HotKeyActionFromJson(json); } - typedef Validator = String? Function(String? value); @freezed @@ -441,4 +440,4 @@ class Field with _$Field { required String value, Validator? validator, }) = _Field; -} \ No newline at end of file +} diff --git a/lib/models/config.dart b/lib/models/config.dart index 6b4e2a0..aa233d7 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -10,7 +10,9 @@ part 'generated/config.g.dart'; part 'generated/config.freezed.dart'; -const defaultAppSetting = AppSetting(); +final defaultAppSetting = const AppSetting().copyWith( + isAnimateToPage: system.isDesktop ? false : true, +); @freezed class AppSetting with _$AppSetting { @@ -36,10 +38,11 @@ class AppSetting with _$AppSetting { _$AppSettingFromJson(json); factory AppSetting.realFromJson(Map? json) { - final appSetting = - json == null ? defaultAppSetting : AppSetting.fromJson(json); + final appSetting = json == null + ? defaultAppSetting + : AppSetting.fromJson(json); return appSetting.copyWith( - isAnimateToPage: system.isDesktop ? false : true, + isAnimateToPage: system.isDesktop ? false : appSetting.isAnimateToPage, ); } } @@ -68,7 +71,7 @@ extension AccessControlExt on AccessControl { @freezed class WindowProps with _$WindowProps { const factory WindowProps({ - @Default(1000) double width, + @Default(900) double width, @Default(600) double height, double? top, double? left, @@ -141,37 +144,39 @@ class ProxiesStyle with _$ProxiesStyle { json == null ? defaultProxiesStyle : _$ProxiesStyleFromJson(json); } -const defaultThemeProps = ThemeProps(); +final defaultThemeProps = Platform.isWindows + ? const ThemeProps().copyWith( + fontFamily: FontFamily.miSans, + primaryColor: defaultPrimaryColor.value, + ) + : const ThemeProps().copyWith( + primaryColor: defaultPrimaryColor.value, + ); @freezed class ThemeProps with _$ThemeProps { const factory ThemeProps({ - @Default(0xFF795548) int? primaryColor, + int? primaryColor, @Default(ThemeMode.system) ThemeMode themeMode, @Default(false) bool prueBlack, @Default(FontFamily.system) FontFamily fontFamily, }) = _ThemeProps; - factory ThemeProps.fromJson(Map json) => _$ThemePropsFromJson(json); + factory ThemeProps.fromJson(Map json) => + _$ThemePropsFromJson(json); factory ThemeProps.realFromJson(Map? json) { if (json == null) { - return Platform.isWindows - ? defaultThemeProps.copyWith(fontFamily: FontFamily.miSans) - : defaultThemeProps; + return defaultThemeProps; } try { return ThemeProps.fromJson(json); } catch (_) { - return Platform.isWindows - ? defaultThemeProps.copyWith(fontFamily: FontFamily.miSans) - : defaultThemeProps; + return defaultThemeProps; } } } -const defaultCustomFontSizeScale = 1.0; - @JsonSerializable() class Config extends ChangeNotifier { AppSetting _appSetting; @@ -479,4 +484,9 @@ class Config extends ChangeNotifier { factory Config.fromJson(Map json) { return _$ConfigFromJson(json); } + + @override + String toString() { + return 'Config{_appSetting: $_appSetting, _profiles: $_profiles, _currentProfileId: $_currentProfileId, _isAccessControl: $_isAccessControl, _accessControl: $_accessControl, _dav: $_dav, _windowProps: $_windowProps, _themeProps: $_themeProps, _vpnProps: $_vpnProps, _desktopProps: $_desktopProps, _overrideDns: $_overrideDns, _hotKeyActions: $_hotKeyActions, _proxiesStyle: $_proxiesStyle}'; + } } diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index ee737c6..49b808f 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -840,7 +840,7 @@ class __$$WindowPropsImplCopyWithImpl<$Res> @JsonSerializable() class _$WindowPropsImpl implements _WindowProps { const _$WindowPropsImpl( - {this.width = 1000, this.height = 600, this.top, this.left}); + {this.width = 900, this.height = 600, this.top, this.left}); factory _$WindowPropsImpl.fromJson(Map json) => _$$WindowPropsImplFromJson(json); @@ -1667,7 +1667,7 @@ class __$$ThemePropsImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemePropsImpl implements _ThemeProps { const _$ThemePropsImpl( - {this.primaryColor = 0xFF795548, + {this.primaryColor, this.themeMode = ThemeMode.system, this.prueBlack = false, this.fontFamily = FontFamily.system}); @@ -1676,7 +1676,6 @@ class _$ThemePropsImpl implements _ThemeProps { _$$ThemePropsImplFromJson(json); @override - @JsonKey() final int? primaryColor; @override @JsonKey() diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 0afb281..f72a083 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -128,7 +128,7 @@ const _$AccessSortTypeEnumMap = { _$WindowPropsImpl _$$WindowPropsImplFromJson(Map json) => _$WindowPropsImpl( - width: (json['width'] as num?)?.toDouble() ?? 1000, + width: (json['width'] as num?)?.toDouble() ?? 900, height: (json['height'] as num?)?.toDouble() ?? 600, top: (json['top'] as num?)?.toDouble(), left: (json['left'] as num?)?.toDouble(), @@ -234,7 +234,7 @@ const _$ProxyCardTypeEnumMap = { _$ThemePropsImpl _$$ThemePropsImplFromJson(Map json) => _$ThemePropsImpl( - primaryColor: (json['primaryColor'] as num?)?.toInt() ?? 0xFF795548, + primaryColor: (json['primaryColor'] as num?)?.toInt(), themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? ThemeMode.system, prueBlack: json['prueBlack'] as bool? ?? false, diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index 57296ce..f92fa5c 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -3,9 +3,11 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; class App { static App? _instance; @@ -20,6 +22,12 @@ class App { if (onExit != null) { await onExit!(); } + case "getText": + try { + return Intl.message(call.arguments as String); + } catch (_) { + return ""; + } default: throw MissingPluginException(); } @@ -78,6 +86,13 @@ class App { }); } + Future initShortcuts() async { + return await methodChannel.invokeMethod( + "initShortcuts", + appLocalizations.toggle, + ); + } + Future updateExcludeFromRecents(bool value) async { return await methodChannel.invokeMethod("updateExcludeFromRecents", { "value": value, diff --git a/lib/router/fade_page.dart b/lib/router/fade_page.dart deleted file mode 100644 index e78ad7f..0000000 --- a/lib/router/fade_page.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; - -class FadePage extends Page { - final Widget child; - final bool maintainState; - final bool fullscreenDialog; - final bool allowSnapshotting; - - const FadePage({ - required this.child, - this.maintainState = true, - this.fullscreenDialog = false, - this.allowSnapshotting = true, - super.key, - super.name, - super.arguments, - super.restorationId, - }); - - @override - Route createRoute(BuildContext context) { - return FadePageRoute(page: this); - } -} - -class FadePageRoute extends PageRoute { - final FadePage page; - - FadePageRoute({ - required this.page, - }) : super(settings: page); - - FadePage get _page => settings as FadePage; - - @override - Widget buildPage(BuildContext context, Animation animation, - Animation secondaryAnimation) { - return _page.child; - } - - @override - Widget buildTransitions(BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { - return FadeTransition( - opacity: animation, - child: child, - ); - } - - @override - Duration get transitionDuration => const Duration(milliseconds: 600); - - @override - bool get maintainState => false; - - @override - Color? get barrierColor => null; - - @override - String? get barrierLabel => null; -} diff --git a/lib/state.dart b/lib/state.dart index 87d6967..f9f172e 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -70,10 +70,7 @@ class GlobalState { appState.versionInfo = clashCore.getVersionInfo(); } - handleStart({ - required Config config, - required ClashConfig clashConfig, - }) async { + handleStart() async { clashCore.start(); if (globalState.isVpnService) { await vpn?.startVpn(); @@ -81,8 +78,6 @@ class GlobalState { return; } startTime ??= DateTime.now(); - await preferences.saveClashConfig(clashConfig); - await preferences.saveConfig(config); await service?.init(); startListenUpdate(); } diff --git a/lib/widgets/builder.dart b/lib/widgets/builder.dart index 703280c..b4bd9c4 100644 --- a/lib/widgets/builder.dart +++ b/lib/widgets/builder.dart @@ -68,6 +68,8 @@ class ProxiesActionsBuilder extends StatelessWidget { typedef StateWidgetBuilder = Widget Function(T state); +typedef StateAndChildWidgetBuilder = Widget Function(T state, Widget? child); + class LocaleBuilder extends StatelessWidget { final StateWidgetBuilder builder; @@ -86,3 +88,30 @@ class LocaleBuilder extends StatelessWidget { ); } } + +class ActiveBuilder extends StatelessWidget { + final String label; + final StateAndChildWidgetBuilder builder; + final Widget? child; + + const ActiveBuilder({ + super.key, + required this.label, + required this.builder, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.currentLabel == label, + builder: (_, state, child) { + return builder( + state, + child, + ); + }, + child: child, + ); + } +} diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 16195d0..9d68da6 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -359,15 +359,12 @@ class ListItem extends StatelessWidget { ); return; } - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => CommonScaffold( - key: Key(nextDelegate.title), - body: nextDelegate.widget, - title: nextDelegate.title, - ), - ), - ); + + BaseNavigator.push(context, CommonScaffold( + key: Key(nextDelegate.title), + body: nextDelegate.widget, + title: nextDelegate.title, + )); }, ); } diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 2bdaca3..b4fdb9c 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -50,6 +50,7 @@ class CommonScaffold extends StatefulWidget { class CommonScaffoldState extends State { final ValueNotifier> _actions = ValueNotifier([]); + final ValueNotifier _floatingActionButton = ValueNotifier(null); final ValueNotifier _loading = ValueNotifier(false); set actions(List actions) { @@ -58,6 +59,12 @@ class CommonScaffoldState extends State { } } + set floatingActionButton(Widget floatingActionButton) { + if (_floatingActionButton.value != floatingActionButton) { + _floatingActionButton.value = floatingActionButton; + } + } + Future loadingRun( Future Function() futureFunction, { String? title, @@ -82,6 +89,7 @@ class CommonScaffoldState extends State { @override void dispose() { _actions.dispose(); + _floatingActionButton.dispose(); super.dispose(); } @@ -90,6 +98,7 @@ class CommonScaffoldState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.title != widget.title) { _actions.value = []; + _floatingActionButton.value = null; } } @@ -99,60 +108,66 @@ class CommonScaffoldState extends State { @override Widget build(BuildContext context) { - final scaffold = Scaffold( - resizeToAvoidBottomInset: true, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ValueListenableBuilder>( - valueListenable: _actions, - builder: (_, actions, __) { - final realActions = + final scaffold = ValueListenableBuilder( + valueListenable: _floatingActionButton, + builder: (_, value, __) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + ValueListenableBuilder>( + valueListenable: _actions, + builder: (_, actions, __) { + final realActions = actions.isNotEmpty ? actions : widget.actions; - return AppBar( - centerTitle: false, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: + return AppBar( + centerTitle: false, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark ? Brightness.light : Brightness.dark, - systemNavigationBarIconBrightness: + systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.dark ? Brightness.light : Brightness.dark, - systemNavigationBarColor: widget.bottomNavigationBar != null - ? context.colorScheme.surfaceContainer - : context.colorScheme.surface, - systemNavigationBarDividerColor: Colors.transparent, - ), - automaticallyImplyLeading: widget.automaticallyImplyLeading, - leading: widget.leading, - title: Text(widget.title), - actions: [ - ...?realActions, - const SizedBox( - width: 8, - ) - ], - ); - }, + systemNavigationBarColor: widget.bottomNavigationBar != null + ? context.colorScheme.surfaceContainer + : context.colorScheme.surface, + systemNavigationBarDividerColor: Colors.transparent, + ), + automaticallyImplyLeading: widget.automaticallyImplyLeading, + leading: widget.leading, + title: Text(widget.title), + actions: [ + ...?realActions, + const SizedBox( + width: 8, + ) + ], + ); + }, + ), + ValueListenableBuilder( + valueListenable: _loading, + builder: (_, value, __) { + return value == true + ? const LinearProgressIndicator() + : Container(); + }, + ), + ], ), - ValueListenableBuilder( - valueListenable: _loading, - builder: (_, value, __) { - return value == true - ? const LinearProgressIndicator() - : Container(); - }, - ), - ], - ), - ), - body: body, - bottomNavigationBar: widget.bottomNavigationBar, + ), + body: body, + floatingActionButton: value, + bottomNavigationBar: widget.bottomNavigationBar, + ); + }, ); return _sideNavigationBar != null ? Row( diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index 63d8627..e409293 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -2,6 +2,7 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'side_sheet.dart'; @@ -23,12 +24,11 @@ showExtendPage( final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; if (isMobile) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => CommonScaffold( - title: title, - body: uniqueBody, - ), + BaseNavigator.push( + context, + CommonScaffold( + title: title, + body: uniqueBody, ), ); return; diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 932b7c5..190228d 100755 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -583,7 +583,7 @@ "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/"; - PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -711,7 +711,7 @@ "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/"; - PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -733,7 +733,7 @@ "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/"; - PRODUCT_BUNDLE_IDENTIFIER = com.clash.follow; + PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 6e3b8ca..a4f796f 100755 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 821aec8..df57c8d 100755 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -21,8 +21,8 @@ CFBundleURLTypes - CFBundleTypeRole - Editor + CFBundleTypeRole + Editor CFBundleURLName CFBundleURLSchemes diff --git a/pubspec.yaml b/pubspec.yaml index 61d32e9..d97286b 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.66+202410262 +version: 0.8.67+202411091 environment: sdk: '>=3.1.0 <4.0.0'