Compare commits
1 Commits
v0.8.92-pr
...
v0.8.92-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae248208b |
@@ -181,18 +181,20 @@ object State {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleStopService() {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.START) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
} finally {
|
||||
if (runStateFlow.value == RunState.PENDING) {
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
fun handleStopService() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.START) {
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
} finally {
|
||||
if (runStateFlow.value == RunState.PENDING) {
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +86,8 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
private fun handleStop(result: MethodChannel.Result) {
|
||||
launch {
|
||||
State.handleStopService()
|
||||
result.success(true)
|
||||
}
|
||||
State.handleStopService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
val semaphore = Semaphore(10)
|
||||
|
||||
@@ -475,5 +475,6 @@
|
||||
"restoreFromFileDesc": "Restore data via file",
|
||||
"restoreOnlyConfig": "Restore configuration files only",
|
||||
"restoreAllData": "Restore all data",
|
||||
"addProfile": "Add Profile"
|
||||
"addProfile": "Add Profile",
|
||||
"delayTest": "Delay Test"
|
||||
}
|
||||
@@ -476,5 +476,6 @@
|
||||
"restoreFromFileDesc": "ファイルを介してデータを復元する",
|
||||
"restoreOnlyConfig": "設定ファイルのみを復元する",
|
||||
"restoreAllData": "すべてのデータを復元する",
|
||||
"addProfile": "プロファイルを追加"
|
||||
"addProfile": "プロファイルを追加",
|
||||
"delayTest": "遅延テスト"
|
||||
}
|
||||
@@ -484,5 +484,6 @@
|
||||
"restoreFromFileDesc": "Восстановить данные из файла",
|
||||
"restoreOnlyConfig": "Восстановить только файлы конфигурации",
|
||||
"restoreAllData": "Восстановить все данные",
|
||||
"addProfile": "Добавить профиль"
|
||||
"addProfile": "Добавить профиль",
|
||||
"delayTest": "Тест задержки"
|
||||
}
|
||||
@@ -476,5 +476,6 @@
|
||||
"restoreFromFileDesc": "通过文件恢复数据",
|
||||
"restoreOnlyConfig": "仅恢复配置文件",
|
||||
"restoreAllData": "恢复所有数据",
|
||||
"addProfile": "添加配置"
|
||||
"addProfile": "添加配置",
|
||||
"delayTest": "延迟测试"
|
||||
}
|
||||
|
||||
@@ -532,6 +532,9 @@ func handleDelFile(path string, result ActionResult) {
|
||||
}
|
||||
|
||||
func handleSetupConfig(bytes []byte) string {
|
||||
if !isInit {
|
||||
return "not initialized"
|
||||
}
|
||||
var params = defaultSetupParams()
|
||||
err := UnmarshalJson(bytes, params)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,14 +20,14 @@ const helperPort = 47890;
|
||||
const maxTextScale = 1.4;
|
||||
const minTextScale = 0.8;
|
||||
final baseInfoEdgeInsets = EdgeInsets.symmetric(
|
||||
vertical: 16.ap,
|
||||
horizontal: 16.ap,
|
||||
vertical: 16.mAp,
|
||||
horizontal: 16.mAp,
|
||||
);
|
||||
final listHeaderPadding = EdgeInsets.only(
|
||||
left: 16.ap,
|
||||
right: 8.ap,
|
||||
top: 24.ap,
|
||||
bottom: 8.ap,
|
||||
left: 16.mAp,
|
||||
right: 8.mAp,
|
||||
top: 24.mAp,
|
||||
bottom: 8.mAp,
|
||||
);
|
||||
|
||||
const watchExecution = true;
|
||||
@@ -102,7 +102,8 @@ const profilesStoreKey = PageStorageKey<String>('profiles');
|
||||
const defaultPrimaryColor = 0XFFD8C0C3;
|
||||
|
||||
double getWidgetHeight(num lines) {
|
||||
return max(lines * 80 + (lines - 1) * 16, 0).ap;
|
||||
final space = 14.mAp;
|
||||
return max(lines * (80.ap + space) - space, 0);
|
||||
}
|
||||
|
||||
const maxLength = 1000;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -20,6 +22,10 @@ extension NumExt on num {
|
||||
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
|
||||
}
|
||||
|
||||
double get mAp {
|
||||
return this * min((1 + (globalState.theme.textScaleFactor - 1) * 0.5), 1);
|
||||
}
|
||||
|
||||
TrafficShow get traffic {
|
||||
final units = TrafficUnit.values;
|
||||
var size = toDouble();
|
||||
@@ -51,7 +57,7 @@ extension NumExt on num {
|
||||
|
||||
extension DoubleExt on double {
|
||||
bool moreOrEqual(double value) {
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 2;
|
||||
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ class Tray {
|
||||
return system.isWindows ? 'ico' : 'png';
|
||||
}
|
||||
|
||||
Future<void> destroy() async {
|
||||
await trayManager.destroy();
|
||||
}
|
||||
|
||||
String getTryIcon({required bool isStart, required bool tunEnable}) {
|
||||
if (system.isMacOS || !isStart) {
|
||||
return 'assets/images/icon/status_1.$trayIconSuffix';
|
||||
|
||||
@@ -83,6 +83,7 @@ class Window {
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await windowManager.close();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ extension InitControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<void> _initStatus() async {
|
||||
if (!globalState.needInitStatus) {
|
||||
commonPrint.log('init status cancel');
|
||||
return;
|
||||
}
|
||||
commonPrint.log('init status');
|
||||
if (system.isAndroid) {
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
@@ -550,9 +555,6 @@ extension SetupControllerExt on AppController {
|
||||
|
||||
Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
|
||||
if (isStart) {
|
||||
_ref.read(runTimeProvider.notifier).update((state) {
|
||||
return state ?? 0;
|
||||
});
|
||||
if (!isInit) {
|
||||
final res = await tryStartCore(true);
|
||||
if (res) {
|
||||
@@ -564,6 +566,7 @@ extension SetupControllerExt on AppController {
|
||||
await globalState.handleStart([updateRunTime, updateTraffic]);
|
||||
applyProfileDebounce(force: true, silence: true);
|
||||
} else {
|
||||
globalState.needInitStatus = false;
|
||||
await applyProfile(
|
||||
force: true,
|
||||
preloadInvoke: () async {
|
||||
@@ -650,9 +653,14 @@ extension SetupControllerExt on AppController {
|
||||
bool force = false,
|
||||
VoidCallback? preloadInvoke,
|
||||
}) async {
|
||||
if (!force && !await needSetup()) {
|
||||
return;
|
||||
}
|
||||
await loadingRun(
|
||||
() async {
|
||||
await _applyProfile(force, preloadInvoke);
|
||||
await _setupConfig(preloadInvoke);
|
||||
await updateGroups();
|
||||
await updateProviders();
|
||||
},
|
||||
silence: true,
|
||||
tag: !silence ? LoadingTag.proxies : null,
|
||||
@@ -722,13 +730,8 @@ extension SetupControllerExt on AppController {
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<void> _setupConfig([
|
||||
bool force = false,
|
||||
VoidCallback? preloadInvoke,
|
||||
]) async {
|
||||
if (!force && !await needSetup()) {
|
||||
return;
|
||||
}
|
||||
Future<void> _setupConfig([VoidCallback? preloadInvoke]) async {
|
||||
commonPrint.log('setup ===>');
|
||||
var profile = _ref.read(currentProfileProvider);
|
||||
final nextProfile = await profile?.checkAndUpdateAndCopy();
|
||||
if (nextProfile != null) {
|
||||
@@ -746,9 +749,6 @@ extension SetupControllerExt on AppController {
|
||||
globalState.lastSetupState = setupState;
|
||||
if (system.isAndroid) {
|
||||
globalState.lastVpnState = _ref.read(vpnStateProvider);
|
||||
}
|
||||
|
||||
if (system.isAndroid) {
|
||||
preferences.saveShareState(this.sharedState);
|
||||
}
|
||||
final config = await getProfile(
|
||||
@@ -768,15 +768,6 @@ extension SetupControllerExt on AppController {
|
||||
}
|
||||
addCheckIp();
|
||||
}
|
||||
|
||||
Future _applyProfile([
|
||||
bool force = false,
|
||||
VoidCallback? preloadInvoke,
|
||||
]) async {
|
||||
await _setupConfig(force, preloadInvoke);
|
||||
await updateGroups();
|
||||
await updateProviders();
|
||||
}
|
||||
}
|
||||
|
||||
extension CoreControllerExt on AppController {
|
||||
@@ -842,7 +833,7 @@ extension CoreControllerExt on AppController {
|
||||
if (coreController.isCompleted) {
|
||||
return false;
|
||||
}
|
||||
await restartCore();
|
||||
await restartCore(start);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -868,11 +859,12 @@ extension SystemControllerExt on AppController {
|
||||
system.exit();
|
||||
});
|
||||
try {
|
||||
if (needSave) {
|
||||
await preferences.saveConfig(config);
|
||||
}
|
||||
await proxy?.stopProxy();
|
||||
await macOS?.updateDns(true);
|
||||
await Future.wait([
|
||||
if (needSave) preferences.saveConfig(config),
|
||||
if (macOS != null) macOS!.updateDns(true),
|
||||
if (proxy != null) proxy!.stopProxy(),
|
||||
if (tray != null) tray!.destroy(),
|
||||
]);
|
||||
await coreController.destroy();
|
||||
commonPrint.log('exit');
|
||||
} finally {
|
||||
|
||||
@@ -86,9 +86,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
try {
|
||||
if (!completer.isCompleted) {
|
||||
return null;
|
||||
}
|
||||
await completer.future.timeout(const Duration(seconds: 10));
|
||||
} catch (e) {
|
||||
commonPrint.log(
|
||||
'Invoke pre ${method.name} timeout $e',
|
||||
|
||||
@@ -270,6 +270,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("Default"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("Delay"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("Sort by delay"),
|
||||
"delayTest": MessageLookupByLibrary.simpleMessage("Delay Test"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("Delete"),
|
||||
"deleteMultipTip": m1,
|
||||
"deleteTip": m2,
|
||||
|
||||
@@ -205,6 +205,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("デフォルト"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("遅延"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("遅延順"),
|
||||
"delayTest": MessageLookupByLibrary.simpleMessage("遅延テスト"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("削除"),
|
||||
"deleteMultipTip": m1,
|
||||
"deleteTip": m2,
|
||||
|
||||
@@ -277,6 +277,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("Задержка"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("Сортировка по задержке"),
|
||||
"delayTest": MessageLookupByLibrary.simpleMessage("Тест задержки"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("Удалить"),
|
||||
"deleteMultipTip": m1,
|
||||
"deleteTip": m2,
|
||||
|
||||
@@ -185,6 +185,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
|
||||
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
|
||||
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
|
||||
"delayTest": MessageLookupByLibrary.simpleMessage("延迟测试"),
|
||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||
"deleteMultipTip": m1,
|
||||
"deleteTip": m2,
|
||||
|
||||
@@ -3743,6 +3743,11 @@ class AppLocalizations {
|
||||
String get addProfile {
|
||||
return Intl.message('Add Profile', name: 'addProfile', desc: '', args: []);
|
||||
}
|
||||
|
||||
/// `Delay Test`
|
||||
String get delayTest {
|
||||
return Intl.message('Delay Test', name: 'delayTest', desc: '', args: []);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
@@ -125,7 +127,7 @@ class _EditorPageState extends ConsumerState<EditorPage> {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
final res = String.fromCharCodes(file.bytes?.toList() ?? []);
|
||||
final res = utf8.decode(file.bytes?.toList() ?? []);
|
||||
_controller.text = res;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fl_clash/common/color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -49,9 +50,9 @@ class InitErrorScreen extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withOpacity(0.5),
|
||||
color: colorScheme.errorContainer.opacity50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: colorScheme.error.withOpacity(0.5)),
|
||||
border: Border.all(color: colorScheme.error.opacity50),
|
||||
),
|
||||
child: SelectableText(
|
||||
error.toString(),
|
||||
@@ -71,7 +72,7 @@ class InitErrorScreen extends StatelessWidget {
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.5)),
|
||||
border: Border.all(color: Colors.grey.opacity50),
|
||||
),
|
||||
child: SelectableText(
|
||||
stack.toString(),
|
||||
|
||||
@@ -38,6 +38,7 @@ class GlobalState {
|
||||
late Measure measure;
|
||||
late CommonTheme theme;
|
||||
late Color accentColor;
|
||||
bool needInitStatus = true;
|
||||
CorePalette? corePalette;
|
||||
DateTime? startTime;
|
||||
UpdateTasks tasks = [];
|
||||
|
||||
@@ -228,7 +228,7 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
Widget build(BuildContext context) {
|
||||
final dashboardState = ref.watch(dashboardStateProvider);
|
||||
final columns = max(4 * ((dashboardState.contentWidth / 280).ceil()), 8);
|
||||
final spacing = 14.ap;
|
||||
final spacing = 14.mAp;
|
||||
final children = [
|
||||
...dashboardState.dashboardWidgets
|
||||
.where(
|
||||
|
||||
@@ -41,45 +41,54 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
final color = context.colorScheme.onSurfaceVariant.opacity80;
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Consumer(
|
||||
builder: (_, ref, _) {
|
||||
final traffics = ref.watch(trafficsProvider).list;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
16,
|
||||
).copyWith(bottom: 0, left: 0, right: 0),
|
||||
child: LineChart(
|
||||
gradient: true,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
points: _getPoints(traffics),
|
||||
child: RepaintBoundary(
|
||||
child: CommonCard(
|
||||
onPressed: () {},
|
||||
child: Consumer(
|
||||
builder: (_, ref, _) {
|
||||
final traffics = ref.watch(trafficsProvider).list;
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: baseInfoEdgeInsets.copyWith(bottom: 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: InfoHeader(
|
||||
padding: EdgeInsets.zero,
|
||||
info: Info(
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
_getLastTraffic(traffics).speedText,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-16, -20),
|
||||
child: Text(
|
||||
_getLastTraffic(traffics).speedText,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: color,
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
16,
|
||||
).copyWith(bottom: 0, left: 0, right: 0),
|
||||
child: LineChart(
|
||||
gradient: true,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
points: _getPoints(traffics),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -33,10 +33,7 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
parent: _controller!,
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
ref.listenManual(runTimeProvider.select((state) => state != null), (
|
||||
prev,
|
||||
next,
|
||||
) {
|
||||
ref.listenManual(isStartProvider, (prev, next) {
|
||||
if (next != isStart) {
|
||||
isStart = next;
|
||||
updateController();
|
||||
@@ -55,7 +52,7 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
isStart = !isStart;
|
||||
updateController();
|
||||
debouncer.call(FunctionTag.updateStatus, () {
|
||||
appController.updateStatus(isStart);
|
||||
appController.updateStatus(isStart, isInit: !ref.read(initProvider));
|
||||
}, duration: commonDuration);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:fl_clash/controller.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/features/overwrite/rule.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/providers/database.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -28,10 +29,33 @@ class _OverwriteViewState extends ConsumerState<OverwriteView> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _handlePreview() async {
|
||||
final profile = ref.read(profileProvider(widget.profileId));
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
final configMap = await appController.getProfileWithId(profile.id);
|
||||
final content = await encodeYamlTask(configMap);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final previewPage = EditorPage(title: profile.realLabel, content: content);
|
||||
BaseNavigator.push<String>(context, previewPage);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonScaffold(
|
||||
title: appLocalizations.override,
|
||||
actions: [
|
||||
CommonMinFilledButtonTheme(
|
||||
child: FilledButton(
|
||||
onPressed: _handlePreview,
|
||||
child: Text(appLocalizations.preview),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
],
|
||||
body: CustomScrollView(
|
||||
slivers: [_Title(widget.profileId), _Content(widget.profileId)],
|
||||
),
|
||||
@@ -341,7 +365,9 @@ class _ScriptContent extends ConsumerWidget {
|
||||
|
||||
void _handleChange(WidgetRef ref, int scriptId) {
|
||||
ref.read(profilesProvider.notifier).updateProfile(profileId, (state) {
|
||||
return state.copyWith(scriptId: scriptId);
|
||||
return state.copyWith(
|
||||
scriptId: state.scriptId == scriptId ? null : scriptId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class ProfilesView extends StatefulWidget {
|
||||
|
||||
class _ProfilesViewState extends State<ProfilesView> {
|
||||
Function? applyConfigDebounce;
|
||||
bool _isUpdating = false;
|
||||
|
||||
void _handleShowAddExtendPage() {
|
||||
showExtend(
|
||||
@@ -40,6 +41,10 @@ class _ProfilesViewState extends State<ProfilesView> {
|
||||
}
|
||||
|
||||
Future<void> _updateProfiles(List<Profile> profiles) async {
|
||||
if (_isUpdating == true) {
|
||||
return;
|
||||
}
|
||||
_isUpdating = true;
|
||||
final List<UpdatingMessage> messages = [];
|
||||
final updateProfiles = profiles.map<Future>((profile) async {
|
||||
if (profile.type == ProfileType.file) return;
|
||||
@@ -55,6 +60,7 @@ class _ProfilesViewState extends State<ProfilesView> {
|
||||
if (messages.isNotEmpty) {
|
||||
globalState.showAllUpdatingMessagesDialog(messages);
|
||||
}
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(List<Profile> profiles) {
|
||||
@@ -86,10 +92,10 @@ class _ProfilesViewState extends State<ProfilesView> {
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
return CommonFloatingActionButton(
|
||||
onPressed: _handleShowAddExtendPage,
|
||||
child: const Icon(Icons.add),
|
||||
icon: const Icon(Icons.add),
|
||||
label: context.appLocalizations.addProfile,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +105,7 @@ class _ProfilesViewState extends State<ProfilesView> {
|
||||
builder: (_, ref, _) {
|
||||
final isLoading = ref.watch(loadingProvider(LoadingTag.profiles));
|
||||
final state = ref.watch(profilesStateProvider);
|
||||
final spacing = 14.ap;
|
||||
final spacing = 14.mAp;
|
||||
return CommonScaffold(
|
||||
isLoading: isLoading,
|
||||
title: appLocalizations.profiles,
|
||||
|
||||
@@ -410,19 +410,15 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: FadeTransition(
|
||||
opacity: _animation,
|
||||
child: ScaleTransition(scale: _animation, child: child),
|
||||
),
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: ScaleTransition(scale: _animation, child: child),
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
child: CommonFloatingActionButton(
|
||||
onPressed: _healthcheck,
|
||||
child: const Icon(Icons.network_ping),
|
||||
label: appLocalizations.delayTest,
|
||||
icon: const Icon(Icons.network_ping),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fl_clash/widgets/inherited.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScrollOverBuilder extends StatefulWidget {
|
||||
@@ -35,58 +36,18 @@ class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
|
||||
}
|
||||
}
|
||||
|
||||
// class ProxiesActionsBuilder extends StatelessWidget {
|
||||
// final Widget? child;
|
||||
// final Widget Function(
|
||||
// ProxiesActionsState state,
|
||||
// Widget? child,
|
||||
// ) builder;
|
||||
//
|
||||
// const ProxiesActionsBuilder({
|
||||
// super.key,
|
||||
// required this.child,
|
||||
// required this.builder,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Selector<AppState, ProxiesActionsState>(
|
||||
// selector: (_, appState) => ProxiesActionsState(
|
||||
// isCurrent: appState.currentLabel == "proxies",
|
||||
// hasProvider: appState.providers.isNotEmpty,
|
||||
// ),
|
||||
// builder: (_, state, child) => builder(state, child),
|
||||
// child: child,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
class FloatingActionButtonExtendedBuilder extends StatelessWidget {
|
||||
final Widget Function(bool isExtend) builder;
|
||||
|
||||
// class ActiveBuilder extends StatelessWidget {
|
||||
// final String label;
|
||||
// final StateAndChildWidgetBuilder<bool> builder;
|
||||
// final Widget? child;
|
||||
//
|
||||
// const ActiveBuilder({
|
||||
// super.key,
|
||||
// required this.label,
|
||||
// required this.builder,
|
||||
// required this.child,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Selector<AppState, bool>(
|
||||
// selector: (_, appState) => appState.currentLabel == label,
|
||||
// builder: (_, state, child) {
|
||||
// return builder(
|
||||
// state,
|
||||
// child,
|
||||
// );
|
||||
// },
|
||||
// child: child,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
const FloatingActionButtonExtendedBuilder({super.key, required this.builder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isExtended =
|
||||
CommonScaffoldFabExtendedProvider.of(context)?.isExtended ?? false;
|
||||
return builder(isExtended);
|
||||
}
|
||||
}
|
||||
|
||||
typedef StateWidgetBuilder<T> = Widget Function(T state);
|
||||
|
||||
|
||||
56
lib/widgets/button.dart
Normal file
56
lib/widgets/button.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
|
||||
class CommonFloatingActionButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final Icon icon;
|
||||
final String label;
|
||||
|
||||
const CommonFloatingActionButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
floatingActionButtonTheme: Theme.of(context).floatingActionButtonTheme
|
||||
.copyWith(
|
||||
extendedIconLabelSpacing: 0,
|
||||
extendedPadding: EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
child: FloatingActionButtonExtendedBuilder(
|
||||
builder: (isExtended) {
|
||||
return FloatingActionButton.extended(
|
||||
heroTag: null,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
isExtended: true,
|
||||
label: AnimatedSize(
|
||||
alignment: Alignment.centerLeft,
|
||||
duration: midDuration,
|
||||
curve: Curves.easeOutBack,
|
||||
child: AnimatedOpacity(
|
||||
duration: midDuration,
|
||||
opacity: isExtended ? 1.0 : 0.0,
|
||||
curve: Curves.linear,
|
||||
child: isExtended
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(label, softWrap: false),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class InfoHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
EdgeInsetsGeometry nextPadding = (padding ?? baseInfoEdgeInsets);
|
||||
if (actions.isNotEmpty) {
|
||||
nextPadding = nextPadding.subtract(EdgeInsets.symmetric(vertical: 8.ap));
|
||||
nextPadding = nextPadding.subtract(EdgeInsets.symmetric(vertical: 8.mAp));
|
||||
}
|
||||
return Padding(
|
||||
padding: nextPadding,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
typedef WrapBuilder = Widget Function(Widget child);
|
||||
|
||||
@@ -27,10 +28,10 @@ class Grid extends MultiChildRenderObjectWidget {
|
||||
TextDirection? textDirection,
|
||||
this.mainAxisExtent,
|
||||
List<Widget>? children,
|
||||
}) : crossAxisCount = crossAxisCount ?? 1,
|
||||
axisDirection = axisDirection ?? AxisDirection.down,
|
||||
textDirection = textDirection ?? TextDirection.ltr,
|
||||
super(children: children ?? const []);
|
||||
}) : crossAxisCount = crossAxisCount ?? 1,
|
||||
axisDirection = axisDirection ?? AxisDirection.down,
|
||||
textDirection = textDirection ?? TextDirection.ltr,
|
||||
super(children: children ?? const []);
|
||||
|
||||
const Grid.baseGap({
|
||||
Key? key,
|
||||
@@ -42,15 +43,15 @@ class Grid extends MultiChildRenderObjectWidget {
|
||||
double? mainAxisExtent,
|
||||
List<Widget>? children,
|
||||
}) : this(
|
||||
key: key,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
crossAxisCount: crossAxisCount,
|
||||
axisDirection: axisDirection,
|
||||
textDirection: textDirection,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
children: children,
|
||||
);
|
||||
key: key,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
crossAxisCount: crossAxisCount,
|
||||
axisDirection: axisDirection,
|
||||
textDirection: textDirection,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
children: children,
|
||||
);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
@@ -65,10 +66,7 @@ class Grid extends MultiChildRenderObjectWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderGrid renderObject,
|
||||
) {
|
||||
void updateRenderObject(BuildContext context, RenderGrid renderObject) {
|
||||
renderObject
|
||||
..mainAxisSpacing = mainAxisSpacing
|
||||
..mainAxisExtent = mainAxisExtent
|
||||
@@ -90,12 +88,12 @@ class RenderGrid extends RenderBox
|
||||
required AxisDirection axisDirection,
|
||||
required TextDirection textDirection,
|
||||
double? mainAxisExtent,
|
||||
}) : _crossAxisCount = crossAxisCount,
|
||||
_crossAxisSpacing = crossAxisSpacing,
|
||||
_mainAxisSpacing = mainAxisSpacing,
|
||||
_axisDirection = axisDirection,
|
||||
_textDirection = textDirection,
|
||||
_mainAxisExtent = mainAxisExtent;
|
||||
}) : _crossAxisCount = crossAxisCount,
|
||||
_crossAxisSpacing = crossAxisSpacing,
|
||||
_mainAxisSpacing = mainAxisSpacing,
|
||||
_axisDirection = axisDirection,
|
||||
_textDirection = textDirection,
|
||||
_mainAxisExtent = mainAxisExtent;
|
||||
|
||||
int _crossAxisCount;
|
||||
|
||||
@@ -214,15 +212,10 @@ class RenderGrid extends RenderBox
|
||||
GridParentData childParentData,
|
||||
int crossAxisCount,
|
||||
) {
|
||||
return math.min(
|
||||
childParentData.crossAxisCellCount ?? 1,
|
||||
crossAxisCount,
|
||||
);
|
||||
return math.min(childParentData.crossAxisCellCount ?? 1, crossAxisCount);
|
||||
}
|
||||
|
||||
Size _computeSize({
|
||||
required BoxConstraints constraints,
|
||||
}) {
|
||||
Size _computeSize({required BoxConstraints constraints}) {
|
||||
final crossAxisExtent = mainAxis == Axis.vertical
|
||||
? constraints.maxWidth
|
||||
: constraints.maxHeight;
|
||||
@@ -245,11 +238,13 @@ class RenderGrid extends RenderBox
|
||||
? BoxConstraints.tightFor(width: crossAxisExtent)
|
||||
: BoxConstraints.tightFor(height: crossAxisExtent);
|
||||
_layoutChild(child, childConstraints, parentUsesSize: true);
|
||||
mainAxisExtent =
|
||||
mainAxis == Axis.vertical ? child.size.height : child.size.width;
|
||||
mainAxisExtent = mainAxis == Axis.vertical
|
||||
? child.size.height
|
||||
: child.size.width;
|
||||
} else {
|
||||
final mainAxisCellCount = childParentData.mainAxisCellCount ?? 1;
|
||||
mainAxisExtent = (this.mainAxisExtent ?? stride) * mainAxisCellCount -
|
||||
mainAxisExtent =
|
||||
(this.mainAxisExtent ?? stride) * mainAxisCellCount -
|
||||
mainAxisSpacing;
|
||||
childParentData.realMainAxisExtent = mainAxisExtent;
|
||||
final childSize = mainAxis == Axis.vertical
|
||||
@@ -265,9 +260,7 @@ class RenderGrid extends RenderBox
|
||||
? Offset(crossAxisOffset, mainAxisOffset)
|
||||
: Offset(mainAxisOffset, crossAxisOffset);
|
||||
childParentData.offset = offset;
|
||||
|
||||
final nextOffset = mainAxisOffset + mainAxisExtent + mainAxisSpacing;
|
||||
|
||||
for (int i = 0; i < crossAxisCellCount; i++) {
|
||||
offsets[origin.crossAxisIndex + i] = nextOffset;
|
||||
}
|
||||
@@ -281,7 +274,8 @@ class RenderGrid extends RenderBox
|
||||
final childParentData = _getParentData(child);
|
||||
final offset = childParentData.offset;
|
||||
final crossAxisOffset = offset.getCrossAxisOffset(mainAxis);
|
||||
final mainAxisOffset = mainAxisExtent -
|
||||
final mainAxisOffset =
|
||||
mainAxisExtent -
|
||||
offset.getMainAxisOffset(mainAxis) -
|
||||
childParentData.realMainAxisExtent!;
|
||||
final newOffset = mainAxis == Axis.vertical
|
||||
@@ -365,15 +359,11 @@ class GridItem extends ParentDataWidget<GridParentData> {
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => GridItem;
|
||||
|
||||
GridItem wrap({
|
||||
required WrapBuilder builder,
|
||||
}) {
|
||||
GridItem wrap({required WrapBuilder builder}) {
|
||||
return GridItem(
|
||||
mainAxisCellCount: mainAxisCellCount,
|
||||
crossAxisCellCount: crossAxisCellCount,
|
||||
child: builder(
|
||||
child,
|
||||
),
|
||||
child: builder(child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -395,11 +385,13 @@ _Origin _getOrigin(List<double> offsets, int crossAxisCount) {
|
||||
}
|
||||
int start = 0;
|
||||
int span = 0;
|
||||
for (int j = 0;
|
||||
span < crossAxisCount &&
|
||||
j < length &&
|
||||
length - j >= crossAxisCount - span;
|
||||
j++) {
|
||||
for (
|
||||
int j = 0;
|
||||
span < crossAxisCount &&
|
||||
j < length &&
|
||||
length - j >= crossAxisCount - span;
|
||||
j++
|
||||
) {
|
||||
if (offset.moreOrEqual(offsets[j])) {
|
||||
span++;
|
||||
if (span == crossAxisCount) {
|
||||
|
||||
41
lib/widgets/inherited.dart
Normal file
41
lib/widgets/inherited.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CommonScaffoldBackActionProvider extends InheritedWidget {
|
||||
final VoidCallback? backAction;
|
||||
|
||||
const CommonScaffoldBackActionProvider({
|
||||
super.key,
|
||||
required this.backAction,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static CommonScaffoldBackActionProvider? of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) =>
|
||||
backAction != oldWidget.backAction;
|
||||
}
|
||||
|
||||
class CommonScaffoldFabExtendedProvider extends InheritedWidget {
|
||||
final bool isExtended;
|
||||
|
||||
const CommonScaffoldFabExtendedProvider({
|
||||
super.key,
|
||||
required this.isExtended,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static CommonScaffoldFabExtendedProvider? of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<
|
||||
CommonScaffoldFabExtendedProvider
|
||||
>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CommonScaffoldFabExtendedProvider oldWidget) =>
|
||||
isExtended != oldWidget.isExtended;
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/widgets/pop_scope.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'chip.dart';
|
||||
import 'inherited.dart';
|
||||
|
||||
typedef OnKeywordsUpdateCallback = void Function(List<String> keywords);
|
||||
|
||||
@@ -47,8 +49,8 @@ class CommonScaffold extends StatefulWidget {
|
||||
|
||||
class CommonScaffoldState extends State<CommonScaffold> {
|
||||
late final ValueNotifier<AppBarState> _appBarState;
|
||||
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _loadingNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isFabExtendedNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]);
|
||||
final _textController = TextEditingController();
|
||||
|
||||
@@ -83,12 +85,6 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
_updateSearchState((state) => state?.copyWith(query: ''));
|
||||
}
|
||||
|
||||
set floatingActionButton(Widget? floatingActionButton) {
|
||||
if (_floatingActionButton.value != floatingActionButton) {
|
||||
_floatingActionButton.value = floatingActionButton;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSearchingAppBarTheme(Widget child) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
@@ -156,7 +152,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
void dispose() {
|
||||
_appBarState.dispose();
|
||||
_textController.dispose();
|
||||
_floatingActionButton.dispose();
|
||||
_isFabExtendedNotifier.dispose();
|
||||
_loadingNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -350,17 +346,31 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(backActionProvider?.backAction),
|
||||
body: body,
|
||||
body: NotificationListener<UserScrollNotification>(
|
||||
child: body,
|
||||
onNotification: (notification) {
|
||||
if (notification.direction == ScrollDirection.reverse) {
|
||||
_isFabExtendedNotifier.value = false;
|
||||
} else if (notification.direction == ScrollDirection.forward) {
|
||||
_isFabExtendedNotifier.value = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
floatingActionButton:
|
||||
widget.floatingActionButton ??
|
||||
ValueListenableBuilder<Widget?>(
|
||||
valueListenable: _floatingActionButton,
|
||||
builder: (_, value, _) {
|
||||
return value ?? SizedBox();
|
||||
},
|
||||
),
|
||||
floatingActionButton: widget.floatingActionButton != null
|
||||
? ValueListenableBuilder<bool>(
|
||||
valueListenable: _isFabExtendedNotifier,
|
||||
builder: (_, isExtended, child) {
|
||||
return CommonScaffoldFabExtendedProvider(
|
||||
isExtended: isExtended,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: widget.floatingActionButton,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -372,25 +382,6 @@ List<Widget> genActions(List<Widget> actions, {double? space}) {
|
||||
];
|
||||
}
|
||||
|
||||
class CommonScaffoldBackActionProvider extends InheritedWidget {
|
||||
final VoidCallback? backAction;
|
||||
|
||||
const CommonScaffoldBackActionProvider({
|
||||
super.key,
|
||||
required this.backAction,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static CommonScaffoldBackActionProvider? of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<CommonScaffoldBackActionProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CommonScaffoldBackActionProvider oldWidget) =>
|
||||
backAction != oldWidget.backAction;
|
||||
}
|
||||
|
||||
class BaseScaffold extends StatelessWidget {
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export 'activate_box.dart';
|
||||
export 'animate_grid.dart';
|
||||
export 'builder.dart';
|
||||
export 'button.dart';
|
||||
export 'card.dart';
|
||||
export 'chip.dart';
|
||||
export 'color_scheme_box.dart';
|
||||
@@ -13,6 +14,7 @@ export 'fade_box.dart';
|
||||
export 'float_layout.dart';
|
||||
export 'grid.dart';
|
||||
export 'icon.dart';
|
||||
export 'inherited.dart';
|
||||
export 'input.dart';
|
||||
export 'keep_scope.dart';
|
||||
export 'line_chart.dart';
|
||||
|
||||
@@ -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.92+2026012501
|
||||
version: 0.8.92+2026012701
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user