Compare commits

..

1 Commits

Author SHA1 Message Date
chen08209
6bef3c7344 Support override script
Support proxies search

Support svg display

Add some scenes auto close connections

Update core

Optimize more details
2025-06-01 21:48:46 +08:00
33 changed files with 370 additions and 606 deletions

View File

@@ -402,8 +402,5 @@
"redirPort": "Redir Port",
"tproxyPort": "Tproxy Port",
"portTip": "{label} must be between 1024 and 49151",
"portConflictTip": "Please enter a different port",
"import": "Import",
"importFile": "Import from file",
"importUrl": "Import from URL"
"portConflictTip": "Please enter a different port"
}

View File

@@ -403,8 +403,5 @@
"redirPort": "Redirポート",
"tproxyPort": "Tproxyポート",
"portTip": "{label} は 1024 から 49151 の間でなければなりません",
"portConflictTip": "別のポートを入力してください",
"import": "インポート",
"importFile": "ファイルからインポート",
"importUrl": "URLからインポート"
"portConflictTip": "別のポートを入力してください"
}

View File

@@ -403,8 +403,5 @@
"redirPort": "Redir-порт",
"tproxyPort": "Tproxy-порт",
"portTip": "{label} должен быть числом от 1024 до 49151",
"portConflictTip": "Введите другой порт",
"import": "Импорт",
"importFile": "Импорт из файла",
"importUrl": "Импорт по URL"
"portConflictTip": "Введите другой порт"
}

View File

@@ -403,8 +403,5 @@
"redirPort": "Redir端口",
"tproxyPort": "Tproxy端口",
"portTip": "{label} 必须在 1024 到 49151 之间",
"portConflictTip": "请输入不同的端口",
"import": "导入",
"importFile": "通过文件导入",
"importUrl": "通过URL导入"
"portConflictTip": "请输入不同的端口"
}

View File

@@ -7,6 +7,7 @@ import 'dart:ui';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/service.dart';

View File

@@ -11,6 +11,7 @@ export 'future.dart';
export 'http.dart';
export 'icons.dart';
export 'iterable.dart';
export 'javascript.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';

View File

@@ -0,0 +1,18 @@
import 'package:flutter_js/flutter_js.dart';
class Javascript {
static Javascript? _instance;
Javascript._internal();
JavascriptRuntime get runTime {
return getJavascriptRuntime();
}
factory Javascript() {
_instance ??= Javascript._internal();
return _instance!;
}
}
final js = Javascript();

View File

