Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
a949b26dec Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies
2025-09-07 01:23:17 +08:00
28 changed files with 379 additions and 267 deletions

View File

@@ -52,6 +52,7 @@ jobs:
if: startsWith(matrix.platform,'android')
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "${{ secrets.SERVICE_JSON }}" | base64 --decode > android/app/google-services.json
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties

View File

@@ -5,6 +5,8 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
val localPropertiesFile = rootProject.file("local.properties")
@@ -18,10 +20,9 @@ val mStoreFile: File = file("keystore.jks")
val mStorePassword: String? = localProperties.getProperty("storePassword")
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
val isRelease = mStoreFile.exists()
&& mStorePassword != null
&& mKeyAlias != null
&& mKeyPassword != null
val isRelease =
mStoreFile.exists() && mStorePassword != null && mKeyAlias != null && mKeyPassword != null
android {
namespace = "com.follow.clash"
@@ -76,8 +77,7 @@ android {
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
@@ -93,6 +93,7 @@ flutter {
source = "../.."
}
dependencies {
implementation(project(":service"))
implementation(project(":common"))
@@ -101,4 +102,7 @@ dependencies {
implementation(libs.smali.dexlib2) {
exclude(group = "com.google.guava", module = "guava")
}
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics.ndk)
implementation(libs.firebase.analytics)
}

View File

@@ -0,0 +1,46 @@
{
"project_info": {
"project_number": "000000000000",
"project_id": "dev"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.follow.clash.debug"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
]
}

View File

@@ -3,6 +3,9 @@ package com.follow.clash
import android.app.Application
import android.content.Context
import com.follow.clash.common.GlobalState
import com.follow.clash.common.processName
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
class Application : Application() {
@@ -10,4 +13,21 @@ class Application : Application() {
super.attachBaseContext(base)
GlobalState.init(this)
}
override fun onCreate() {
super.onCreate()
initRemoteCrashlytics(this)
}
private fun initRemoteCrashlytics(context: Context) {
try {
if (processName?.endsWith(":remote") == true) {
FirebaseApp.initializeApp(context)
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = true
GlobalState.log("init remote crashlytics")
}
} catch (e: Exception) {
GlobalState.log("initRemoteCrashlytics error: $e")
}
}
}

View File

@@ -1,6 +1,7 @@
package com.follow.clash.common
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -20,6 +21,7 @@ import android.os.IBinder
import android.os.Looper
import android.os.RemoteException
import android.util.Log
import androidx.core.content.getSystemService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -67,6 +69,16 @@ val QuickAction.quickIntent: Intent
val BroadcastAction.action: String
get() = "${GlobalState.application.packageName}.intent.action.${this.name}"
val Context.processName: String?
get() {
val pid = android.os.Process.myPid()
val activityManager = getSystemService<ActivityManager>()
activityManager?.runningAppProcesses?.find { it.pid == pid }?.let {
return it.processName
}
return null
}
val BroadcastAction.quickIntent: Intent
get() = Components.BROADCAST_RECEIVER.intent.apply {
action = this@quickIntent.action

View File

@@ -1,5 +1,6 @@
package com.follow.clash.common
import android.app.Application
import android.util.Log
import kotlinx.coroutines.CoroutineScope

View File

@@ -1,5 +1,6 @@
[versions]
#agp = "8.10.1"
firebaseBom = "34.2.0"
minSdk = "23"
targetSdk = "36"
compileSdk = "36"
@@ -10,11 +11,18 @@ coreSplashscreen = "1.0.1"
gson = "2.13.1"
kotlin = "2.2.10"
smaliDexlib2 = "3.0.9"
firebaseCrashlyticsKtx = "20.0.1"
firebaseCommonKtx = "22.0.0"
[libraries]
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
annotation-jvm = { module = "androidx.annotation:annotation-jvm", version.ref = "annotationJvm" }
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics-ndk" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }
firebase-crashlytics-ktx = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" }
firebase-common-ktx = { group = "com.google.firebase", name = "firebase-common-ktx", version.ref = "firebaseCommonKtx" }

View File

@@ -13,7 +13,11 @@ val Traffic.speedText: String
get() = "${up.formatBytes}/s↑ ${down.formatBytes}/s↓"
fun Core.getSpeedTrafficText(onlyStatisticsProxy: Boolean): String {
val res = getTraffic(onlyStatisticsProxy)
val traffic = Gson().fromJson(res, Traffic::class.java)
return traffic.speedText
try {
val res = getTraffic(onlyStatisticsProxy)
val traffic = Gson().fromJson(res, Traffic::class.java)
return traffic.speedText
} catch (_: Exception) {
return ""
}
}

View File

@@ -18,8 +18,10 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.12.1" apply false
id("com.android.application") version "8.12.2" apply false
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
id("com.google.gms.google-services") version ("4.3.15") apply false
id("com.google.firebase.crashlytics") version ("2.8.1") apply false
}

View File

@@ -409,7 +409,7 @@
"autoSetSystemDns": "Auto set system DNS",
"details": "{label} details",
"creationTime": "Creation time",
"progress": "Progress",
"process": "Process",
"host": "Host",
"destination": "Destination",
"destinationGeoIP": "Destination GeoIP",

View File

@@ -410,7 +410,7 @@
"autoSetSystemDns": "オートセットシステムDNS",
"details": "{label}詳細",
"creationTime": "作成時間",
"progress": "進捗",
"process": "プロセス",
"host": "ホスト",
"destination": "宛先",
"destinationGeoIP": "宛先地理情報",

View File

@@ -410,7 +410,7 @@
"autoSetSystemDns": "Автоматическая настройка системного DNS",
"details": "Детали {}",
"creationTime": "Время создания",
"progress": "Прогресс",
"process": "процесс",
"host": "Хост",
"destination": "Назначение",
"destinationGeoIP": "Геолокация назначения",

View File

@@ -410,7 +410,7 @@
"autoSetSystemDns": "自动设置系统DNS",
"details": "{label}详情",
"creationTime": "创建时间",
"progress": "进",
"process": "进",
"host": "主机",
"destination": "目标地址",
"destinationGeoIP": "目标地理定位",

View File

@@ -3,7 +3,6 @@ package main
import (
"context"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable"
@@ -145,7 +144,7 @@ func handleGetTraffic(onlyStatisticsProxy bool) string {
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
log.Errorln("Error: %s", err)
return ""
}
return string(data)
@@ -159,7 +158,7 @@ func handleGetTotalTraffic(onlyStatisticsProxy bool) string {
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
log.Errorln("Error: %s", err)
return ""
}
return string(data)
@@ -229,7 +228,7 @@ func handleGetConnections() string {
snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot)
if err != nil {
fmt.Println("Error:", err)
log.Errorln("Error: %s", err)
return ""
}
return string(data)

View File

@@ -125,10 +125,8 @@ class ApplicationState extends ConsumerState<Application> {
builder: (_, child) {
return AppEnvManager(
child: _buildApp(
child: AppSidebarContainer(
child: _buildPlatformState(
child: _buildState(child: _buildPlatformApp(child: child!)),
),
child: _buildPlatformState(
child: _buildState(child: _buildPlatformApp(child: child!)),
),
),
);

View File

@@ -7,28 +7,17 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<CommonScaffoldState>();
}
Future<void>? showNotifier(String text) {
void showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
void showSnackBar(
String message, {
SnackBarAction? action,
}) {
void showSnackBar(String message, {SnackBarAction? action}) {
final width = viewWidth;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 16,
right: 16,
left: 16,
);
margin = const EdgeInsets.only(bottom: 16, right: 16, left: 16);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
margin = EdgeInsets.only(bottom: 16, left: 16, right: width - 316);
}
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
@@ -76,8 +65,11 @@ extension BuildContextExtension on BuildContext {
class BackHandleInherited extends InheritedWidget {
final Function handleBack;
const BackHandleInherited(
{super.key, required this.handleBack, required super.child});
const BackHandleInherited({
super.key,
required this.handleBack,
required super.child,
});
static BackHandleInherited? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<BackHandleInherited>();

View File

@@ -528,11 +528,9 @@ class AppController {
Future<void> init() async {
FlutterError.onError = (details) {
if (kDebugMode) {
commonPrint.log(
'exception: ${details.exception} stack: ${details.stack}',
);
}
commonPrint.log(
'exception: ${details.exception} stack: ${details.stack}',
);
};
updateTray(true);
autoUpdateProfiles();
@@ -622,6 +620,7 @@ class AppController {
Future<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
context: context,
dismissible: false,
child: CommonDialog(
title: appLocalizations.disclaimer,

View File

@@ -521,6 +521,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Please press the keyboard.",
),
"preview": MessageLookupByLibrary.simpleMessage("Preview"),
"process": MessageLookupByLibrary.simpleMessage("Process"),
"profile": MessageLookupByLibrary.simpleMessage("Profile"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage(
@@ -547,7 +548,6 @@ class MessageLookup extends MessageLookupByLibrary {
),
"profiles": MessageLookupByLibrary.simpleMessage("Profiles"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"),
"progress": MessageLookupByLibrary.simpleMessage("Progress"),
"project": MessageLookupByLibrary.simpleMessage("Project"),
"providers": MessageLookupByLibrary.simpleMessage("Providers"),
"proxies": MessageLookupByLibrary.simpleMessage("Proxies"),

View File

@@ -395,6 +395,7 @@ class MessageLookup extends MessageLookupByLibrary {
"preferH3Desc": MessageLookupByLibrary.simpleMessage("DOHのHTTP/3を優先使用"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("キーボードを押してください"),
"preview": MessageLookupByLibrary.simpleMessage("プレビュー"),
"process": MessageLookupByLibrary.simpleMessage("プロセス"),
"profile": MessageLookupByLibrary.simpleMessage("プロファイル"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("有効な間隔形式を入力してください"),
@@ -417,7 +418,6 @@ class MessageLookup extends MessageLookupByLibrary {
),
"profiles": MessageLookupByLibrary.simpleMessage("プロファイル一覧"),
"profilesSort": MessageLookupByLibrary.simpleMessage("プロファイルの並び替え"),
"progress": MessageLookupByLibrary.simpleMessage("進捗"),
"project": MessageLookupByLibrary.simpleMessage("プロジェクト"),
"providers": MessageLookupByLibrary.simpleMessage("プロバイダー"),
"proxies": MessageLookupByLibrary.simpleMessage("プロキシ"),

View File

@@ -548,6 +548,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Пожалуйста, нажмите клавишу.",
),
"preview": MessageLookupByLibrary.simpleMessage("Предпросмотр"),
"process": MessageLookupByLibrary.simpleMessage("процесс"),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage(
@@ -574,7 +575,6 @@ class MessageLookup extends MessageLookupByLibrary {
),
"profiles": MessageLookupByLibrary.simpleMessage("Профили"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Сортировка профилей"),
"progress": MessageLookupByLibrary.simpleMessage("Прогресс"),
"project": MessageLookupByLibrary.simpleMessage("Проект"),
"providers": MessageLookupByLibrary.simpleMessage("Провайдеры"),
"proxies": MessageLookupByLibrary.simpleMessage("Прокси"),

View File

@@ -347,6 +347,7 @@ class MessageLookup extends MessageLookupByLibrary {
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
"preview": MessageLookupByLibrary.simpleMessage("预览"),
"process": MessageLookupByLibrary.simpleMessage("进程"),
"profile": MessageLookupByLibrary.simpleMessage("配置"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
@@ -367,7 +368,6 @@ class MessageLookup extends MessageLookupByLibrary {
),
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"progress": MessageLookupByLibrary.simpleMessage("进度"),
"project": MessageLookupByLibrary.simpleMessage("项目"),
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"),

View File

@@ -3159,9 +3159,9 @@ class AppLocalizations {
);
}
/// `Progress`
String get progress {
return Intl.message('Progress', name: 'progress', desc: '', args: []);
/// `Process`
String get process {
return Intl.message('Process', name: 'process', desc: '', args: []);
}
/// `Host`

View File

@@ -181,102 +181,99 @@ class AppSidebarContainer extends ConsumerWidget {
return child;
}
final currentIndex = navigationState.currentIndex;
final isExtend = ref.watch(appSettingProvider).showLabel;
final showLabel = ref.watch(appSettingProvider).showLabel;
return Row(
children: [
Stack(
alignment: Alignment.topRight,
children: [
_buildBackground(
context: context,
child: SafeArea(
child: AnimatedSize(
curve: Curves.easeOut,
alignment: Alignment.centerLeft,
duration: commonDuration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (system.isMacOS) SizedBox(height: 22),
SizedBox(height: 10),
if (!system.isMacOS) ...[
ClipRect(
child: Padding(
padding: EdgeInsets.only(left: 20),
child: AppIcon(),
),
),
SizedBox(height: 12),
],
Expanded(
child: ScrollConfiguration(
behavior: HiddenBarScrollBehavior(),
child: SingleChildScrollView(
child: IntrinsicHeight(
child: NavigationRail(
backgroundColor: Colors.transparent,
selectedLabelTextStyle: context
.textTheme
.labelLarge!
.copyWith(
color: context.colorScheme.onSurface,
_buildBackground(
context: context,
child: Stack(
alignment: Alignment.topRight,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (system.isMacOS) SizedBox(height: 22),
SizedBox(height: 10),
if (!system.isMacOS) ...[
ClipRect(
child: Padding(
padding: EdgeInsets.only(left: 20),
child: AppIcon(),
),
),
SizedBox(height: 12),
],
Expanded(
child: ScrollConfiguration(
behavior: HiddenBarScrollBehavior(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: NavigationRail(
scrollable: true,
minExtendedWidth: 200,
backgroundColor: Colors.transparent,
selectedLabelTextStyle: context
.textTheme
.labelLarge!
.copyWith(
color: context.colorScheme.onSurface,
),
unselectedLabelTextStyle: context
.textTheme
.labelLarge!
.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(Intl.message(e.label.name)),
),
unselectedLabelTextStyle: context
.textTheme
.labelLarge!
.copyWith(
color: context.colorScheme.onSurface,
),
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(Intl.message(e.label.name)),
),
)
.toList(),
onDestinationSelected: (index) {
globalState.appController.toPage(
navigationItems[index].label,
);
},
extended: isExtend,
selectedIndex: currentIndex,
// labelType: showLabel
// ? NavigationRailLabelType.all
// : NavigationRailLabelType.none,
),
)
.toList(),
onDestinationSelected: (index) {
globalState.appController.toPage(
navigationItems[index].label,
);
},
extended: false,
selectedIndex: currentIndex,
labelType: showLabel
? NavigationRailLabelType.all
: NavigationRailLabelType.none,
),
),
),
],
),
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: 20),
child: IconButton(
onPressed: () {
ref
.read(appSettingProvider.notifier)
.updateState(
(state) => state.copyWith(
showLabel: !state.showLabel,
),
);
},
icon: Icon(
Icons.menu,
color: context.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 16),
],
),
),
),
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: 20),
child: IconButton(
onPressed: () {
ref
.read(appSettingProvider.notifier)
.updateState(
(state) =>
state.copyWith(showLabel: !state.showLabel),
);
},
icon: Icon(
Icons.menu,
color: context.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(height: 16),
],
),
),
_buildLoading(),
],
_buildLoading(),
],
),
),
Expanded(
flex: 1,

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
@@ -17,8 +18,9 @@ class MessageManager extends StatefulWidget {
class MessageManagerState extends State<MessageManager> {
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
final List<CommonMessage> _bufferMessages = [];
bool _pushing = false;
final _bufferMessages = Queue<CommonMessage>();
final _activeTimers = <String, Timer>{};
bool _isDisplayingMessage = false;
@override
void initState() {
@@ -28,38 +30,48 @@ class MessageManagerState extends State<MessageManager> {
@override
void dispose() {
_messagesNotifier.dispose();
for (final timer in _activeTimers.values) {
timer.cancel();
}
_activeTimers.clear();
_bufferMessages.clear();
super.dispose();
}
Future<void> message(String text) async {
void message(String text) {
final commonMessage = CommonMessage(id: utils.uuidV4, text: text);
commonPrint.log(text);
_bufferMessages.add(commonMessage);
await _showMessage();
commonPrint.log('message: $text');
_processQueue();
}
Future<void> _showMessage() async {
if (_pushing == true) {
void _cancelMessage(String id) {
_bufferMessages.removeWhere((msg) => msg.id == id);
if (_activeTimers.containsKey(id)) {
_removeMessage(id);
}
}
void _processQueue() {
if (_isDisplayingMessage || _bufferMessages.isEmpty) {
return;
}
_pushing = true;
while (_bufferMessages.isNotEmpty) {
final commonMessage = _bufferMessages.removeAt(0);
_messagesNotifier.value = List.from(_messagesNotifier.value)
..add(commonMessage);
await Future.delayed(Duration(seconds: 1));
Future.delayed(commonMessage.duration, () {
_handleRemove(commonMessage);
});
}
_isDisplayingMessage = true;
final message = _bufferMessages.removeFirst();
_messagesNotifier.value = List.from(_messagesNotifier.value)..add(message);
final timer = Timer(message.duration, () {
_removeMessage(message.id);
});
_activeTimers[message.id] = timer;
}
Future<void> _handleRemove(CommonMessage commonMessage) async {
_messagesNotifier.value = List<CommonMessage>.from(_messagesNotifier.value)
..remove(commonMessage);
if (_bufferMessages.isEmpty) {
_pushing = false;
}
void _removeMessage(String id) {
_activeTimers.remove(id)?.cancel();
final currentMessages = List<CommonMessage>.from(_messagesNotifier.value);
currentMessages.removeWhere((msg) => msg.id == id);
_messagesNotifier.value = currentMessages;
_isDisplayingMessage = false;
_processQueue();
}
@override
@@ -83,35 +95,45 @@ class MessageManagerState extends State<MessageManager> {
: LayoutBuilder(
key: Key(messages.last.id),
builder: (_, constraints) {
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12.0),
return Dismissible(
key: ValueKey(messages.last.id),
onDismissed: (_) {
_cancelMessage(messages.last.id);
},
child: Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(14),
),
),
),
elevation: 10,
color: context.colorScheme.surfaceContainerHigh,
child: Container(
width: min(constraints.maxWidth, 500),
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(messages.last.text)),
IconButton(
visualDensity: VisualDensity.compact,
iconSize: 18,
padding: EdgeInsets.zero,
onPressed: () {
_handleRemove(messages.last);
},
icon: Icon(Icons.close),
),
],
elevation: 10,
color: context.colorScheme.surfaceContainerHigh,
child: Container(
width: min(constraints.maxWidth, 500),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
messages.last.text,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 16),
TextButton(
onPressed: () {
_cancelMessage(messages.last.id);
},
child: Text(appLocalizations.cancel),
),
],
),
),
),
);

View File

@@ -1,5 +1,6 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/manager/app_manager.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
@@ -16,83 +17,85 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomeBackScopeContainer(
child: Material(
color: context.colorScheme.surface,
child: Consumer(
builder: (context, ref, _) {
final state = ref.watch(navigationStateProvider);
final isMobile = state.viewMode == ViewMode.mobile;
final navigationItems = state.navigationItems;
final pageView = _HomePageView(
pageBuilder: (_, index) {
final navigationItem = state.navigationItems[index];
final navigationView = navigationItem.builder(context);
final view = KeepScope(
keep: navigationItem.keep,
child: isMobile
? navigationView
: Navigator(
pages: [MaterialPage(child: navigationView)],
onDidRemovePage: (_) {},
),
);
return view;
},
);
final currentIndex = state.currentIndex;
final bottomNavigationBar = NavigationBarTheme(
data: _NavigationBarDefaultsM3(context),
child: NavigationBar(
destinations: navigationItems
.map(
(e) => NavigationDestination(
icon: e.icon,
label: Intl.message(e.label.name),
),
)
.toList(),
onDestinationSelected: (index) {
globalState.appController.toPage(
navigationItems[index].label,
child: AppSidebarContainer(
child: Material(
color: context.colorScheme.surface,
child: Consumer(
builder: (context, ref, _) {
final state = ref.watch(navigationStateProvider);
final isMobile = state.viewMode == ViewMode.mobile;
final navigationItems = state.navigationItems;
final pageView = _HomePageView(
pageBuilder: (_, index) {
final navigationItem = state.navigationItems[index];
final navigationView = navigationItem.builder(context);
final view = KeepScope(
keep: navigationItem.keep,
child: isMobile
? navigationView
: Navigator(
pages: [MaterialPage(child: navigationView)],
onDidRemovePage: (_) {},
),
);
return view;
},
selectedIndex: currentIndex,
),
);
if (isMobile) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: globalState.appState.systemUiOverlayStyle.copyWith(
systemNavigationBarColor:
context.colorScheme.surfaceContainer,
);
final currentIndex = state.currentIndex;
final bottomNavigationBar = NavigationBarTheme(
data: _NavigationBarDefaultsM3(context),
child: NavigationBar(
destinations: navigationItems
.map(
(e) => NavigationDestination(
icon: e.icon,
label: Intl.message(e.label.name),
),
)
.toList(),
onDestinationSelected: (index) {
globalState.appController.toPage(
navigationItems[index].label,
);
},
selectedIndex: currentIndex,
),
child: Column(
children: [
Flexible(
flex: 1,
child: MediaQuery.removePadding(
removeTop: false,
removeBottom: true,
);
if (isMobile) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: globalState.appState.systemUiOverlayStyle.copyWith(
systemNavigationBarColor:
context.colorScheme.surfaceContainer,
),
child: Column(
children: [
Flexible(
flex: 1,
child: MediaQuery.removePadding(
removeTop: false,
removeBottom: true,
removeLeft: true,
removeRight: true,
context: context,
child: pageView,
),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: false,
removeLeft: true,
removeRight: true,
context: context,
child: pageView,
child: bottomNavigationBar,
),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: false,
removeLeft: true,
removeRight: true,
context: context,
child: bottomNavigationBar,
),
],
),
);
} else {
return pageView;
}
},
],
),
);
} else {
return pageView;
}
},
),
),
),
);

View File

@@ -183,12 +183,14 @@ class GlobalState {
}
Future<bool?> showMessage({
String? title,
required InlineSpan message,
BuildContext? context,
String? title,
String? confirmText,
bool cancelable = true,
}) async {
return await showCommonDialog<bool>(
context: context,
child: Builder(
builder: (context) {
return CommonDialog(
@@ -246,10 +248,12 @@ class GlobalState {
Future<T?> showCommonDialog<T>({
required Widget child,
BuildContext? context,
bool dismissible = true,
}) async {
return await showModal<T>(
context: navigatorKey.currentState!.context,
useRootNavigator: false,
context: context ?? globalState.navigatorKey.currentContext!,
configuration: FadeScaleTransitionConfiguration(
barrierColor: Colors.black38,
barrierDismissible: dismissible,

View File

@@ -202,7 +202,7 @@ class TrackerInfoDetailView extends StatelessWidget {
return rule;
}
String _getProgressText() {
String _getProcessText() {
final process = trackerInfo.metadata.process;
final uid = trackerInfo.metadata.uid;
if (uid != 0) {
@@ -297,8 +297,8 @@ class TrackerInfoDetailView extends StatelessWidget {
title: appLocalizations.creationTime,
desc: trackerInfo.start.showFull,
),
if (_getProgressText().isNotEmpty)
_buildItem(title: appLocalizations.progress, desc: _getProgressText()),
if (_getProcessText().isNotEmpty)
_buildItem(title: appLocalizations.process, desc: _getProcessText()),
_buildItem(
title: appLocalizations.networkType,
desc: trackerInfo.metadata.network,

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.88+2025090501
version: 0.8.88+2025090701
environment:
sdk: '>=3.8.0 <4.0.0'