Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details
290 lines
7.9 KiB
Dart
290 lines
7.9 KiB
Dart
import 'dart:async';
|
|
import 'package:animations/animations.dart';
|
|
import 'package:dynamic_color/dynamic_color.dart';
|
|
import 'package:fl_clash/clash/clash.dart';
|
|
import 'package:fl_clash/common/theme.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/l10n/l10n.dart';
|
|
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:material_color_utilities/palettes/core_palette.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import 'common/common.dart';
|
|
import 'controller.dart';
|
|
import 'models/models.dart';
|
|
|
|
typedef UpdateTasks = List<FutureOr Function()>;
|
|
|
|
class GlobalState {
|
|
static GlobalState? _instance;
|
|
Map<CacheTag, double> cacheScrollPosition = {};
|
|
Map<CacheTag, FixedMap<String, double>> cacheHeightMap = {};
|
|
bool isService = false;
|
|
Timer? timer;
|
|
Timer? groupsUpdateTimer;
|
|
late Config config;
|
|
late AppState appState;
|
|
bool isPre = true;
|
|
String? coreSHA256;
|
|
late PackageInfo packageInfo;
|
|
Function? updateCurrentDelayDebounce;
|
|
late Measure measure;
|
|
late CommonTheme theme;
|
|
late Color accentColor;
|
|
CorePalette? corePalette;
|
|
DateTime? startTime;
|
|
UpdateTasks tasks = [];
|
|
final navigatorKey = GlobalKey<NavigatorState>();
|
|
late AppController appController;
|
|
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
|
|
|
|
bool get isStart => startTime != null && startTime!.isBeforeNow;
|
|
|
|
GlobalState._internal();
|
|
|
|
factory GlobalState() {
|
|
_instance ??= GlobalState._internal();
|
|
return _instance!;
|
|
}
|
|
|
|
initApp(int version) async {
|
|
appState = AppState(
|
|
version: version,
|
|
viewSize: Size.zero,
|
|
requests: FixedList(maxLength),
|
|
logs: FixedList(maxLength),
|
|
traffics: FixedList(30),
|
|
totalTraffic: Traffic(),
|
|
);
|
|
await _initDynamicColor();
|
|
await init();
|
|
}
|
|
|
|
_initDynamicColor() async {
|
|
try {
|
|
corePalette = await DynamicColorPlugin.getCorePalette();
|
|
accentColor = await DynamicColorPlugin.getAccentColor() ??
|
|
Color(defaultPrimaryColor);
|
|
} catch (_) {}
|
|
}
|
|
|
|
init() async {
|
|
packageInfo = await PackageInfo.fromPlatform();
|
|
config = await preferences.getConfig() ??
|
|
Config(
|
|
themeProps: defaultThemeProps,
|
|
);
|
|
await globalState.migrateOldData(config);
|
|
await AppLocalizations.load(
|
|
utils.getLocaleForString(config.appSetting.locale) ??
|
|
WidgetsBinding.instance.platformDispatcher.locale,
|
|
);
|
|
}
|
|
|
|
String get ua => config.patchClashConfig.globalUa ?? packageInfo.ua;
|
|
|
|
startUpdateTasks([UpdateTasks? tasks]) async {
|
|
if (timer != null && timer!.isActive == true) return;
|
|
if (tasks != null) {
|
|
this.tasks = tasks;
|
|
}
|
|
await executorUpdateTask();
|
|
timer = Timer(const Duration(seconds: 1), () async {
|
|
startUpdateTasks();
|
|
});
|
|
}
|
|
|
|
executorUpdateTask() async {
|
|
for (final task in tasks) {
|
|
await task();
|
|
}
|
|
timer = null;
|
|
}
|
|
|
|
stopUpdateTasks() {
|
|
if (timer == null || timer?.isActive == false) return;
|
|
timer?.cancel();
|
|
timer = null;
|
|
}
|
|
|
|
handleStart([UpdateTasks? tasks]) async {
|
|
startTime ??= DateTime.now();
|
|
await clashCore.startListener();
|
|
await service?.startVpn();
|
|
startUpdateTasks(tasks);
|
|
}
|
|
|
|
Future updateStartTime() async {
|
|
startTime = await clashLib?.getRunTime();
|
|
}
|
|
|
|
Future handleStop() async {
|
|
startTime = null;
|
|
await clashCore.stopListener();
|
|
await service?.stopVpn();
|
|
stopUpdateTasks();
|
|
}
|
|
|
|
Future<bool?> showMessage({
|
|
String? title,
|
|
required InlineSpan message,
|
|
String? confirmText,
|
|
bool cancelable = true,
|
|
}) async {
|
|
return await showCommonDialog<bool>(
|
|
child: Builder(
|
|
builder: (context) {
|
|
return CommonDialog(
|
|
title: title ?? appLocalizations.tip,
|
|
actions: [
|
|
if (cancelable)
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(false);
|
|
},
|
|
child: Text(appLocalizations.cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(true);
|
|
},
|
|
child: Text(confirmText ?? appLocalizations.confirm),
|
|
)
|
|
],
|
|
child: Container(
|
|
width: 300,
|
|
constraints: const BoxConstraints(maxHeight: 200),
|
|
child: SingleChildScrollView(
|
|
child: SelectableText.rich(
|
|
TextSpan(
|
|
style: Theme.of(context).textTheme.labelLarge,
|
|
children: [message],
|
|
),
|
|
style: const TextStyle(
|
|
overflow: TextOverflow.visible,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<T?> showCommonDialog<T>({
|
|
required Widget child,
|
|
bool dismissible = true,
|
|
}) async {
|
|
return await showModal<T>(
|
|
context: navigatorKey.currentState!.context,
|
|
configuration: FadeScaleTransitionConfiguration(
|
|
barrierColor: Colors.black38,
|
|
barrierDismissible: dismissible,
|
|
),
|
|
builder: (_) => child,
|
|
filter: commonFilter,
|
|
);
|
|
}
|
|
|
|
Future<T?> safeRun<T>(
|
|
FutureOr<T> Function() futureFunction, {
|
|
String? title,
|
|
bool silence = true,
|
|
}) async {
|
|
try {
|
|
final res = await futureFunction();
|
|
return res;
|
|
} catch (e) {
|
|
commonPrint.log("$e");
|
|
if (silence) {
|
|
showNotifier(e.toString());
|
|
} else {
|
|
showMessage(
|
|
title: title ?? appLocalizations.tip,
|
|
message: TextSpan(
|
|
text: e.toString(),
|
|
),
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
showNotifier(String text) {
|
|
if (text.isEmpty) {
|
|
return;
|
|
}
|
|
navigatorKey.currentContext?.showNotifier(text);
|
|
}
|
|
|
|
openUrl(String url) async {
|
|
final res = await showMessage(
|
|
message: TextSpan(text: url),
|
|
title: appLocalizations.externalLink,
|
|
confirmText: appLocalizations.go,
|
|
);
|
|
if (res != true) {
|
|
return;
|
|
}
|
|
launchUrl(Uri.parse(url));
|
|
}
|
|
|
|
Future<void> migrateOldData(Config config) async {
|
|
final clashConfig = await preferences.getClashConfig();
|
|
if (clashConfig != null) {
|
|
config = config.copyWith(
|
|
patchClashConfig: clashConfig,
|
|
);
|
|
preferences.clearClashConfig();
|
|
preferences.saveConfig(config);
|
|
}
|
|
}
|
|
|
|
CoreState getCoreState() {
|
|
final currentProfile = config.currentProfile;
|
|
return CoreState(
|
|
vpnProps: config.vpnProps,
|
|
onlyStatisticsProxy: config.appSetting.onlyStatisticsProxy,
|
|
currentProfileName: currentProfile?.label ?? currentProfile?.id ?? "",
|
|
bypassDomain: config.networkProps.bypassDomain,
|
|
);
|
|
}
|
|
|
|
getUpdateConfigParams([bool? isPatch]) {
|
|
final currentProfile = config.currentProfile;
|
|
final clashConfig = config.patchClashConfig;
|
|
final routeAddress =
|
|
config.networkProps.routeMode == RouteMode.bypassPrivate
|
|
? defaultBypassPrivateRouteAddress
|
|
: clashConfig.tun.routeAddress;
|
|
return UpdateConfigParams(
|
|
profileId: config.currentProfileId ?? "",
|
|
config: clashConfig.copyWith(
|
|
globalUa: ua,
|
|
tun: clashConfig.tun.copyWith(
|
|
autoRoute: routeAddress.isEmpty ? true : false,
|
|
routeAddress: routeAddress,
|
|
),
|
|
rule: currentProfile?.overrideData.runningRule ?? [],
|
|
),
|
|
params: ConfigExtendedParams(
|
|
isPatch: isPatch ?? false,
|
|
selectedMap: currentProfile?.selectedMap ?? {},
|
|
overrideDns: config.overrideDns,
|
|
testUrl: config.appSetting.testUrl,
|
|
overrideRule:
|
|
currentProfile?.overrideData.rule.type == OverrideRuleType.override
|
|
? true
|
|
: false,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
final globalState = GlobalState();
|