splittunneling_selectapp_view.dart 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:nomo/app/base/base_view.dart';
  5. import 'package:nomo/app/widgets/click_opacity.dart';
  6. import 'package:nomo/app/widgets/ix_app_bar.dart';
  7. import 'package:nomo/app/widgets/ix_image.dart';
  8. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  9. import '../../../../../config/translations/strings_enum.dart';
  10. import '../../../../../utils/event_bus.dart';
  11. import '../../../../constants/iconfont/iconfont.dart';
  12. import '../../../../widgets/state/state_wrapper.dart';
  13. import '../controllers/splittunneling_selectapp_controller.dart';
  14. class SplittunnelingSelectappView
  15. extends BaseView<SplittunnelingSelectappController> {
  16. SplittunnelingSelectappView({super.key});
  17. // 用于存储应用项的位置信息
  18. final Map<String, GlobalKey> _selectedAppsKeys = {};
  19. final Map<String, GlobalKey> _unselectedAppsKeys = {};
  20. @override
  21. PreferredSizeWidget? get appBar => IXAppBar(
  22. title: Strings.splitTunneling.tr,
  23. onBackPressed: () {
  24. eventBus.fire(
  25. SplitTunnelingPageEvent(mode: controller.currentMode.value),
  26. );
  27. Get.back();
  28. },
  29. );
  30. @override
  31. Widget buildContent(BuildContext context) {
  32. return StateWrapper(
  33. controller: controller,
  34. onRefresh: () => controller.getApps(),
  35. child: SingleChildScrollView(
  36. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  37. child: Column(
  38. crossAxisAlignment: CrossAxisAlignment.start,
  39. children: [
  40. // 信息横幅
  41. _buildInfoBanner(),
  42. // 已选择应用区域
  43. _buildSelectedAppsSection(context),
  44. 10.verticalSpaceFromWidth,
  45. // 所有应用区域
  46. _buildAllAppsSection(context),
  47. ],
  48. ),
  49. ),
  50. );
  51. }
  52. /// 构建信息横幅
  53. Widget _buildInfoBanner() {
  54. return Obx(() {
  55. final isExcludeMode =
  56. controller.currentMode.value == SplitTunnelingMode.exclude;
  57. final message = isExcludeMode
  58. ? Strings.selectAppsExclude.tr
  59. : Strings.selectAppsInclude.tr;
  60. return Container(
  61. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.w),
  62. decoration: BoxDecoration(
  63. color: Get.reactiveTheme.cardColor,
  64. borderRadius: BorderRadius.circular(12.r),
  65. ),
  66. child: Row(
  67. children: [
  68. Icon(
  69. IconFont.icon22,
  70. size: 20.w,
  71. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  72. ),
  73. SizedBox(width: 12.w),
  74. Expanded(
  75. child: Text(
  76. message,
  77. style: TextStyle(
  78. fontSize: 12.sp,
  79. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  80. fontWeight: FontWeight.w500,
  81. ),
  82. ),
  83. ),
  84. ],
  85. ),
  86. );
  87. });
  88. }
  89. /// 构建已选择应用区域
  90. Widget _buildSelectedAppsSection(BuildContext context) {
  91. return Obx(() {
  92. if (controller.selectedApps.isEmpty) {
  93. return const SizedBox.shrink();
  94. }
  95. return AnimatedContainer(
  96. duration: const Duration(milliseconds: 300),
  97. curve: Curves.easeInOut,
  98. child: Column(
  99. crossAxisAlignment: CrossAxisAlignment.start,
  100. children: [
  101. 10.verticalSpaceFromWidth,
  102. Row(
  103. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  104. children: [
  105. Text(
  106. Strings.selectApps.tr,
  107. style: TextStyle(
  108. fontSize: 16.sp,
  109. fontWeight: FontWeight.w500,
  110. color: Get.reactiveTheme.hintColor,
  111. ),
  112. ),
  113. ClickOpacity(
  114. onTap: controller.deselectAllApps,
  115. child: Text(
  116. Strings.deselectAll.tr,
  117. style: TextStyle(
  118. fontSize: 14.sp,
  119. color: Get.reactiveTheme.shadowColor,
  120. fontWeight: FontWeight.w500,
  121. ),
  122. ),
  123. ),
  124. ],
  125. ),
  126. 10.verticalSpaceFromWidth,
  127. Container(
  128. decoration: BoxDecoration(
  129. color: Get.reactiveTheme.highlightColor,
  130. borderRadius: BorderRadius.circular(12.r),
  131. ),
  132. child: Column(
  133. children: controller.selectedApps.asMap().entries.map((entry) {
  134. final key = GlobalKey();
  135. _selectedAppsKeys[entry.value.packageName] = key;
  136. return Container(
  137. key: key,
  138. child: _buildAppItem(
  139. context,
  140. entry.value,
  141. isLast: entry.key == controller.selectedApps.length - 1,
  142. ),
  143. );
  144. }).toList(),
  145. ),
  146. ),
  147. ],
  148. ),
  149. );
  150. });
  151. }
  152. /// 构建所有应用区域
  153. Widget _buildAllAppsSection(BuildContext context) {
  154. return Column(
  155. crossAxisAlignment: CrossAxisAlignment.start,
  156. children: [
  157. Text(
  158. Strings.allApps.tr,
  159. style: TextStyle(
  160. fontSize: 16.sp,
  161. fontWeight: FontWeight.w500,
  162. color: Get.reactiveTheme.hintColor,
  163. ),
  164. ),
  165. 10.verticalSpaceFromWidth,
  166. Obx(() {
  167. final unselectedApps = controller.unselectedApps;
  168. return Container(
  169. decoration: BoxDecoration(
  170. color: Get.reactiveTheme.highlightColor,
  171. borderRadius: BorderRadius.circular(12.r),
  172. ),
  173. child: Column(
  174. children: unselectedApps.asMap().entries.map((entry) {
  175. final key = GlobalKey();
  176. _unselectedAppsKeys[entry.value.packageName] = key;
  177. return Container(
  178. key: key,
  179. child: _buildAppItem(
  180. context,
  181. entry.value,
  182. isLast: entry.key == unselectedApps.length - 1,
  183. ),
  184. );
  185. }).toList(),
  186. ),
  187. );
  188. }),
  189. ],
  190. );
  191. }
  192. /// 构建应用项
  193. Widget _buildAppItem(
  194. BuildContext context,
  195. AppInfo app, {
  196. bool isLast = false,
  197. }) {
  198. return AnimatedContainer(
  199. duration: const Duration(milliseconds: 200),
  200. curve: Curves.easeInOut,
  201. child: GestureDetector(
  202. onTap: () {
  203. // 直接切换选择状态
  204. controller.toggleAppSelection(app);
  205. },
  206. child: AnimatedContainer(
  207. duration: const Duration(milliseconds: 150),
  208. curve: Curves.easeInOut,
  209. child: Container(
  210. height: 68.w,
  211. padding: EdgeInsets.symmetric(horizontal: 14.w),
  212. decoration: BoxDecoration(
  213. border: isLast
  214. ? null
  215. : Border(
  216. bottom: BorderSide(
  217. color: Get.reactiveTheme.dividerColor,
  218. width: 1.w,
  219. ),
  220. ),
  221. ),
  222. child: Row(
  223. children: [
  224. // 应用图标
  225. AnimatedContainer(
  226. duration: const Duration(milliseconds: 200),
  227. curve: Curves.easeInOut,
  228. child: Transform.scale(
  229. scale: 1.0,
  230. child: IXImage(
  231. key: ValueKey('app_icon_${app.packageName}'),
  232. source: app.icon,
  233. width: 40.w,
  234. height: 40.w,
  235. sourceType: ImageSourceType.memory,
  236. ),
  237. ),
  238. ),
  239. 12.horizontalSpace,
  240. // 应用名称
  241. Expanded(
  242. child: Text(
  243. app.name,
  244. style: TextStyle(
  245. fontSize: 14.sp,
  246. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  247. fontWeight: FontWeight.w500,
  248. ),
  249. ),
  250. ),
  251. // 选择状态指示器
  252. Obx(() {
  253. final isSelected = controller.isAppSelected(app);
  254. return AnimatedContainer(
  255. duration: const Duration(milliseconds: 300),
  256. curve: Curves.elasticOut,
  257. width: 20.w,
  258. height: 20.w,
  259. decoration: BoxDecoration(
  260. shape: BoxShape.circle,
  261. color: isSelected
  262. ? Get.reactiveTheme.shadowColor
  263. : Colors.transparent,
  264. border: Border.all(
  265. color: isSelected
  266. ? Get.reactiveTheme.shadowColor
  267. : Colors.grey[400]!,
  268. width: 1.5.w,
  269. ),
  270. ),
  271. child: AnimatedScale(
  272. scale: isSelected ? 1.0 : 0.0,
  273. duration: const Duration(milliseconds: 300),
  274. curve: Curves.elasticOut,
  275. child: Icon(Icons.check, color: Colors.white, size: 14.w),
  276. ),
  277. );
  278. }),
  279. ],
  280. ),
  281. ),
  282. ),
  283. ),
  284. );
  285. }
  286. }