diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a7693a8..2f499cf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ ? = packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { - it.packageName == context?.packageName - || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == false - || it.packageName != "android" + it.packageName != context?.packageName + || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true + || it.packageName == "android" }?.map { Package( diff --git a/lib/application.dart b/lib/application.dart index ad9e2f6..f3352b5 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -53,6 +53,15 @@ class Application extends StatefulWidget { class ApplicationState extends State { late SystemColorSchemes systemColorSchemes; + final _pageTransitionsTheme = const PageTransitionsTheme( + builders: { + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.windows: CupertinoPageTransitionsBuilder(), + TargetPlatform.linux: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + }, + ); + ColorScheme _getAppColorScheme({ required Brightness brightness, int? primaryColor, @@ -73,6 +82,7 @@ class ApplicationState extends State { super.initState(); globalState.appController = AppController(context); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + globalState.appController.updateViewWidth(); globalState.appController.afterInit(); globalState.appController.initLink(); _updateGroups(); @@ -114,7 +124,7 @@ class ApplicationState extends State { globalState.groupsUpdateTimer = null; } globalState.groupsUpdateTimer ??= Timer.periodic( - appConstant.httpTimeoutDuration, + httpTimeoutDuration, (timer) async { await globalState.appController.updateGroups(); globalState.appController.appState.sortNum++; @@ -145,12 +155,13 @@ class ApplicationState extends State { GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate ], - title: appConstant.name, + title: appName, locale: other.getLocaleForString(state.locale), supportedLocales: - AppLocalizations.delegate.supportedLocales, + AppLocalizations.delegate.supportedLocales, themeMode: state.themeMode, theme: ThemeData( + pageTransitionsTheme: _pageTransitionsTheme, useMaterial3: true, colorScheme: _getAppColorScheme( brightness: Brightness.light, @@ -160,6 +171,7 @@ class ApplicationState extends State { ), darkTheme: ThemeData( useMaterial3: true, + pageTransitionsTheme: _pageTransitionsTheme, colorScheme: _getAppColorScheme( brightness: Brightness.dark, systemColorSchemes: systemColorSchemes, diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 26e160c..5a6f415 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -140,7 +140,7 @@ class ClashCore { bool delay(String proxyName) { final delayParams = { "proxy-name": proxyName, - "timeout": appConstant.httpTimeoutDuration.inMilliseconds, + "timeout": httpTimeoutDuration.inMilliseconds, }; clashFFI.asyncTestDelay(json.encode(delayParams).toNativeUtf8().cast()); return true; diff --git a/lib/common/constant.dart b/lib/common/constant.dart index 656e9b0..5f47226 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -3,28 +3,25 @@ import 'dart:ui'; import 'package:flutter/material.dart'; const appName = "FlClash"; +const coreName = "clash.meta"; +const packageName = "FlClash"; +const httpTimeoutDuration = Duration(milliseconds: 5000); +const moreDuration = Duration(milliseconds: 100); +const defaultUpdateDuration = Duration(days: 1); +const mmdbFileName = "geoip.metadb"; +const profilesDirectoryName = "profiles"; +const localhost = "127.0.0.1"; +const clashConfigKey = "clash_config"; +const configKey = "config"; +const listItemPadding = EdgeInsets.symmetric(horizontal: 16); +const double dialogCommonWidth = 300; +const repository = "chen08209/FlClash"; +const maxMobileWidth = 600; +const maxLaptopWidth = 840; +final filter = ImageFilter.blur( + sigmaX: 5, + sigmaY: 5, + tileMode: TileMode.mirror, +); -class AppConstant { - final packageName = "com.follow.clash"; - final name = "FlClash"; - final httpTimeoutDuration = const Duration(milliseconds: 5000); - final moreDuration = const Duration(milliseconds: 100); - final defaultUpdateDuration = const Duration(days: 1); - final mmdbFileName = "geoip.metadb"; - final profilesDirectoryName = "profiles"; - final configFileName = "config.yaml"; - final localhost = "127.0.0.1"; - final clashKey = "clash"; - final configKey = "config"; - final listItemPadding = const EdgeInsets.symmetric(horizontal: 16); - final dialogCommonWidth = 300; - final repository = "chen08209/FlClash"; - final filter = ImageFilter.blur( - sigmaX: 5, - sigmaY: 5, - tileMode: TileMode.mirror, - ); - final defaultPrimaryColor = Colors.brown; -} - -final appConstant = AppConstant(); +const defaultPrimaryColor = Colors.brown; diff --git a/lib/common/context.dart b/lib/common/context.dart index 6aca2a5..6bc2e56 100644 --- a/lib/common/context.dart +++ b/lib/common/context.dart @@ -11,8 +11,6 @@ extension BuildContextExtension on BuildContext { return MediaQuery.of(this).size.width; } - bool get isMobile => width < 600; - ColorScheme get colorScheme => Theme.of(this).colorScheme; TextTheme get textTheme => Theme.of(this).textTheme; diff --git a/lib/common/dav_client.dart b/lib/common/dav_client.dart new file mode 100644 index 0000000..e546739 --- /dev/null +++ b/lib/common/dav_client.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:path/path.dart'; +import 'package:webdav_client/webdav_client.dart'; + +class DAVClient { + late Client client; + Completer pingCompleter = Completer(); + + DAVClient(DAV dav) { + client = newClient( + dav.uri, + user: dav.user, + password: dav.password, + ); + client.setHeaders( + { + 'accept-charset': 'utf-8', + 'Content-Type': 'text/xml', + }, + ); + client.setConnectTimeout(8000); + client.setSendTimeout(8000); + client.setReceiveTimeout(8000); + pingCompleter.complete(_ping()); + } + + Future _ping() async { + try { + await client.ping(); + await client.mkdir("/$appName"); + await client.mkdir("/$appName/$profilesDirectoryName"); + return true; + } catch (_) { + return false; + } + } + + get root => "/$appName"; + + get remoteConfig => "$root/$configKey.json"; + + get remoteClashConfig => "$root/$clashConfigKey.json"; + + get remoteProfiles => "$root/$profilesDirectoryName"; + + backup() async { + final appController = globalState.appController; + final config = appController.config; + final clashConfig = appController.clashConfig; + await client.mkdir("$root"); + client.write( + remoteConfig, + utf8.encode( + json.encode(config.toJson()), + ), + ); + client.write( + remoteClashConfig, + utf8.encode( + json.encode(clashConfig.toJson()), + ), + ); + await client.remove(remoteProfiles); + for (final profile in config.profiles) { + final path = await appPath.getProfilePath(profile.id); + if (path == null) continue; + await client.writeFromFile( + path, + "$remoteProfiles/${basename(path)}", + ); + } + return true; + } + + recovery({required RecoveryOption recoveryOption}) async { + final profiles = await client.readDir(remoteProfiles); + final profilesPath = await appPath.getProfilesPath(); + for (final file in profiles) { + await client.read2File( + "$remoteProfiles/${file.name}", + join( + profilesPath, + file.name, + ), + ); + } + final configRaw = utf8.decode((await client.read(remoteConfig))); + final clashConfigRaw = utf8.decode(await client.read(remoteClashConfig)); + final config = Config.fromJson(json.decode(configRaw)); + final clashConfig = ClashConfig.fromJson(json.decode(clashConfigRaw)); + if(recoveryOption == RecoveryOption.onlyProfiles){ + globalState.appController.config.update(config, RecoveryOption.onlyProfiles); + }else{ + globalState.appController.config.update(config, RecoveryOption.all); + globalState.appController.clashConfig.update(clashConfig); + } + await globalState.appController.applyProfile(); + globalState.appController.savePreferences(); + return true; + } +} diff --git a/lib/common/launch.dart b/lib/common/launch.dart index a00ea0e..2094757 100644 --- a/lib/common/launch.dart +++ b/lib/common/launch.dart @@ -10,7 +10,7 @@ class AutoLaunch { AutoLaunch._internal() { launchAtStartup.setup( - appName: appConstant.name, + appName: appName, appPath: Platform.resolvedExecutable, ); } diff --git a/lib/common/other.dart b/lib/common/other.dart index f7f2c43..abec888 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -147,6 +147,26 @@ class Other { } }); } + + String? getFileNameForDisposition(String? disposition) { + if (disposition == null) return null; + final parseValue = HeaderValue.parse(disposition); + final parameters = parseValue.parameters; + final key = parameters.keys + .firstWhere((key) => key.startsWith("filename"), orElse: () => ''); + if (key.isEmpty) return null; + if (key == "filename*") { + return Uri.decodeComponent((parameters[key] ?? "").split("'").last); + } else { + return parameters[key]; + } + } + + double getViewWidth(){ + final view = WidgetsBinding.instance.platformDispatcher.views.first; + final size = view.physicalSize / view.devicePixelRatio; + return size.width; + } } final other = Other(); diff --git a/lib/common/path.dart b/lib/common/path.dart index 18449ad..dcdc3a5 100644 --- a/lib/common/path.dart +++ b/lib/common/path.dart @@ -26,14 +26,9 @@ class AppPath { return directory.path; } - Future getConfigPath() async { - final directory = await applicationSupportDirectoryCompleter.future; - return join(directory.path, appConstant.configFileName); - } - Future getProfilesPath() async { final directory = await applicationSupportDirectoryCompleter.future; - return join(directory.path, appConstant.profilesDirectoryName); + return join(directory.path, profilesDirectoryName); } Future getProfilePath(String? id) async { @@ -44,7 +39,7 @@ class AppPath { Future getMMDBPath() async { var directory = await applicationSupportDirectoryCompleter.future; - return join(directory.path, appConstant.mmdbFileName); + return join(directory.path, mmdbFileName); } } diff --git a/lib/common/picker.dart b/lib/common/picker.dart index 9684365..4aec5c7 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -23,9 +23,9 @@ class Picker { } final file = filePickerResult?.files.first; if (file == null) { - return Result.error(message: appLocalizations.pleaseUploadFile); + return Result.error(appLocalizations.pleaseUploadFile); } - return Result.success(data: file); + return Result.success(file); } Future> pickerConfigQRCode() async { @@ -34,9 +34,9 @@ class Picker { if (bytes == null) return Result.error(); final result = await other.parseQRCode(bytes); if (result == null || !result.isUrl) { - return Result.error(message: appLocalizations.pleaseUploadValidQrcode); + return Result.error(appLocalizations.pleaseUploadValidQrcode); } - return Result.success(data: result); + return Result.success(result); } } diff --git a/lib/common/preferences.dart b/lib/common/preferences.dart index e5b68ad..c49b4b5 100644 --- a/lib/common/preferences.dart +++ b/lib/common/preferences.dart @@ -22,7 +22,7 @@ class Preferences { Future getClashConfig() async { final preferences = await sharedPreferencesCompleter.future; - final clashConfigString = preferences.getString(appConstant.clashKey); + final clashConfigString = preferences.getString(clashConfigKey); if (clashConfigString == null) return null; final clashConfigMap = json.decode(clashConfigString); try { @@ -35,14 +35,14 @@ class Preferences { Future saveClashConfig(ClashConfig clashConfig) async { final preferences = await sharedPreferencesCompleter.future; return preferences.setString( - appConstant.clashKey, + clashConfigKey, json.encode(clashConfig), ); } Future getConfig() async { final preferences = await sharedPreferencesCompleter.future; - final configString = preferences.getString(appConstant.configKey); + final configString = preferences.getString(configKey); if (configString == null) return null; final configMap = json.decode(configString); try { @@ -55,7 +55,7 @@ class Preferences { Future saveConfig(Config config) async { final preferences = await sharedPreferencesCompleter.future; return preferences.setString( - appConstant.configKey, + configKey, json.encode(config), ); } diff --git a/lib/common/request.dart b/lib/common/request.dart index f7333f8..918ac4e 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -6,21 +6,21 @@ import '../models/models.dart'; class Request { static Future> getFileResponseForUrl(String url) async { - final headers = {'User-Agent': appConstant.name}; + final headers = {'User-Agent': coreName}; try { final response = await get(Uri.parse(url), headers: headers).timeout( - appConstant.httpTimeoutDuration, + httpTimeoutDuration, ); - return Result.success(data: response); + return Result.success(response); } catch (err) { - return Result.error(message: err.toString()); + return Result.error(err.toString()); } } static Future> checkForUpdate() async { final response = await get( Uri.parse( - "https://api.github.com/repos/${appConstant.repository}/releases/latest", + "https://api.github.com/repos/$repository/releases/latest", ), ); if (response.statusCode != 200) return Result.error(); @@ -31,6 +31,6 @@ class Request { final hasUpdate = other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; if (!hasUpdate) return Result.error(); - return Result.success(data: body['body']); + return Result.success(body['body']); } } diff --git a/lib/controller.dart b/lib/controller.dart index 935e80e..1d632b1 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -116,7 +116,7 @@ class AppController { ); } - applyProfile() async { + Future applyProfile() async { await globalState.applyProfile( appState: appState, config: config, @@ -208,6 +208,17 @@ class AppController { } } + autoCheckUpdate() async { + final res = await Request.checkForUpdate(); + if(res.type != ResultType.success) return; + globalState.showMessage( + title: appLocalizations.checkUpdate, + message: TextSpan( + text: res.data, + ), + ); + } + afterInit() async { if (config.autoRun) { await updateSystemProxy(true); @@ -220,6 +231,7 @@ class AppController { if (!config.silentLaunch) { window?.show(); } + autoCheckUpdate(); } healthcheck() { @@ -244,8 +256,7 @@ class AppController { } toPage(int index, {bool hasAnimate = false}) { - final nextLabel = globalState.currentNavigationItems[index].label; - appState.currentLabel = nextLabel; + appState.currentLabel = appState.currentNavigationItems[index].label; if ((config.isAnimateToPage || hasAnimate)) { globalState.pageController?.animateToPage( index, @@ -257,12 +268,9 @@ class AppController { } } - updatePackages() async { - await globalState.updatePackages(appState); - } toProfiles() { - final index = globalState.currentNavigationItems.indexWhere( + final index = appState.currentNavigationItems.indexWhere( (element) => element.label == "profiles", ); if (index != -1) { @@ -385,4 +393,13 @@ class AppController { globalState.updateCurrentDelay(showProxyDelay); } } + + updateViewWidth(){ + appState.viewWidth = context.width; + if(appState.viewWidth == 0){ + Future.delayed(moreDuration,(){ + updateViewWidth(); + }); + } + } } diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index dde8d6b..3627500 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -34,6 +34,8 @@ extension UsedProxyExtension on UsedProxy { enum Mode { rule, global, direct } +enum ViewMode { mobile, laptop, desktop } + enum LogLevel { debug, info, warning, error, silent } enum TransportProtocol { udp, tcp } @@ -55,3 +57,8 @@ enum ProfileType { file, url } enum ResultType { success, error } enum MessageType { log, tun, delay, process, now } + +enum RecoveryOption { + all, + onlyProfiles, +} diff --git a/lib/fragments/about.dart b/lib/fragments/about.dart index af2938c..2cacfcb 100644 --- a/lib/fragments/about.dart +++ b/lib/fragments/about.dart @@ -1,4 +1,6 @@ import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -7,6 +9,31 @@ import 'package:url_launcher/url_launcher.dart'; class AboutFragment extends StatelessWidget { const AboutFragment({super.key}); + _checkUpdate(BuildContext context) async { + final commonScaffoldState = context.commonScaffoldState; + if (commonScaffoldState?.mounted != true) return; + final res = await commonScaffoldState?.loadingRun>( + Request.checkForUpdate, + title: appLocalizations.checkUpdate, + ); + if (res == null) return; + if (res.type == ResultType.success) { + globalState.showMessage( + title: appLocalizations.checkUpdate, + message: TextSpan( + text: res.data, + ), + ); + } else { + globalState.showMessage( + title: appLocalizations.checkUpdate, + message: TextSpan( + text: appLocalizations.checkUpdateError, + ), + ); + } + } + @override Widget build(BuildContext context) { return ListView( @@ -32,7 +59,7 @@ class AboutFragment extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - appConstant.name, + appName, style: Theme.of(context).textTheme.headlineSmall, ), FutureBuilder( @@ -65,19 +92,8 @@ class AboutFragment extends StatelessWidget { ), ListTile( title: Text(appLocalizations.checkUpdate), - onTap: () { - final commonScaffoldState = context.commonScaffoldState; - if (commonScaffoldState?.mounted != true) return; - commonScaffoldState?.loadingRun(() async { - await globalState.checkUpdate( - () { - launchUrl( - Uri.parse( - "https://github.com/${appConstant.repository}/releases/latest"), - ); - }, - ); - }); + onTap: (){ + _checkUpdate(context); }, ), ListTile( @@ -93,7 +109,7 @@ class AboutFragment extends StatelessWidget { title: Text(appLocalizations.project), onTap: () { launchUrl( - Uri.parse("https://github.com/${appConstant.repository}"), + Uri.parse("https://github.com/$repository"), ); }, trailing: const Icon(Icons.launch), diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 814fb92..21ea332 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -2,62 +2,26 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AccessFragment extends StatelessWidget { +class AccessFragment extends StatefulWidget { const AccessFragment({super.key}); - Widget _buildPackageItem({ - required Package package, - required bool value, - required bool isActive, - required void Function(bool?) onChanged, - }) { - return AbsorbPointer( - absorbing: !isActive, - child: ListItem.checkbox( - leading: SizedBox( - width: 48, - height: 48, - child: FutureBuilder( - future: app?.getPackageIcon(package.packageName), - builder: (_, snapshot) { - if (!snapshot.hasData && snapshot.data == null) { - return Container(); - } else { - return Image( - image: snapshot.data!, - gaplessPlayback: true, - width: 48, - height: 48, - ); - } - }, - ), - ), - title: Text( - package.label, - style: const TextStyle( - overflow: TextOverflow.ellipsis, - ), - maxLines: 1, - ), - subtitle: Text( - package.packageName, - style: const TextStyle( - overflow: TextOverflow.ellipsis, - ), - maxLines: 1, - ), - delegate: CheckboxDelegate( - value: value, - onChanged: onChanged, - ), - ), - ); + @override + State createState() => _AccessFragmentState(); +} + +class _AccessFragmentState extends State { + final packagesListenable = ValueNotifier>([]); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + packagesListenable.value = await app?.getPackages() ?? []; + }); } Widget _buildAppProxyModePopup() { @@ -144,148 +108,165 @@ class AccessFragment extends StatelessWidget { ); } - Widget _buildPackageList(bool isAccessControl) { - return Selector2( - selector: (_, appState, config) => PackageListSelectorState( - accessControl: config.accessControl, - packages: appState.packages, - ), - builder: (context, state, __) { - final accessControl = state.accessControl; - final isFilterSystemApp = accessControl.isFilterSystemApp; - final packages = isFilterSystemApp - ? state.packages - .where((element) => element.isSystem == false) - .toList() - : state.packages; - final packageNameList = packages.map((e) => e.packageName).toList(); - final accessControlMode = accessControl.mode; - final valueList = - accessControl.currentList.intersection(packageNameList); - final describe = accessControlMode == AccessControlMode.acceptSelected - ? appLocalizations.accessControlAllowDesc - : appLocalizations.accessControlNotAllowDesc; - - final listView = ListView.builder( - itemCount: packages.length, - itemBuilder: (_, index) { - final package = packages[index]; - return _buildPackageItem( - package: package, - value: valueList.contains(package.packageName), - isActive: isAccessControl, - onChanged: (value) { - if (value == true) { - valueList.add(package.packageName); - } else { - valueList.remove(package.packageName); - } - final config = context.read(); - config.accessControl.currentList = valueList; - config.accessControl = config.accessControl.copyWith(); - }, - ); - }, - ); - - return DisabledMask( - status: !isAccessControl, - child: Column( - children: [ - AbsorbPointer( - absorbing: !isAccessControl, - child: Padding( - padding: const EdgeInsets.only( - top: 4, - bottom: 4, - left: 16, - right: 8, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Row( - children: [ - Flexible( - child: Text( - appLocalizations.selected, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - const Flexible( - child: SizedBox( - width: 8, - ), - ), - Flexible( - child: Text( - "${valueList.length}", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - ], - ), - ), - Flexible( - child: Text(describe), - ) - ], - ), - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, + Widget _actionHeader({ + required bool isAccessControl, + required List valueList, + required String describe, + required List packageNameList, + }) { + return AbsorbPointer( + absorbing: !isAccessControl, + child: Padding( + padding: const EdgeInsets.only( + top: 4, + bottom: 4, + left: 16, + right: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( children: [ Flexible( - child: _buildSelectedAllButton( - isSelectedAll: - valueList.length == packageNameList.length, - allValueList: packageNameList, + child: Text( + appLocalizations.selected, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ), + ), + const Flexible( + child: SizedBox( + width: 8, + ), + ), + Flexible( + child: Text( + "${valueList.length}", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + Theme.of(context).colorScheme.primary, + ), ), ), - Flexible(child: _buildFilterSystemAppButton()), - Flexible(child: _buildAppProxyModePopup()), ], ), - ], + ), + Flexible( + child: Text(describe), + ) + ], + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: _buildSelectedAllButton( + isSelectedAll: valueList.length == packageNameList.length, + allValueList: packageNameList, ), ), + Flexible(child: _buildFilterSystemAppButton()), + Flexible(child: _buildAppProxyModePopup()), + ], + ), + ], + ), + ), + ); + } + + Widget _buildPackageList(bool isAccessControl) { + return ValueListenableBuilder( + valueListenable: packagesListenable, + builder: (_, packages, ___) { + return Selector( + selector: (_, config) => config.accessControl, + builder: (context, accessControl, __) { + final isFilterSystemApp = accessControl.isFilterSystemApp; + final currentPackages = isFilterSystemApp + ? packages + .where((element) => element.isSystem == false) + .toList() + : packages; + final packageNameList = + currentPackages.map((e) => e.packageName).toList(); + final accessControlMode = accessControl.mode; + final valueList = + accessControl.currentList.intersection(packageNameList); + final describe = + accessControlMode == AccessControlMode.acceptSelected + ? appLocalizations.accessControlAllowDesc + : appLocalizations.accessControlNotAllowDesc; + + return DisabledMask( + status: !isAccessControl, + child: Column( + children: [ + _actionHeader( + isAccessControl: isAccessControl, + valueList: valueList, + describe: describe, + packageNameList: packageNameList, + ), + Expanded( + flex: 1, + child: FadeBox( + key: const Key("fade_box"), + child: currentPackages.isEmpty + ? const Center( + child: CircularProgressIndicator(), + ) + : ListView.builder( + itemCount: currentPackages.length, + itemBuilder: (_, index) { + final package = currentPackages[index]; + return PackageListItem( + key: Key(package.label), + package: package, + value: + valueList.contains(package.packageName), + isActive: isAccessControl, + onChanged: (value) { + if (value == true) { + valueList.add(package.packageName); + } else { + valueList.remove(package.packageName); + } + final config = context.read(); + config.accessControl.currentList = + valueList; + config.accessControl = + config.accessControl.copyWith(); + }, + ); + }, + ), + ), + ), + ], ), - Flexible( - flex: 1, - child: FadeBox( - child: packages.isEmpty - ? const Center( - child: CircularProgressIndicator(), - ) - : listView, - ), - ), - ], - ), + ); + }, ); }, ); @@ -293,11 +274,6 @@ class AccessFragment extends StatelessWidget { @override Widget build(BuildContext context) { - if (globalState.appController.appState.packages.isEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - globalState.appController.updatePackages(); - }); - } return Selector( selector: (_, config) => config.isAccessControl, builder: (_, isAccessControl, __) { @@ -332,3 +308,64 @@ class AccessFragment extends StatelessWidget { ); } } + +class PackageListItem extends StatelessWidget { + final Package package; + final bool value; + final bool isActive; + final void Function(bool?) onChanged; + + const PackageListItem({ + super.key, + required this.package, + required this.value, + required this.isActive, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return AbsorbPointer( + absorbing: !isActive, + child: ListItem.checkbox( + leading: SizedBox( + width: 48, + height: 48, + child: FutureBuilder( + future: app?.getPackageIcon(package.packageName), + builder: (_, snapshot) { + if (!snapshot.hasData && snapshot.data == null) { + return Container(); + } else { + return Image( + image: snapshot.data!, + gaplessPlayback: true, + width: 48, + height: 48, + ); + } + }, + ), + ), + title: Text( + package.label, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + subtitle: Text( + package.packageName, + style: const TextStyle( + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + delegate: CheckboxDelegate( + value: value, + onChanged: onChanged, + ), + ), + ); + } +} diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart index 06bdd81..e0bc6b7 100644 --- a/lib/fragments/application_setting.dart +++ b/lib/fragments/application_setting.dart @@ -36,26 +36,6 @@ class ApplicationSettingFragment extends StatelessWidget { ); }, ), - Selector( - selector: (_, config) => config.isCompatible, - builder: (_, isCompatible, __) { - return ListItem.switchItem( - leading: const Icon(Icons.expand), - title: Text(appLocalizations.compatible), - subtitle: Text(appLocalizations.compatibleDesc), - delegate: SwitchDelegate( - value: isCompatible, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.config.isCompatible = value; - await appController.updateClashConfig(isPatch: false); - await appController.updateGroups(); - appController.changeProxy(); - }, - ), - ); - }, - ), if (system.isDesktop) Selector( selector: (_, config) => config.autoLaunch, @@ -109,24 +89,6 @@ class ApplicationSettingFragment extends StatelessWidget { ); }, ), - Selector( - selector: (_, config) => config.openLogs, - builder: (_, openLogs, child) { - return ListItem.switchItem( - leading: const Icon(Icons.bug_report), - title: Text(appLocalizations.logcat), - subtitle: Text(appLocalizations.logcatDesc), - delegate: SwitchDelegate( - value: openLogs, - onChanged: (bool value) { - final config = context.read(); - config.openLogs = value; - globalState.appController.updateLogStatus(); - }, - ), - ); - }, - ), if (Platform.isAndroid) Selector( selector: (_, config) => config.isAnimateToPage, @@ -145,6 +107,41 @@ class ApplicationSettingFragment extends StatelessWidget { ); }, ), + Selector( + selector: (_, config) => config.openLogs, + builder: (_, openLogs, child) { + return ListItem.switchItem( + leading: const Icon(Icons.bug_report), + title: Text(appLocalizations.logcat), + subtitle: Text(appLocalizations.logcatDesc), + delegate: SwitchDelegate( + value: openLogs, + onChanged: (bool value) { + final config = context.read(); + config.openLogs = value; + globalState.appController.updateLogStatus(); + }, + ), + ); + }, + ), + Selector( + selector: (_, config) => config.autoCheckUpdate, + builder: (_, autoCheckUpdate, child) { + return ListItem.switchItem( + leading: const Icon(Icons.system_update), + title: Text(appLocalizations.autoCheckUpdate), + subtitle: Text(appLocalizations.autoCheckUpdateDesc), + delegate: SwitchDelegate( + value: autoCheckUpdate, + onChanged: (bool value) { + final config = context.read(); + config.autoCheckUpdate = value; + }, + ), + ); + }, + ), ]; return ListView.separated( itemBuilder: (_, index) { diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart new file mode 100644 index 0000000..e7f511e --- /dev/null +++ b/lib/fragments/backup_and_recovery.dart @@ -0,0 +1,362 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/common/dav_client.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/config.dart'; +import 'package:fl_clash/models/dav.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/fade_box.dart'; +import 'package:fl_clash/widgets/list.dart'; +import 'package:fl_clash/widgets/section.dart'; +import 'package:fl_clash/widgets/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class BackupAndRecovery extends StatefulWidget { + const BackupAndRecovery({super.key}); + + @override + State createState() => _BackupAndRecoveryState(); +} + +class _BackupAndRecoveryState extends State { + DAVClient? _client; + + _showAddWebDAV(DAV? dav) async { + await globalState.showCommonDialog( + child: WebDAVFormDialog( + dav: dav?.copyWith(), + ), + ); + } + + _backup() async { + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun(() async { + return await _client?.backup(); + }); + if(res != true) return; + globalState.showMessage( + title: appLocalizations.recovery, + message: TextSpan(text: appLocalizations.backupSuccess), + ); + } + + _recovery(RecoveryOption recoveryOption) async { + final commonScaffoldState = context.commonScaffoldState; + final res = await commonScaffoldState?.loadingRun(() async { + return await _client?.recovery(recoveryOption: recoveryOption); + }); + if(res != true) return; + globalState.showMessage( + title: appLocalizations.recovery, + message: TextSpan(text: appLocalizations.recoverySuccess), + ); + } + + _handleRecovery() async { + final recoveryOption = await globalState.showCommonDialog( + child: const RecoveryOptionsDialog(), + ); + if (recoveryOption == null) return; + _recovery(recoveryOption); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.dav, + builder: (_, dav, __) { + if (dav == null) { + return ListView( + children: [ + Section( + title: appLocalizations.account, + child: Builder( + builder: (_) { + return ListItem( + leading: const Icon(Icons.account_box), + title: Text(appLocalizations.noInfo), + subtitle: Text(appLocalizations.pleaseBindWebDAV), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); + }, + child: Text( + appLocalizations.bind, + ), + ), + ); + }, + ), + ) + ], + ); + } + _client = DAVClient(dav); + final pingFuture = _client!.pingCompleter.future; + return ListView( + children: [ + Section( + title: appLocalizations.account, + child: ListItem( + leading: const Icon(Icons.account_box), + title: TooltipText( + text: Text( + dav.user, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(appLocalizations.connectivity), + FutureBuilder( + future: pingFuture, + builder: (_, snapshot) { + return Center( + child: FadeBox( + key: const Key("fade_box_1"), + child: snapshot.connectionState == + ConnectionState.waiting + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + : Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: snapshot.data == true + ? Colors.green + : Colors.red, + ), + width: 12, + height: 12, + ), + ), + ); + }, + ), + ], + ), + ), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); + }, + child: Text( + appLocalizations.edit, + ), + ), + ), + ), + FutureBuilder( + future: pingFuture, + builder: (_, snapshot) { + return FadeBox( + key: const Key("fade_box_2"), + child: snapshot.data == true + ? Section( + title: appLocalizations.backupAndRecovery, + child: Column( + children: [ + ListItem( + onTab: _backup, + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.backupDesc), + ), + ListItem( + onTab: _handleRecovery, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.recoveryDesc), + ), + ], + ), + ) + : Container(), + ); + }, + ), + ], + ); + }, + ); + } +} + +class WebDAVFormDialog extends StatefulWidget { + final DAV? dav; + + const WebDAVFormDialog({super.key, this.dav}); + + @override + State createState() => _WebDAVFormDialogState(); +} + +class _WebDAVFormDialogState extends State { + late TextEditingController uriController; + late TextEditingController userController; + late TextEditingController passwordController; + final _obscureController = ValueNotifier(true); + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + uriController = TextEditingController(text: widget.dav?.uri); + userController = TextEditingController(text: widget.dav?.user); + passwordController = TextEditingController(text: widget.dav?.password); + } + + _submit() { + if (!_formKey.currentState!.validate()) return; + globalState.appController.config.dav = DAV( + uri: uriController.text, + user: userController.text, + password: passwordController.text, + ); + Navigator.pop(context); + } + + _delete() { + globalState.appController.config.dav = null; + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.webDAVConfiguration), + content: Form( + key: _formKey, + child: SizedBox( + width: dialogCommonWidth, + child: Wrap( + runSpacing: 16, + children: [ + TextFormField( + controller: uriController, + maxLines: 2, + minLines: 1, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link), + border: const OutlineInputBorder(), + labelText: appLocalizations.address, + helperText: appLocalizations.addressHelp, + ), + validator: (String? value) { + if (value == null || value.isEmpty || !value.isUrl) { + return appLocalizations.addressTip; + } + return null; + }, + ), + TextFormField( + controller: userController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.account_circle), + border: const OutlineInputBorder(), + labelText: appLocalizations.account, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.accountTip; + } + return null; + }, + ), + ValueListenableBuilder( + valueListenable: _obscureController, + builder: (_, obscure, __) { + return TextFormField( + controller: passwordController, + obscureText: obscure, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.password), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: Icon( + obscure ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () { + _obscureController.value = !obscure; + }, + ), + labelText: appLocalizations.password, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.passwordTip; + } + return null; + }, + ); + }, + ), + ], + ), + ), + ), + actions: [ + if (widget.dav != null) + TextButton( + onPressed: _delete, + child: Text(appLocalizations.delete), + ), + TextButton( + onPressed: _submit, + child: Text(appLocalizations.save), + ) + ], + ); + } +} + +class RecoveryOptionsDialog extends StatefulWidget { + const RecoveryOptionsDialog({super.key}); + + @override + State createState() => _RecoveryOptionsDialogState(); +} + +class _RecoveryOptionsDialogState extends State { + _handleOnTab(RecoveryOption? value) { + if (value == null) return; + Navigator.of(context).pop(value); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(appLocalizations.recovery), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + ListItem( + onTab: () { + _handleOnTab(RecoveryOption.onlyProfiles); + }, + title: Text(appLocalizations.recoveryProfiles), + ), + ListItem( + onTab: () { + _handleOnTab(RecoveryOption.all); + }, + title: Text(appLocalizations.recoveryAll), + ) + ], + ), + ), + ); + } +} diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index 61873a3..00bd24d 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -47,6 +47,27 @@ class _ConfigFragmentState extends State { @override Widget build(BuildContext context) { List items = [ + Selector( + selector: (_, clashConfig) => clashConfig.mixedPort, + builder: (_, mixedPort, __) { + return ListItem( + onTab: () { + _modifyMixedPort(mixedPort); + }, + padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4), + leading: const Icon(Icons.adjust), + title: Text(appLocalizations.proxyPort), + trailing: FilledButton.tonal( + onPressed: () { + _modifyMixedPort(mixedPort); + }, + child: Text( + "$mixedPort", + ), + ), + ); + }, + ), Selector( selector: (_, clashConfig) => clashConfig.allowLan, builder: (_, allowLan, __) { @@ -84,62 +105,64 @@ class _ConfigFragmentState extends State { ); }, ), - Selector( - selector: (_, clashConfig) => clashConfig.mixedPort, - builder: (_, mixedPort, __) { - return ListItem( - onTab: () { - _modifyMixedPort(mixedPort); - }, - leading: const Icon(Icons.adjust), - title: Text(appLocalizations.proxyPort), - trailing: FilledButton.tonal( - onPressed: () { - _modifyMixedPort(mixedPort); + Selector( + selector: (_, config) => config.isCompatible, + builder: (_, isCompatible, __) { + return ListItem.switchItem( + leading: const Icon(Icons.expand), + title: Text(appLocalizations.compatible), + subtitle: Text(appLocalizations.compatibleDesc), + delegate: SwitchDelegate( + value: isCompatible, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.isCompatible = value; + await appController.updateClashConfig(isPatch: false); + await appController.updateGroups(); + appController.changeProxy(); }, - child: Text( - "$mixedPort", - ), ), ); }, ), - Selector( - selector: (_, clashConfig) => clashConfig.logLevel, - builder: (_, value, __) { - return ListItem( - leading: const Icon(Icons.feedback), - title: Text(appLocalizations.logLevel), - trailing: SizedBox( - height: 48, - child: DropdownMenu( - width: 124, - inputDecorationTheme: const InputDecorationTheme( - filled: true, - contentPadding: EdgeInsets.symmetric( - vertical: 5, - horizontal: 16, + Padding( + padding: kMaterialListPadding, + child: Selector( + selector: (_, clashConfig) => clashConfig.logLevel, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.feedback), + title: Text(appLocalizations.logLevel), + trailing: SizedBox( + height: 48, + child: DropdownMenu( + width: 124, + inputDecorationTheme: const InputDecorationTheme( + filled: true, + contentPadding: EdgeInsets.symmetric( + vertical: 5, + horizontal: 16, + ), ), + initialSelection: value, + dropdownMenuEntries: [ + for (final logLevel in LogLevel.values) + DropdownMenuEntry( + value: logLevel, + label: logLevel.name, + ) + ], + onSelected: _updateLoglevel, ), - initialSelection: value, - dropdownMenuEntries: [ - for (final logLevel in LogLevel.values) - DropdownMenuEntry( - value: logLevel, - label: logLevel.name, - ) - ], - onSelected: _updateLoglevel, ), - ), - ); - }, + ); + }, + ), ), ]; return ListView.separated( itemBuilder: (_, index) { return Container( - padding: kMaterialListPadding, alignment: Alignment.center, child: items[index], ); diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index 5c3646d..852c6ea 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -1,6 +1,8 @@ +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:fl_clash/widgets/widgets.dart'; +import 'package:provider/provider.dart'; import 'network_detection.dart'; import 'core_info.dart'; @@ -17,63 +19,51 @@ class DashboardFragment extends StatefulWidget { } class _DashboardFragmentState extends State { - _buildGrid(bool isDesktop) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Grid( - crossAxisCount: 12, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - children: [ - GridItem( - crossAxisCellCount: isDesktop ? 8 : 12, - child: const NetworkSpeed(), - ), - GridItem( - crossAxisCellCount: isDesktop ? 4 : 6, - child: const OutboundMode(), - ), - GridItem( - crossAxisCellCount: isDesktop ? 4 : 6, - child: const NetworkDetection(), - ), - GridItem( - crossAxisCellCount: isDesktop ? 4 : 6, - child: const TrafficUsage(), - ), - GridItem( - crossAxisCellCount: isDesktop ? 4 : 6, - child: const CoreInfo(), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (_, container) { - if (container.maxWidth < 200) return Container(); - return FloatLayout( - floatingWidget: const FloatWrapper( - child: StartButton(), - ), - child: Align( - alignment: Alignment.topCenter, - child: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('dashboard_small'), - builder: (_) => _buildGrid(false), - ), - Breakpoints.mediumAndUp: SlotLayout.from( - key: const Key('dashboard_mediumAndUp'), - builder: (_) => _buildGrid(true), - ), + return FloatLayout( + floatingWidget: const FloatWrapper( + child: StartButton(), + ), + child: Align( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Selector( + selector: (_, appState) => appState.viewMode, + builder: (_, viewMode, ___) { + final isDesktop = viewMode == ViewMode.desktop; + return Grid( + crossAxisCount: 12, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: [ + GridItem( + crossAxisCellCount: isDesktop ? 8 : 12, + child: const NetworkSpeed(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const OutboundMode(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const NetworkDetection(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const TrafficUsage(), + ), + GridItem( + crossAxisCellCount: isDesktop ? 4 : 6, + child: const CoreInfo(), + ), + ], + ); }, ), ), - ); - }); + ), + ); } } diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index ec786d6..aadacaa 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -73,28 +73,6 @@ class _NetworkDetectionState extends State { ); } - _updateCurrentDelay( - String? currentProxyName, - int? delay, - bool isCurrent, - bool isInit, - ) { - if (!isCurrent || currentProxyName == null || !isInit) return; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (delay == null) { - globalState.appController.setDelay( - Delay( - name: currentProxyName, - value: 0, - ), - ); - globalState.updateCurrentDelay( - currentProxyName, - ); - } - }); - } - @override Widget build(BuildContext context) { return CommonCard( diff --git a/lib/fragments/fragments.dart b/lib/fragments/fragments.dart index 20640a5..8bb8332 100644 --- a/lib/fragments/fragments.dart +++ b/lib/fragments/fragments.dart @@ -7,4 +7,5 @@ export 'connections.dart'; export 'access.dart'; export 'config.dart'; export 'application_setting.dart'; -export 'about.dart'; \ No newline at end of file +export 'about.dart'; +export 'backup_and_recovery.dart'; \ No newline at end of file diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index 42ed6ff..9b3f84a 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -1,47 +1,16 @@ -import 'dart:async'; - +import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/models.dart'; import '../widgets/widgets.dart'; -class LogsFragment extends StatefulWidget { +class LogsFragment extends StatelessWidget { const LogsFragment({super.key}); - @override - State createState() => _LogsFragmentState(); -} - -class _LogsFragmentState extends State { - final logsNotifier = ValueNotifier>([]); - Timer? timer; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - logsNotifier.value = context.read().logs; - if (timer != null) { - timer?.cancel(); - timer = null; - } - timer = Timer.periodic(const Duration(seconds: 3), (timer) { - if (mounted) { - logsNotifier.value = context.read().logs; - } - }); - }); - } - - @override - void dispose() { - super.dispose(); - timer?.cancel(); - timer = null; - } - - _initActions() { + _initActions(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final commonScaffoldState = context.findAncestorStateOfType(); @@ -51,7 +20,7 @@ class _LogsFragmentState extends State { showSearch( context: context, delegate: LogsSearchDelegate( - logs: logsNotifier.value.reversed.toList(), + logs: globalState.appController.appState.logs.reversed.toList(), ), ); }, @@ -62,8 +31,10 @@ class _LogsFragmentState extends State { } _buildList() { - return ValueListenableBuilder>( - valueListenable: logsNotifier, + return Selector>( + selector: (_, appState) => appState.logs, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), builder: (_, List logs, __) { if (logs.isEmpty) { return NullStatus( @@ -77,6 +48,7 @@ class _LogsFragmentState extends State { itemBuilder: (BuildContext context, int index) { final log = logs[index]; return LogItem( + key: ValueKey(log.dateTime), log: log, ); }, @@ -93,12 +65,14 @@ class _LogsFragmentState extends State { @override Widget build(BuildContext context) { return Selector( - selector: (_, appState) => - appState.currentLabel == 'logs' || - context.isMobile && appState.currentLabel == "tools", + selector: (_, appState) { + return appState.currentLabel == 'logs' || + appState.viewMode == ViewMode.mobile && + appState.currentLabel == "tools"; + }, builder: (_, isCurrent, child) { if (isCurrent == null || isCurrent) { - _initActions(); + _initActions(context); } return child!; }, @@ -114,13 +88,16 @@ class LogsSearchDelegate extends SearchDelegate { required this.logs, }); - List get _results => logs - .where( - (log) => - (log.payload?.contains(query) ?? false) || - log.logLevel.name.contains(query), - ) - .toList(); + List get _results { + final lowQuery = query.toLowerCase(); + return logs + .where( + (log) => + (log.payload?.toLowerCase().contains(lowQuery) ?? false) || + log.logLevel.name.contains(lowQuery), + ) + .toList(); + } @override List? buildActions(BuildContext context) { @@ -161,6 +138,7 @@ class LogsSearchDelegate extends SearchDelegate { itemBuilder: (BuildContext context, int index) { final log = _results[index]; return LogItem( + key: ValueKey(log.dateTime), log: log, ); }, diff --git a/lib/fragments/profiles/edit_profile.dart b/lib/fragments/profiles/edit_profile.dart index 57279e8..44ca1b1 100644 --- a/lib/fragments/profiles/edit_profile.dart +++ b/lib/fragments/profiles/edit_profile.dart @@ -86,6 +86,7 @@ class _EditProfileState extends State { ListItem( title: TextFormField( controller: urlController, + minLines: 1, maxLines: 2, decoration: InputDecoration( border: const OutlineInputBorder(), diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index e40c20b..f892feb 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -1,10 +1,10 @@ +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:provider/provider.dart'; import 'add_profile.dart'; @@ -25,6 +25,157 @@ class ProfilesFragment extends StatefulWidget { } class _ProfilesFragmentState extends State { + + _handleDeleteProfile(String id) async { + globalState.appController.deleteProfile(id); + } + + _handleUpdateProfile(String id) async { + context.findAncestorStateOfType()?.loadingRun( + () => globalState.appController.updateProfile(id), + ); + } + + _handleShowAddExtendPage() { + showExtendPage( + globalState.navigatorKey.currentState!.context, + body: AddProfile( + context: globalState.navigatorKey.currentState!.context, + ), + title: "${appLocalizations.add}${appLocalizations.profile}", + ); + } + + _handleShowEditExtendPage(Profile profile) { + showExtendPage( + context, + body: EditProfile( + profile: profile.copyWith(), + context: context, + ), + title: "${appLocalizations.edit}${appLocalizations.profile}", + ); + } + + _buildGrid({ + required ProfilesSelectorState state, + int crossAxisCount = 1, + }) { + return SingleChildScrollView( + padding: crossAxisCount > 1 + ? const EdgeInsets.symmetric(horizontal: 16) + : EdgeInsets.zero, + child: Grid.baseGap( + crossAxisCount: crossAxisCount, + children: [ + for (final profile in state.profiles) + GridItem( + child: ProfileItem( + profile: profile, + commonPopupMenu: CommonPopupMenu( + items: [ + CommonPopupMenuItem( + action: ProfileActions.edit, + label: appLocalizations.edit, + iconData: Icons.edit, + ), + if (profile.url != null) + CommonPopupMenuItem( + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, + ), + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(profile); + break; + case ProfileActions.delete: + _handleDeleteProfile(profile.id); + break; + case ProfileActions.update: + _handleUpdateProfile(profile.id); + break; + case null: + break; + } + }, + ), + groupValue: state.currentProfileId, + onChanged: globalState.appController.changeProfile, + ), + ), + ], + ), + ); + } + + _getColumns(ViewMode viewMode) { + switch (viewMode) { + case ViewMode.mobile: + return 1; + case ViewMode.laptop: + return 1; + case ViewMode.desktop: + return 2; + } + } + + @override + Widget build(BuildContext context) { + return FloatLayout( + floatingWidget: Container( + margin: const EdgeInsets.all(kFloatingActionButtonMargin), + child: FloatingActionButton( + heroTag: null, + onPressed: _handleShowAddExtendPage, + child: const Icon(Icons.add), + ), + ), + child: Selector2( + selector: (_, appState, config) => ProfilesSelectorState( + profiles: config.profiles, + currentProfileId: config.currentProfileId, + viewMode: appState.viewMode), + builder: (context, state, child) { + if (state.profiles.isEmpty) { + return NullStatus( + label: appLocalizations.nullProfileDesc, + ); + } + return Align( + alignment: Alignment.topCenter, + child: _buildGrid( + state: state, + crossAxisCount: _getColumns(state.viewMode), + ), + ); + }, + ), + ); + } +} + +class ProfileItem extends StatelessWidget { + final Profile profile; + final String? groupValue; + final CommonPopupMenu commonPopupMenu; + final void Function(String? value) onChanged; + + const ProfileItem({ + super.key, + required this.profile, + required this.commonPopupMenu, + required this.groupValue, + required this.onChanged, + }); + String _getLastUpdateTimeDifference(DateTime lastDateTime) { final currentDateTime = DateTime.now(); final difference = currentDateTime.difference(lastDateTime); @@ -49,21 +200,8 @@ class _ProfilesFragmentState extends State { return appLocalizations.just; } - _handleDeleteProfile(String id) async { - globalState.appController.deleteProfile(id); - } - - _handleUpdateProfile(String id) async { - context.findAncestorStateOfType()?.loadingRun( - () => globalState.appController.updateProfile(id), - ); - } - - Widget _profileItem({ - required Profile profile, - required String? groupValue, - required void Function(String? value) onChanged, - }) { + @override + Widget build(BuildContext context) { String useShow; String totalShow; double progress; @@ -87,41 +225,7 @@ class _ProfilesFragmentState extends State { onChanged: onChanged, ), padding: const EdgeInsets.symmetric(horizontal: 16), - trailing: CommonPopupMenu( - items: [ - CommonPopupMenuItem( - action: ProfileActions.edit, - label: appLocalizations.edit, - iconData: Icons.edit, - ), - if (profile.url != null) - CommonPopupMenuItem( - action: ProfileActions.update, - label: appLocalizations.update, - iconData: Icons.sync, - ), - CommonPopupMenuItem( - action: ProfileActions.delete, - label: appLocalizations.delete, - iconData: Icons.delete, - ), - ], - onSelected: (ProfileActions? action) async { - switch (action) { - case ProfileActions.edit: - _handleShowEditExtendPage(profile); - break; - case ProfileActions.delete: - _handleDeleteProfile(profile.id); - break; - case ProfileActions.update: - _handleUpdateProfile(profile.id); - break; - case null: - break; - } - }, - ), + trailing: commonPopupMenu, title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -172,104 +276,4 @@ class _ProfilesFragmentState extends State { ), ); } - - _handleShowAddExtendPage() { - showExtendPage( - globalState.navigatorKey.currentState!.context, - body: AddProfile( - context: globalState.navigatorKey.currentState!.context, - ), - title: "${appLocalizations.add}${appLocalizations.profile}", - ); - } - - _handleShowEditExtendPage(Profile profile) { - showExtendPage( - context, - body: EditProfile( - profile: profile.copyWith(), - context: context, - ), - title: "${appLocalizations.edit}${appLocalizations.profile}", - ); - } - - _buildGrid({ - required ProfilesSelectorState state, - int crossAxisCount = 1, - }) { - return SingleChildScrollView( - padding: crossAxisCount > 1 - ? const EdgeInsets.symmetric(horizontal: 16) - : EdgeInsets.zero, - child: Grid.baseGap( - crossAxisCount: crossAxisCount, - children: [ - for (final profile in state.profiles) - GridItem( - child: _profileItem( - profile: profile, - groupValue: state.currentProfileId, - onChanged: globalState.appController.changeProfileDebounce, - ), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return FloatLayout( - floatingWidget: Container( - margin: const EdgeInsets.all(kFloatingActionButtonMargin), - child: FloatingActionButton( - heroTag: null, - onPressed: _handleShowAddExtendPage, - child: const Icon(Icons.add), - ), - ), - child: Selector( - selector: (_, config) => ProfilesSelectorState( - profiles: config.profiles, - currentProfileId: config.currentProfileId, - ), - builder: (context, state, child) { - if (state.profiles.isEmpty) { - return NullStatus( - label: appLocalizations.nullProfileDesc, - ); - } - return Align( - alignment: Alignment.topCenter, - child: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('profiles_grid_small'), - builder: (_) => _buildGrid( - state: state, - crossAxisCount: 1, - ), - ), - Breakpoints.medium: SlotLayout.from( - key: const Key('profiles_grid_medium'), - builder: (_) => _buildGrid( - state: state, - crossAxisCount: 1, - ), - ), - Breakpoints.large: SlotLayout.from( - key: const Key('profiles_grid_large'), - builder: (_) => _buildGrid( - state: state, - crossAxisCount: 2, - ), - ), - }, - ), - ); - }, - ), - ); - } } diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart index 5690ae2..588d789 100644 --- a/lib/fragments/proxies.dart +++ b/lib/fragments/proxies.dart @@ -1,6 +1,5 @@ import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:provider/provider.dart'; import '../enum/enum.dart'; @@ -98,7 +97,7 @@ class _ProxiesFragmentState extends State isScrollable: true, tabAlignment: TabAlignment.start, overlayColor: - const MaterialStatePropertyAll(Colors.transparent), + const WidgetStatePropertyAll(Colors.transparent), tabs: [ for (final groupName in state.groupNames) Tab( @@ -186,168 +185,15 @@ class ProxiesTabView extends StatelessWidget { 8 * 2; } - _card( - BuildContext context, { - required void Function() onPressed, - required bool isSelected, - required Proxy proxy, - }) { - final measure = globalState.appController.measure; - return CommonCard( - isSelected: isSelected, - onPressed: onPressed, - selectWidget: Container( - alignment: Alignment.topRight, - margin: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: const SelectIcon(), - ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: measure.bodyMediumHeight * 2, - child: Text( - proxy.name, - maxLines: 2, - style: context.textTheme.bodyMedium?.copyWith( - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox( - height: 8, - ), - SizedBox( - height: measure.bodySmallHeight, - child: Selector( - selector: (context, appState) => appState.getDesc( - proxy.type, - proxy.name, - ), - builder: (_, desc, __) { - return TooltipText( - text: Text( - desc, - style: context.textTheme.bodySmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: context.textTheme.bodySmall?.color?.toLight(), - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - SizedBox( - height: measure.labelSmallHeight, - child: Selector( - selector: (context, appState) => appState.getDelay( - proxy.name, - ), - builder: (_, delay, __) { - return FadeBox( - child: Builder( - builder: (_) { - if (delay == null) { - return Container(); - } - if (delay == 0) { - return SizedBox( - height: measure.labelSmallHeight, - width: measure.labelSmallHeight, - child: const CircularProgressIndicator( - strokeWidth: 2, - ), - ); - } - return Text( - delay > 0 ? '$delay ms' : "Timeout", - style: context.textTheme.labelSmall?.copyWith( - overflow: TextOverflow.ellipsis, - color: other.getDelayColor( - delay, - ), - ), - ); - }, - ), - ); - }, - ), - ), - ], - ), - ), - ); - } - - Widget _buildGrid( - BuildContext context, { - required List proxies, - required int columns, - }) { - return GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(context), - ), - itemCount: proxies.length, - itemBuilder: (_, index) { - final proxy = proxies[index]; - return Selector3( - selector: (_, appState, config, clashConfig) { - final group = appState.getGroupWithName(groupName)!; - bool isSelected = - config.currentSelectedMap[group.name] == proxy.name || - (config.currentSelectedMap[group.name] == null && - group.now == proxy.name); - return ProxiesCardSelectorState( - isSelected: isSelected, - ); - }, - builder: (_, state, __) { - return _card( - context, - isSelected: state.isSelected, - onPressed: () { - final appController = globalState.appController; - final group = - appController.appState.getGroupWithName(groupName)!; - if (group.type != GroupType.Selector) { - globalState.showSnackBar( - context, - message: appLocalizations.notSelectedTip, - ); - return; - } - globalState.appController.config.updateCurrentSelectedMap( - groupName, - proxy.name, - ); - globalState.appController.changeProxy(); - }, - proxy: proxy, - ); - }, - ); - }, - ); + int _getColumns(ViewMode viewMode) { + switch (viewMode) { + case ViewMode.mobile: + return 2; + case ViewMode.laptop: + return 3; + case ViewMode.desktop: + return 4; + } } @override @@ -358,6 +204,7 @@ class ProxiesTabView extends StatelessWidget { proxiesSortType: config.proxiesSortType, sortNum: appState.sortNum, group: appState.getGroupWithName(groupName)!, + viewMode: appState.viewMode, ); }, builder: (_, state, __) { @@ -368,32 +215,22 @@ class ProxiesTabView extends StatelessWidget { ); return Align( alignment: Alignment.topCenter, - child: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('proxies_grid_small'), - builder: (_) => _buildGrid( - context, - proxies: proxies, - columns: 2, - ), - ), - Breakpoints.medium: SlotLayout.from( - key: const Key('proxies_grid_medium'), - builder: (_) => _buildGrid( - context, - proxies: proxies, - columns: 3, - ), - ), - Breakpoints.large: SlotLayout.from( - key: const Key('proxies_grid_large'), - builder: (_) => _buildGrid( - context, - proxies: proxies, - columns: 4, - ), - ), + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _getColumns(state.viewMode), + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: _getItemHeight(context), + ), + itemCount: proxies.length, + itemBuilder: (_, index) { + final proxy = proxies[index]; + return ProxyCard( + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); }, ), ); @@ -402,6 +239,149 @@ class ProxiesTabView extends StatelessWidget { } } +class ProxyCard extends StatelessWidget { + final String groupName; + final Proxy proxy; + + const ProxyCard({ + super.key, + required this.groupName, + required this.proxy, + }); + + @override + Widget build(BuildContext context) { + final measure = globalState.appController.measure; + return Selector3( + selector: (_, appState, config, clashConfig) { + final group = appState.getGroupWithName(groupName)!; + bool isSelected = config.currentSelectedMap[group.name] == proxy.name || + (config.currentSelectedMap[group.name] == null && + group.now == proxy.name); + return ProxiesCardSelectorState( + isSelected: isSelected, + ); + }, + builder: (_, state, __) { + return CommonCard( + isSelected: state.isSelected, + onPressed: () { + final appController = globalState.appController; + final group = appController.appState.getGroupWithName(groupName)!; + if (group.type != GroupType.Selector) { + globalState.showSnackBar( + context, + message: appLocalizations.notSelectedTip, + ); + return; + } + globalState.appController.config.updateCurrentSelectedMap( + groupName, + proxy.name, + ); + globalState.appController.changeProxy(); + }, + selectWidget: Container( + alignment: Alignment.topRight, + margin: const EdgeInsets.all(8), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: const SelectIcon(), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: measure.bodyMediumHeight * 2, + child: Text( + proxy.name, + maxLines: 2, + style: context.textTheme.bodyMedium?.copyWith( + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + height: measure.bodySmallHeight, + child: Selector( + selector: (context, appState) => appState.getDesc( + proxy.type, + proxy.name, + ), + builder: (_, desc, __) { + return TooltipText( + text: Text( + desc, + style: context.textTheme.bodySmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: + context.textTheme.bodySmall?.color?.toLight(), + ), + ), + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + SizedBox( + height: measure.labelSmallHeight, + child: Selector( + selector: (context, appState) => appState.getDelay( + proxy.name, + ), + builder: (_, delay, __) { + return FadeBox( + child: Builder( + builder: (_) { + if (delay == null) { + return Container(); + } + if (delay == 0) { + return SizedBox( + height: measure.labelSmallHeight, + width: measure.labelSmallHeight, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ); + } + return Text( + delay > 0 ? '$delay ms' : "Timeout", + style: context.textTheme.labelSmall?.copyWith( + overflow: TextOverflow.ellipsis, + color: other.getDelayColor( + delay, + ), + ), + ); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } +} + class DelayTestButtonContainer extends StatefulWidget { final Widget child; @@ -421,13 +401,11 @@ class _DelayTestButtonContainerState extends State late Animation _scale; late Animation _opacity; - _healthcheck() async - { - if(globalState.healthcheckLock) return; + _healthcheck() async { + if (globalState.healthcheckLock) return; _controller.forward(); globalState.appController.healthcheck(); - Future.delayed(appConstant.httpTimeoutDuration + appConstant.moreDuration, - () { + Future.delayed(httpTimeoutDuration + moreDuration, () { _controller.reverse(); }); } diff --git a/lib/fragments/theme.dart b/lib/fragments/theme.dart index c654db4..cb97d9e 100644 --- a/lib/fragments/theme.dart +++ b/lib/fragments/theme.dart @@ -109,7 +109,7 @@ class ThemeFragment extends StatelessWidget { ]; List primaryColors = [ null, - appConstant.defaultPrimaryColor, + defaultPrimaryColor, Colors.pinkAccent, Colors.greenAccent, Colors.yellowAccent, diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index 50c713d..89fdf2a 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../widgets/widgets.dart'; +import 'backup_and_recovery.dart'; import 'theme.dart'; class ToolsFragment extends StatefulWidget { @@ -51,33 +52,6 @@ class _ToolboxFragmentState extends State { ); } - Widget _buildSection({ - required String title, - required Widget content, - }) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Text( - title, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - Expanded( - flex: 0, - child: content, - ) - ], - ); - } - String _getLocaleString(Locale? locale) { if (locale == null) return appLocalizations.defaultText; return Intl.message(locale.toString()); @@ -161,9 +135,19 @@ class _ToolboxFragmentState extends State { title: Text(appLocalizations.theme), subtitle: Text(appLocalizations.themeDesc), delegate: OpenDelegate( - title: appLocalizations.theme, - widget: const ThemeFragment(), - extendPageWidth: 360), + title: appLocalizations.theme, + widget: const ThemeFragment(), + extendPageWidth: 360, + ), + ), + ListItem.open( + leading: const Icon(Icons.cloud_sync), + title: Text(appLocalizations.backupAndRecovery), + subtitle: Text(appLocalizations.backupAndRecoveryDesc), + delegate: OpenDelegate( + title: appLocalizations.backupAndRecovery, + widget: const BackupAndRecovery(), + ), ), if (Platform.isAndroid) ListItem.open( @@ -210,36 +194,47 @@ class _ToolboxFragmentState extends State { @override Widget build(BuildContext context) { - final items = [ - Selector>( - selector: (_, appState) => appState.navigationItems, - builder: (_, navigationItems, __) { - final moreNavigationItems = navigationItems - .where( - (element) => element.modes.contains(NavigationItemMode.more), - ) - .toList(); - if (moreNavigationItems.isEmpty) { - return Container(); - } - return _buildSection( - title: appLocalizations.more, - content: _buildNavigationMenu(moreNavigationItems), - ); - }, - ), - _buildSection( - title: appLocalizations.settings, - content: _getSettingList(), - ), - _buildSection( - title: appLocalizations.other, - content: _getOtherList(), - ), - ]; - return ListView.builder( - itemCount: items.length, - itemBuilder: (_, index) => items[index], + return Selector( + selector: (_, config) => config.locale, + builder: (_, __, ___) { + final items = [ + Selector( + selector: (_, appState) { + return MoreToolsSelectorState( + navigationItems: appState.viewMode == ViewMode.mobile + ? appState.navigationItems.where( + (element) { + return element.modes + .contains(NavigationItemMode.more); + }, + ).toList() + : [], + ); + }, + builder: (_, state, __) { + if (state.navigationItems.isEmpty) { + return Container(); + } + return Section( + title: appLocalizations.more, + child: _buildNavigationMenu(state.navigationItems), + ); + }, + ), + Section( + title: appLocalizations.settings, + child: _getSettingList(), + ), + Section( + title: appLocalizations.other, + child: _getOtherList(), + ), + ]; + return ListView.builder( + itemCount: items.length, + itemBuilder: (_, index) => items[index], + ); + }, ); } } diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 3e5a074..fc24448 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -47,6 +47,8 @@ "autoRunDesc": "Auto run when the application is opened", "logcat": "Logcat", "logcatDesc": "Disabling will hide the log entry", + "autoCheckUpdate": "Auto check updates", + "autoCheckUpdateDesc": "Auto check for updates when the app starts", "accessControl": "AccessControl", "accessControlDesc": "Configure application access proxy", "application": "Application", @@ -114,7 +116,6 @@ "systemProxy": "SystemProxy", "project": "Project", "core": "Core", - "checkUpdate": "Check update", "tabAnimation": "Tab animation", "tabAnimationDesc": "When enabled, the home tab will add a toggle animation", "desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", @@ -124,5 +125,29 @@ "compatible": "Compatibility mode", "compatibleDesc": "Opening it will lose part of its application ability and gain the support of full amount of Clash.", "notSelectedTip": "The current proxy group cannot be selected.", - "tip": "tip" + "tip": "tip", + "backupAndRecovery": "Backup and Recovery", + "backupAndRecoveryDesc": "Sync data by WebDAV", + "account": "Account", + "backup": "Backup", + "backupDesc": "Backup local data to WebDAV", + "recovery": "Recovery", + "recoveryDesc": "Recovery data from WebDAV", + "recoveryProfiles": "Only recovery profiles", + "recoveryAll": "Recovery all data", + "recoverySuccess": "Recovery success", + "backupSuccess": "Backup success", + "noInfo": "No info", + "pleaseBindWebDAV": "Please bind WebDAV", + "bind": "Bind", + "connectivity": "Connectivity:", + "webDAVConfiguration": "WebDAV configuration", + "address": "Address", + "addressHelp": "WebDAV server address", + "addressTip": "Please enter a valid WebDAV address", + "password": "Password", + "passwordTip": "Password cannot be empty", + "accountTip": "Account cannot be empty", + "checkUpdate": "Check for updates", + "checkUpdateError": "The current application is already the latest version" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 3af974a..d0084fe 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -47,6 +47,8 @@ "autoRunDesc": "应用打开时自动运行", "logcat": "日志捕获", "logcatDesc": "禁用将会隐藏日志入口", + "autoCheckUpdate": "自动检查更新", + "autoCheckUpdateDesc": "应用启动时自动检查更新", "accessControl": "访问控制", "accessControlDesc": "配置应用访问代理", "application": "应用程序", @@ -114,7 +116,6 @@ "systemProxy": "系统代理", "project": "项目", "core": "内核", - "checkUpdate": "检查更新", "tabAnimation": "选项卡动画", "tabAnimationDesc": "开启后,主页选项卡将添加切换动画", "desc": "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", @@ -124,5 +125,29 @@ "compatible": "兼容模式", "compatibleDesc": "开启将失去部分应用能力,获得全量的Clash的支持", "notSelectedTip": "当前代理组无法选中", - "tip": "提示" + "tip": "提示", + "backupAndRecovery": "备份与恢复", + "backupAndRecoveryDesc": "通过WebDAV同步数据", + "account": "账号", + "backup": "备份", + "backupDesc": "备份数据到WebDAV", + "recovery": "恢复", + "recoveryDesc": "从WebDAV恢复数据", + "recoveryProfiles": "仅恢复配置文件", + "recoveryAll": "恢复所有数据", + "recoverySuccess": "恢复成功", + "backupSuccess": "备份成功", + "noInfo": "暂无信息", + "pleaseBindWebDAV": "请绑定WebDAV", + "bind": "绑定", + "connectivity": "连通性:", + "webDAVConfiguration": "WebDAV配置", + "address": "地址", + "addressHelp": "WebDAV服务器地址", + "addressTip": "请输入有效的WebDAV地址", + "password": "密码", + "passwordTip": "密码不能为空", + "accountTip": "账号不能为空", + "checkUpdate": "检查更新", + "checkUpdateError": "当前应用已经是最新版了" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index f3143c9..f9a358b 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -30,7 +30,15 @@ class MessageLookup extends MessageLookupByLibrary { "Configure application access proxy"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage( "The selected application will be excluded from VPN"), + "account": MessageLookupByLibrary.simpleMessage("Account"), + "accountTip": + MessageLookupByLibrary.simpleMessage("Account cannot be empty"), "add": MessageLookupByLibrary.simpleMessage("Add"), + "address": MessageLookupByLibrary.simpleMessage("Address"), + "addressHelp": + MessageLookupByLibrary.simpleMessage("WebDAV server address"), + "addressTip": MessageLookupByLibrary.simpleMessage( + "Please enter a valid WebDAV address"), "ago": MessageLookupByLibrary.simpleMessage(" Ago"), "allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"), "allowLanDesc": MessageLookupByLibrary.simpleMessage( @@ -41,6 +49,10 @@ class MessageLookup extends MessageLookupByLibrary { "applicationDesc": MessageLookupByLibrary.simpleMessage( "Modify application related settings"), "auto": MessageLookupByLibrary.simpleMessage("Auto"), + "autoCheckUpdate": + MessageLookupByLibrary.simpleMessage("Auto check updates"), + "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage( + "Auto check for updates when the app starts"), "autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage( "Follow the system self startup"), @@ -50,17 +62,30 @@ class MessageLookup extends MessageLookupByLibrary { "autoUpdate": MessageLookupByLibrary.simpleMessage("Auto update"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage( "Auto update interval (minutes)"), + "backup": MessageLookupByLibrary.simpleMessage("Backup"), + "backupAndRecovery": + MessageLookupByLibrary.simpleMessage("Backup and Recovery"), + "backupAndRecoveryDesc": + MessageLookupByLibrary.simpleMessage("Sync data by WebDAV"), + "backupDesc": + MessageLookupByLibrary.simpleMessage("Backup local data to WebDAV"), + "backupSuccess": MessageLookupByLibrary.simpleMessage("Backup success"), + "bind": MessageLookupByLibrary.simpleMessage("Bind"), "blacklistMode": MessageLookupByLibrary.simpleMessage("Blacklist mode"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("Cancel filter system app"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Cancel select all"), - "checkUpdate": MessageLookupByLibrary.simpleMessage("Check update"), + "checkUpdate": + MessageLookupByLibrary.simpleMessage("Check for updates"), + "checkUpdateError": MessageLookupByLibrary.simpleMessage( + "The current application is already the latest version"), "compatible": MessageLookupByLibrary.simpleMessage("Compatibility mode"), "compatibleDesc": MessageLookupByLibrary.simpleMessage( "Opening it will lose part of its application ability and gain the support of full amount of Clash."), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "connectivity": MessageLookupByLibrary.simpleMessage("Connectivity:"), "core": MessageLookupByLibrary.simpleMessage("Core"), "coreInfo": MessageLookupByLibrary.simpleMessage("Core info"), "create": MessageLookupByLibrary.simpleMessage("Create"), @@ -112,6 +137,7 @@ class MessageLookup extends MessageLookupByLibrary { "networkDetection": MessageLookupByLibrary.simpleMessage("Network detection"), "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), + "noInfo": MessageLookupByLibrary.simpleMessage("No info"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("No more info"), "noProxy": MessageLookupByLibrary.simpleMessage("No proxy"), "noProxyDesc": MessageLookupByLibrary.simpleMessage( @@ -128,6 +154,11 @@ class MessageLookup extends MessageLookupByLibrary { "override": MessageLookupByLibrary.simpleMessage("Override"), "overrideDesc": MessageLookupByLibrary.simpleMessage( "Override Proxy related config"), + "password": MessageLookupByLibrary.simpleMessage("Password"), + "passwordTip": + MessageLookupByLibrary.simpleMessage("Password cannot be empty"), + "pleaseBindWebDAV": + MessageLookupByLibrary.simpleMessage("Please bind WebDAV"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("Please upload file"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( @@ -156,6 +187,15 @@ class MessageLookup extends MessageLookupByLibrary { "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage( "Scan QR code to obtain profile"), + "recovery": MessageLookupByLibrary.simpleMessage("Recovery"), + "recoveryAll": + MessageLookupByLibrary.simpleMessage("Recovery all data"), + "recoveryDesc": + MessageLookupByLibrary.simpleMessage("Recovery data from WebDAV"), + "recoveryProfiles": + MessageLookupByLibrary.simpleMessage("Only recovery profiles"), + "recoverySuccess": + MessageLookupByLibrary.simpleMessage("Recovery success"), "rule": MessageLookupByLibrary.simpleMessage("Rule"), "save": MessageLookupByLibrary.simpleMessage("Save"), "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), @@ -191,6 +231,8 @@ class MessageLookup extends MessageLookupByLibrary { "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("Obtain profile through URL"), + "webDAVConfiguration": + MessageLookupByLibrary.simpleMessage("WebDAV configuration"), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), "years": MessageLookupByLibrary.simpleMessage("Years"), "zh_CN": MessageLookupByLibrary.simpleMessage("Simplified Chinese") diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index faa25e6..3a70eee 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -29,7 +29,12 @@ class MessageLookup extends MessageLookupByLibrary { "accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), + "account": MessageLookupByLibrary.simpleMessage("账号"), + "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), "add": MessageLookupByLibrary.simpleMessage("添加"), + "address": MessageLookupByLibrary.simpleMessage("地址"), + "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), + "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "ago": MessageLookupByLibrary.simpleMessage("前"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), @@ -37,6 +42,9 @@ class MessageLookup extends MessageLookupByLibrary { "application": MessageLookupByLibrary.simpleMessage("应用程序"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"), "auto": MessageLookupByLibrary.simpleMessage("自动"), + "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"), + "autoCheckUpdateDesc": + MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"), "autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), "autoRun": MessageLookupByLibrary.simpleMessage("自动运行"), @@ -44,15 +52,24 @@ class MessageLookup extends MessageLookupByLibrary { "autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), "autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"), + "backup": MessageLookupByLibrary.simpleMessage("备份"), + "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), + "backupAndRecoveryDesc": + MessageLookupByLibrary.simpleMessage("通过WebDAV同步数据"), + "backupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), + "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), + "bind": MessageLookupByLibrary.simpleMessage("绑定"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), + "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatibleDesc": MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力,获得全量的Clash的支持"), "confirm": MessageLookupByLibrary.simpleMessage("确定"), + "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "core": MessageLookupByLibrary.simpleMessage("内核"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "create": MessageLookupByLibrary.simpleMessage("创建"), @@ -96,6 +113,7 @@ class MessageLookup extends MessageLookupByLibrary { "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), + "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "noProxyDesc": @@ -109,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary { "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆写"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), + "password": MessageLookupByLibrary.simpleMessage("密码"), + "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), + "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage("请上传有效的二维码"), @@ -133,6 +154,11 @@ class MessageLookup extends MessageLookupByLibrary { "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), + "recovery": MessageLookupByLibrary.simpleMessage("恢复"), + "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), + "recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"), + "recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"), + "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "rule": MessageLookupByLibrary.simpleMessage("规则"), "save": MessageLookupByLibrary.simpleMessage("保存"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"), @@ -163,6 +189,7 @@ class MessageLookup extends MessageLookupByLibrary { "upload": MessageLookupByLibrary.simpleMessage("上传"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), + "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "years": MessageLookupByLibrary.simpleMessage("年"), "zh_CN": MessageLookupByLibrary.simpleMessage("中文简体") diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 1cd7c7d..1ae8b1a 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -530,6 +530,26 @@ class AppLocalizations { ); } + /// `Auto check updates` + String get autoCheckUpdate { + return Intl.message( + 'Auto check updates', + name: 'autoCheckUpdate', + desc: '', + args: [], + ); + } + + /// `Auto check for updates when the app starts` + String get autoCheckUpdateDesc { + return Intl.message( + 'Auto check for updates when the app starts', + name: 'autoCheckUpdateDesc', + desc: '', + args: [], + ); + } + /// `AccessControl` String get accessControl { return Intl.message( @@ -1200,16 +1220,6 @@ class AppLocalizations { ); } - /// `Check update` - String get checkUpdate { - return Intl.message( - 'Check update', - name: 'checkUpdate', - desc: '', - args: [], - ); - } - /// `Tab animation` String get tabAnimation { return Intl.message( @@ -1309,6 +1319,246 @@ class AppLocalizations { args: [], ); } + + /// `Backup and Recovery` + String get backupAndRecovery { + return Intl.message( + 'Backup and Recovery', + name: 'backupAndRecovery', + desc: '', + args: [], + ); + } + + /// `Sync data by WebDAV` + String get backupAndRecoveryDesc { + return Intl.message( + 'Sync data by WebDAV', + name: 'backupAndRecoveryDesc', + desc: '', + args: [], + ); + } + + /// `Account` + String get account { + return Intl.message( + 'Account', + name: 'account', + desc: '', + args: [], + ); + } + + /// `Backup` + String get backup { + return Intl.message( + 'Backup', + name: 'backup', + desc: '', + args: [], + ); + } + + /// `Backup local data to WebDAV` + String get backupDesc { + return Intl.message( + 'Backup local data to WebDAV', + name: 'backupDesc', + desc: '', + args: [], + ); + } + + /// `Recovery` + String get recovery { + return Intl.message( + 'Recovery', + name: 'recovery', + desc: '', + args: [], + ); + } + + /// `Recovery data from WebDAV` + String get recoveryDesc { + return Intl.message( + 'Recovery data from WebDAV', + name: 'recoveryDesc', + desc: '', + args: [], + ); + } + + /// `Only recovery profiles` + String get recoveryProfiles { + return Intl.message( + 'Only recovery profiles', + name: 'recoveryProfiles', + desc: '', + args: [], + ); + } + + /// `Recovery all data` + String get recoveryAll { + return Intl.message( + 'Recovery all data', + name: 'recoveryAll', + desc: '', + args: [], + ); + } + + /// `Recovery success` + String get recoverySuccess { + return Intl.message( + 'Recovery success', + name: 'recoverySuccess', + desc: '', + args: [], + ); + } + + /// `Backup success` + String get backupSuccess { + return Intl.message( + 'Backup success', + name: 'backupSuccess', + desc: '', + args: [], + ); + } + + /// `No info` + String get noInfo { + return Intl.message( + 'No info', + name: 'noInfo', + desc: '', + args: [], + ); + } + + /// `Please bind WebDAV` + String get pleaseBindWebDAV { + return Intl.message( + 'Please bind WebDAV', + name: 'pleaseBindWebDAV', + desc: '', + args: [], + ); + } + + /// `Bind` + String get bind { + return Intl.message( + 'Bind', + name: 'bind', + desc: '', + args: [], + ); + } + + /// `Connectivity:` + String get connectivity { + return Intl.message( + 'Connectivity:', + name: 'connectivity', + desc: '', + args: [], + ); + } + + /// `WebDAV configuration` + String get webDAVConfiguration { + return Intl.message( + 'WebDAV configuration', + name: 'webDAVConfiguration', + desc: '', + args: [], + ); + } + + /// `Address` + String get address { + return Intl.message( + 'Address', + name: 'address', + desc: '', + args: [], + ); + } + + /// `WebDAV server address` + String get addressHelp { + return Intl.message( + 'WebDAV server address', + name: 'addressHelp', + desc: '', + args: [], + ); + } + + /// `Please enter a valid WebDAV address` + String get addressTip { + return Intl.message( + 'Please enter a valid WebDAV address', + name: 'addressTip', + desc: '', + args: [], + ); + } + + /// `Password` + String get password { + return Intl.message( + 'Password', + name: 'password', + desc: '', + args: [], + ); + } + + /// `Password cannot be empty` + String get passwordTip { + return Intl.message( + 'Password cannot be empty', + name: 'passwordTip', + desc: '', + args: [], + ); + } + + /// `Account cannot be empty` + String get accountTip { + return Intl.message( + 'Account cannot be empty', + name: 'accountTip', + desc: '', + args: [], + ); + } + + /// `Check for updates` + String get checkUpdate { + return Intl.message( + 'Check for updates', + name: 'checkUpdate', + desc: '', + args: [], + ); + } + + /// `The current application is already the latest version` + String get checkUpdateError { + return Intl.message( + 'The current application is already the latest version', + name: 'checkUpdateError', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 3e55956..ddbab29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ Future main() async { mode: clashConfig.mode, isCompatible: config.isCompatible, selectedMap: config.currentSelectedMap, + viewWidth: other.getViewWidth(), ); await globalState.init( appState: appState, diff --git a/lib/models/app.dart b/lib/models/app.dart index 5aaae6e..f111523 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; import 'ffi.dart'; import 'log.dart'; import 'navigation.dart'; -import 'package.dart'; import 'profile.dart'; import 'proxy.dart'; import 'system_color_scheme.dart'; @@ -20,7 +20,6 @@ class AppState with ChangeNotifier { VersionInfo? _versionInfo; List _traffics; List _logs; - List _packages; String _currentLabel; SystemColorSchemes _systemColorSchemes; num _sortNum; @@ -29,9 +28,11 @@ class AppState with ChangeNotifier { SelectedMap _selectedMap; bool _isCompatible; List _groups; + double _viewWidth; AppState({ required Mode mode, + double? viewWidth, required bool isCompatible, required SelectedMap selectedMap, }) : _navigationItems = [], @@ -39,8 +40,8 @@ class AppState with ChangeNotifier { _currentLabel = "dashboard", _traffics = [], _logs = [], + _viewWidth = viewWidth ?? 0, _selectedMap = selectedMap, - _packages = [], _sortNum = 0, _mode = mode, _delayMap = {}, @@ -66,6 +67,20 @@ class AppState with ChangeNotifier { } } + List get currentNavigationItems { + NavigationItemMode navigationItemMode; + if (_viewWidth <= maxMobileWidth) { + navigationItemMode = NavigationItemMode.mobile; + } else { + navigationItemMode = NavigationItemMode.desktop; + } + return navigationItems + .where( + (element) => element.modes.contains(navigationItemMode), + ) + .toList(); + } + bool get isInit => _isInit; set isInit(bool value) { @@ -164,14 +179,6 @@ class AppState with ChangeNotifier { } } - List get packages => _packages; - - set packages(List value) { - if (_packages != value) { - _packages = value; - notifyListeners(); - } - } List get groups => _groups; @@ -200,19 +207,6 @@ class AppState with ChangeNotifier { } } - // String? get currentProxyName { - // if (mode == Mode.direct) return UsedProxy.DIRECT.name; - // if (_currentProxyName != null) return _currentProxyName!; - // return currentGroup?.now; - // } - // - // set currentProxyName(String? value) { - // if (_currentProxyName != value) { - // _currentProxyName = value; - // notifyListeners(); - // } - // } - bool get isCompatible { return _isCompatible; } @@ -250,6 +244,21 @@ class AppState with ChangeNotifier { } } + double get viewWidth => _viewWidth; + + set viewWidth(double value) { + if (_viewWidth != value) { + _viewWidth = value; + notifyListeners(); + } + } + + ViewMode get viewMode { + if (_viewWidth <= maxMobileWidth) return ViewMode.mobile; + if (_viewWidth <= maxLaptopWidth) return ViewMode.laptop; + return ViewMode.desktop; + } + DelayMap get delayMap { return _delayMap; } diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index dfb245a..7583d4f 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -11,15 +11,13 @@ part 'generated/clash_config.g.dart'; part 'generated/clash_config.freezed.dart'; - @freezed class Tun with _$Tun { const factory Tun({ @Default(false) bool enable, @Default(appName) String device, @Default(TunStack.gvisor) TunStack stack, - @JsonKey(name: "dns-hijack") @Default(["any:53"]) - List dnsHijack, + @JsonKey(name: "dns-hijack") @Default(["any:53"]) List dnsHijack, }) = _Tun; factory Tun.fromJson(Map json) => _$TunFromJson(json); @@ -198,6 +196,19 @@ class ClashConfig extends ChangeNotifier { } } + update([ClashConfig? clashConfig]) { + if (clashConfig != null) { + _mixedPort = clashConfig._mixedPort; + _allowLan = clashConfig._allowLan; + _mode = clashConfig._mode; + _logLevel = clashConfig._logLevel; + _tun = clashConfig._tun; + _dns = clashConfig._dns; + _rules = clashConfig._rules; + } + notifyListeners(); + } + Map toJson() { return _$ClashConfigToJson(this); } @@ -216,4 +227,9 @@ class ClashConfig extends ChangeNotifier { allowLan: allowLan, ); } -} \ No newline at end of file + + @override + String toString() { + return 'ClashConfig{_mixedPort: $_mixedPort, _allowLan: $_allowLan, _mode: $_mode, _logLevel: $_logLevel, _tun: $_tun, _dns: $_dns, _rules: $_rules}'; + } +} diff --git a/lib/models/common.dart b/lib/models/common.dart index b72042e..a6106af 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -11,14 +11,10 @@ class Result { this.data, }); - Result.success({ - this.data, - }) : type = ResultType.success, + Result.success([this.data]) : type = ResultType.success, message = null; - Result.error({ - this.message, - }) : type = ResultType.error, + Result.error([this.message]) : type = ResultType.error, data = null; @override diff --git a/lib/models/config.dart b/lib/models/config.dart index 6765033..996db92 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -56,6 +56,23 @@ class AccessControl { factory AccessControl.fromJson(Map json) { return _$AccessControlFromJson(json); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AccessControl && + runtimeType == other.runtimeType && + mode == other.mode && + acceptList == other.acceptList && + rejectList == other.rejectList && + isFilterSystemApp == other.isFilterSystemApp; + + @override + int get hashCode => + mode.hashCode ^ + acceptList.hashCode ^ + rejectList.hashCode ^ + isFilterSystemApp.hashCode; } @JsonSerializable() @@ -75,6 +92,8 @@ class Config extends ChangeNotifier { bool _isAccessControl; AccessControl _accessControl; bool _isAnimateToPage; + bool _autoCheckUpdate; + DAV? _dav; Config() : _profiles = [], @@ -84,10 +103,11 @@ class Config extends ChangeNotifier { _themeMode = ThemeMode.system, _openLog = false, _isCompatible = false, - _primaryColor = appConstant.defaultPrimaryColor.value, + _primaryColor = defaultPrimaryColor.value, _proxiesSortType = ProxiesSortType.none, _isMinimizeOnExit = true, _isAccessControl = false, + _autoCheckUpdate = true, _accessControl = AccessControl(), _isAnimateToPage = true; @@ -108,17 +128,18 @@ class Config extends ChangeNotifier { } String? _getLabel(String? label, String id) { + final realLabel = label ?? id; final hasDup = _profiles.indexWhere( - (element) => element.label == label && element.id != id) != + (element) => element.label == realLabel && element.id != id) != -1; if (hasDup) { - return _getLabel(other.getOverwriteLabel(label!), id); + return _getLabel(other.getOverwriteLabel(realLabel), id); } else { return label; } } - setProfile(Profile profile) { + _setProfile(Profile profile) { final List profilesTemp = List.from(_profiles); final index = profilesTemp.indexWhere((element) => element.id == profile.id); @@ -131,6 +152,10 @@ class Config extends ChangeNotifier { profilesTemp[index] = updateProfile; } _profiles = profilesTemp; + } + + setProfile(Profile profile) { + _setProfile(profile); notifyListeners(); } @@ -161,7 +186,6 @@ class Config extends ChangeNotifier { } } - SelectedMap get currentSelectedMap { return currentProfile?.selectedMap ?? {}; } @@ -280,9 +304,18 @@ class Config extends ChangeNotifier { AccessControl get accessControl => _accessControl; - set accessControl(AccessControl? value) { + set accessControl(AccessControl value) { if (_accessControl != value) { - _accessControl = value ?? AccessControl(); + _accessControl = value; + notifyListeners(); + } + } + + DAV? get dav => _dav; + + set dav(DAV? value) { + if (_dav != value) { + _dav = value; notifyListeners(); } } @@ -312,7 +345,46 @@ class Config extends ChangeNotifier { } } - update() { + @JsonKey(defaultValue: true) + bool get autoCheckUpdate { + return _autoCheckUpdate; + } + + set autoCheckUpdate(bool value) { + if (_autoCheckUpdate != value) { + _autoCheckUpdate = value; + notifyListeners(); + } + } + + update([Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) { + if (config != null) { + _profiles = config._profiles; + for (final profile in config._profiles) { + _setProfile(profile); + } + final onlyProfiles = recoveryOptions == RecoveryOption.onlyProfiles; + if(_currentProfileId == null && onlyProfiles && profiles.isNotEmpty){ + _currentProfileId = _profiles.first.id; + } + if(onlyProfiles) return; + _currentProfileId = config._currentProfileId; + _isCompatible = config._isCompatible; + _autoLaunch = config._autoLaunch; + _silentLaunch = config._silentLaunch; + _autoRun = config._autoRun; + _openLog = config._openLog; + _themeMode = config._themeMode; + _locale = config._locale; + _primaryColor = config._primaryColor; + _proxiesSortType = config._proxiesSortType; + _isMinimizeOnExit = config._isMinimizeOnExit; + _isAccessControl = config._isAccessControl; + _accessControl = config._accessControl; + _isAnimateToPage = config._isAnimateToPage; + _autoCheckUpdate = config._autoCheckUpdate; + _dav = config._dav; + } notifyListeners(); } @@ -323,4 +395,9 @@ class Config extends ChangeNotifier { factory Config.fromJson(Map json) { return _$ConfigFromJson(json); } + + @override + String toString() { + return 'Config{_profiles: $_profiles, _isCompatible: $_isCompatible, _currentProfileId: $_currentProfileId, _autoLaunch: $_autoLaunch, _silentLaunch: $_silentLaunch, _autoRun: $_autoRun, _openLog: $_openLog, _themeMode: $_themeMode, _locale: $_locale, _primaryColor: $_primaryColor, _proxiesSortType: $_proxiesSortType, _isMinimizeOnExit: $_isMinimizeOnExit, _isAccessControl: $_isAccessControl, _accessControl: $_accessControl, _isAnimateToPage: $_isAnimateToPage, _dav: $_dav}'; + } } diff --git a/lib/models/dav.dart b/lib/models/dav.dart new file mode 100644 index 0000000..e6cfc63 --- /dev/null +++ b/lib/models/dav.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/dav.g.dart'; + +part 'generated/dav.freezed.dart'; + +@freezed +class DAV with _$DAV{ + const factory DAV({ + required String uri, + required String user, + required String password, + }) = _DAV; + + factory DAV.fromJson(Map json) => + _$DAVFromJson(json); +} \ No newline at end of file diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 8aebf1f..45bcaba 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -55,8 +55,12 @@ Config _$ConfigFromJson(Map json) => Config() ..isAccessControl = json['isAccessControl'] as bool? ?? false ..accessControl = AccessControl.fromJson(json['accessControl'] as Map) + ..dav = json['dav'] == null + ? null + : DAV.fromJson(json['dav'] as Map) ..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true - ..isCompatible = json['isCompatible'] as bool? ?? false; + ..isCompatible = json['isCompatible'] as bool? ?? false + ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true; Map _$ConfigToJson(Config instance) => { 'profiles': instance.profiles, @@ -72,8 +76,10 @@ Map _$ConfigToJson(Config instance) => { 'isMinimizeOnExit': instance.isMinimizeOnExit, 'isAccessControl': instance.isAccessControl, 'accessControl': instance.accessControl, + 'dav': instance.dav, 'isAnimateToPage': instance.isAnimateToPage, 'isCompatible': instance.isCompatible, + 'autoCheckUpdate': instance.autoCheckUpdate, }; const _$ThemeModeEnumMap = { diff --git a/lib/models/generated/dav.freezed.dart b/lib/models/generated/dav.freezed.dart new file mode 100644 index 0000000..5dea372 --- /dev/null +++ b/lib/models/generated/dav.freezed.dart @@ -0,0 +1,180 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../dav.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +DAV _$DAVFromJson(Map json) { + return _DAV.fromJson(json); +} + +/// @nodoc +mixin _$DAV { + String get uri => throw _privateConstructorUsedError; + String get user => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DAVCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DAVCopyWith<$Res> { + factory $DAVCopyWith(DAV value, $Res Function(DAV) then) = + _$DAVCopyWithImpl<$Res, DAV>; + @useResult + $Res call({String uri, String user, String password}); +} + +/// @nodoc +class _$DAVCopyWithImpl<$Res, $Val extends DAV> implements $DAVCopyWith<$Res> { + _$DAVCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uri = null, + Object? user = null, + Object? password = null, + }) { + return _then(_value.copyWith( + uri: null == uri + ? _value.uri + : uri // ignore: cast_nullable_to_non_nullable + as String, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DAVImplCopyWith<$Res> implements $DAVCopyWith<$Res> { + factory _$$DAVImplCopyWith(_$DAVImpl value, $Res Function(_$DAVImpl) then) = + __$$DAVImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String uri, String user, String password}); +} + +/// @nodoc +class __$$DAVImplCopyWithImpl<$Res> extends _$DAVCopyWithImpl<$Res, _$DAVImpl> + implements _$$DAVImplCopyWith<$Res> { + __$$DAVImplCopyWithImpl(_$DAVImpl _value, $Res Function(_$DAVImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? uri = null, + Object? user = null, + Object? password = null, + }) { + return _then(_$DAVImpl( + uri: null == uri + ? _value.uri + : uri // ignore: cast_nullable_to_non_nullable + as String, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$DAVImpl implements _DAV { + const _$DAVImpl( + {required this.uri, required this.user, required this.password}); + + factory _$DAVImpl.fromJson(Map json) => + _$$DAVImplFromJson(json); + + @override + final String uri; + @override + final String user; + @override + final String password; + + @override + String toString() { + return 'DAV(uri: $uri, user: $user, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DAVImpl && + (identical(other.uri, uri) || other.uri == uri) && + (identical(other.user, user) || other.user == user) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, uri, user, password); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DAVImplCopyWith<_$DAVImpl> get copyWith => + __$$DAVImplCopyWithImpl<_$DAVImpl>(this, _$identity); + + @override + Map toJson() { + return _$$DAVImplToJson( + this, + ); + } +} + +abstract class _DAV implements DAV { + const factory _DAV( + {required final String uri, + required final String user, + required final String password}) = _$DAVImpl; + + factory _DAV.fromJson(Map json) = _$DAVImpl.fromJson; + + @override + String get uri; + @override + String get user; + @override + String get password; + @override + @JsonKey(ignore: true) + _$$DAVImplCopyWith<_$DAVImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/dav.g.dart b/lib/models/generated/dav.g.dart new file mode 100644 index 0000000..d321ac6 --- /dev/null +++ b/lib/models/generated/dav.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../dav.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$DAVImpl _$$DAVImplFromJson(Map json) => _$DAVImpl( + uri: json['uri'] as String, + user: json['user'] as String, + password: json['password'] as String, + ); + +Map _$$DAVImplToJson(_$DAVImpl instance) => { + 'uri': instance.uri, + 'user': instance.user, + 'password': instance.password, + }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 07b637c..c74e0aa 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -497,6 +497,7 @@ abstract class _NetworkDetectionSelectorState mixin _$ProfilesSelectorState { List get profiles => throw _privateConstructorUsedError; String? get currentProfileId => throw _privateConstructorUsedError; + ViewMode get viewMode => throw _privateConstructorUsedError; @JsonKey(ignore: true) $ProfilesSelectorStateCopyWith get copyWith => @@ -509,7 +510,8 @@ abstract class $ProfilesSelectorStateCopyWith<$Res> { $Res Function(ProfilesSelectorState) then) = _$ProfilesSelectorStateCopyWithImpl<$Res, ProfilesSelectorState>; @useResult - $Res call({List profiles, String? currentProfileId}); + $Res call( + {List profiles, String? currentProfileId, ViewMode viewMode}); } /// @nodoc @@ -528,6 +530,7 @@ class _$ProfilesSelectorStateCopyWithImpl<$Res, $Res call({ Object? profiles = null, Object? currentProfileId = freezed, + Object? viewMode = null, }) { return _then(_value.copyWith( profiles: null == profiles @@ -538,6 +541,10 @@ class _$ProfilesSelectorStateCopyWithImpl<$Res, ? _value.currentProfileId : currentProfileId // ignore: cast_nullable_to_non_nullable as String?, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, ) as $Val); } } @@ -551,7 +558,8 @@ abstract class _$$ProfilesSelectorStateImplCopyWith<$Res> __$$ProfilesSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({List profiles, String? currentProfileId}); + $Res call( + {List profiles, String? currentProfileId, ViewMode viewMode}); } /// @nodoc @@ -568,6 +576,7 @@ class __$$ProfilesSelectorStateImplCopyWithImpl<$Res> $Res call({ Object? profiles = null, Object? currentProfileId = freezed, + Object? viewMode = null, }) { return _then(_$ProfilesSelectorStateImpl( profiles: null == profiles @@ -578,6 +587,10 @@ class __$$ProfilesSelectorStateImplCopyWithImpl<$Res> ? _value.currentProfileId : currentProfileId // ignore: cast_nullable_to_non_nullable as String?, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, )); } } @@ -586,7 +599,9 @@ class __$$ProfilesSelectorStateImplCopyWithImpl<$Res> class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState { const _$ProfilesSelectorStateImpl( - {required final List profiles, required this.currentProfileId}) + {required final List profiles, + required this.currentProfileId, + required this.viewMode}) : _profiles = profiles; final List _profiles; @@ -599,10 +614,12 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState { @override final String? currentProfileId; + @override + final ViewMode viewMode; @override String toString() { - return 'ProfilesSelectorState(profiles: $profiles, currentProfileId: $currentProfileId)'; + return 'ProfilesSelectorState(profiles: $profiles, currentProfileId: $currentProfileId, viewMode: $viewMode)'; } @override @@ -612,12 +629,17 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState { other is _$ProfilesSelectorStateImpl && const DeepCollectionEquality().equals(other._profiles, _profiles) && (identical(other.currentProfileId, currentProfileId) || - other.currentProfileId == currentProfileId)); + other.currentProfileId == currentProfileId) && + (identical(other.viewMode, viewMode) || + other.viewMode == viewMode)); } @override - int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(_profiles), currentProfileId); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_profiles), + currentProfileId, + viewMode); @JsonKey(ignore: true) @override @@ -630,13 +652,16 @@ class _$ProfilesSelectorStateImpl implements _ProfilesSelectorState { abstract class _ProfilesSelectorState implements ProfilesSelectorState { const factory _ProfilesSelectorState( {required final List profiles, - required final String? currentProfileId}) = _$ProfilesSelectorStateImpl; + required final String? currentProfileId, + required final ViewMode viewMode}) = _$ProfilesSelectorStateImpl; @override List get profiles; @override String? get currentProfileId; @override + ViewMode get viewMode; + @override @JsonKey(ignore: true) _$$ProfilesSelectorStateImplCopyWith<_$ProfilesSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; @@ -951,159 +976,6 @@ abstract class _ApplicationSelectorState implements ApplicationSelectorState { get copyWith => throw _privateConstructorUsedError; } -/// @nodoc -mixin _$HomeLayoutSelectorState { - List get navigationItems => - throw _privateConstructorUsedError; - int get currentIndex => throw _privateConstructorUsedError; - - @JsonKey(ignore: true) - $HomeLayoutSelectorStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $HomeLayoutSelectorStateCopyWith<$Res> { - factory $HomeLayoutSelectorStateCopyWith(HomeLayoutSelectorState value, - $Res Function(HomeLayoutSelectorState) then) = - _$HomeLayoutSelectorStateCopyWithImpl<$Res, HomeLayoutSelectorState>; - @useResult - $Res call({List navigationItems, int currentIndex}); -} - -/// @nodoc -class _$HomeLayoutSelectorStateCopyWithImpl<$Res, - $Val extends HomeLayoutSelectorState> - implements $HomeLayoutSelectorStateCopyWith<$Res> { - _$HomeLayoutSelectorStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? navigationItems = null, - Object? currentIndex = null, - }) { - return _then(_value.copyWith( - navigationItems: null == navigationItems - ? _value.navigationItems - : navigationItems // ignore: cast_nullable_to_non_nullable - as List, - currentIndex: null == currentIndex - ? _value.currentIndex - : currentIndex // ignore: cast_nullable_to_non_nullable - as int, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$HomeLayoutSelectorStateImplCopyWith<$Res> - implements $HomeLayoutSelectorStateCopyWith<$Res> { - factory _$$HomeLayoutSelectorStateImplCopyWith( - _$HomeLayoutSelectorStateImpl value, - $Res Function(_$HomeLayoutSelectorStateImpl) then) = - __$$HomeLayoutSelectorStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({List navigationItems, int currentIndex}); -} - -/// @nodoc -class __$$HomeLayoutSelectorStateImplCopyWithImpl<$Res> - extends _$HomeLayoutSelectorStateCopyWithImpl<$Res, - _$HomeLayoutSelectorStateImpl> - implements _$$HomeLayoutSelectorStateImplCopyWith<$Res> { - __$$HomeLayoutSelectorStateImplCopyWithImpl( - _$HomeLayoutSelectorStateImpl _value, - $Res Function(_$HomeLayoutSelectorStateImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? navigationItems = null, - Object? currentIndex = null, - }) { - return _then(_$HomeLayoutSelectorStateImpl( - navigationItems: null == navigationItems - ? _value._navigationItems - : navigationItems // ignore: cast_nullable_to_non_nullable - as List, - currentIndex: null == currentIndex - ? _value.currentIndex - : currentIndex // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc - -class _$HomeLayoutSelectorStateImpl implements _HomeLayoutSelectorState { - const _$HomeLayoutSelectorStateImpl( - {required final List navigationItems, - required this.currentIndex}) - : _navigationItems = navigationItems; - - final List _navigationItems; - @override - List get navigationItems { - if (_navigationItems is EqualUnmodifiableListView) return _navigationItems; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_navigationItems); - } - - @override - final int currentIndex; - - @override - String toString() { - return 'HomeLayoutSelectorState(navigationItems: $navigationItems, currentIndex: $currentIndex)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$HomeLayoutSelectorStateImpl && - const DeepCollectionEquality() - .equals(other._navigationItems, _navigationItems) && - (identical(other.currentIndex, currentIndex) || - other.currentIndex == currentIndex)); - } - - @override - int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(_navigationItems), currentIndex); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$HomeLayoutSelectorStateImplCopyWith<_$HomeLayoutSelectorStateImpl> - get copyWith => __$$HomeLayoutSelectorStateImplCopyWithImpl< - _$HomeLayoutSelectorStateImpl>(this, _$identity); -} - -abstract class _HomeLayoutSelectorState implements HomeLayoutSelectorState { - const factory _HomeLayoutSelectorState( - {required final List navigationItems, - required final int currentIndex}) = _$HomeLayoutSelectorStateImpl; - - @override - List get navigationItems; - @override - int get currentIndex; - @override - @JsonKey(ignore: true) - _$$HomeLayoutSelectorStateImplCopyWith<_$HomeLayoutSelectorStateImpl> - get copyWith => throw _privateConstructorUsedError; -} - /// @nodoc mixin _$TrayContainerSelectorState { Mode get mode => throw _privateConstructorUsedError; @@ -1429,31 +1301,35 @@ abstract class _UpdateNavigationsSelector implements UpdateNavigationsSelector { } /// @nodoc -mixin _$HomeCommonScaffoldSelectorState { +mixin _$HomeSelectorState { String get currentLabel => throw _privateConstructorUsedError; + List get navigationItems => + throw _privateConstructorUsedError; + ViewMode get viewMode => throw _privateConstructorUsedError; String? get locale => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $HomeCommonScaffoldSelectorStateCopyWith - get copyWith => throw _privateConstructorUsedError; + $HomeSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $HomeCommonScaffoldSelectorStateCopyWith<$Res> { - factory $HomeCommonScaffoldSelectorStateCopyWith( - HomeCommonScaffoldSelectorState value, - $Res Function(HomeCommonScaffoldSelectorState) then) = - _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, - HomeCommonScaffoldSelectorState>; +abstract class $HomeSelectorStateCopyWith<$Res> { + factory $HomeSelectorStateCopyWith( + HomeSelectorState value, $Res Function(HomeSelectorState) then) = + _$HomeSelectorStateCopyWithImpl<$Res, HomeSelectorState>; @useResult - $Res call({String currentLabel, String? locale}); + $Res call( + {String currentLabel, + List navigationItems, + ViewMode viewMode, + String? locale}); } /// @nodoc -class _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, - $Val extends HomeCommonScaffoldSelectorState> - implements $HomeCommonScaffoldSelectorStateCopyWith<$Res> { - _$HomeCommonScaffoldSelectorStateCopyWithImpl(this._value, this._then); +class _$HomeSelectorStateCopyWithImpl<$Res, $Val extends HomeSelectorState> + implements $HomeSelectorStateCopyWith<$Res> { + _$HomeSelectorStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -1464,6 +1340,8 @@ class _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, @override $Res call({ Object? currentLabel = null, + Object? navigationItems = null, + Object? viewMode = null, Object? locale = freezed, }) { return _then(_value.copyWith( @@ -1471,6 +1349,14 @@ class _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, ? _value.currentLabel : currentLabel // ignore: cast_nullable_to_non_nullable as String, + navigationItems: null == navigationItems + ? _value.navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable @@ -1480,38 +1366,49 @@ class _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, } /// @nodoc -abstract class _$$HomeCommonScaffoldSelectorStateImplCopyWith<$Res> - implements $HomeCommonScaffoldSelectorStateCopyWith<$Res> { - factory _$$HomeCommonScaffoldSelectorStateImplCopyWith( - _$HomeCommonScaffoldSelectorStateImpl value, - $Res Function(_$HomeCommonScaffoldSelectorStateImpl) then) = - __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl<$Res>; +abstract class _$$HomeSelectorStateImplCopyWith<$Res> + implements $HomeSelectorStateCopyWith<$Res> { + factory _$$HomeSelectorStateImplCopyWith(_$HomeSelectorStateImpl value, + $Res Function(_$HomeSelectorStateImpl) then) = + __$$HomeSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({String currentLabel, String? locale}); + $Res call( + {String currentLabel, + List navigationItems, + ViewMode viewMode, + String? locale}); } /// @nodoc -class __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl<$Res> - extends _$HomeCommonScaffoldSelectorStateCopyWithImpl<$Res, - _$HomeCommonScaffoldSelectorStateImpl> - implements _$$HomeCommonScaffoldSelectorStateImplCopyWith<$Res> { - __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl( - _$HomeCommonScaffoldSelectorStateImpl _value, - $Res Function(_$HomeCommonScaffoldSelectorStateImpl) _then) +class __$$HomeSelectorStateImplCopyWithImpl<$Res> + extends _$HomeSelectorStateCopyWithImpl<$Res, _$HomeSelectorStateImpl> + implements _$$HomeSelectorStateImplCopyWith<$Res> { + __$$HomeSelectorStateImplCopyWithImpl(_$HomeSelectorStateImpl _value, + $Res Function(_$HomeSelectorStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ Object? currentLabel = null, + Object? navigationItems = null, + Object? viewMode = null, Object? locale = freezed, }) { - return _then(_$HomeCommonScaffoldSelectorStateImpl( + return _then(_$HomeSelectorStateImpl( currentLabel: null == currentLabel ? _value.currentLabel : currentLabel // ignore: cast_nullable_to_non_nullable as String, + navigationItems: null == navigationItems + ? _value._navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, locale: freezed == locale ? _value.locale : locale // ignore: cast_nullable_to_non_nullable @@ -1522,86 +1419,105 @@ class __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl<$Res> /// @nodoc -class _$HomeCommonScaffoldSelectorStateImpl - implements _HomeCommonScaffoldSelectorState { - const _$HomeCommonScaffoldSelectorStateImpl( - {required this.currentLabel, required this.locale}); +class _$HomeSelectorStateImpl implements _HomeSelectorState { + const _$HomeSelectorStateImpl( + {required this.currentLabel, + required final List navigationItems, + required this.viewMode, + required this.locale}) + : _navigationItems = navigationItems; @override final String currentLabel; + final List _navigationItems; + @override + List get navigationItems { + if (_navigationItems is EqualUnmodifiableListView) return _navigationItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_navigationItems); + } + + @override + final ViewMode viewMode; @override final String? locale; @override String toString() { - return 'HomeCommonScaffoldSelectorState(currentLabel: $currentLabel, locale: $locale)'; + return 'HomeSelectorState(currentLabel: $currentLabel, navigationItems: $navigationItems, viewMode: $viewMode, locale: $locale)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$HomeCommonScaffoldSelectorStateImpl && + other is _$HomeSelectorStateImpl && (identical(other.currentLabel, currentLabel) || other.currentLabel == currentLabel) && + const DeepCollectionEquality() + .equals(other._navigationItems, _navigationItems) && + (identical(other.viewMode, viewMode) || + other.viewMode == viewMode) && (identical(other.locale, locale) || other.locale == locale)); } @override - int get hashCode => Object.hash(runtimeType, currentLabel, locale); + int get hashCode => Object.hash(runtimeType, currentLabel, + const DeepCollectionEquality().hash(_navigationItems), viewMode, locale); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$HomeCommonScaffoldSelectorStateImplCopyWith< - _$HomeCommonScaffoldSelectorStateImpl> - get copyWith => __$$HomeCommonScaffoldSelectorStateImplCopyWithImpl< - _$HomeCommonScaffoldSelectorStateImpl>(this, _$identity); + _$$HomeSelectorStateImplCopyWith<_$HomeSelectorStateImpl> get copyWith => + __$$HomeSelectorStateImplCopyWithImpl<_$HomeSelectorStateImpl>( + this, _$identity); } -abstract class _HomeCommonScaffoldSelectorState - implements HomeCommonScaffoldSelectorState { - const factory _HomeCommonScaffoldSelectorState( +abstract class _HomeSelectorState implements HomeSelectorState { + const factory _HomeSelectorState( {required final String currentLabel, - required final String? locale}) = _$HomeCommonScaffoldSelectorStateImpl; + required final List navigationItems, + required final ViewMode viewMode, + required final String? locale}) = _$HomeSelectorStateImpl; @override String get currentLabel; @override + List get navigationItems; + @override + ViewMode get viewMode; + @override String? get locale; @override @JsonKey(ignore: true) - _$$HomeCommonScaffoldSelectorStateImplCopyWith< - _$HomeCommonScaffoldSelectorStateImpl> - get copyWith => throw _privateConstructorUsedError; + _$$HomeSelectorStateImplCopyWith<_$HomeSelectorStateImpl> get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -mixin _$HomeNavigationSelectorState { - int get currentIndex => throw _privateConstructorUsedError; - String? get locale => throw _privateConstructorUsedError; +mixin _$HomeBodySelectorState { + List get navigationItems => + throw _privateConstructorUsedError; @JsonKey(ignore: true) - $HomeNavigationSelectorStateCopyWith - get copyWith => throw _privateConstructorUsedError; + $HomeBodySelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $HomeNavigationSelectorStateCopyWith<$Res> { - factory $HomeNavigationSelectorStateCopyWith( - HomeNavigationSelectorState value, - $Res Function(HomeNavigationSelectorState) then) = - _$HomeNavigationSelectorStateCopyWithImpl<$Res, - HomeNavigationSelectorState>; +abstract class $HomeBodySelectorStateCopyWith<$Res> { + factory $HomeBodySelectorStateCopyWith(HomeBodySelectorState value, + $Res Function(HomeBodySelectorState) then) = + _$HomeBodySelectorStateCopyWithImpl<$Res, HomeBodySelectorState>; @useResult - $Res call({int currentIndex, String? locale}); + $Res call({List navigationItems}); } /// @nodoc -class _$HomeNavigationSelectorStateCopyWithImpl<$Res, - $Val extends HomeNavigationSelectorState> - implements $HomeNavigationSelectorStateCopyWith<$Res> { - _$HomeNavigationSelectorStateCopyWithImpl(this._value, this._then); +class _$HomeBodySelectorStateCopyWithImpl<$Res, + $Val extends HomeBodySelectorState> + implements $HomeBodySelectorStateCopyWith<$Res> { + _$HomeBodySelectorStateCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -1611,114 +1527,103 @@ class _$HomeNavigationSelectorStateCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? currentIndex = null, - Object? locale = freezed, + Object? navigationItems = null, }) { return _then(_value.copyWith( - currentIndex: null == currentIndex - ? _value.currentIndex - : currentIndex // ignore: cast_nullable_to_non_nullable - as int, - locale: freezed == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as String?, + navigationItems: null == navigationItems + ? _value.navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } /// @nodoc -abstract class _$$HomeNavigationSelectorStateImplCopyWith<$Res> - implements $HomeNavigationSelectorStateCopyWith<$Res> { - factory _$$HomeNavigationSelectorStateImplCopyWith( - _$HomeNavigationSelectorStateImpl value, - $Res Function(_$HomeNavigationSelectorStateImpl) then) = - __$$HomeNavigationSelectorStateImplCopyWithImpl<$Res>; +abstract class _$$HomeBodySelectorStateImplCopyWith<$Res> + implements $HomeBodySelectorStateCopyWith<$Res> { + factory _$$HomeBodySelectorStateImplCopyWith( + _$HomeBodySelectorStateImpl value, + $Res Function(_$HomeBodySelectorStateImpl) then) = + __$$HomeBodySelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({int currentIndex, String? locale}); + $Res call({List navigationItems}); } /// @nodoc -class __$$HomeNavigationSelectorStateImplCopyWithImpl<$Res> - extends _$HomeNavigationSelectorStateCopyWithImpl<$Res, - _$HomeNavigationSelectorStateImpl> - implements _$$HomeNavigationSelectorStateImplCopyWith<$Res> { - __$$HomeNavigationSelectorStateImplCopyWithImpl( - _$HomeNavigationSelectorStateImpl _value, - $Res Function(_$HomeNavigationSelectorStateImpl) _then) +class __$$HomeBodySelectorStateImplCopyWithImpl<$Res> + extends _$HomeBodySelectorStateCopyWithImpl<$Res, + _$HomeBodySelectorStateImpl> + implements _$$HomeBodySelectorStateImplCopyWith<$Res> { + __$$HomeBodySelectorStateImplCopyWithImpl(_$HomeBodySelectorStateImpl _value, + $Res Function(_$HomeBodySelectorStateImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @override $Res call({ - Object? currentIndex = null, - Object? locale = freezed, + Object? navigationItems = null, }) { - return _then(_$HomeNavigationSelectorStateImpl( - currentIndex: null == currentIndex - ? _value.currentIndex - : currentIndex // ignore: cast_nullable_to_non_nullable - as int, - locale: freezed == locale - ? _value.locale - : locale // ignore: cast_nullable_to_non_nullable - as String?, + return _then(_$HomeBodySelectorStateImpl( + navigationItems: null == navigationItems + ? _value._navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, )); } } /// @nodoc -class _$HomeNavigationSelectorStateImpl - implements _HomeNavigationSelectorState { - const _$HomeNavigationSelectorStateImpl( - {required this.currentIndex, required this.locale}); +class _$HomeBodySelectorStateImpl implements _HomeBodySelectorState { + const _$HomeBodySelectorStateImpl( + {required final List navigationItems}) + : _navigationItems = navigationItems; + final List _navigationItems; @override - final int currentIndex; - @override - final String? locale; + List get navigationItems { + if (_navigationItems is EqualUnmodifiableListView) return _navigationItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_navigationItems); + } @override String toString() { - return 'HomeNavigationSelectorState(currentIndex: $currentIndex, locale: $locale)'; + return 'HomeBodySelectorState(navigationItems: $navigationItems)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$HomeNavigationSelectorStateImpl && - (identical(other.currentIndex, currentIndex) || - other.currentIndex == currentIndex) && - (identical(other.locale, locale) || other.locale == locale)); + other is _$HomeBodySelectorStateImpl && + const DeepCollectionEquality() + .equals(other._navigationItems, _navigationItems)); } @override - int get hashCode => Object.hash(runtimeType, currentIndex, locale); + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_navigationItems)); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$HomeNavigationSelectorStateImplCopyWith<_$HomeNavigationSelectorStateImpl> - get copyWith => __$$HomeNavigationSelectorStateImplCopyWithImpl< - _$HomeNavigationSelectorStateImpl>(this, _$identity); + _$$HomeBodySelectorStateImplCopyWith<_$HomeBodySelectorStateImpl> + get copyWith => __$$HomeBodySelectorStateImplCopyWithImpl< + _$HomeBodySelectorStateImpl>(this, _$identity); } -abstract class _HomeNavigationSelectorState - implements HomeNavigationSelectorState { - const factory _HomeNavigationSelectorState( - {required final int currentIndex, - required final String? locale}) = _$HomeNavigationSelectorStateImpl; +abstract class _HomeBodySelectorState implements HomeBodySelectorState { + const factory _HomeBodySelectorState( + {required final List navigationItems}) = + _$HomeBodySelectorStateImpl; @override - int get currentIndex; - @override - String? get locale; + List get navigationItems; @override @JsonKey(ignore: true) - _$$HomeNavigationSelectorStateImplCopyWith<_$HomeNavigationSelectorStateImpl> + _$$HomeBodySelectorStateImplCopyWith<_$HomeBodySelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1980,6 +1885,7 @@ mixin _$ProxiesTabViewSelectorState { ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError; num get sortNum => throw _privateConstructorUsedError; Group get group => throw _privateConstructorUsedError; + ViewMode get viewMode => throw _privateConstructorUsedError; @JsonKey(ignore: true) $ProxiesTabViewSelectorStateCopyWith @@ -1994,7 +1900,11 @@ abstract class $ProxiesTabViewSelectorStateCopyWith<$Res> { _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, ProxiesTabViewSelectorState>; @useResult - $Res call({ProxiesSortType proxiesSortType, num sortNum, Group group}); + $Res call( + {ProxiesSortType proxiesSortType, + num sortNum, + Group group, + ViewMode viewMode}); $GroupCopyWith<$Res> get group; } @@ -2016,6 +1926,7 @@ class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, Object? proxiesSortType = null, Object? sortNum = null, Object? group = null, + Object? viewMode = null, }) { return _then(_value.copyWith( proxiesSortType: null == proxiesSortType @@ -2030,6 +1941,10 @@ class _$ProxiesTabViewSelectorStateCopyWithImpl<$Res, ? _value.group : group // ignore: cast_nullable_to_non_nullable as Group, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, ) as $Val); } @@ -2051,7 +1966,11 @@ abstract class _$$ProxiesTabViewSelectorStateImplCopyWith<$Res> __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({ProxiesSortType proxiesSortType, num sortNum, Group group}); + $Res call( + {ProxiesSortType proxiesSortType, + num sortNum, + Group group, + ViewMode viewMode}); @override $GroupCopyWith<$Res> get group; @@ -2073,6 +1992,7 @@ class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res> Object? proxiesSortType = null, Object? sortNum = null, Object? group = null, + Object? viewMode = null, }) { return _then(_$ProxiesTabViewSelectorStateImpl( proxiesSortType: null == proxiesSortType @@ -2087,6 +2007,10 @@ class __$$ProxiesTabViewSelectorStateImplCopyWithImpl<$Res> ? _value.group : group // ignore: cast_nullable_to_non_nullable as Group, + viewMode: null == viewMode + ? _value.viewMode + : viewMode // ignore: cast_nullable_to_non_nullable + as ViewMode, )); } } @@ -2098,7 +2022,8 @@ class _$ProxiesTabViewSelectorStateImpl const _$ProxiesTabViewSelectorStateImpl( {required this.proxiesSortType, required this.sortNum, - required this.group}); + required this.group, + required this.viewMode}); @override final ProxiesSortType proxiesSortType; @@ -2106,10 +2031,12 @@ class _$ProxiesTabViewSelectorStateImpl final num sortNum; @override final Group group; + @override + final ViewMode viewMode; @override String toString() { - return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group)'; + return 'ProxiesTabViewSelectorState(proxiesSortType: $proxiesSortType, sortNum: $sortNum, group: $group, viewMode: $viewMode)'; } @override @@ -2120,11 +2047,14 @@ class _$ProxiesTabViewSelectorStateImpl (identical(other.proxiesSortType, proxiesSortType) || other.proxiesSortType == proxiesSortType) && (identical(other.sortNum, sortNum) || other.sortNum == sortNum) && - (identical(other.group, group) || other.group == group)); + (identical(other.group, group) || other.group == group) && + (identical(other.viewMode, viewMode) || + other.viewMode == viewMode)); } @override - int get hashCode => Object.hash(runtimeType, proxiesSortType, sortNum, group); + int get hashCode => + Object.hash(runtimeType, proxiesSortType, sortNum, group, viewMode); @JsonKey(ignore: true) @override @@ -2139,7 +2069,8 @@ abstract class _ProxiesTabViewSelectorState const factory _ProxiesTabViewSelectorState( {required final ProxiesSortType proxiesSortType, required final num sortNum, - required final Group group}) = _$ProxiesTabViewSelectorStateImpl; + required final Group group, + required final ViewMode viewMode}) = _$ProxiesTabViewSelectorStateImpl; @override ProxiesSortType get proxiesSortType; @@ -2148,7 +2079,143 @@ abstract class _ProxiesTabViewSelectorState @override Group get group; @override + ViewMode get viewMode; + @override @JsonKey(ignore: true) _$$ProxiesTabViewSelectorStateImplCopyWith<_$ProxiesTabViewSelectorStateImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$MoreToolsSelectorState { + List get navigationItems => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $MoreToolsSelectorStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MoreToolsSelectorStateCopyWith<$Res> { + factory $MoreToolsSelectorStateCopyWith(MoreToolsSelectorState value, + $Res Function(MoreToolsSelectorState) then) = + _$MoreToolsSelectorStateCopyWithImpl<$Res, MoreToolsSelectorState>; + @useResult + $Res call({List navigationItems}); +} + +/// @nodoc +class _$MoreToolsSelectorStateCopyWithImpl<$Res, + $Val extends MoreToolsSelectorState> + implements $MoreToolsSelectorStateCopyWith<$Res> { + _$MoreToolsSelectorStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? navigationItems = null, + }) { + return _then(_value.copyWith( + navigationItems: null == navigationItems + ? _value.navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MoreToolsSelectorStateImplCopyWith<$Res> + implements $MoreToolsSelectorStateCopyWith<$Res> { + factory _$$MoreToolsSelectorStateImplCopyWith( + _$MoreToolsSelectorStateImpl value, + $Res Function(_$MoreToolsSelectorStateImpl) then) = + __$$MoreToolsSelectorStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List navigationItems}); +} + +/// @nodoc +class __$$MoreToolsSelectorStateImplCopyWithImpl<$Res> + extends _$MoreToolsSelectorStateCopyWithImpl<$Res, + _$MoreToolsSelectorStateImpl> + implements _$$MoreToolsSelectorStateImplCopyWith<$Res> { + __$$MoreToolsSelectorStateImplCopyWithImpl( + _$MoreToolsSelectorStateImpl _value, + $Res Function(_$MoreToolsSelectorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? navigationItems = null, + }) { + return _then(_$MoreToolsSelectorStateImpl( + navigationItems: null == navigationItems + ? _value._navigationItems + : navigationItems // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$MoreToolsSelectorStateImpl implements _MoreToolsSelectorState { + const _$MoreToolsSelectorStateImpl( + {required final List navigationItems}) + : _navigationItems = navigationItems; + + final List _navigationItems; + @override + List get navigationItems { + if (_navigationItems is EqualUnmodifiableListView) return _navigationItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_navigationItems); + } + + @override + String toString() { + return 'MoreToolsSelectorState(navigationItems: $navigationItems)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MoreToolsSelectorStateImpl && + const DeepCollectionEquality() + .equals(other._navigationItems, _navigationItems)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_navigationItems)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MoreToolsSelectorStateImplCopyWith<_$MoreToolsSelectorStateImpl> + get copyWith => __$$MoreToolsSelectorStateImplCopyWithImpl< + _$MoreToolsSelectorStateImpl>(this, _$identity); +} + +abstract class _MoreToolsSelectorState implements MoreToolsSelectorState { + const factory _MoreToolsSelectorState( + {required final List navigationItems}) = + _$MoreToolsSelectorStateImpl; + + @override + List get navigationItems; + @override + @JsonKey(ignore: true) + _$$MoreToolsSelectorStateImplCopyWith<_$MoreToolsSelectorStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/models.dart b/lib/models/models.dart index 8e88abd..3f883e9 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -12,4 +12,5 @@ export 'package.dart'; export 'common.dart'; export 'ffi.dart'; export 'selector.dart'; -export 'navigation.dart'; \ No newline at end of file +export 'navigation.dart'; +export 'dav.dart'; \ No newline at end of file diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 554cc43..8bab58d 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -84,42 +84,40 @@ class Profile { this.autoUpdate = true, }) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(), autoUpdateDuration = - autoUpdateDuration ?? appConstant.defaultUpdateDuration, + autoUpdateDuration ?? defaultUpdateDuration, selectedMap = selectedMap ?? {}; ProfileType get type => url == null ? ProfileType.file : ProfileType.url; + Future> checkAndUpdate() async { + final isExists = await check(); + if(!isExists){ + if(url != null){ + return await update(); + } + return Result.error(); + } + return Result.success(); + } + Future> update() async { if (url == null) { return Result.error( - message: appLocalizations.unableToUpdateCurrentProfileDesc, + appLocalizations.unableToUpdateCurrentProfileDesc, ); } final responseResult = await Request.getFileResponseForUrl(url!); final response = responseResult.data; if (responseResult.type != ResultType.success || response == null) { - return Result.error(message: responseResult.message); + return Result.error(responseResult.message); } final disposition = response.headers['content-disposition']; - if (disposition != null && label == null) { - final parseValue = HeaderValue.parse(disposition); - parseValue.parameters.forEach( - (key, value) { - if (key.startsWith("filename")) { - if (key == "filename*") { - label = Uri.decodeComponent((value ?? "").split("'").last); - } else { - label = value ?? id; - } - } - }, - ); - } + label ??= other.getFileNameForDisposition(disposition) ?? id; final userinfo = response.headers['subscription-userinfo']; userInfo = UserInfo.formHString(userinfo); final saveResult = await saveFile(response.bodyBytes); if (saveResult.type == ResultType.error) { - return Result.error(message: saveResult.message); + return Result.error(saveResult.message); } lastUpdateDate = DateTime.now(); return Result.success(); @@ -133,7 +131,7 @@ class Profile { Future> saveFile(Uint8List bytes) async { final isValidate = clashCore.validateConfig(utf8.decode(bytes)); if (!isValidate) { - return Result.error(message: appLocalizations.profileParseErrorDesc); + return Result.error(appLocalizations.profileParseErrorDesc); } final path = await appPath.getProfilePath(id); final file = File(path!); diff --git a/lib/models/selector.dart b/lib/models/selector.dart index b5e0929..3bb3301 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -40,6 +40,7 @@ class ProfilesSelectorState with _$ProfilesSelectorState { const factory ProfilesSelectorState({ required List profiles, required String? currentProfileId, + required ViewMode viewMode, }) = _ProfilesSelectorState; } @@ -60,14 +61,6 @@ class ApplicationSelectorState with _$ApplicationSelectorState { }) = _ApplicationSelectorState; } -@freezed -class HomeLayoutSelectorState with _$HomeLayoutSelectorState{ - const factory HomeLayoutSelectorState({ - required List navigationItems, - required int currentIndex, - })=_HomeLayoutSelectorState; -} - @freezed class TrayContainerSelectorState with _$TrayContainerSelectorState{ const factory TrayContainerSelectorState({ @@ -86,20 +79,22 @@ class UpdateNavigationsSelector with _$UpdateNavigationsSelector{ }) = _UpdateNavigationsSelector; } + @freezed -class HomeCommonScaffoldSelectorState with _$HomeCommonScaffoldSelectorState { - const factory HomeCommonScaffoldSelectorState({ +class HomeSelectorState with _$HomeSelectorState { + const factory HomeSelectorState({ required String currentLabel, + required List navigationItems, + required ViewMode viewMode, required String? locale, - }) = _HomeCommonScaffoldSelectorState; + }) = _HomeSelectorState; } @freezed -class HomeNavigationSelectorState with _$HomeNavigationSelectorState{ - const factory HomeNavigationSelectorState({ - required int currentIndex, - required String? locale, - }) = _HomeNavigationSelectorState; +class HomeBodySelectorState with _$HomeBodySelectorState { + const factory HomeBodySelectorState({ + required List navigationItems, + }) = _HomeBodySelectorState; } @freezed @@ -122,5 +117,13 @@ class ProxiesTabViewSelectorState with _$ProxiesTabViewSelectorState{ required ProxiesSortType proxiesSortType, required num sortNum, required Group group, + required ViewMode viewMode, }) = _ProxiesTabViewSelectorState; } + +@freezed +class MoreToolsSelectorState with _$MoreToolsSelectorState { + const factory MoreToolsSelectorState({ + required List navigationItems, + }) = _MoreToolsSelectorState; +} \ No newline at end of file diff --git a/lib/models/system_color_scheme.dart b/lib/models/system_color_scheme.dart index 25af319..eb13a2e 100644 --- a/lib/models/system_color_scheme.dart +++ b/lib/models/system_color_scheme.dart @@ -6,10 +6,10 @@ class SystemColorSchemes { ColorScheme? lightColorScheme, ColorScheme? darkColorScheme, }) : lightColorScheme = lightColorScheme ?? - ColorScheme.fromSeed(seedColor: appConstant.defaultPrimaryColor), + ColorScheme.fromSeed(seedColor: defaultPrimaryColor), darkColorScheme = darkColorScheme ?? ColorScheme.fromSeed( - seedColor: appConstant.defaultPrimaryColor, + seedColor: defaultPrimaryColor, brightness: Brightness.dark, ); ColorScheme lightColorScheme; diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 79c9045..fc23f34 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,7 +1,6 @@ import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -13,28 +12,133 @@ typedef OnSelected = void Function(int index); class HomePage extends StatelessWidget { const HomePage({super.key}); - Widget _buildBody({ + _getNavigationBar({ + required ViewMode viewMode, required List navigationItems, + required int currentIndex, }) { - globalState.currentNavigationItems = navigationItems; - return Selector( - selector: (_, appState) { - final index = navigationItems.lastIndexWhere( - (element) => element.label == appState.currentLabel, - ); - return index == -1 ? 0 : index; - }, - builder: (context, currentIndex, __) { - if (globalState.pageController != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - globalState.appController.toPage(currentIndex, hasAnimate: true); - }); - } else { - globalState.pageController = PageController( - initialPage: currentIndex, - keepPage: true, + if (viewMode == ViewMode.mobile) { + return NavigationBar( + destinations: navigationItems + .map( + (e) => NavigationDestination( + icon: e.icon, + label: Intl.message(e.label), + ), + ) + .toList(), + onDestinationSelected: globalState.appController.toPage, + selectedIndex: currentIndex, + ); + } + final extended = viewMode == ViewMode.desktop; + return NavigationRail( + destinations: navigationItems + .map( + (e) => NavigationRailDestination( + icon: e.icon, + label: Text( + Intl.message(e.label), + ), + ), + ) + .toList(), + onDestinationSelected: globalState.appController.toPage, + extended: extended, + minExtendedWidth: 172, + selectedIndex: currentIndex, + labelType: extended + ? NavigationRailLabelType.none + : NavigationRailLabelType.selected, + ); + } + + @override + Widget build(BuildContext context) { + return PopContainer( + child: Selector2( + selector: (_, appState, config) => HomeSelectorState( + currentLabel: appState.currentLabel, + navigationItems: appState.currentNavigationItems, + viewMode: appState.viewMode, + locale: config.locale, + ), + builder: (_, state, child) { + final viewMode = state.viewMode; + final navigationItems = state.navigationItems; + final currentLabel = state.currentLabel; + final index = navigationItems.lastIndexWhere( + (element) => element.label == currentLabel, ); - } + final currentIndex = index == -1 ? 0 : index; + final navigationBar = _getNavigationBar( + viewMode: viewMode, + navigationItems: navigationItems, + currentIndex: currentIndex, + ); + final bottomNavigationBar = + viewMode == ViewMode.mobile ? navigationBar : null; + Widget body; + if (viewMode != ViewMode.mobile) { + body = Row( + children: [ + navigationBar, + Expanded( + flex: 1, + child: child!, + ) + ], + ); + } else { + body = child!; + } + return CommonScaffold( + key: globalState.homeScaffoldKey, + title: Intl.message( + currentLabel, + ), + body: body, + bottomNavigationBar: bottomNavigationBar, + ); + }, + child: const HomeBody( + key: Key("home_boy"), + ), + ), + ); + } +} + +class HomeBody extends StatelessWidget { + const HomeBody({super.key}); + + _updatePageIndex(List navigationItems) { + final currentLabel = globalState.appController.appState.currentLabel; + final index = navigationItems.lastIndexWhere( + (element) => element.label == currentLabel, + ); + final currentIndex = index == -1 ? 0 : index; + if (globalState.pageController != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + globalState.appController.toPage(currentIndex); + }); + } else { + globalState.pageController = PageController( + initialPage: currentIndex, + keepPage: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => HomeBodySelectorState( + navigationItems: appState.currentNavigationItems, + ), + builder: (_, state, __) { + final navigationItems = state.navigationItems; + _updatePageIndex(navigationItems); return PageView.builder( controller: globalState.pageController, physics: const NeverScrollableScrollPhysics(), @@ -42,6 +146,7 @@ class HomePage extends StatelessWidget { itemBuilder: (_, index) { final navigationItem = navigationItems[index]; return KeepContainer( + key: Key(navigationItem.label), child: navigationItem.fragment, ); }, @@ -49,157 +154,4 @@ class HomePage extends StatelessWidget { }, ); } - - _buildNavigationRail({ - required List navigationItems, - bool extended = false, - }) { - return Selector2( - selector: (_, appState, config) { - final index = navigationItems.lastIndexWhere( - (element) => element.label == appState.currentLabel, - ); - return HomeNavigationSelectorState( - currentIndex: index == -1 ? 0 : index, - locale: config.locale, - ); - }, - builder: (context, state, __) { - return AdaptiveScaffold.standardNavigationRail( - onDestinationSelected: globalState.appController.toPage, - destinations: navigationItems - .map( - (e) => NavigationRailDestination( - icon: e.icon, - label: Text( - Intl.message(e.label), - ), - ), - ) - .toList(), - extended: extended, - width: extended ? 160 : 80, - selectedIndex: state.currentIndex, - labelType: extended - ? NavigationRailLabelType.none - : NavigationRailLabelType.selected, - ); - }, - ); - } - - _buildBottomNavigationBar({ - required List navigationItems, - }) { - return Selector2( - selector: (_, appState, config) { - final index = navigationItems.lastIndexWhere( - (element) => element.label == appState.currentLabel, - ); - return HomeNavigationSelectorState( - currentIndex: index == -1 ? 0 : index, - locale: config.locale, - ); - }, - builder: (context, state, __) { - final mobileDestinations = navigationItems - .map( - (e) => NavigationDestination( - icon: e.icon, - label: Intl.message(e.label), - ), - ) - .toList(); - return AdaptiveScaffold.standardBottomNavigationBar( - destinations: mobileDestinations, - onDestinationSelected: globalState.appController.toPage, - currentIndex: state.currentIndex, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return PopContainer( - child: Selector2( - selector: (_, appState, config) => HomeCommonScaffoldSelectorState( - currentLabel: appState.currentLabel, - locale: config.locale, - ), - builder: (_, state, child) { - return CommonScaffold( - key: globalState.homeScaffoldKey, - title: Text( - Intl.message(state.currentLabel), - ), - body: child!, - ); - }, - child: Selector>( - selector: (_, appState) => appState.navigationItems, - builder: (_, navigationItems, __) { - final desktopNavigationItems = navigationItems - .where( - (element) => - element.modes.contains(NavigationItemMode.desktop), - ) - .toList(); - final mobileNavigationItems = navigationItems - .where( - (element) => - element.modes.contains(NavigationItemMode.mobile), - ) - .toList(); - return AdaptiveLayout( - transitionDuration: kThemeAnimationDuration, - primaryNavigation: SlotLayout( - config: { - Breakpoints.medium: SlotLayout.from( - key: const Key('primary_navigation_medium'), - builder: (_) => _buildNavigationRail( - navigationItems: desktopNavigationItems, - ), - ), - Breakpoints.large: SlotLayout.from( - key: const Key('primary_navigation_large'), - builder: (_) => _buildNavigationRail( - navigationItems: desktopNavigationItems, - extended: true, - ), - ), - }, - ), - body: SlotLayout( - config: { - Breakpoints.mediumAndUp: SlotLayout.from( - key: const Key('body_mediumAndUp'), - builder: (_) => _buildBody( - navigationItems: desktopNavigationItems, - ), - ), - Breakpoints.small: SlotLayout.from( - key: const Key('body_small'), - builder: (_) => _buildBody( - navigationItems: mobileNavigationItems, - ), - ) - }, - ), - bottomNavigation: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('bottom_navigation_small'), - builder: (_) => _buildBottomNavigationBar( - navigationItems: mobileNavigationItems, - ), - ) - }, - ), - ); - }, - ), - ), - ); - } } diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart index 5647028..b76a639 100644 --- a/lib/pages/scan.dart +++ b/lib/pages/scan.dart @@ -83,8 +83,8 @@ class _ScanPageState extends State with WidgetsBindingObserver { automaticallyImplyLeading: false, leading: IconButton( style: const ButtonStyle( - iconSize: MaterialStatePropertyAll(32), - foregroundColor: MaterialStatePropertyAll(Colors.white), + iconSize: WidgetStatePropertyAll(32), + foregroundColor: WidgetStatePropertyAll(Colors.white), ), onPressed: () { Navigator.of(context).pop(); @@ -92,44 +92,54 @@ class _ScanPageState extends State with WidgetsBindingObserver { icon: const Icon(Icons.close), ), actions: [ - IconButton( - onPressed: globalState.appController.addProfileFormQrCode, - icon: const Icon(Icons.add_photo_alternate_outlined), + ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, _) { + var icon = const Icon(Icons.flash_off); + var backgroundColor = Colors.black12; + switch (state.torchState) { + case TorchState.off: + icon = const Icon(Icons.flash_off); + backgroundColor = Colors.black12; + case TorchState.on: + icon = const Icon(Icons.flash_on); + backgroundColor = Colors.orange; + case TorchState.unavailable: + icon = const Icon(Icons.flash_off); + backgroundColor = Colors.transparent; + } + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: AbsorbPointer( + absorbing: state.torchState == TorchState.unavailable, + child: IconButton( + color: Colors.white, + icon: icon, + style: ButtonStyle( + foregroundColor: const WidgetStatePropertyAll(Colors.white), + backgroundColor: WidgetStatePropertyAll(backgroundColor), + ), + onPressed: () => controller.toggleTorch(), + ), + ), + ); + }, ) ], ), Container( margin: const EdgeInsets.only(bottom: 32), alignment: Alignment.bottomCenter, - child: ValueListenableBuilder( - valueListenable: controller, - builder: (context, state, _) { - var icon = const Icon(Icons.flash_off); - var backgroundColor = Colors.black12; - switch (state.torchState) { - case TorchState.off: - icon = const Icon(Icons.flash_off); - backgroundColor = Colors.black12; - case TorchState.on: - icon = const Icon(Icons.flash_on); - backgroundColor = Colors.orange; - case TorchState.unavailable: - icon = const Icon(Icons.no_flash); - backgroundColor = Colors.grey; - } - return IconButton( - color: Colors.white, - icon: icon, - style: ButtonStyle( - foregroundColor: - const MaterialStatePropertyAll(Colors.white), - backgroundColor: MaterialStatePropertyAll(backgroundColor), - ), - padding: const EdgeInsets.all(16), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ); - }, + child: IconButton( + color: Colors.white, + style: const ButtonStyle( + foregroundColor: WidgetStatePropertyAll(Colors.white), + backgroundColor: WidgetStatePropertyAll(Colors.grey), + ), + padding: const EdgeInsets.all(16), + iconSize: 32.0, + onPressed: globalState.appController.addProfileFormQrCode, + icon: const Icon(Icons.photo_camera_back), ), ), ], diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index 69da37d..b2b68fb 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; @@ -44,9 +45,11 @@ class App { Future> getPackages() async { final packagesString = await methodChannel?.invokeMethod("getPackages"); - final List packagesRaw = - packagesString != null ? json.decode(packagesString) : []; - return packagesRaw.map((e) => Package.fromJson(e)).toList(); + return Isolate.run>(() { + final List packagesRaw = + packagesString != null ? json.decode(packagesString) : []; + return packagesRaw.map((e) => Package.fromJson(e)).toList(); + }); } Future getPackageIcon(String packageName) async { diff --git a/lib/state.dart b/lib/state.dart index 0f69589..4d5898d 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -4,8 +4,6 @@ import 'dart:io'; import 'package:animations/animations.dart'; import 'package:fl_clash/clash/clash.dart'; -import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; @@ -24,8 +22,6 @@ class GlobalState { late AppController appController; GlobalKey homeScaffoldKey = GlobalKey(); List updateFunctionLists = []; - List currentNavigationItems = []; - bool updatePackagesLock = false; bool healthcheckLock = false; startListenUpdate() { @@ -48,6 +44,7 @@ class GlobalState { bool isPatch = true, }) async { final profilePath = await appPath.getProfilePath(config.currentProfileId); + await config.currentProfile?.checkAndUpdate(); debugPrint("update config"); return clashCore.updateConfig(UpdateConfigParams( profilePath: profilePath, @@ -89,7 +86,7 @@ class GlobalState { config: config, isPatch: false, ); - if (res.isNotEmpty) return Result.error(message: res); + if (res.isNotEmpty) return Result.error(res); await updateGroups(appState); changeProxy( appState: appState, @@ -140,14 +137,6 @@ class GlobalState { }); } - updatePackages(AppState appState) async { - if (appState.packages.isEmpty && updatePackagesLock == false) { - updatePackagesLock = true; - appState.packages = await app?.getPackages() ?? []; - updatePackagesLock = false; - } - } - updateNavigationItems({ required AppState appState, required Config config, @@ -179,8 +168,9 @@ class GlobalState { width: 300, child: RichText( text: TextSpan( - style: Theme.of(context).textTheme.labelLarge, - children: [message]), + style: Theme.of(context).textTheme.labelLarge, + children: [message], + ), ), ), actions: [ @@ -198,7 +188,7 @@ class GlobalState { ); } - showCommonDialog({ + Future showCommonDialog({ required Widget child, }) async { return await showModal( @@ -207,23 +197,9 @@ class GlobalState { barrierColor: Colors.black38, ), builder: (_) => child, - filter: appConstant.filter, + filter: filter, ); } - - checkUpdate(Function()? onTab) async { - final result = await Request.checkForUpdate(); - if (result.type == ResultType.success) { - showMessage( - title: appLocalizations.discovery, - message: TextSpan( - text: result.data, - ), - onTab: onTab, - ); - } - } - updateTraffic({ AppState? appState, required Config config, @@ -274,8 +250,8 @@ class GlobalState { } void updateCurrentDelay( - String? proxyName, - ) { + String? proxyName, + ) { updateCurrentDelayDebounce ??= debounce((proxyName) { if (proxyName != null) { debugPrint("[delay]=====> $proxyName"); @@ -286,7 +262,6 @@ class GlobalState { }); updateCurrentDelayDebounce!([proxyName]); } - } final globalState = GlobalState(); diff --git a/lib/widgets/android_container.dart b/lib/widgets/android_container.dart index 6e64f1a..e671316 100644 --- a/lib/widgets/android_container.dart +++ b/lib/widgets/android_container.dart @@ -16,6 +16,7 @@ class AndroidContainer extends StatefulWidget { class _AndroidContainerState extends State with WidgetsBindingObserver { + @override void initState() { super.initState(); diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index b4773ab..7dd823b 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -66,25 +66,25 @@ class CommonCard extends StatelessWidget { final Widget child; final Info? info; - BorderSide getBorderSide(BuildContext context, Set states) { + BorderSide getBorderSide(BuildContext context, Set states) { final colorScheme = Theme.of(context).colorScheme; var hoverColor = isSelected ? colorScheme.primary.toLight() : colorScheme.primary.toLighter(); - if (states.contains(MaterialState.hovered) || - states.contains(MaterialState.focused) || - states.contains(MaterialState.pressed)) { + if (states.contains(WidgetState.hovered) || + states.contains(WidgetState.focused) || + states.contains(WidgetState.pressed)) { return BorderSide( color: hoverColor, ); } return BorderSide( color: - isSelected ? colorScheme.primary : colorScheme.onBackground.toSoft(), + isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(), ); } - Color? getBackgroundColor(BuildContext context, Set states) { + Color? getBackgroundColor(BuildContext context, Set states) { final colorScheme = Theme.of(context).colorScheme; if (isSelected) { return colorScheme.secondaryContainer; @@ -123,16 +123,16 @@ class CommonCard extends StatelessWidget { return OutlinedButton( clipBehavior: Clip.antiAlias, style: ButtonStyle( - padding: const MaterialStatePropertyAll(EdgeInsets.zero), - shape: MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll(EdgeInsets.zero), + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) => getBackgroundColor(context, states), ), - side: MaterialStateProperty.resolveWith( + side: WidgetStateProperty.resolveWith( (states) => getBorderSide(context, states), ), ), diff --git a/lib/widgets/extend_page.dart b/lib/widgets/extend_page.dart index d940010..ec6d09f 100644 --- a/lib/widgets/extend_page.dart +++ b/lib/widgets/extend_page.dart @@ -1,6 +1,10 @@ import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'side_sheet.dart'; showExtendPage( @@ -19,9 +23,11 @@ showExtendPage( navigator.push( ModalSideSheetRoute( modalBarrierColor: Colors.black38, - builder: (context) => LayoutBuilder( - builder: (_, __) { - final isMobile = context.isMobile; + builder: (context) => Selector( + selector: (_, appState) => appState.viewWidth, + builder: (_, viewWidth, __) { + final isMobile = + globalState.appController.appState.viewMode == ViewMode.mobile; final commonScaffold = CommonScaffold( automaticallyImplyLeading: isMobile ? true : false, actions: isMobile @@ -33,18 +39,18 @@ showExtendPage( child: CloseButton(), ), ], - title: Text(title), + title: title, body: uniqueBody, ); return AnimatedContainer( duration: kThemeAnimationDuration, - width: isMobile ? context.width : extendPageWidth ?? 300, + width: isMobile ? viewWidth : extendPageWidth ?? 300, child: commonScaffold, ); }, ), constraints: const BoxConstraints(), - filter: appConstant.filter, + filter: filter, ), ); } diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 55c5c0a..58c99ee 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -1,4 +1,5 @@ -import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/open_container.dart'; import 'package:flutter/material.dart'; @@ -56,10 +57,12 @@ class OpenDelegate extends Delegate { class NextDelegate extends Delegate { final Widget widget; final String title; + final double? extendPageWidth; const NextDelegate({ required this.title, required this.widget, + this.extendPageWidth, }); } @@ -203,7 +206,7 @@ class ListItem extends StatelessWidget { return OpenContainer( closedBuilder: (_, action) { openAction() { - final isMobile = context.isMobile; + final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; if (!isMobile) { showExtendPage( context, @@ -220,8 +223,9 @@ class ListItem extends StatelessWidget { }, openBuilder: (_, action) { return CommonScaffold.open( + key: Key(openDelegate.title), onBack: action, - title: Text(openDelegate.title), + title: openDelegate.title, body: openDelegate.widget, ); }, @@ -231,11 +235,22 @@ class ListItem extends StatelessWidget { final nextDelegate = delegate as NextDelegate; return _buildListTile( onTab: () { + final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; + if (!isMobile) { + showExtendPage( + context, + body: nextDelegate.widget, + title: nextDelegate.title, + extendPageWidth: nextDelegate.extendPageWidth, + ); + return; + } Navigator.of(context).push( MaterialPageRoute( builder: (context) => CommonScaffold( + key: Key(nextDelegate.title), body: nextDelegate.widget, - title: Text(nextDelegate.title), + title: nextDelegate.title, ), ), ); diff --git a/lib/widgets/open_container.dart b/lib/widgets/open_container.dart index 5f952d7..5197d37 100644 --- a/lib/widgets/open_container.dart +++ b/lib/widgets/open_container.dart @@ -448,8 +448,8 @@ class _OpenContainerRoute extends ModalRoute { builder: (_, __, ___) { _colorTween = _getColorTween( transitionType: transitionType, - closedColor: Theme.of(context).colorScheme.background, - openColor: Theme.of(context).colorScheme.background, + closedColor: Theme.of(context).colorScheme.surface, + openColor: Theme.of(context).colorScheme.surface, middleColor: middleColor, ); return Align( diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index a14c093..72f232d 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -1,11 +1,13 @@ +import 'package:fl_clash/common/app_localizations.dart'; import 'package:fl_clash/common/system.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class CommonScaffold extends StatefulWidget { final Widget body; final Widget? bottomNavigationBar; - final Widget? title; + final String title; final Widget? leading; final List? actions; final bool automaticallyImplyLeading; @@ -15,7 +17,7 @@ class CommonScaffold extends StatefulWidget { required this.body, this.bottomNavigationBar, this.leading, - this.title, + required this.title, this.actions, this.automaticallyImplyLeading = true, }); @@ -23,7 +25,7 @@ class CommonScaffold extends StatefulWidget { CommonScaffold.open({ Key? key, required Widget body, - Widget? title, + required String title, required Function onBack, }) : this( key: key, @@ -56,10 +58,26 @@ class CommonScaffoldState extends State { } } - loadingRun(Future Function() futureFunction) async { + Future loadingRun( + Future Function() futureFunction, { + String? title, + }) async { + if (_loading.value == true) return null; _loading.value = true; - await futureFunction(); - _loading.value = false; + try { + final res = await futureFunction(); + _loading.value = false; + return res; + } catch (e) { + globalState.showMessage( + title: title ?? appLocalizations.tip, + message: TextSpan( + text: e.toString(), + ), + ); + _loading.value = false; + return null; + } } @override @@ -102,7 +120,7 @@ class CommonScaffoldState extends State { return AppBar( automaticallyImplyLeading: widget.automaticallyImplyLeading, leading: widget.leading, - title: widget.title, + title: Text(widget.title), actions: actions.isNotEmpty ? actions : widget.actions, ); }, diff --git a/lib/widgets/section.dart b/lib/widgets/section.dart new file mode 100644 index 0000000..9c4d579 --- /dev/null +++ b/lib/widgets/section.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class Section extends StatelessWidget { + final String title; + final Widget child; + + const Section({ + super.key, + required this.title, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text( + title, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + Expanded( + flex: 0, + child: child, + ) + ], + ); + } +} diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart index b745866..cc6dbd0 100644 --- a/lib/widgets/side_sheet.dart +++ b/lib/widgets/side_sheet.dart @@ -3,7 +3,7 @@ import 'package:flutter/rendering.dart'; const Duration _bottomSheetEnterDuration = Duration(milliseconds: 300); const Duration _bottomSheetExitDuration = Duration(milliseconds: 200); -const Curve _modalBottomSheetCurve = decelerateEasing; +const Curve _modalBottomSheetCurve = Easing.standardDecelerate; const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0; class SideSheet extends StatefulWidget { diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 0e6844f..acc0ae9 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -22,4 +22,5 @@ export 'tile_container.dart'; export 'chip.dart'; export 'fade_box.dart'; export 'app_state_container.dart'; -export 'text.dart'; \ No newline at end of file +export 'text.dart'; +export 'section.dart'; \ No newline at end of file diff --git a/lib/widgets/window_container.dart b/lib/widgets/window_container.dart index 869c144..e563828 100644 --- a/lib/widgets/window_container.dart +++ b/lib/widgets/window_container.dart @@ -27,12 +27,18 @@ class _WindowContainerState extends State windowManager.addListener(this); } + @override + void onWindowResize() { + globalState.appController.updateViewWidth(); + } + @override void onWindowClose() async { await globalState.appController.handleBackOrExit(); super.onWindowClose(); } + @override void onWindowMinimize() async { await globalState.appController.savePreferences(); diff --git a/pubspec.lock b/pubspec.lock index 9cd6896..05410a1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.6" + dio: + dependency: transitive + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.4.3+1" dynamic_color: dependency: "direct main" description: @@ -310,14 +318,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_adaptive_scaffold: - dependency: "direct main" - description: - name: flutter_adaptive_scaffold - sha256: "9a1d5e9f728815e27b7b612883db19107ba8a35a46a97c757ea00896cb027451" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.10+2" flutter_lints: dependency: "direct dev" description: @@ -1081,6 +1081,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.5" + webdav_client: + dependency: "direct main" + description: + name: webdav_client + sha256: "682fffc50b61dc0e8f46717171db03bf9caaa17347be41c0c91e297553bf86b2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c4597c9..2d3d482 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.3 +version: 0.8.4 environment: sdk: '>=3.1.0 <4.0.0' @@ -33,11 +33,11 @@ dependencies: animations: ^2.0.11 package_info_plus: ^7.0.0 url_launcher: ^6.2.6 - flutter_adaptive_scaffold: ^0.1.10+1 freezed_annotation: ^2.4.1 image_picker: ^1.1.1 zxing2: ^0.2.3 image: ^4.1.7 + webdav_client: ^1.2.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/test/command_test.dart b/test/command_test.dart index f958fa6..a86603a 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -1,18 +1,13 @@ // ignore_for_file: avoid_print +import 'package:http/io_client.dart'; import 'dart:io'; -main() async { - final result = await Process.run( - 'netstat', - ["-ano","|","findstr",":7890","|","findstr","LISTENING"], - runInShell: true, - ); - final output = result.stdout as String; - final line = output.split('\n').first; - final pid = line.split(' ').firstWhere( - (value) => value.trim().contains(RegExp(r'^\d+$')), - orElse: () => '', - ); - print(pid); +void main() async { + HttpClient httpClient = HttpClient(); + httpClient.findProxy = HttpClient.findProxyFromEnvironment; + + IOClient ioClient = IOClient(httpClient); + var response = await ioClient.get(Uri.parse('https://mirror.ghproxy.com/https://raw.githubusercontent.com/Ruk1ng001/freeSub/main/clash_top30.yaml')); + print(response.body); }