Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
72bef5f672 Add android shortcuts
Fix init params issues

Fix dynamic color issues

Optimize navigator animate

Optimize window init

Optimize fab

Optimize save
2024-11-09 17:42:08 +08:00
52 changed files with 612 additions and 460 deletions

View File

@@ -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

View File

@@ -10,7 +10,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -23,8 +24,8 @@
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
@@ -73,11 +74,11 @@
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.START" />
<action android:name="${applicationId}.action.STOP" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.STOP" />
<action android:name="${applicationId}.action.CHANGE" />
</intent-filter>
</activity>

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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 <T> 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)
}
})
}
}

View File

@@ -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<Boolean>("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<Package> {
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<String>("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() {

View File

@@ -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

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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)
}
}
})

View File

@@ -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<Application> {
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
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<Application> {
}
await globalState.appController.init();
globalState.appController.initLink();
app?.initShortcuts();
});
}

View File

@@ -29,4 +29,5 @@ export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'network.dart';
export 'network.dart';
export 'navigator.dart';

11
lib/common/navigator.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
return await Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) => child,
),
);
}
}

View File

@@ -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<String> parseReleaseBody(String? body) {

View File

@@ -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 {

View File

@@ -29,6 +29,7 @@ class AppController {
late Function updateGroupDebounce;
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -38,6 +39,9 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
});
savePreferencesDebounce = debounce<Function()>(() async {
await savePreferences();
});
applyProfileDebounce = debounce<Function()>(() 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,
);

View File

@@ -21,12 +21,25 @@ class DashboardFragment extends StatefulWidget {
}
class _DashboardFragmentState extends State<DashboardFragment> {
_initFab(bool isCurrent) {
if(!isCurrent){
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
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(

View File

@@ -19,9 +19,10 @@ class _StartButtonState extends State<StartButton>
@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<StartButton>
)
.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<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
),
child: Selector<AppFlowingState, int?>(
selector: (_, appFlowingState) => appFlowingState.runTime,
builder: (_, int? value, __) {
final text = other.getTimeText(value);
return Text(
text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
);
},
),
);
}

View File

@@ -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<String>(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),

View File

@@ -80,7 +80,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
}
}
_initScaffoldState() {
_initScaffold() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
if (!mounted) return;
@@ -112,71 +112,67 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
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<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
columns: other.getProfilesColumns(appState.viewWidth),
),
),
child: Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'profiles',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initScaffoldState();
}
return child!;
},
child: Selector2<AppState, Config, ProfilesSelectorState>(
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,
),
),
],
),
),
);
},
),
);
}

View File

@@ -191,7 +191,7 @@ class ProxiesSetting extends StatelessWidget {
_buildGroupStyleSetting() {
return generateSection(
title: "图标样式",
title: appLocalizations.iconStyle,
items: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),

View File

@@ -278,6 +278,23 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
);
}
initFab(bool isCurrent, List<Proxy> proxies) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest(
proxies,
);
},
);
});
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
@@ -303,11 +320,11 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
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<ProxyGroupView> {
}
}
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<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
State<DelayTestButton> createState() => _DelayTestButtonState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
class _DelayTestButtonState extends State<DelayTestButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
@@ -401,29 +415,23 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
@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,
);
}
}

View File

@@ -163,7 +163,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,

View File

@@ -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"
}

View File

@@ -322,5 +322,6 @@
"adminAutoLaunch": "管理员自启动",
"adminAutoLaunchDesc": "使用管理员模式开机自启动",
"fontFamily": "字体",
"systemFont": "系统字体"
"systemFont": "系统字体",
"toggle": "切换"
}

View File

@@ -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"),

View File

@@ -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("虚拟网卡"),

View File

