2024-06-03 11:24:41 +08:00
|
|
|
import 'package:collection/collection.dart';
|
2024-04-30 23:38:49 +08:00
|
|
|
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';
|
2024-06-03 11:24:41 +08:00
|
|
|
import 'package:fl_clash/state.dart';
|
2024-04-30 23:38:49 +08:00
|
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
|
|
2024-05-20 15:15:09 +08:00
|
|
|
class AccessFragment extends StatefulWidget {
|
2024-04-30 23:38:49 +08:00
|
|
|
const AccessFragment({super.key});
|
|
|
|
|
|
2024-05-20 15:15:09 +08:00
|
|
|
@override
|
|
|
|
|
State<AccessFragment> createState() => _AccessFragmentState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _AccessFragmentState extends State<AccessFragment> {
|
|
|
|
|
final packagesListenable = ValueNotifier<List<Package>>([]);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2024-06-03 11:24:41 +08:00
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
Future.delayed(const Duration(milliseconds: 300), () async {
|
|
|
|
|
packagesListenable.value = await app?.getPackages() ?? [];
|
|
|
|
|
});
|
2024-05-20 15:15:09 +08:00
|
|
|
});
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildAppProxyModePopup() {
|
|
|
|
|
final items = [
|
|
|
|
|
CommonPopupMenuItem(
|
|
|
|
|
action: AccessControlMode.rejectSelected,
|
|
|
|
|
label: appLocalizations.blacklistMode,
|
|
|
|
|
),
|
|
|
|
|
CommonPopupMenuItem(
|
|
|
|
|
action: AccessControlMode.acceptSelected,
|
|
|
|
|
label: appLocalizations.whitelistMode,
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
return Selector<Config, AccessControlMode>(
|
|
|
|
|
selector: (_, config) => config.accessControl.mode,
|
|
|
|
|
builder: (context, mode, __) {
|
|
|
|
|
return CommonPopupMenu<AccessControlMode>.radio(
|
|
|
|
|
icon: Icon(
|
|
|
|
|
Icons.mode_standby,
|
|
|
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
|
|
|
),
|
|
|
|
|
items: items,
|
|
|
|
|
onSelected: (value) {
|
|
|
|
|
final config = context.read<Config>();
|
|
|
|
|
config.accessControl = config.accessControl.copyWith(
|
|
|
|
|
mode: value,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
selectedValue: mode,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildFilterSystemAppButton() {
|
|
|
|
|
return Selector<Config, bool>(
|
|
|
|
|
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
|
|
|
|
builder: (context, isFilterSystemApp, __) {
|
|
|
|
|
final tooltip = isFilterSystemApp
|
|
|
|
|
? appLocalizations.cancelFilterSystemApp
|
|
|
|
|
: appLocalizations.filterSystemApp;
|
|
|
|
|
return IconButton(
|
|
|
|
|
tooltip: tooltip,
|
|
|
|
|
onPressed: () {
|
|
|
|
|
final config = context.read<Config>();
|
|
|
|
|
config.accessControl = config.accessControl.copyWith(
|
|
|
|
|
isFilterSystemApp: !isFilterSystemApp,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
icon: isFilterSystemApp
|
|
|
|
|
? const Icon(Icons.filter_list_off)
|
|
|
|
|
: const Icon(Icons.filter_list),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildSelectedAllButton({
|
|
|
|
|
required bool isSelectedAll,
|
|
|
|
|
required List<String> allValueList,
|
|
|
|
|
}) {
|
|
|
|
|
return Builder(
|
|
|
|
|
builder: (context) {
|
|
|
|
|
final tooltip = isSelectedAll
|
|
|
|
|
? appLocalizations.cancelSelectAll
|
|
|
|
|
: appLocalizations.selectAll;
|
|
|
|
|
return IconButton(
|
|
|
|
|
tooltip: tooltip,
|
|
|
|
|
onPressed: () {
|
2024-06-03 11:24:41 +08:00
|
|
|
final config = globalState.appController.config;
|
|
|
|
|
final isAccept =
|
|
|
|
|
config.accessControl.mode == AccessControlMode.acceptSelected;
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
if (isSelectedAll) {
|
2024-06-03 11:24:41 +08:00
|
|
|
config.accessControl = switch (isAccept) {
|
|
|
|
|
true => config.accessControl.copyWith(
|
|
|
|
|
acceptList: [],
|
|
|
|
|
),
|
|
|
|
|
false => config.accessControl.copyWith(
|
|
|
|
|
rejectList: [],
|
|
|
|
|
),
|
|
|
|
|
};
|
2024-04-30 23:38:49 +08:00
|
|
|
} else {
|
2024-06-03 11:24:41 +08:00
|
|
|
config.accessControl = switch (isAccept) {
|
|
|
|
|
true => config.accessControl.copyWith(
|
|
|
|
|
acceptList: allValueList,
|
|
|
|
|
),
|
|
|
|
|
false => config.accessControl.copyWith(
|
|
|
|
|
rejectList: allValueList,
|
|
|
|
|
),
|
|
|
|
|
};
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
icon: isSelectedAll
|
|
|
|
|
? const Icon(Icons.deselect)
|
|
|
|
|
: const Icon(Icons.select_all),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-20 15:15:09 +08:00
|
|
|
Widget _actionHeader({
|
|
|
|
|
required bool isAccessControl,
|
|
|
|
|
required List<String> valueList,
|
|
|
|
|
required String describe,
|
|
|
|
|
required List<String> 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(
|
2024-04-30 23:38:49 +08:00
|
|
|
children: [
|
|
|
|
|
Flexible(
|
2024-05-20 15:15:09 +08:00
|
|
|
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,
|
|
|
|
|
),
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2024-05-20 15:15:09 +08:00
|
|
|
),
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text(describe),
|
|
|
|
|
)
|
|
|
|
|
],
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
|
|
|
|
),
|
2024-05-20 15:15:09 +08:00
|
|
|
),
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Flexible(
|
|
|
|
|
child: _buildSelectedAllButton(
|
2024-06-03 11:24:41 +08:00
|
|
|
isSelectedAll: const ListEquality<String>()
|
|
|
|
|
.equals(valueList, packageNameList),
|
2024-05-20 15:15:09 +08:00
|
|
|
allValueList: packageNameList,
|
|
|
|
|
),
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
2024-05-20 15:15:09 +08:00
|
|
|
Flexible(child: _buildFilterSystemAppButton()),
|
|
|
|
|
Flexible(child: _buildAppProxyModePopup()),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-03 11:24:41 +08:00
|
|
|
Widget _buildPackageList() {
|
2024-05-20 15:15:09 +08:00
|
|
|
return ValueListenableBuilder(
|
|
|
|
|
valueListenable: packagesListenable,
|
|
|
|
|
builder: (_, packages, ___) {
|
2024-06-03 14:46:16 +08:00
|
|
|
final accessControl = globalState.appController.config.accessControl;
|
|
|
|
|
final acceptList = accessControl.acceptList;
|
|
|
|
|
final rejectList = accessControl.rejectList;
|
|
|
|
|
final acceptPackages = packages.sorted((a, b) {
|
|
|
|
|
final isSelectA = acceptList.contains(a.packageName);
|
|
|
|
|
final isSelectB = acceptList.contains(b.packageName);
|
|
|
|
|
if (isSelectA && isSelectB) return 0;
|
|
|
|
|
if (isSelectA) return -1;
|
|
|
|
|
if (isSelectB) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
});
|
|
|
|
|
final rejectPackages = packages.sorted((a, b) {
|
|
|
|
|
final isSelectA = rejectList.contains(a.packageName);
|
|
|
|
|
final isSelectB = rejectList.contains(b.packageName);
|
|
|
|
|
if (isSelectA && isSelectB) return 0;
|
|
|
|
|
if (isSelectA) return -1;
|
|
|
|
|
if (isSelectB) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
});
|
2024-06-03 11:24:41 +08:00
|
|
|
return Selector<Config, PackageListSelectorState>(
|
|
|
|
|
selector: (_, config) => PackageListSelectorState(
|
|
|
|
|
accessControl: config.accessControl,
|
|
|
|
|
isAccessControl: config.isAccessControl,
|
|
|
|
|
),
|
|
|
|
|
builder: (context, state, __) {
|
|
|
|
|
final accessControl = state.accessControl;
|
|
|
|
|
final isAccessControl = state.isAccessControl;
|
2024-05-20 15:15:09 +08:00
|
|
|
final isFilterSystemApp = accessControl.isFilterSystemApp;
|
|
|
|
|
final accessControlMode = accessControl.mode;
|
2024-06-03 14:46:16 +08:00
|
|
|
final packages =
|
|
|
|
|
accessControlMode == AccessControlMode.acceptSelected
|
|
|
|
|
? acceptPackages
|
|
|
|
|
: rejectPackages;
|
2024-06-03 11:24:41 +08:00
|
|
|
final currentList =
|
|
|
|
|
accessControlMode == AccessControlMode.acceptSelected
|
|
|
|
|
? accessControl.acceptList
|
|
|
|
|
: accessControl.rejectList;
|
2024-06-03 14:46:16 +08:00
|
|
|
final currentPackages = isFilterSystemApp
|
|
|
|
|
? packages
|
|
|
|
|
.where((element) => element.isSystem == false)
|
|
|
|
|
.toList()
|
|
|
|
|
: packages;
|
|
|
|
|
final packageNameList =
|
|
|
|
|
currentPackages.map((e) => e.packageName).toList();
|
2024-06-03 11:24:41 +08:00
|
|
|
final valueList = currentList.intersection(packageNameList);
|
2024-05-20 15:15:09 +08:00
|
|
|
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(
|
2024-06-03 11:24:41 +08:00
|
|
|
key: Key(package.packageName),
|
2024-05-20 15:15:09 +08:00
|
|
|
package: package,
|
|
|
|
|
value:
|
|
|
|
|
valueList.contains(package.packageName),
|
|
|
|
|
isActive: isAccessControl,
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
if (value == true) {
|
|
|
|
|
valueList.add(package.packageName);
|
|
|
|
|
} else {
|
|
|
|
|
valueList.remove(package.packageName);
|
|
|
|
|
}
|
2024-06-03 11:24:41 +08:00
|
|
|
final config =
|
|
|
|
|
globalState.appController.config;
|
|
|
|
|
if (accessControlMode ==
|
|
|
|
|
AccessControlMode.acceptSelected) {
|
|
|
|
|
config.accessControl =
|
|
|
|
|
config.accessControl.copyWith(
|
|
|
|
|
acceptList: valueList,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
config.accessControl =
|
|
|
|
|
config.accessControl.copyWith(
|
|
|
|
|
rejectList: valueList,
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-05-20 15:15:09 +08:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
2024-05-20 15:15:09 +08:00
|
|
|
);
|
|
|
|
|
},
|
2024-04-30 23:38:49 +08:00
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Selector<Config, bool>(
|
|
|
|
|
selector: (_, config) => config.isAccessControl,
|
2024-06-03 11:24:41 +08:00
|
|
|
builder: (_, isAccessControl, child) {
|
2024-04-30 23:38:49 +08:00
|
|
|
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>();
|
|
|
|
|
config.isAccessControl = isAccessControl;
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const Padding(
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
|
|
|
child: Divider(
|
|
|
|
|
height: 12,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Flexible(
|
2024-06-03 11:24:41 +08:00
|
|
|
child: child!,
|
2024-04-30 23:38:49 +08:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
2024-06-03 11:24:41 +08:00
|
|
|
child: _buildPackageList(),
|
2024-04-30 23:38:49 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-20 15:15:09 +08:00
|
|
|
|
|
|
|
|
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<ImageProvider?>(
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|