Fix url validate issues 2

Add android hidden from the recent task

Add geoip file

Support modify geoData URL
This commit is contained in:
chen08209
2024-07-08 17:34:14 +08:00
parent b20d9edec2
commit 53af86238e
29 changed files with 423 additions and 105 deletions

View File

@@ -17,6 +17,7 @@ class ClashService {
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -193,5 +193,7 @@
"copy": "复制",
"paste": "粘贴",
"testUrl": "测速链接",
"sync": "同步"
"sync": "同步",
"exclude": "从最近任务中隐藏",
"excludeDesc": "应用在后台时,从最近任务中隐藏应用"
}

View File

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

View File

@@ -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("外部控制器"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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