@@ -3289,6 +3289,16 @@ class AppLocalizations {
args: [],
);
}
/// `Toggle`
String get toggle {
return Intl.message(
'Toggle',
name: 'toggle',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -53,6 +53,10 @@ Future<void> 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<void> 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(

View File

@@ -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<AppStateManager>
with WidgetsBindingObserver {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
@@ -45,6 +46,22 @@ class _AppStateManagerState extends State<AppStateManager>
);
}
_cacheStateChange(Widget child) {
return Selector2<Config, ClashConfig, String>(
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<AppStateManager>
Future<void> 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<AppStateManager>
@override
Widget build(BuildContext context) {
return _updateNavigationsContainer(
widget.child,
return _cacheStateChange(
_updateNavigationsContainer(
widget.child,
),
);
}
}

View File

@@ -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<TileManager> with TileListener {
}
@override
void onStop() {
Future<void> onStop() async {
globalState.appController.updateStatus(false);
super.onStop();
}

View File

@@ -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;

View File

@@ -20,7 +20,8 @@ class WindowManager extends StatefulWidget {
State<WindowManager> createState() => _WindowContainerState();
}
class _WindowContainerState extends State<WindowManager> with WindowListener, WindowExtListener {
class _WindowContainerState extends State<WindowManager>
with WindowListener, WindowExtListener {
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) {
@@ -82,7 +83,7 @@ class _WindowContainerState extends State<WindowManager> with WindowListener, Wi
@override
void onWindowMinimize() async {
await globalState.appController.savePreferences();
globalState.appController.savePreferencesDebounce();
super.onWindowMinimize();
}

View File

@@ -371,4 +371,9 @@ class ClashConfig extends ChangeNotifier {
factory ClashConfig.fromJson(Map<String, dynamic> 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}';
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, Object?>? 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<String, Object?> json) => _$ThemePropsFromJson(json);
factory ThemeProps.fromJson(Map<String, Object?> json) =>
_$ThemePropsFromJson(json);
factory ThemeProps.realFromJson(Map<String, Object?>? 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<String, dynamic> 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}';
}
}

View File

@@ -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<String, dynamic> 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()

View File

@@ -128,7 +128,7 @@ const _$AccessSortTypeEnumMap = {
_$WindowPropsImpl _$$WindowPropsImplFromJson(Map<String, dynamic> 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<String, dynamic> 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,

View File

@@ -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<bool?> initShortcuts() async {
return await methodChannel.invokeMethod<bool>(
"initShortcuts",
appLocalizations.toggle,
);
}
Future<bool?> updateExcludeFromRecents(bool value) async {
return await methodChannel.invokeMethod<bool>("updateExcludeFromRecents", {
"value": value,

View File

@@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
class FadePage<T> extends Page<T> {
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<T> createRoute(BuildContext context) {
return FadePageRoute<T>(page: this);
}
}
class FadePageRoute<T> extends PageRoute<T> {
final FadePage page;
FadePageRoute({
required this.page,
}) : super(settings: page);
FadePage<T> get _page => settings as FadePage<T>;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return _page.child;
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> 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;
}

View File

@@ -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();
}

View File

@@ -68,6 +68,8 @@ class ProxiesActionsBuilder extends StatelessWidget {
typedef StateWidgetBuilder<T> = Widget Function(T state);
typedef StateAndChildWidgetBuilder<T> = Widget Function(T state, Widget? child);
class LocaleBuilder extends StatelessWidget {
final StateWidgetBuilder<String?> builder;
@@ -86,3 +88,30 @@ class LocaleBuilder extends StatelessWidget {
);
}
}
class ActiveBuilder extends StatelessWidget {
final String label;
final StateAndChildWidgetBuilder<bool> builder;
final Widget? child;
const ActiveBuilder({
super.key,
required this.label,
required this.builder,
required this.child,
});
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == label,
builder: (_, state, child) {
return builder(
state,
child,
);
},
child: child,
);
}
}

View File

@@ -359,15 +359,12 @@ class ListItem<T> 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,
));
},
);
}

View File

@@ -50,6 +50,7 @@ class CommonScaffold extends StatefulWidget {
class CommonScaffoldState extends State<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<dynamic> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) {
@@ -58,6 +59,12 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget floatingActionButton) {
if (_floatingActionButton.value != floatingActionButton) {
_floatingActionButton.value = floatingActionButton;
}
}
Future<T?> loadingRun<T>(
Future<T> Function() futureFunction, {
String? title,
@@ -82,6 +89,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
void dispose() {
_actions.dispose();
_floatingActionButton.dispose();
super.dispose();
}
@@ -90,6 +98,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
super.didUpdateWidget(oldWidget);
if (oldWidget.title != widget.title) {
_actions.value = [];
_floatingActionButton.value = null;
}
}
@@ -99,60 +108,66 @@ class CommonScaffoldState extends State<CommonScaffold> {
@override
Widget build(BuildContext context) {
final scaffold = Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
children: [
ValueListenableBuilder<List<Widget>>(
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<List<Widget>>(
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(

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -1,7 +1,7 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {

View File

@@ -21,8 +21,8 @@
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>

View File

@@ -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'