@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'constant.dart';
class AppPath {
static AppPath? _instance;
Completer<Directory> dataDir = Completer();
@@ -77,28 +78,17 @@ class AppPath {
return join(directory, "$id.yaml");
}
Future<String> getProvidersDirPath(String id) async {
Future<String> getProvidersPath(String id, {String filePath = ""}) async {
final directory = await profilesPath;
return join(
final path = join(
directory,
"providers",
id,
);
}
Future<String> getProvidersFilePath(
String id,
String type,
String url,
) async {
final directory = await profilesPath;
return join(
directory,
"providers",
id,
type,
url.toMd5(),
);
if (filePath.isNotEmpty) {
return join(path, filePath);
}
return path;
}
Future<String> get tempPath async {

View File

@@ -1,8 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'print.dart';
extension StringExtension on String {
@@ -61,11 +59,6 @@ extension StringExtension on String {
}
}
String toMd5() {
final bytes = utf8.encode(this);
return md5.convert(bytes).toString();
}
// bool containsToLower(String target) {
// return toLowerCase().contains(target);
// }

View File

@@ -774,14 +774,14 @@ class AppController {
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersDirPath = await appPath.getProvidersDirPath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
final profileFile = File(profilePath);
final isExists = await profileFile.exists();
if (isExists) {
profileFile.delete(recursive: true);
}
final providersFileDir = File(providersDirPath);
final providersFileDir = File(providersPath);
final providersFileIsExists = await providersFileDir.exists();
if (providersFileIsExists) {
providersFileDir.delete(recursive: true);

View File

@@ -499,8 +499,3 @@ enum Language {
yaml,
javaScript,
}
enum ImportOption {
file,
url,
}

View File

@@ -323,10 +323,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Icon configuration",
),
"iconStyle": MessageLookupByLibrary.simpleMessage("Icon style"),
"import": MessageLookupByLibrary.simpleMessage("Import"),
"importFile": MessageLookupByLibrary.simpleMessage("Import from file"),
"importFromURL": MessageLookupByLibrary.simpleMessage("Import from URL"),
"importUrl": MessageLookupByLibrary.simpleMessage("Import from URL"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("Long term effective"),
"init": MessageLookupByLibrary.simpleMessage("Init"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage(

View File

@@ -245,10 +245,7 @@ class MessageLookup extends MessageLookupByLibrary {
"icon": MessageLookupByLibrary.simpleMessage("アイコン"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("アイコン設定"),
"iconStyle": MessageLookupByLibrary.simpleMessage("アイコンスタイル"),
"import": MessageLookupByLibrary.simpleMessage("インポート"),
"importFile": MessageLookupByLibrary.simpleMessage("ファイルからインポート"),
"importFromURL": MessageLookupByLibrary.simpleMessage("URLからインポート"),
"importUrl": MessageLookupByLibrary.simpleMessage("URLからインポート"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("長期有効"),
"init": MessageLookupByLibrary.simpleMessage("初期化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("正しいホットキーを入力"),

View File

@@ -342,10 +342,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Конфигурация иконки",
),
"iconStyle": MessageLookupByLibrary.simpleMessage("Стиль иконки"),
"import": MessageLookupByLibrary.simpleMessage("Импорт"),
"importFile": MessageLookupByLibrary.simpleMessage("Импорт из файла"),
"importFromURL": MessageLookupByLibrary.simpleMessage("Импорт из URL"),
"importUrl": MessageLookupByLibrary.simpleMessage("Импорт по URL"),
"infiniteTime": MessageLookupByLibrary.simpleMessage(
"Долгосрочное действие",
),

View File

@@ -221,10 +221,7 @@ class MessageLookup extends MessageLookupByLibrary {
"icon": MessageLookupByLibrary.simpleMessage("图片"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
"import": MessageLookupByLibrary.simpleMessage("导入"),
"importFile": MessageLookupByLibrary.simpleMessage("通过文件导入"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"importUrl": MessageLookupByLibrary.simpleMessage("通过URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),

View File

@@ -3104,31 +3104,6 @@ class AppLocalizations {
args: [],
);
}
/// `Import`
String get import {
return Intl.message('Import', name: 'import', desc: '', args: []);
}
/// `Import from file`
String get importFile {
return Intl.message(
'Import from file',
name: 'importFile',
desc: '',
args: [],
);
}
/// `Import from URL`
String get importUrl {
return Intl.message(
'Import from URL',
name: 'importUrl',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -41,11 +41,6 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
},
fireImmediately: true,
);
ref.listenManual(configStateProvider, (prev, next) {
if (prev != next) {
globalState.appController.savePreferencesDebounce();
}
});
}
@override

View File

@@ -102,10 +102,9 @@ class _WindowContainerState extends ConsumerState<WindowManager>
}
@override
void onWindowRestore() {
commonPrint.log("restore");
render?.resume();
super.onWindowRestore();
Future<void> onTaskbarCreated() async {
// globalState.appController.updateTray(true);
super.onTaskbarCreated();
}
@override

View File

@@ -111,25 +111,10 @@ class _EditorPageState extends ConsumerState<EditorPage> {
_findController.findMode();
}
_handleImport() async {
final option = await globalState.showCommonDialog<ImportOption>(
child: _ImportOptionsDialog(),
);
if (option == null) {
return;
}
if (option == ImportOption.file) {
final file = await picker.pickerFile();
if (file == null) {
return;
}
final res = String.fromCharCodes(file.bytes?.toList() ?? []);
_controller.text = res;
return;
}
_handleRemoteDownload() async {
final url = await globalState.showCommonDialog(
child: InputDialog(
title: "导入",
title: appLocalizations.download,
value: "",
labelText: appLocalizations.url,
validator: (value) {
@@ -201,7 +186,7 @@ class _EditorPageState extends ConsumerState<EditorPage> {
),
if (widget.supportRemoteDownload)
IconButton(
onPressed: _handleImport,
onPressed: _handleRemoteDownload,
icon: Icon(
Icons.arrow_downward,
),
@@ -251,7 +236,6 @@ class _EditorPageState extends ConsumerState<EditorPage> {
padding: EdgeInsets.only(
right: 16,
),
autocompleteSymbols: true,
focusNode: _focusNode,
scrollbarBuilder: (context, child, details) {
return CommonScrollBar(
@@ -678,45 +662,7 @@ class _NoInputBorder extends InputBorder {
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection? textDirection,
}) {}
}
class _ImportOptionsDialog extends StatefulWidget {
const _ImportOptionsDialog();
@override
State<_ImportOptionsDialog> createState() => _ImportOptionsDialogState();
}
class _ImportOptionsDialogState extends State<_ImportOptionsDialog> {
_handleOnTab(ImportOption value) {
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.import,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: Wrap(
children: [
ListItem(
onTap: () {
_handleOnTab(ImportOption.url);
},
title: Text(appLocalizations.importUrl),
),
ListItem(
onTap: () {
_handleOnTab(ImportOption.file);
},
title: Text(appLocalizations.importFile),
)
],
),
);
}) {
// Do not paint.
}
}

View File

@@ -285,10 +285,6 @@ class ScriptState extends _$ScriptState with AutoDisposeNotifierMixin {
currentId: nextId,
);
}
isExits(String label) {
return state.scripts.indexWhere((item) => item.label == label) != -1;
}
}
@riverpod

View File

@@ -178,7 +178,7 @@ final proxiesStyleSettingProvider =
);
typedef _$ProxiesStyleSetting = AutoDisposeNotifier<ProxiesStyle>;
String _$scriptStateHash() => r'884581c71fd5afa3c9d34f31625d967cf561cdbe';
String _$scriptStateHash() => r'16d669009ffb233d95b2cb206cf771342ebc1cc1';
/// See also [ScriptState].
@ProviderFor(ScriptState)

View File

@@ -6,22 +6,6 @@ part of '../state.dart';
// RiverpodGenerator
// **************************************************************************
String _$configStateHash() => r'1f4ea3cc8f6461ba734e7e0c5d7295bfa4fd5afb';
/// See also [configState].
@ProviderFor(configState)
final configStateProvider = AutoDisposeProvider<Config>.internal(
configState,
name: r'configStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$configStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ConfigStateRef = AutoDisposeProviderRef<Config>;
String _$currentGroupsStateHash() =>
r'6222c006e1970e7435268d32903b9019cf1a4351';
@@ -1975,11 +1959,11 @@ class _GenColorSchemeProviderElement
bool get ignoreConfig => (origin as GenColorSchemeProvider).ignoreConfig;
}
String _$needSetupHash() => r'db01ec73ea3232c99d1c5445c80e31b98785f416';
String _$needSetupHash() => r'1116c73bb2964321de63bdca631a983d31e30d69';
/// See also [needSetup].
@ProviderFor(needSetup)
final needSetupProvider = AutoDisposeProvider<VM3>.internal(
final needSetupProvider = AutoDisposeProvider<VM2>.internal(
needSetup,
name: r'needSetupProvider',
debugGetCreateSourceHash:
@@ -1990,7 +1974,7 @@ final needSetupProvider = AutoDisposeProvider<VM3>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef NeedSetupRef = AutoDisposeProviderRef<VM3>;
typedef NeedSetupRef = AutoDisposeProviderRef<VM2>;
String _$profileOverrideStateHash() =>
r'fa26570a355ab39e27b1f93d1d2f358717065592';

View File

@@ -12,38 +12,6 @@ import 'config.dart';
part 'generated/state.g.dart';
@riverpod
Config configState(Ref ref) {
final themeProps = ref.watch(themeSettingProvider);
final patchClashConfig = ref.watch(patchClashConfigProvider);
final appSetting = ref.watch(appSettingProvider);
final profiles = ref.watch(profilesProvider);
final currentProfileId = ref.watch(currentProfileIdProvider);
final overrideDns = ref.watch(overrideDnsProvider);
final networkProps = ref.watch(networkSettingProvider);
final vpnProps = ref.watch(vpnSettingProvider);
final proxiesStyle = ref.watch(proxiesStyleSettingProvider);
final scriptProps = ref.watch(scriptStateProvider);
final hotKeyActions = ref.watch(hotKeyActionsProvider);
final dav = ref.watch(appDAVSettingProvider);
final windowProps = ref.watch(windowSettingProvider);
return Config(
dav: dav,
windowProps: windowProps,
hotKeyActions: hotKeyActions,
scriptProps: scriptProps,
proxiesStyle: proxiesStyle,
vpnProps: vpnProps,
networkProps: networkProps,
overrideDns: overrideDns,
currentProfileId: currentProfileId,
profiles: profiles,
appSetting: appSetting,
themeProps: themeProps,
patchClashConfig: patchClashConfig,
);
}
@riverpod
GroupsState currentGroupsState(Ref ref) {
final mode =
@@ -622,19 +590,9 @@ ColorScheme genColorScheme(
}
@riverpod
VM3 needSetup(Ref ref) {
VM2 needSetup(Ref ref) {
final profileId = ref.watch(currentProfileIdProvider);
final content = ref.watch(
scriptStateProvider.select((state) => state.currentScript?.content));
final overrideDns = ref.watch(overrideDnsProvider);
final dns = overrideDns == true
? ref.watch(patchClashConfigProvider.select(
(state) => state.dns,
))
: null;
return VM3(
a: profileId,
b: content,
c: dns,
);
return VM2(a: profileId, b: content);
}

View File

@@ -13,7 +13,6 @@ import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/widgets/dialog.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:material_color_utilities/palettes/core_palette.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -313,7 +312,10 @@ class GlobalState {
return {};
}
final profileId = profile.id;
final configMap = await getProfileConfig(profileId);
final configMap = await switch (clashLibHandler != null) {
true => clashLibHandler!.getConfig(profileId),
false => clashCore.getConfig(profileId),
};
final rawConfig = await handleEvaluate(configMap);
final routeAddress =
config.networkProps.routeMode == RouteMode.bypassPrivate
@@ -367,15 +369,9 @@ class GlobalState {
final proxyProviders = rawConfig["proxy-providers"] as Map;
for (final key in proxyProviders.keys) {
final proxyProvider = proxyProviders[key];
if (proxyProvider["type"] != "http") {
continue;
}
if (proxyProvider["url"] != null) {
proxyProvider["path"] = await appPath.getProvidersFilePath(
profile.id,
"proxies",
proxyProvider["url"],
);
if (proxyProvider["path"] != null) {
proxyProvider["path"] = await appPath.getProvidersPath(profile.id,
filePath: proxyProvider["path"]);
}
}
}
@@ -384,15 +380,9 @@ class GlobalState {
final ruleProviders = rawConfig["rule-providers"] as Map;
for (final key in ruleProviders.keys) {
final ruleProvider = ruleProviders[key];
if (ruleProvider["type"] != "http") {
continue;
}
if (ruleProvider["url"] != null) {
ruleProvider["path"] = await appPath.getProvidersFilePath(
profile.id,
"rules",
ruleProvider["url"],
);
if (ruleProvider["path"] != null) {
ruleProvider["path"] = await appPath.getProvidersPath(profile.id,
filePath: ruleProvider["path"]);
}
}
}
@@ -422,7 +412,7 @@ class GlobalState {
rawConfig["dns"]["enable"] = true;
}
}
var rules = [];
var rules = rawConfig["rule"] ?? [];
if (rawConfig["rules"] != null) {
rules = rawConfig["rules"];
}
@@ -440,16 +430,6 @@ class GlobalState {
return rawConfig;
}
Future<Map<String, dynamic>> getProfileConfig(String profileId) async {
final configMap = await switch (clashLibHandler != null) {
true => clashLibHandler!.getConfig(profileId),
false => clashCore.getConfig(profileId),
};
configMap["rules"] = configMap["rule"];
configMap.remove("rule");
return configMap;
}
Future<Map<String, dynamic>> handleEvaluate(
Map<String, dynamic> config,
) async {
@@ -457,12 +437,9 @@ class GlobalState {
if (currentScript == null) {
return config;
}
if (config["proxy-providers"] == null) {
config["proxy-providers"] = {};
}
final configJs = json.encode(config);
final runtime = getJavascriptRuntime();
final res = await runtime.evaluateAsync("""
final runtime = js.runTime;
final res = await js.runTime.evaluateAsync("""
${currentScript.content}
main($configJs)
""");

View File

@@ -47,6 +47,15 @@ class AboutView extends StatelessWidget {
_checkUpdate(context);
},
),
// ListItem(
// title: Text(appLocalizations.contactMe),
// onTap: () {
// globalState.showMessage(
// title: appLocalizations.contactMe,
// message: TextSpan(text: "chen08209@gmail.com"),
// );
// },
// ),
ListItem(
title: const Text("Telegram"),
onTap: () {

View File

@@ -1,6 +1,5 @@
import 'dart:math';
import 'package:defer_pointer/defer_pointer.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
@@ -10,8 +9,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart';
typedef _IsEditWidgetBuilder = Widget Function(bool isEdit);
class DashboardView extends ConsumerStatefulWidget {
const DashboardView({super.key});
@@ -21,8 +18,6 @@ class DashboardView extends ConsumerStatefulWidget {
class _DashboardViewState extends ConsumerState<DashboardView> with PageMixin {
final key = GlobalKey<SuperGridState>();
final _isEditNotifier = ValueNotifier<bool>(false);
final _addedWidgetsNotifier = ValueNotifier<List<GridItem>>([]);
@override
initState() {
@@ -38,282 +33,115 @@ class _DashboardViewState extends ConsumerState<DashboardView> with PageMixin {
return super.initState();
}
@override
dispose() {
_isEditNotifier.dispose();
super.dispose();
}
@override
Widget? get floatingActionButton => const StartButton();
Widget _buildIsEdit(_IsEditWidgetBuilder builder) {
return ValueListenableBuilder(
valueListenable: _isEditNotifier,
builder: (_, isEdit, ___) {
return builder(isEdit);
},
);
}
@override
List<Widget> get actions => [
_buildIsEdit((isEdit) {
return isEdit
? ValueListenableBuilder(
valueListenable: _addedWidgetsNotifier,
builder: (_, addedChildren, child) {
if (addedChildren.isEmpty) {
return Container();
}
return child!;
},
child: IconButton(
onPressed: () {
_showAddWidgetsModal();
},
icon: Icon(
Icons.add_circle,
),
),
)
: SizedBox();
}),
ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) {
return ValueListenableBuilder(
valueListenable: key.currentState!.isEditNotifier,
builder: (_, isEdit, child) {
if (!isEdit || addedChildren.isEmpty) {
return Container();
}
return child!;
},
child: child,
);
},
child: IconButton(
onPressed: () {
key.currentState!.showAddModal();
},
icon: Icon(
Icons.add_circle,
),
),
),
IconButton(
icon: _buildIsEdit((isEdit) {
return isEdit
? Icon(Icons.save)
: Icon(
Icons.edit,
);
}),
onPressed: _handleUpdateIsEdit,
icon: ValueListenableBuilder(
valueListenable: key.currentState!.isEditNotifier,
builder: (_, isEdit, ___) {
return isEdit
? SystemBackBlock(
child: CommonPopScope(
child: Icon(Icons.save),
onPop: () {
key.currentState!.isEditNotifier.value =
!key.currentState!.isEditNotifier.value;
return false;
},
),
)
: Icon(
Icons.edit,
);
},
),
onPressed: () {
key.currentState!.isEditNotifier.value =
!key.currentState!.isEditNotifier.value;
},
),
];
_showAddWidgetsModal() {
showSheet(
builder: (_, type) {
return ValueListenableBuilder(
valueListenable: _addedWidgetsNotifier,
builder: (_, value, __) {
return AdaptiveSheetScaffold(
type: type,
body: _AddDashboardWidgetModal(
items: value,
onAdd: (gridItem) {
key.currentState?.handleAdd(gridItem);
},
),
title: appLocalizations.add,
);
},
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
},
context: context,
);
}
_handleUpdateIsEdit() {
if (_isEditNotifier.value == true) {
_handleSave();
}
_isEditNotifier.value = !_isEditNotifier.value;
}
_handleSave() {
final children = key.currentState?.children;
if (children == null) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final dashboardWidgets = children
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
});
}
@override
Widget build(BuildContext context) {
final dashboardState = ref.watch(dashboardStateProvider);
final columns = max(4 * ((dashboardState.viewWidth / 320).ceil()), 8);
final spacing = 16.ap;
final children = [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
];
WidgetsBinding.instance.addPostFrameCallback((_) {
_addedWidgetsNotifier.value = DashboardWidget.values
.where(
(item) =>
!children.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
});
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: _buildIsEdit((isEdit) {
return isEdit
? SystemBackBlock(
child: CommonPopScope(
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
children: [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
],
onUpdate: () {
_handleSave();
},
),
onPop: () {
_handleUpdateIsEdit();
return false;
},
),
)
: Grid(
crossAxisCount: columns,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
children: children,
);
})),
);
}
}
class _AddDashboardWidgetModal extends StatelessWidget {
final List<GridItem> items;
final Function(GridItem item) onAdd;
const _AddDashboardWidgetModal({
required this.items,
required this.onAdd,
});
@override
Widget build(BuildContext context) {
return DeferredPointerHandler(
child: SingleChildScrollView(
padding: EdgeInsets.all(
16,
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Grid(
crossAxisCount: 8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: items
.map(
(item) => item.wrap(
builder: (child) {
return _AddedContainer(
onAdd: () {
onAdd(item);
},
child: child,
);
},
child: SuperGrid(
key: key,
crossAxisCount: columns,
crossAxisSpacing: 16.ap,
mainAxisSpacing: 16.ap,
children: [
...dashboardState.dashboardWidgets
.where(
(item) => item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map(
(item) => item.widget,
),
)
.toList(),
],
onSave: (girdItems) {
_handleSave(girdItems, ref);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
},
),
),
);
}
}
class _AddedContainer extends StatefulWidget {
final Widget child;
final VoidCallback onAdd;
const _AddedContainer({
required this.child,
required this.onAdd,
});
@override
State<_AddedContainer> createState() => _AddedContainerState();
}
class _AddedContainerState extends State<_AddedContainer> {
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(_AddedContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.child != widget.child) {}
}
_handleAdd() async {
widget.onAdd();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
ActivateBox(
child: widget.child,
),
Positioned(
top: -8,
right: -8,
child: DeferPointer(
child: SizedBox(
width: 24,
height: 24,
child: IconButton.filled(
iconSize: 20,
padding: EdgeInsets.all(2),
onPressed: _handleAdd,
icon: Icon(
Icons.add,
),
),
),
),
)
],
);
}
}

View File

@@ -136,9 +136,12 @@ class _ScriptsViewState extends ConsumerState<ScriptsView> {
return appLocalizations.emptyTip(appLocalizations.name);
}
if (value != script?.label) {
final isExits =
ref.read(scriptStateProvider.notifier).isExits(value);
if (isExits) {
final index = ref
.read(scriptStateProvider.select((state) => state.scripts))
.indexWhere(
(item) => item.label == value,
);
if (index != -1) {
return appLocalizations.existsTip(
appLocalizations.name,
);
@@ -153,18 +156,6 @@ class _ScriptsViewState extends ConsumerState<ScriptsView> {
}
newScript = newScript.copyWith(label: res);
}
if (newScript.label != script?.label) {
final isExits =
ref.read(scriptStateProvider.notifier).isExits(newScript.label);
if (isExits) {
globalState.showMessage(
message: TextSpan(
text: appLocalizations.existsTip(appLocalizations.name),
),
);
return;
}
}
ref.read(scriptStateProvider.notifier).setScript(newScript);
if (mounted) {
Navigator.of(context).pop();

View File

@@ -7,6 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/widgets/activate_box.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/grid.dart';
import 'package:fl_clash/widgets/sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
@@ -17,7 +18,8 @@ class SuperGrid extends StatefulWidget {
final double mainAxisSpacing;
final double crossAxisSpacing;
final int crossAxisCount;
final VoidCallback? onUpdate;
final void Function(List<GridItem> newChildren)? onSave;
final List<GridItem> Function(List<GridItem> newChildren)? addedItemsBuilder;
const SuperGrid({
super.key,
@@ -25,7 +27,8 @@ class SuperGrid extends StatefulWidget {
this.crossAxisCount = 1,
this.mainAxisSpacing = 0,
this.crossAxisSpacing = 0,
this.onUpdate,
this.onSave,
this.addedItemsBuilder,
});
@override
@@ -34,7 +37,7 @@ class SuperGrid extends StatefulWidget {
class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
final ValueNotifier<List<GridItem>> _childrenNotifier = ValueNotifier([]);
List<GridItem> children = [];
final ValueNotifier<List<GridItem>> addedChildrenNotifier = ValueNotifier([]);
int get length => _childrenNotifier.value.length;
List<int> _tempIndexList = [];
@@ -46,6 +49,8 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
List<Offset> _offsets = [];
Offset _parentOffset = Offset.zero;
EdgeDraggingAutoScroller? _edgeDraggingAutoScroller;
final ValueNotifier<bool> isEditNotifier = ValueNotifier(false);
Map<int, Tween<Offset>> _transformTweenMap = {};
final ValueNotifier<bool> _animating = ValueNotifier(false);
@@ -90,6 +95,35 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_containerSize = context.size!;
}
showAddModal() {
if (!isEditNotifier.value) {
return;
}
showSheet(
builder: (_, type) {
return ValueListenableBuilder(
valueListenable: addedChildrenNotifier,
builder: (_, value, __) {
return AdaptiveSheetScaffold(
type: type,
body: _AddedWidgetsModal(
items: value,
onAdd: (gridItem) {
_childrenNotifier.value = List.from(_childrenNotifier.value)
..add(
gridItem,
);
},
),
title: appLocalizations.add,
);
},
);
},
context: context,
);
}
_initState() {
_transformController.value = 0;
_sizes = List.generate(length, (index) => Size.zero);
@@ -105,18 +139,22 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_targetIndex = -1;
}
_handleChildrenNotifierChange() {
addedChildrenNotifier.value = widget.addedItemsBuilder != null
? widget.addedItemsBuilder!(_childrenNotifier.value)
: [];
}
@override
void initState() {
super.initState();
_childrenNotifier.addListener(() {
children = _childrenNotifier.value;
if (widget.onUpdate != null) {
widget.onUpdate!();
}
});
_childrenNotifier.value = widget.children;
_childrenNotifier.addListener(_handleChildrenNotifierChange);
isEditNotifier.addListener(_handleIsEditChange);
_fakeDragWidgetController = AnimationController.unbounded(
vsync: this,
duration: commonDuration,
@@ -126,7 +164,6 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
vsync: this,
duration: Duration(milliseconds: 120),
);
_shakeAnimation = Tween<double>(
begin: -0.012,
end: 0.012,
@@ -145,11 +182,15 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_initState();
}
handleAdd(GridItem gridItem) {
_childrenNotifier.value = List.from(_childrenNotifier.value)
..add(
gridItem,
);
_handleIsEditChange() async {
_handleChildrenNotifierChange();
if (isEditNotifier.value == false) {
if (widget.onSave != null) {
await _transformCompleter?.future;
await Future.delayed(commonDuration);
widget.onSave!(_childrenNotifier.value);
}
}
}
@override
@@ -264,9 +305,6 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
}
_handleDragEnd(DraggableDetails details) async {
final children = List<GridItem>.from(_childrenNotifier.value);
children.insert(_targetIndex, children.removeAt(_dragIndexNotifier.value));
this.children = children;
debouncer.cancel(FunctionTag.handleWill);
if (_targetIndex == -1) {
return;
@@ -296,6 +334,8 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_fakeDragWidgetAnimation = null;
_transformTweenMap.clear();
_transformAnimationMap.clear();
final children = List<GridItem>.from(_childrenNotifier.value);
children.insert(_targetIndex, children.removeAt(_dragIndexNotifier.value));
_childrenNotifier.value = children;
_initState();
}
@@ -345,15 +385,17 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_initState();
}
Widget _buildTransform(Widget rawChild, int index) {
Widget _wrapTransform(Widget rawChild, int index) {
return ValueListenableBuilder(
valueListenable: _animating,
builder: (_, animating, child) {
if (animating && _dragIndexNotifier.value == index) {
return _buildSizeBox(
Container(),
index,
);
if (animating) {
if (_dragIndexNotifier.value == index) {
return _sizeBoxWrap(
Container(),
index,
);
}
}
return child!;
},
@@ -400,7 +442,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
return nextOffset;
}
Widget _buildSizeBox(Widget child, int index) {
Widget _sizeBoxWrap(Widget child, int index) {
return ValueListenableBuilder(
valueListenable: _dragWidgetSizeNotifier,
builder: (_, size, child) {
@@ -413,7 +455,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
);
}
Widget _buildInactivate(Widget child) {
Widget _ignoreWrap(Widget child) {
return ValueListenableBuilder(
valueListenable: _animating,
builder: (_, animating, child) {
@@ -429,7 +471,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
);
}
Widget _buildShake(Widget child) {
Widget _shakeWrap(Widget child) {
final random = 0.7 + Random().nextDouble() * 0.3;
_shakeController.stop();
_shakeController.repeat(reverse: true);
@@ -445,7 +487,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
);
}
Widget _buildDraggable({
Widget _draggableWrap({
required Widget childWhenDragging,
required Widget feedback,
required Widget item,
@@ -481,7 +523,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
if (dragIndex == index) {
return child!;
}
return _buildShake(
return _shakeWrap(
_DeletableContainer(
onDelete: () {
_handleDelete(index);
@@ -524,7 +566,16 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
},
child: shakeTarget,
);
return draggableChild;
return ValueListenableBuilder(
valueListenable: isEditNotifier,
builder: (_, isEdit, child) {
if (!isEdit) {
return item;
}
return child!;
},
child: draggableChild,
);
}
Widget _builderItem(int index) {
@@ -539,7 +590,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
final childWhenDragging = ActivateBox(
child: Opacity(
opacity: 0.6,
child: _buildSizeBox(
child: _sizeBoxWrap(
CommonCard(
child: child,
),
@@ -548,7 +599,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
),
);
final feedback = ActivateBox(
child: _buildSizeBox(
child: _sizeBoxWrap(
CommonCard(
child: Material(
elevation: 6,
@@ -558,8 +609,8 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
index,
),
);
return _buildTransform(
_buildDraggable(
return _wrapTransform(
_draggableWrap(
childWhenDragging: childWhenDragging,
feedback: feedback,
item: child,
@@ -580,7 +631,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
if (!animating || _fakeDragWidgetAnimation == null || index == -1) {
return Container();
}
return _buildSizeBox(
return _sizeBoxWrap(
AnimatedBuilder(
animation: _fakeDragWidgetAnimation!,
builder: (_, child) {
@@ -607,7 +658,10 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
_transformController.dispose();
_dragIndexNotifier.dispose();
_animating.dispose();
_childrenNotifier.removeListener(_handleChildrenNotifierChange);
_childrenNotifier.dispose();
isEditNotifier.removeListener(_handleIsEditChange);
isEditNotifier.dispose();
super.dispose();
}
@@ -616,7 +670,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
return DeferredPointerHandler(
child: Stack(
children: [
_buildInactivate(
_ignoreWrap(
ValueListenableBuilder(
valueListenable: _childrenNotifier,
builder: (_, children, __) {
@@ -640,6 +694,46 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
}
}
class _AddedWidgetsModal extends StatelessWidget {
final List<GridItem> items;
final Function(GridItem item) onAdd;
const _AddedWidgetsModal({
required this.items,
required this.onAdd,
});
@override
Widget build(BuildContext context) {
return DeferredPointerHandler(
child: SingleChildScrollView(
padding: EdgeInsets.all(
16,
),
child: Grid(
crossAxisCount: 8,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: items
.map(
(item) => item.wrap(
builder: (child) {
return _AddedContainer(
onAdd: () {
onAdd(item);
},
child: child,
);
},
),
)
.toList(),
),
),
);
}
}
class _DeletableContainer extends StatefulWidget {
final Widget child;
final VoidCallback onDelete;
@@ -747,3 +841,68 @@ class _DeletableContainerState extends State<_DeletableContainer>
);
}
}
class _AddedContainer extends StatefulWidget {
final Widget child;
final VoidCallback onAdd;
const _AddedContainer({
required this.child,
required this.onAdd,
});
@override
State<_AddedContainer> createState() => _AddedContainerState();
}
class _AddedContainerState extends State<_AddedContainer> {
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(_AddedContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.child != widget.child) {}
}
_handleAdd() async {
widget.onAdd();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
ActivateBox(
child: widget.child,
),
Positioned(
top: -8,
right: -8,
child: DeferPointer(
child: SizedBox(
width: 24,
height: 24,
child: IconButton.filled(
iconSize: 20,
padding: EdgeInsets.all(2),
onPressed: _handleAdd,
icon: Icon(
Icons.add,
),
),
),
),
)
],
);
}
}

View File

@@ -81,24 +81,6 @@ static gboolean my_application_local_command_line(GApplication* application, gch
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
//MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
@@ -109,21 +91,12 @@ static void my_application_dispose(GObject* object) {
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
nullptr));
}
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
nullptr)); }

View File

@@ -40,7 +40,7 @@ PODS:
- FlutterMacOS
- window_ext (0.0.1):
- FlutterMacOS
- window_manager (0.5.0):
- window_manager (0.2.0):
- FlutterMacOS
DEPENDENCIES:
@@ -128,7 +128,7 @@ SPEC CHECKSUMS:
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
window_ext: 4afef727fe428b30c68ce800ba92e890fd329f63
window_manager: b729e31d38fb04905235df9ea896128991cad99e
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367

View File

@@ -250,7 +250,7 @@ packages:
source: hosted
version: "0.3.4+2"
crypto:
dependency: "direct main"
dependency: "direct dev"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
@@ -1565,10 +1565,10 @@ packages:
dependency: "direct main"
description:
name: window_manager
sha256: "51d50168ab267d344b975b15390426b1243600d436770d3f13de67e55b05ec16"
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.4.3"
xdg_directories:
dependency: transitive
description:

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.85+202506062
version: 0.8.85+202506012
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -14,7 +14,7 @@ dependencies:
path_provider: ^2.1.0
path: ^1.9.0
shared_preferences: ^2.5.3
window_manager: ^0.5.0
window_manager: ^0.4.3
dynamic_color: ^1.7.0
proxy:
path: plugins/proxy
@@ -59,7 +59,6 @@ dependencies:
# yaml: ^3.1.3
flutter_svg: ^2.1.0
flutter_cache_manager: ^3.4.1
crypto: ^3.0.3
dev_dependencies:
flutter_test:
sdk: flutter
@@ -70,8 +69,9 @@ dev_dependencies:
args: ^2.4.2
freezed: ^2.5.1
riverpod_generator: ^2.6.3
riverpod_lint: ^2.6.3
custom_lint: ^0.7.0
riverpod_lint: ^2.6.3
crypto: ^3.0.3
flutter:
uses-material-design: true