Fix url validate issues 2
Add android hidden from the recent task Add geoip file Support modify geoData URL
This commit is contained in:
@@ -2,21 +2,19 @@ package com.follow.clash.plugins
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.extensions.getBase64
|
||||
import com.follow.clash.extensions.getInetSocketAddress
|
||||
import com.follow.clash.extensions.getProtocol
|
||||
import com.follow.clash.models.Process
|
||||
import com.follow.clash.models.Package
|
||||
import com.follow.clash.models.Process
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
@@ -30,6 +28,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private var activity: Activity? = null
|
||||
@@ -71,6 +70,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"updateExcludeFromRecents" -> {
|
||||
val value = call.argument<Boolean>("value")
|
||||
updateExcludeFromRecents(value)
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
"getPackages" -> {
|
||||
scope.launch {
|
||||
result.success(getPackages())
|
||||
@@ -115,7 +120,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q){
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
result.success(null)
|
||||
return@withContext
|
||||
}
|
||||
@@ -158,6 +163,20 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
if (context == null) return
|
||||
val am = getSystemService(context!!, ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull { task ->
|
||||
task.taskInfo.baseIntent.component?.packageName == context!!.packageName
|
||||
}
|
||||
when (value) {
|
||||
true -> task?.setExcludeFromRecents(value)
|
||||
false -> task?.setExcludeFromRecents(value)
|
||||
null -> task?.setExcludeFromRecents(false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = context?.packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
|
||||
BIN
assets/data/GeoIP.dat
Normal file
BIN
assets/data/GeoIP.dat
Normal file
Binary file not shown.
Submodule core/Clash.Meta updated: 8a64960265...bf7f866d02
@@ -337,7 +337,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.ExternalUIURL = ""
|
||||
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
|
||||
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
|
||||
targetConfig.GeodataMode = false
|
||||
//targetConfig.GeodataMode = false
|
||||
targetConfig.IPv6 = patchConfig.IPv6
|
||||
targetConfig.LogLevel = patchConfig.LogLevel
|
||||
targetConfig.Port = 0
|
||||
|
||||
21
core/hub.go
21
core/hub.go
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/structure"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
@@ -384,24 +385,30 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
case "MMDB":
|
||||
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := mmdb.DownloadGeoSite(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString))
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
|
||||
@@ -17,6 +17,7 @@ class ClashService {
|
||||
}
|
||||
const geoFileNameList = [
|
||||
mmdbFileName,
|
||||
geoIpFileName,
|
||||
geoSiteFileName,
|
||||
asnFileName,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fl_clash/models/clash_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const appName = "FlClash";
|
||||
@@ -10,8 +11,15 @@ const moreDuration = Duration(milliseconds: 100);
|
||||
const animateDuration = Duration(milliseconds: 100);
|
||||
const defaultUpdateDuration = Duration(days: 1);
|
||||
const mmdbFileName = "geoip.metadb";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
const asnFileName = "ASN.mmdb";
|
||||
const geoIpFileName = "GeoIP.dat";
|
||||
const geoSiteFileName = "GeoSite.dat";
|
||||
const GeoXMap defaultGeoXMap = {
|
||||
"mmdb":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
|
||||
"asn":"https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
|
||||
"geoip":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
|
||||
"geosite":"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
|
||||
};
|
||||
const profilesDirectoryName = "profiles";
|
||||
const localhost = "127.0.0.1";
|
||||
const clashConfigKey = "clash_config";
|
||||
|
||||
@@ -39,6 +39,9 @@ class Other {
|
||||
}
|
||||
final diff = timeStamp / 1000;
|
||||
final inHours = (diff / 3600).floor();
|
||||
if (inHours > 99) {
|
||||
return "99:59:59";
|
||||
}
|
||||
final inMinutes = (diff / 60 % 60).floor();
|
||||
final inSeconds = (diff % 60).floor();
|
||||
|
||||
@@ -171,7 +174,7 @@ class Other {
|
||||
}
|
||||
|
||||
List<String> parseReleaseBody(String? body) {
|
||||
if(body == null) return [];
|
||||
if (body == null) return [];
|
||||
const pattern = r'- (.+?)\. \[.+?\]';
|
||||
final regex = RegExp(pattern);
|
||||
return regex
|
||||
@@ -181,7 +184,7 @@ class Other {
|
||||
.toList();
|
||||
}
|
||||
|
||||
ViewMode getViewMode(double viewWidth){
|
||||
ViewMode getViewMode(double viewWidth) {
|
||||
if (viewWidth <= maxMobileWidth) return ViewMode.mobile;
|
||||
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
|
||||
return ViewMode.desktop;
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
return RegExp(
|
||||
r'^(https?:\/\/)?'
|
||||
r'((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|'
|
||||
r'((\d{1,3}\.){3}\d{1,3}))'
|
||||
r'(:\d+)?'
|
||||
r'(\/[-a-z\d%_.~+]*)*'
|
||||
r'(\?[;&a-z\d%_.~+=-]*)?'
|
||||
r'(\#[-a-z\d_]*)?$',
|
||||
caseSensitive: false,
|
||||
).hasMatch(this);
|
||||
return RegExp(r'^(http|https|ftp)://').hasMatch(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,24 @@ class ApplicationSettingFragment extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.isExclude,
|
||||
builder: (_, isExclude, child) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.visibility_off),
|
||||
title: Text(appLocalizations.exclude),
|
||||
subtitle: Text(appLocalizations.excludeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isExclude,
|
||||
onChanged: (value) {
|
||||
final config = context.read<Config>();
|
||||
config.isExclude = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.isAnimateToPage,
|
||||
|
||||
@@ -35,7 +35,7 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
|
||||
});
|
||||
if (res != true) return;
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.recovery,
|
||||
title: appLocalizations.backup,
|
||||
message: TextSpan(text: appLocalizations.backupSuccess),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -387,7 +387,6 @@ class _ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
final lines = (sortedProxies.length / columns).ceil();
|
||||
final minLines =
|
||||
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
|
||||
final hasScrollable = lines > minLines;
|
||||
final height = (itemHeight + 8) * min(lines, minLines) - 8;
|
||||
return Selector<Config, Set<String>>(
|
||||
selector: (_, config) => config.currentUnfoldSet,
|
||||
|
||||
@@ -7,14 +7,17 @@ import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' hide context;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@immutable
|
||||
class GeoItem {
|
||||
final String label;
|
||||
final String key;
|
||||
final String fileName;
|
||||
|
||||
const GeoItem({
|
||||
required this.label,
|
||||
required this.key,
|
||||
required this.fileName,
|
||||
});
|
||||
}
|
||||
@@ -52,11 +55,12 @@ class _ResourcesState extends State<Resources> {
|
||||
|
||||
_syncExternalProviders() async {
|
||||
externalProviders = await clashCore.getExternalProviders();
|
||||
setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
_updateProviders() async {
|
||||
print(providerItemKeys);
|
||||
final updateProviders = providerItemKeys.map<Future>(
|
||||
(key) async => await key.currentState?.updateProvider(false),
|
||||
);
|
||||
@@ -66,13 +70,18 @@ class _ResourcesState extends State<Resources> {
|
||||
|
||||
List<Widget> _buildExternalProviderSection() {
|
||||
List<GlobalObjectKey<_ProviderItemState>> keys = [];
|
||||
final res = generateSection(
|
||||
title: appLocalizations.externalResources,
|
||||
final res = generateInfoSection(
|
||||
info: Info(
|
||||
iconData: Icons.source,
|
||||
label: appLocalizations.externalResources,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
IconButton.filledTonal(
|
||||
onPressed: () {
|
||||
_updateProviders();
|
||||
},
|
||||
padding: const EdgeInsets.all(4),
|
||||
iconSize: 20,
|
||||
icon: const Icon(
|
||||
Icons.sync,
|
||||
),
|
||||
@@ -99,12 +108,25 @@ class _ResourcesState extends State<Resources> {
|
||||
|
||||
List<Widget> _buildGeoDataSection() {
|
||||
const geoItems = <GeoItem>[
|
||||
GeoItem(label: "GeoIp", fileName: mmdbFileName),
|
||||
GeoItem(label: "GeoSite", fileName: geoSiteFileName),
|
||||
GeoItem(label: "ASN", fileName: asnFileName),
|
||||
GeoItem(
|
||||
label: "GeoIp",
|
||||
fileName: geoIpFileName,
|
||||
key: "geoip",
|
||||
),
|
||||
GeoItem(label: "GeoSite", fileName: geoSiteFileName, key: "geosite"),
|
||||
GeoItem(
|
||||
label: "MMDB",
|
||||
fileName: mmdbFileName,
|
||||
key: "mmdb",
|
||||
),
|
||||
GeoItem(label: "ASN", fileName: asnFileName, key: "asn"),
|
||||
];
|
||||
return generateSection(
|
||||
title: appLocalizations.geoData,
|
||||
|
||||
return generateInfoSection(
|
||||
info: Info(
|
||||
iconData: Icons.storage,
|
||||
label: appLocalizations.geoData,
|
||||
),
|
||||
items: geoItems.map(
|
||||
(geoItem) => GeoDataListItem(
|
||||
geoItem: geoItem,
|
||||
@@ -141,6 +163,33 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
|
||||
GeoItem get geoItem => widget.geoItem;
|
||||
|
||||
_updateUrl(String url) async {
|
||||
final newUrl = await globalState.showCommonDialog<String>(
|
||||
child: UpdateGeoUrlFormDialog(
|
||||
title: geoItem.label,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
if (newUrl != null && newUrl != url && mounted) {
|
||||
try {
|
||||
if (!newUrl.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geoXUrl =
|
||||
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
|
||||
appController.updateClashConfigDebounce();
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: geoItem.label,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
final file = File(join(homePath, fileName));
|
||||
@@ -168,7 +217,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}";
|
||||
}
|
||||
|
||||
Widget _buildSubtitle() {
|
||||
Widget _buildSubtitle(String url) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -197,6 +246,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
url,
|
||||
style: context.textTheme.bodyMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
@@ -204,13 +260,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
runSpacing: 6,
|
||||
spacing: 12,
|
||||
children: [
|
||||
// CommonChip(
|
||||
// avatar: const Icon(Icons.upload),
|
||||
// label: "编辑",
|
||||
// onPressed: () {
|
||||
// _uploadGeoFile(geoItem.fileName);
|
||||
// },
|
||||
// ),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.edit),
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_updateUrl(url);
|
||||
},
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.sync),
|
||||
label: appLocalizations.sync,
|
||||
@@ -259,7 +315,12 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
vertical: 4,
|
||||
),
|
||||
title: Text(geoItem.label),
|
||||
subtitle: _buildSubtitle(),
|
||||
subtitle: Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.geoXUrl[geoItem.key]!,
|
||||
builder: (_, value, __) {
|
||||
return _buildSubtitle(value);
|
||||
},
|
||||
),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
@@ -391,3 +452,62 @@ class _ProviderItemState extends State<ProviderItem> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateGeoUrlFormDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
|
||||
const UpdateGeoUrlFormDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpdateGeoUrlFormDialog> createState() => _UpdateGeoUrlFormDialogState();
|
||||
}
|
||||
|
||||
class _UpdateGeoUrlFormDialogState extends State<UpdateGeoUrlFormDialog> {
|
||||
late TextEditingController urlController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
urlController = TextEditingController(text: widget.url);
|
||||
}
|
||||
|
||||
_handleUpdate() async {
|
||||
final url = urlController.value.text;
|
||||
if (url.isEmpty) return;
|
||||
Navigator.of(context).pop<String>(url);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _handleUpdate,
|
||||
child: Text(appLocalizations.submit),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -193,5 +193,7 @@
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"testUrl": "Test url",
|
||||
"sync": "Sync"
|
||||
"sync": "Sync",
|
||||
"exclude": "Hidden from recent tasks",
|
||||
"excludeDesc": "When the app is in the background, the app is hidden from the recent task"
|
||||
}
|
||||
@@ -193,5 +193,7 @@
|
||||
"copy": "复制",
|
||||
"paste": "粘贴",
|
||||
"testUrl": "测速链接",
|
||||
"sync": "同步"
|
||||
"sync": "同步",
|
||||
"exclude": "从最近任务中隐藏",
|
||||
"excludeDesc": "应用在后台时,从最近任务中隐藏应用"
|
||||
}
|
||||
@@ -121,6 +121,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"download": MessageLookupByLibrary.simpleMessage("Download"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
|
||||
"en": MessageLookupByLibrary.simpleMessage("English"),
|
||||
"exclude":
|
||||
MessageLookupByLibrary.simpleMessage("Hidden from recent tasks"),
|
||||
"excludeDesc": MessageLookupByLibrary.simpleMessage(
|
||||
"When the app is in the background, the app is hidden from the recent task"),
|
||||
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
|
||||
"expirationTime":
|
||||
MessageLookupByLibrary.simpleMessage("Expiration time"),
|
||||
|
||||
@@ -100,6 +100,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
||||
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
|
||||
"excludeDesc":
|
||||
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
|
||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||
|
||||
@@ -1999,6 +1999,26 @@ class AppLocalizations {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Hidden from recent tasks`
|
||||
String get exclude {
|
||||
return Intl.message(
|
||||
'Hidden from recent tasks',
|
||||
name: 'exclude',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `When the app is in the background, the app is hidden from the recent task`
|
||||
String get excludeDesc {
|
||||
return Intl.message(
|
||||
'When the app is in the background, the app is hidden from the recent task',
|
||||
name: 'excludeDesc',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -106,6 +107,8 @@ class Dns {
|
||||
}
|
||||
}
|
||||
|
||||
typedef GeoXMap = Map<String, String>;
|
||||
|
||||
@JsonSerializable()
|
||||
class ClashConfig extends ChangeNotifier {
|
||||
int _mixedPort;
|
||||
@@ -120,6 +123,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
bool _tcpConcurrent;
|
||||
Tun _tun;
|
||||
Dns _dns;
|
||||
GeoXMap _geoXUrl;
|
||||
List<String> _rules;
|
||||
String? _globalRealUa;
|
||||
|
||||
@@ -136,6 +140,7 @@ class ClashConfig extends ChangeNotifier {
|
||||
_geodataLoader = geodataLoaderMemconservative,
|
||||
_externalController = '',
|
||||
_dns = Dns(),
|
||||
_geoXUrl = defaultGeoXMap,
|
||||
_rules = [];
|
||||
|
||||
@JsonKey(name: "mixed-port", defaultValue: 7890)
|
||||
@@ -289,6 +294,16 @@ class ClashConfig extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(name: "geox-url", defaultValue: defaultGeoXMap)
|
||||
GeoXMap get geoXUrl => _geoXUrl;
|
||||
|
||||
set geoXUrl(GeoXMap value) {
|
||||
if (!const MapEquality<String, String>().equals(value, _geoXUrl)) {
|
||||
_geoXUrl = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
update([ClashConfig? clashConfig]) {
|
||||
if (clashConfig != null) {
|
||||
_mixedPort = clashConfig._mixedPort;
|
||||
|
||||
@@ -56,6 +56,7 @@ class Config extends ChangeNotifier {
|
||||
bool _autoCheckUpdate;
|
||||
bool _allowBypass;
|
||||
bool _systemProxy;
|
||||
bool _isExclude;
|
||||
DAV? _dav;
|
||||
ProxiesType _proxiesType;
|
||||
ProxyCardType _proxyCardType;
|
||||
@@ -80,6 +81,7 @@ class Config extends ChangeNotifier {
|
||||
_accessControl = const AccessControl(),
|
||||
_isAnimateToPage = true,
|
||||
_allowBypass = true,
|
||||
_isExclude = false,
|
||||
_proxyCardType = ProxyCardType.expand,
|
||||
_proxiesType = ProxiesType.tab,
|
||||
_proxiesColumns = 2;
|
||||
@@ -416,7 +418,6 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(name: "test-url", defaultValue: defaultTestUrl)
|
||||
String get testUrl => _testUrl;
|
||||
|
||||
@@ -427,6 +428,16 @@ class Config extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool get isExclude => _isExclude;
|
||||
|
||||
set isExclude(bool value) {
|
||||
if (_isExclude != value) {
|
||||
_isExclude = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
update([
|
||||
Config? config,
|
||||
RecoveryOption recoveryOptions = RecoveryOption.all,
|
||||
|
||||
@@ -52,7 +52,20 @@ ClashConfig _$ClashConfigFromJson(Map<String, dynamic> json) => ClashConfig()
|
||||
..tun = Tun.fromJson(json['tun'] as Map<String, dynamic>)
|
||||
..dns = Dns.fromJson(json['dns'] as Map<String, dynamic>)
|
||||
..rules = (json['rules'] as List<dynamic>).map((e) => e as String).toList()
|
||||
..globalRealUa = json['global-real-ua'] as String?;
|
||||
..globalRealUa = json['global-real-ua'] as String?
|
||||
..geoXUrl = (json['geox-url'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
{
|
||||
'mmdb':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||||
'asn':
|
||||
'https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb',
|
||||
'geoip':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat',
|
||||
'geosite':
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
|
||||
};
|
||||
|
||||
Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -70,6 +83,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
|
||||
'dns': instance.dns,
|
||||
'rules': instance.rules,
|
||||
'global-real-ua': instance.globalRealUa,
|
||||
'geox-url': instance.geoXUrl,
|
||||
};
|
||||
|
||||
const _$ModeEnumMap = {
|
||||
|
||||
@@ -43,7 +43,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
|
||||
ProxyCardType.expand
|
||||
..proxiesColumns = (json['proxiesColumns'] as num?)?.toInt() ?? 2
|
||||
..testUrl =
|
||||
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204';
|
||||
json['test-url'] as String? ?? 'https://www.gstatic.com/generate_204'
|
||||
..isExclude = json['isExclude'] as bool? ?? false;
|
||||
|
||||
Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
||||
'profiles': instance.profiles,
|
||||
@@ -69,6 +70,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
|
||||
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
|
||||
'proxiesColumns': instance.proxiesColumns,
|
||||
'test-url': instance.testUrl,
|
||||
'isExclude': instance.isExclude,
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
||||
@@ -44,10 +44,10 @@ class App {
|
||||
|
||||
Future<List<Package>> getPackages() async {
|
||||
final packagesString =
|
||||
await methodChannel?.invokeMethod<String>("getPackages");
|
||||
await methodChannel?.invokeMethod<String>("getPackages");
|
||||
return Isolate.run<List<Package>>(() {
|
||||
final List<dynamic> packagesRaw =
|
||||
packagesString != null ? json.decode(packagesString) : [];
|
||||
packagesString != null ? json.decode(packagesString) : [];
|
||||
return packagesRaw.map((e) => Package.fromJson(e)).toList();
|
||||
});
|
||||
}
|
||||
@@ -68,6 +68,12 @@ class App {
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> updateExcludeFromRecents(bool value) async {
|
||||
return await methodChannel?.invokeMethod<bool>("updateExcludeFromRecents", {
|
||||
"value": value,
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> resolverProcess(Process process) async {
|
||||
return await methodChannel?.invokeMethod<String>("resolverProcess", {
|
||||
"data": json.encode(process),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AndroidContainer extends StatefulWidget {
|
||||
final Widget child;
|
||||
@@ -16,6 +19,16 @@ class AndroidContainer extends StatefulWidget {
|
||||
|
||||
class _AndroidContainerState extends State<AndroidContainer>
|
||||
with WidgetsBindingObserver {
|
||||
_excludeContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isExclude,
|
||||
builder: (_, isExclude, child) {
|
||||
app?.updateExcludeFromRecents(isExclude);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -34,7 +47,9 @@ class _AndroidContainerState extends State<AndroidContainer>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
return _excludeContainer(
|
||||
widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -42,5 +57,4 @@ class _AndroidContainerState extends State<AndroidContainer>
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,17 +12,6 @@ class AppStateContainer extends StatelessWidget {
|
||||
required this.child,
|
||||
});
|
||||
|
||||
_autoLaunchContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.autoLaunch,
|
||||
builder: (_, isAutoLaunch, child) {
|
||||
autoLaunch?.updateStatus(isAutoLaunch);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_updateNavigationsContainer(Widget child) {
|
||||
return Selector2<AppState, Config, UpdateNavigationsSelector>(
|
||||
selector: (_, appState, config) {
|
||||
@@ -51,10 +40,8 @@ class AppStateContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _autoLaunchContainer(
|
||||
_updateNavigationsContainer(
|
||||
child,
|
||||
),
|
||||
return _updateNavigationsContainer(
|
||||
child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,43 +6,70 @@ import 'text.dart';
|
||||
|
||||
class Info {
|
||||
final String label;
|
||||
final IconData iconData;
|
||||
final IconData? iconData;
|
||||
|
||||
const Info({
|
||||
required this.label,
|
||||
required this.iconData,
|
||||
this.iconData,
|
||||
});
|
||||
}
|
||||
|
||||
class InfoHeader extends StatelessWidget {
|
||||
final Info info;
|
||||
final List<Widget> actions;
|
||||
|
||||
const InfoHeader({
|
||||
super.key,
|
||||
required this.info,
|
||||
});
|
||||
List<Widget>? actions,
|
||||
}) : actions = actions ?? const [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
info.iconData,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
info.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (info.iconData != null) ...[
|
||||
Icon(
|
||||
info.iconData,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
info.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
...actions,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -70,10 +97,12 @@ class CommonCard extends StatelessWidget {
|
||||
final CommonCardType type;
|
||||
|
||||
BorderSide getBorderSide(BuildContext context, Set<WidgetState> states) {
|
||||
if(type == CommonCardType.filled){
|
||||
if (type == CommonCardType.filled) {
|
||||
return BorderSide.none;
|
||||
}
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = Theme
|
||||
.of(context)
|
||||
.colorScheme;
|
||||
final hoverColor = isSelected
|
||||
? colorScheme.primary.toLight()
|
||||
: colorScheme.primary.toLighter();
|
||||
@@ -85,14 +114,15 @@ class CommonCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return BorderSide(
|
||||
color:
|
||||
isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(),
|
||||
);
|
||||
}
|
||||
|
||||
Color? getBackgroundColor(BuildContext context, Set<WidgetState> states) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
switch(type){
|
||||
final colorScheme = Theme
|
||||
.of(context)
|
||||
.colorScheme;
|
||||
switch (type) {
|
||||
case CommonCardType.plain:
|
||||
if (isSelected) {
|
||||
return colorScheme.secondaryContainer;
|
||||
@@ -100,7 +130,8 @@ class CommonCard extends StatelessWidget {
|
||||
if (states.isEmpty) {
|
||||
return colorScheme.secondaryContainer.toLittle();
|
||||
}
|
||||
return Theme.of(context)
|
||||
return Theme
|
||||
.of(context)
|
||||
.outlinedButtonTheme
|
||||
.style
|
||||
?.backgroundColor
|
||||
@@ -147,10 +178,10 @@ class CommonCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) => getBackgroundColor(context, states),
|
||||
(states) => getBackgroundColor(context, states),
|
||||
),
|
||||
side: WidgetStateProperty.resolveWith(
|
||||
(states) => getBorderSide(context, states),
|
||||
(states) => getBorderSide(context, states),
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
@@ -180,7 +211,10 @@ class SelectIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.inversePrimary,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.inversePrimary,
|
||||
shape: const CircleBorder(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/open_container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'extend_page.dart';
|
||||
import 'scaffold.dart';
|
||||
|
||||
@@ -390,6 +391,30 @@ List<Widget> generateSection({
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> generateInfoSection({
|
||||
required Info info,
|
||||
required Iterable<Widget> items,
|
||||
List<Widget>? actions,
|
||||
bool separated = true,
|
||||
}) {
|
||||
final genItems = separated
|
||||
? items.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
: items;
|
||||
return [
|
||||
if (items.isNotEmpty)
|
||||
InfoHeader(
|
||||
info: info,
|
||||
actions: actions,
|
||||
),
|
||||
...genItems,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Widget generateListView(List<Widget> items) {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowContainer extends StatefulWidget {
|
||||
@@ -14,11 +17,22 @@ class WindowContainer extends StatefulWidget {
|
||||
State<WindowContainer> createState() => _WindowContainerState();
|
||||
}
|
||||
|
||||
class _WindowContainerState extends State<WindowContainer>
|
||||
with WindowListener {
|
||||
class _WindowContainerState extends State<WindowContainer> with WindowListener {
|
||||
|
||||
_autoLaunchContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.autoLaunch,
|
||||
builder: (_, isAutoLaunch, child) {
|
||||
autoLaunch?.updateStatus(isAutoLaunch);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
return _autoLaunchContainer(widget.child);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -33,7 +47,6 @@ class _WindowContainerState extends State<WindowContainer>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void onWindowMinimize() async {
|
||||
await globalState.appController.savePreferences();
|
||||
|
||||
@@ -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.35+202407071
|
||||
version: 0.8.36+202407081
|
||||
environment:
|
||||
sdk: '>=3.1.0 <4.0.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user