import 'dart:convert'; 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:flutter/services.dart'; import 'package:provider/provider.dart'; class AccessFragment extends StatefulWidget { const AccessFragment({super.key}); @override State createState() => _AccessFragmentState(); } class _AccessFragmentState extends State { List acceptList = []; List rejectList = []; late ScrollController _controller; @override void initState() { super.initState(); _updateInitList(); _controller = ScrollController(); WidgetsBinding.instance.addPostFrameCallback((_) { final appState = globalState.appController.appState; if (appState.packages.isEmpty) { Future.delayed(const Duration(milliseconds: 300), () async { appState.packages = await app?.getPackages() ?? []; }); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } _updateInitList() { final accessControl = globalState.appController.config.accessControl; acceptList = accessControl.acceptList; rejectList = accessControl.rejectList; } Widget _buildSearchButton() { return IconButton( tooltip: appLocalizations.search, onPressed: () { showSearch( context: context, delegate: AccessControlSearchDelegate( acceptList: acceptList, rejectList: rejectList, ), ).then((_) => setState(() { _updateInitList(); })); }, icon: const Icon(Icons.search), ); } Widget _buildSelectedAllButton({ required bool isSelectedAll, required List allValueList, }) { final tooltip = isSelectedAll ? appLocalizations.cancelSelectAll : appLocalizations.selectAll; return IconButton( tooltip: tooltip, onPressed: () { final config = globalState.appController.config; final isAccept = config.accessControl.mode == AccessControlMode.acceptSelected; if (isSelectedAll) { config.accessControl = switch (isAccept) { true => config.accessControl.copyWith( acceptList: [], ), false => config.accessControl.copyWith( rejectList: [], ), }; } else { config.accessControl = switch (isAccept) { true => config.accessControl.copyWith( acceptList: allValueList, ), false => config.accessControl.copyWith( rejectList: allValueList, ), }; } }, icon: isSelectedAll ? const Icon(Icons.deselect) : const Icon(Icons.select_all), ); } Widget _buildSettingButton() { return IconButton( onPressed: () { showSheet( title: appLocalizations.proxiesSetting, context: context, body: AccessControlWidget( context: context, ), ); }, icon: const Icon(Icons.tune), ); } @override Widget build(BuildContext context) { return Selector( selector: (_, config) => config.isAccessControl, builder: (_, isAccessControl, child) { return Column( mainAxisSize: MainAxisSize.max, children: [ Flexible( flex: 0, child: ListItem.switchItem( title: Text(appLocalizations.appAccessControl), delegate: SwitchDelegate( value: isAccessControl, onChanged: (isAccessControl) { final config = context.read(); config.isAccessControl = isAccessControl; }, ), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Divider( height: 12, ), ), Flexible( child: child!, ), ], ); }, child: Selector>( selector: (_, appState) => appState.packages, builder: (_, packages, ___) { return Selector2( selector: (_, appState, config) => PackageListSelectorState( accessControl: config.accessControl, isAccessControl: config.isAccessControl, packages: appState.packages, ), builder: (context, state, __) { final accessControl = state.accessControl; final isAccessControl = state.isAccessControl; final accessControlMode = accessControl.mode; final packages = state.getList( accessControlMode == AccessControlMode.acceptSelected ? acceptList : rejectList, ); final currentList = accessControl.currentList; final packageNameList = packages.map((e) => e.packageName).toList(); final valueList = currentList.intersection(packageNameList); final describe = accessControlMode == AccessControlMode.acceptSelected ? appLocalizations.accessControlAllowDesc : appLocalizations.accessControlNotAllowDesc; return DisabledMask( status: !isAccessControl, child: Column( children: [ ActivateBox( active: 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, children: [ Flexible( child: _buildSearchButton(), ), Flexible( child: _buildSelectedAllButton( isSelectedAll: valueList.length == packageNameList.length, allValueList: packageNameList, ), ), Flexible( child: _buildSettingButton(), ), ], ), ], ), ), ), Expanded( flex: 1, child: packages.isEmpty ? const Center( child: CircularProgressIndicator(), ) : CommonScrollBar( controller: _controller, child: ListView.builder( controller: _controller, itemCount: packages.length, itemExtent: 72, itemBuilder: (_, index) { final package = packages[index]; return PackageListItem( key: Key(package.packageName), 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 = globalState.appController.config; if (accessControlMode == AccessControlMode.acceptSelected) { config.accessControl = config.accessControl.copyWith( acceptList: valueList, ); } else { config.accessControl = config.accessControl.copyWith( rejectList: valueList, ); } }, ); }, ), ), ), ], ), ); }, ); }, ), ); } } 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 ActivateBox( active: 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, ), ), ); } } class AccessControlSearchDelegate extends SearchDelegate { List acceptList = []; List rejectList = []; AccessControlSearchDelegate({ required this.acceptList, required this.rejectList, }); @override List? buildActions(BuildContext context) { return [ IconButton( onPressed: () { if (query.isEmpty) { close(context, null); return; } query = ''; }, icon: const Icon(Icons.clear), ), const SizedBox( width: 8, ) ]; } @override Widget? buildLeading(BuildContext context) { return IconButton( onPressed: () { close(context, null); }, icon: const Icon(Icons.arrow_back), ); } Widget _packageList() { final lowQuery = query.toLowerCase(); return Selector2( selector: (_, appState, config) => PackageListSelectorState( packages: appState.packages, accessControl: config.accessControl, isAccessControl: config.isAccessControl, ), builder: (context, state, __) { final accessControl = state.accessControl; final accessControlMode = accessControl.mode; final packages = state.getList( accessControlMode == AccessControlMode.acceptSelected ? acceptList : rejectList, ); final queryPackages = packages .where( (package) => package.label.toLowerCase().contains(lowQuery) || package.packageName.contains(lowQuery), ) .toList(); final isAccessControl = state.isAccessControl; final currentList = accessControl.currentList; final packageNameList = packages.map((e) => e.packageName).toList(); final valueList = currentList.intersection(packageNameList); return DisabledMask( status: !isAccessControl, child: ListView.builder( itemCount: queryPackages.length, itemBuilder: (_, index) { final package = queryPackages[index]; return PackageListItem( key: Key(package.packageName), 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 = globalState.appController.config; if (accessControlMode == AccessControlMode.acceptSelected) { config.accessControl = config.accessControl.copyWith( acceptList: valueList, ); } else { config.accessControl = config.accessControl.copyWith( rejectList: valueList, ); } }, ); }, ), ); }, ); } @override Widget buildResults(BuildContext context) { return buildSuggestions(context); } @override Widget buildSuggestions(BuildContext context) { return _packageList(); } } class AccessControlWidget extends StatelessWidget { final BuildContext context; const AccessControlWidget({ super.key, required this.context, }); IconData _getIconWithAccessControlMode(AccessControlMode mode) { return switch (mode) { AccessControlMode.acceptSelected => Icons.adjust_outlined, AccessControlMode.rejectSelected => Icons.block_outlined, }; } String _getTextWithAccessControlMode(AccessControlMode mode) { return switch (mode) { AccessControlMode.acceptSelected => appLocalizations.whitelistMode, AccessControlMode.rejectSelected => appLocalizations.blacklistMode, }; } String _getTextWithAccessSortType(AccessSortType type) { return switch (type) { AccessSortType.none => appLocalizations.defaultText, AccessSortType.name => appLocalizations.name, AccessSortType.time => appLocalizations.time, }; } IconData _getIconWithProxiesSortType(AccessSortType type) { return switch (type) { AccessSortType.none => Icons.sort, AccessSortType.name => Icons.sort_by_alpha, AccessSortType.time => Icons.timeline, }; } String _getTextWithIsFilterSystemApp(bool isFilterSystemApp) { return switch (isFilterSystemApp) { true => appLocalizations.onlyOtherApps, false => appLocalizations.allApps, }; } List _buildModeSetting() { return generateSection( title: appLocalizations.mode, items: [ SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: Selector( selector: (_, config) => config.accessControl.mode, builder: (_, accessControlMode, __) { return Wrap( spacing: 16, children: [ for (final item in AccessControlMode.values) SettingInfoCard( Info( label: _getTextWithAccessControlMode(item), iconData: _getIconWithAccessControlMode(item), ), isSelected: accessControlMode == item, onPressed: () { final config = globalState.appController.config; config.accessControl = config.accessControl.copyWith( mode: item, ); }, ) ], ); }, ), ) ], ); } List _buildSortSetting() { return generateSection( title: appLocalizations.sort, items: [ SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: Selector( selector: (_, config) => config.accessControl.sort, builder: (_, accessSortType, __) { return Wrap( spacing: 16, children: [ for (final item in AccessSortType.values) SettingInfoCard( Info( label: _getTextWithAccessSortType(item), iconData: _getIconWithProxiesSortType(item), ), isSelected: accessSortType == item, onPressed: () { final config = globalState.appController.config; config.accessControl = config.accessControl.copyWith( sort: item, ); }, ), ], ); }, ), ), ], ); } List _buildSourceSetting() { return generateSection( title: appLocalizations.source, items: [ SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: Selector( selector: (_, config) => config.accessControl.isFilterSystemApp, builder: (_, isFilterSystemApp, __) { return Wrap( spacing: 16, children: [ for (final item in [false, true]) SettingTextCard( _getTextWithIsFilterSystemApp(item), isSelected: isFilterSystemApp == item, onPressed: () { final config = globalState.appController.config; config.accessControl = config.accessControl.copyWith( isFilterSystemApp: item, ); }, ) ], ); }, ), ) ], ); } _intelligentSelected() async { final appState = globalState.appController.appState; final config = globalState.appController.config; final accessControl = config.accessControl; final packageNames = appState.packages .where( (item) => accessControl.isFilterSystemApp ? item.isSystem == false : true, ) .map((item) => item.packageName); Navigator.of(context).pop(); final commonScaffoldState = context.commonScaffoldState; if (commonScaffoldState?.mounted != true) return; final selectedPackageNames = (await commonScaffoldState?.loadingRun>( () async { return await app?.getChinaPackageNames() ?? []; }, )) ?.toSet() ?? {}; final acceptList = packageNames .where((item) => !selectedPackageNames.contains(item)) .toList(); final rejectList = packageNames .where((item) => selectedPackageNames.contains(item)) .toList(); config.accessControl = accessControl.copyWith( acceptList: acceptList, rejectList: rejectList, ); } _copyToClipboard() async { await globalState.safeRun(() { final data = globalState.appController.config.accessControl.toJson(); Clipboard.setData( ClipboardData( text: json.encode(data), ), ); }); if (!context.mounted) return; Navigator.of(context).pop(); } _pasteToClipboard() async { await globalState.safeRun(() async { final config = globalState.appController.config; final data = await Clipboard.getData('text/plain'); final text = data?.text; if (text == null) return; config.accessControl = AccessControl.fromJson( json.decode(text), ); }); if (!context.mounted) return; Navigator.of(context).pop(); } List _buildActionSetting() { return generateSection( title: appLocalizations.action, items: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, ), child: Wrap( runSpacing: 16, spacing: 16, children: [ CommonChip( avatar: const Icon(Icons.auto_awesome), label: appLocalizations.intelligentSelected, onPressed: _intelligentSelected, ), CommonChip( avatar: const Icon(Icons.paste), label: appLocalizations.clipboardImport, onPressed: _pasteToClipboard, ), CommonChip( avatar: const Icon(Icons.content_copy), label: appLocalizations.clipboardExport, onPressed: _copyToClipboard, ) ], ), ) ], ); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ ..._buildModeSetting(), ..._buildSortSetting(), ..._buildSourceSetting(), ..._buildActionSetting(), ], ), ); } }